Compare commits

..

560 Commits

Author SHA1 Message Date
Felix Roos ec26392509 fix: return silence when no pattern is returned 2025-11-27 22:53:38 +01:00
froos 30cd486f37 Merge pull request 'fix(tool/dbpatch): add missing package name' (#1765) from peterpf/strudel:bugfix/add-package-name-for-dbpatch into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1765
2025-11-27 22:47:01 +01:00
froos cde0520ed9 Merge pull request 'add CHANGELOG.md + basic script to generate' (#1794) from changelog into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1794
2025-11-27 22:38:04 +01:00
Felix Roos 405338e4f0 add CHANGELOG.md + basic script to generate 2025-11-27 22:25:32 +01:00
froos afbcaa54d0 Merge pull request '[perf] in noise, let noiseMix do the disconnect when it exists' (#1783) from jeromew/strudel:fix-perf9 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1783
2025-11-27 22:03:51 +01:00
froos 6a1c6081e0 Merge pull request 'Bug Fix: Retries for sounds tab' (#1754) from glossing/sounds-tab-preview into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1754
2025-11-27 22:01:00 +01:00
Felix Roos 3c4ba667c8 cleanup 2025-11-27 21:29:37 +01:00
Felix Roos 7fcb5262b8 Merge remote-tracking branch 'origin/main' into glossing/sounds-tab-preview 2025-11-27 21:14:22 +01:00
Felix Roos b824c45ba4 Merge remote-tracking branch 'origin/main' into glossing/sounds-tab-preview 2025-11-27 21:11:50 +01:00
froos e47e6c87fc Merge pull request '[hydra] return the hydra object when await initHydra(..) is called' (#1784) from jeromew/strudel:return-hydra into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1784
2025-11-27 20:36:44 +01:00
froos 69b5ba89cb Merge pull request 'Use errorLogger for query and tonal errors' (#1782) from glossing/query-error into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1782
2025-11-27 20:36:12 +01:00
froos 33a51761ce Merge pull request '[perf] level 5 connect-leak on vowel' (#1779) from jeromew/strudel:fix-perf7 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1779
2025-11-27 20:27:09 +01:00
Peter Egger d217119391 fix(tool/dbpatch): add missing package name 2025-11-26 19:48:48 +01:00
Switch Angel AKA Jade Rose d5d6284a90 Merge pull request 'feat: add prebake script import button for loading .strudel files' (#1774) from pre_eval into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1774
2025-11-26 08:23:37 +01:00
Switch Angel AKA Jade Rose fb790de26c Merge branch 'main' into pre_eval 2025-11-26 08:05:05 +01:00
Aria ec6f6a1f1c Merge pull request 'Fix Sampler port trampling' (#1717) from Dayglo/strudel:fix-port-trampling into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1717
Reviewed-by: Aria <glossing@noreply.codeberg.org>
2025-11-26 07:39:27 +01:00
W-A-James a126bc9b86 Fix lint 2025-11-25 22:20:55 -05:00
Aria a5c7fcc136 Add back options code (will resolve in another PR) 2025-11-25 22:16:23 -05:00
Aria d70b758065 Remove base url from sample server and properly handle url schemes 2025-11-25 22:16:11 -05:00
W-A-James 9fdba6374c remove extraneous eslint ignore directive 2025-11-25 22:14:19 -05:00
W-A-James af90baea33 Fix trampling of port env variable 2025-11-25 22:14:15 -05:00
Aria 743e239e52 Merge pull request 'Feat: Hook up octave and fix example' (#1773) from glossing/hook-up-octave into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1773
2025-11-25 20:59:08 +01:00
Aria f44a96a5ce Merge branch 'main' into glossing/hook-up-octave 2025-11-25 20:50:49 +01:00
Aria b94a686394 Merge pull request '[perf] fix connect-leak in tremolo param' (#1780) from jeromew/strudel:fix-perf8 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1780
Reviewed-by: Aria <glossing@noreply.codeberg.org>
2025-11-25 20:48:07 +01:00
Aria 728552b669 Merge branch 'main' into fix-perf8 2025-11-25 20:30:18 +01:00
Aria 8b427bc1dd Add oct synonym 2025-11-25 13:19:39 -06:00
Aria ae47d767ce Merge branch 'main' into glossing/hook-up-octave 2025-11-25 20:02:43 +01:00
jeromew ab64451d65 [hydra] return the hydra object when await initHydra(..) is called 2025-11-25 10:49:30 +00:00
jeromew 89da9bbdb0 [perf] in noise, let noiseMix do the disconnect when it exists 2025-11-25 10:31:53 +00:00
Aria 2bc208d8f5 Use errorLogger for query and tonal errors 2025-11-24 19:05:42 -06:00
Switch Angel AKA Jade Rose cfeff26d42 Merge pull request 'filter modulation improvements!' (#1769) from filter_mod_improvements into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1769
2025-11-24 17:51:19 +01:00
jeromew e60ecba393 Merge branch 'fix-perf8' of https://codeberg.org/jeromew/strudel into fix-perf8 2025-11-24 16:36:14 +00:00
jeromew f0d49e0e22 [perf] fix connect-leak from audioworklet=>param in lfo/tremolo 2025-11-24 16:35:48 +00:00
Jade (Rose) Rowland 7ee6b933cc add tests 2025-11-24 11:33:21 -05:00
jeromew a3947d1ccf [perf] fix connect-leak from audioworklet=>param in lfo/tremolo 2025-11-24 16:33:20 +00:00
Jade (Rose) Rowland e86850b6d8 frequency mod 2025-11-24 11:19:49 -05:00
jeromew 89a6efb69e [perf] level 5 connect-leak on vowel 2025-11-24 16:10:00 +00:00
Jade (Rose) Rowland 31c694b596 Merge branch 'main' into filter_mod_improvements 2025-11-24 10:44:11 -05:00
Jade (Rose) Rowland 7a4babf6e1 format 2025-11-24 10:40:16 -05:00
Jade (Rose) Rowland d5e5ab83ac fix style 2025-11-24 10:39:37 -05:00
Jade (Rose) Rowland 20016fd9aa update hash 2025-11-23 20:25:13 -05:00
Switch Angel AKA Jade Rose 132a124644 Merge pull request 'Add o as a synonym for orbit' (#1766) from jade/orbitsynonym into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1766
Reviewed-by: Aria <glossing@noreply.codeberg.org>
2025-11-24 01:20:52 +01:00
Jade (Rose) Rowland 4caacb0de1 only eval if script 2025-11-23 19:12:28 -05:00
Jade (Rose) Rowland 8325b4cfc4 Merge branch 'main' into jade/orbitsynonym 2025-11-23 19:06:16 -05:00
Jade (Rose) Rowland 3b2df0b725 synonym: 2025-11-23 19:05:57 -05:00
Jade (Rose) Rowland f80fdbc38a Merge branch 'pre_eval' of ssh://codeberg.org/uzu/strudel into pre_eval 2025-11-23 18:59:26 -05:00
Jade (Rose) Rowland 3a7b237765 change variable name 2025-11-23 18:59:16 -05:00
Switch Angel AKA Jade Rose f5af55b63b Merge branch 'main' into pre_eval 2025-11-24 00:47:37 +01:00
Jade (Rose) Rowland d5dfbd7aa4 cleanup 2025-11-23 18:45:13 -05:00
Jade (Rose) Rowland 6221099af8 working 2025-11-23 18:09:38 -05:00
froos 22f1512697 Merge pull request '[perf] fix connect-leak in fm modulation' (#1758) from jeromew/strudel:fix-perf6 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1758
2025-11-23 22:23:24 +01:00
Aria 6c0d550b6b Fix example 2025-11-23 12:25:40 -06:00
Aria cf12e3e1d8 Hook up the octave control 2025-11-23 12:21:55 -06:00
froos 9a05a679aa Merge pull request 'Fix interoperability issue between all and await initHydra()' (#1663) from jeromew/strudel:jeromew-hydra-all into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1663
2025-11-23 09:47:22 +01:00
Switch Angel AKA Jade Rose 321e76166a Merge branch 'main' into filter_mod_improvements 2025-11-23 06:35:22 +01:00
Jade (Rose) Rowland bde17594ce test 2025-11-23 00:33:52 -05:00
Jade (Rose) Rowland 7bbb653963 fixed 2025-11-22 23:42:20 -05:00
Jade (Rose) Rowland 30e672a027 wip 2025-11-22 22:49:23 -05:00
Aria f7f3aa7668 Merge pull request 'Bug fix: Swap l/r gains with temp variable' (#1768) from glossing/stereo-bug into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1768
2025-11-23 04:23:04 +01:00
Aria adc3a9ac6c Swap with temp variable 2025-11-22 20:51:30 -06:00
Switch Angel AKA Jade Rose 2518b2aed2 Merge pull request 'FIX: prevent LFO filter modulation from popping from negative values' (#1767) from lpmod_improvements into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1767
2025-11-23 00:52:27 +01:00
Jade (Rose) Rowland 1d5f3a4f30 prevent filter modulation pops 2025-11-22 18:21:24 -05:00
Jade (Rose) Rowland 2efd56e331 add synonym 2025-11-22 18:10:28 -05:00
Switch Angel AKA Jade Rose 5fa6cb4653 Merge pull request 'prefix "S" for solo' (#1481) from solo into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1481
2025-11-23 00:03:31 +01:00
Jade (Rose) Rowland 19fd0fc649 rm unessecary file 2025-11-22 17:17:41 -05:00
Jade (Rose) Rowland e72e26eb81 add comment 2025-11-22 17:11:17 -05:00
Jade (Rose) Rowland 1c4b05d55f solo search n = 1 2025-11-22 16:40:31 -05:00
jeromew d453c861d3 Merge branch 'fix-perf6' of https://codeberg.org/jeromew/strudel into fix-perf6 2025-11-21 09:01:02 +00:00
jeromew f637b8b0dd [perf] fix connect-leak in fm modulation 2025-11-21 09:00:17 +00:00
Aria 06bf17f444 Merge pull request 'Bug Fix: Use frac due to negative frequencies from FM' (#1759) from glossing/droponelettertosaveears into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1759
2025-11-21 01:56:10 +01:00
Aria 21543956c6 Also for wavetable 2025-11-20 18:30:53 -06:00
Aria 4758effdc6 Use frac due to negative frequencies from FM 2025-11-20 18:22:44 -06:00
froos 6f58e74bea Merge pull request '[perf] fix connect-leak added by #1742 when noise() is not used' (#1757) from jeromew/strudel:fix-perf5 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1757
2025-11-20 19:48:15 +01:00
jeromew 907f03f3bf [perf] fix connect-leak in fm modulation 2025-11-20 17:40:51 +00:00
jeromew 0435711051 [perf] fix connect-leak added by #1742 when noise() is not used 2025-11-20 17:36:00 +00:00
froos 8f6718b9e8 Merge pull request '[perf] fix connect-leak in delay effect' (#1755) from jeromew/strudel:fix-perf4 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1755
2025-11-20 14:50:59 +01:00
froos aa77255d98 Merge pull request '[perf] fix connect leak when .noise() is in the mix' (#1742) from jeromew/strudel:fix-perf3 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1742
2025-11-20 14:49:40 +01:00
jeromew 7267990e20 Fix codeformat 2025-11-20 13:35:37 +00:00
jeromew 387e5db0fe Merge branch 'fix-perf3' of https://codeberg.org/jeromew/strudel into fix-perf3 2025-11-20 13:24:03 +00:00
jeromew d9f63b8dfa [perf] fix connect leak when .noise() is in the mix 2025-11-20 13:21:09 +00:00
jeromew 8ed72b4573 [perf] fix connect-leak in delay effect 2025-11-20 13:05:41 +00:00
Aria 74488f8480 Stray log message 2025-11-19 18:41:56 -06:00
Aria 8144ec46bd Attempt system for sounds tab previews 2025-11-19 18:35:35 -06:00
Felix Roos dd2c808cd7 hotfix: reduce sounds-tab click to play latency 2025-11-19 21:55:35 +01:00
Alex McLean 389a63a850 Merge pull request 'wchooseCycles has now notes in an example' (#1748) from scrappy_fiddler/strudel:documentation into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1748
2025-11-18 23:52:43 +01:00
scrappy_fiddler 99beb4454d update snapshot with note example 2025-11-17 20:49:26 +01:00
scrappy_fiddler 30cc46ea66 wchooseCycles has now notes in an example 2025-11-17 20:04:40 +01:00
Aria ea7dc7a732 Merge pull request 'Feature: Partials' (#1659) from glossing/strudel:glossing/partials into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1659
2025-11-17 05:31:53 +01:00
Aria 641bd2f356 Properly patternify everything 2025-11-16 22:20:17 -06:00
Aria f5ed6dbe79 Top level functions 2025-11-16 20:28:22 -06:00
Aria 7a3bea6f90 Working version 2025-11-16 19:38:59 -06:00
Aria cc10122b71 Testing out approach to allow list patterns in partials 2025-11-16 19:20:42 -06:00
Aria b66c0d18b3 Merge branch 'main' into glossing/partials 2025-11-16 18:44:04 -06:00
froos 0331c91b80 Merge pull request 'README: update superdough documentation' (#1741) from TristanMlct/strudel:update-readme-superdough into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1741
2025-11-16 22:06:12 +01:00
Aria 4d5ebfc204 Merge pull request 'Feature: Envelope worklet' (#1524) from glossing/strudel:glossing/snappy-envs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1524
2025-11-16 22:00:42 +01:00
Aria 921cb17d33 Merge branch 'main' into glossing/snappy-envs 2025-11-16 14:39:40 -06:00
Aria 832fd04a3d Merge pull request 'Worklet optimizations' (#1730) from glossing/optimizations into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1730
Reviewed-by: Switch Angel AKA Jade Rose <daslyfe@noreply.codeberg.org>
2025-11-16 21:08:53 +01:00
Aria d9c21489f8 Merge branch 'main' into glossing/optimizations 2025-11-16 20:13:31 +01:00
Aria 3962558c09 Some final tweaks 2025-11-16 13:12:47 -06:00
jeromew bfa8fa3c75 [perf] fix connect leak when .noise() is in the mix 2025-11-16 09:23:11 +00:00
Tristan Mlct 91799f2014 README: update superdough documentation 2025-11-15 15:06:44 +01:00
froos f6bb3869de Merge pull request '[perf] disconnect Offline AudioNode connections in generateReverb' (#1740) from jeromew/strudel:fix-perf2 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1740
2025-11-15 14:59:28 +01:00
froos b9e9c9e9e6 Merge pull request '[perf] disconnect send effect AudioNode when room is used' (#1736) from jeromew/strudel:fix-perf1 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1736
2025-11-15 14:57:46 +01:00
jeromew 92d87093e0 [perf] disconnect Offline AudioNode connections in generateReverb 2025-11-15 13:39:24 +00:00
jeromew 3e05525dc2 [perf] disconnect send effect AudioNode when room is used 2025-11-14 22:06:39 +00:00
Aria 22807b3833 Use weak references for sound sources 2025-11-14 16:05:09 -06:00
froos 0960741156 Merge pull request '[supradough] fix: unstable filter' (#1593) from supradough-filter-fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1593
2025-11-12 21:22:33 +01:00
Alex McLean bbb977afcc Merge pull request 'Fix link syntax in project-start' (#1724) from Kissaki/strudel:kissaki-patch-1 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1724
2025-11-12 21:12:06 +01:00
Alex McLean 81a4981a68 Merge pull request 'Fix web README sample code' (#1725) from Kissaki/strudel:kissaki-patch-2 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1725
2025-11-12 21:06:12 +01:00
Alex McLean 90d82c8988 Merge pull request 'Fix typo: 'studel' -> 'strudel'' (#1726) from drhayes/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1726
2025-11-12 20:59:39 +01:00
Aria 2875814a36 Square instead of self-multiply; more voices; more cleanup 2025-11-12 13:56:14 -06:00
Aria 157b05536a Final cleanup 2025-11-12 13:36:01 -06:00
Aria f8c9425569 Simplify spread 2025-11-12 13:13:37 -06:00
Aria be3a88b09c Optimize phase wrap; make voices k-rate 2025-11-12 12:23:40 -06:00
Aria aabf122161 Further optimizations, including detuner 2025-11-12 11:53:38 -06:00
Aria 3c6de3be6d First pass of worklets optimizations 2025-11-12 10:48:20 -06:00
froos 15f7b7c1a2 Merge pull request 'fix for node 24 support in tests - #1718' (#1719) from ausav/strudel:ausav.nodever into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1719
2025-11-12 15:23:36 +01:00
froos b6e382508f Merge pull request 'soundAlias example fix' (#1720) from PepsiiMan/strudel:docfix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1720
2025-11-11 09:00:47 +01:00
drhayes 3e7ae88043 Fix typo: 'studel' -> 'strudel' 2025-11-10 17:02:55 +01:00
froos 897a2016d0 Merge pull request 'fix: Make Supradough package build' (#1711) from munshkr/strudel:fix/1702-supradough-build into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1711
2025-11-10 08:18:35 +01:00
froos 7a1c1a75b1 Merge pull request 'added docs for spaces in scale names' (#1715) from ondras/strudel:scale-docs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1715
2025-11-10 08:18:00 +01:00
ondras 7f06f11e96 Merge branch 'main' into scale-docs 2025-11-10 08:08:04 +01:00
Switch Angel AKA Jade Rose 5acb00795e Merge pull request 'Feature: LFOs for Filters' (#1636) from glossing/strudel:glossing/filter-lfo into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1636
2025-11-10 02:44:00 +01:00
Switch Angel AKA Jade Rose f4b68fa2e6 Merge branch 'main' into glossing/filter-lfo 2025-11-10 02:20:20 +01:00
PepsiiMan 1b37f1ed1d soundAlias example fix 2025-11-06 15:33:06 +01:00
ondras f8943dbc94 Merge branch 'main' into scale-docs 2025-11-06 08:16:34 +01:00
Ondřej Žára a3f82f90a9 test fix 2025-11-06 08:15:55 +01:00
Switch Angel AKA Jade Rose 670f75b7e8 Merge pull request 'improvement: sync midi messages to audio context clock' (#1708) from sync_midi_timing into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1708
2025-11-05 17:06:59 +01:00
Jade (Rose) Rowland 79875ebc50 fix branch 2025-11-05 01:26:36 -05:00
Switch Angel AKA Jade Rose a1bd905c06 Merge branch 'main' into sync_midi_timing 2025-11-05 07:25:06 +01:00
Alex Victoria 247bf9b930 fix for node 24 support #1718 2025-11-04 20:54:21 -06:00
froos 8f24ce223f Merge pull request 'Add stick button mappings to gamepad implementation and improve docs' (#1696) from kaosuryoko/strudel:gamepad-stick-button into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1696
2025-11-04 19:36:13 +01:00
froos 51648d3e6e Merge pull request 'Refactor sound stopping and triggering logic in SoundsTab component' (#1688) from IJOL/strudel:bugfix/multi-sample-loading into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1688
2025-11-04 19:29:34 +01:00
froos d4e67e1c20 Merge pull request 'Add setting to toggle pattern auto-start on pattern change' (#1690) from moumar/strudel:pattern-auto-start-setting into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1690
2025-11-04 19:13:20 +01:00
froos edf4739cc6 Merge pull request 'Voicings JSDoc' (#1686) from sharkeys_lunchbox/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1686
2025-11-04 18:58:50 +01:00
froos 3e636bd6f0 Merge branch 'main' into main 2025-11-04 18:55:34 +01:00
Ondřej Žára 4df2cc3086 added docs for spaces in scale names 2025-11-03 22:03:39 +01:00
moumar 809eb7132c codeformat 2025-11-02 15:20:31 +01:00
froos dd6429007e Merge branch 'main' into bugfix/multi-sample-loading 2025-11-02 14:56:30 +01:00
froos e211d5a6bb Merge branch 'main' into pattern-auto-start-setting 2025-11-02 14:44:31 +01:00
Damián Silvani 404d9999f3 fix: Update github.com links 2025-11-02 09:21:40 -03:00
Damián Silvani bc1c83ed64 fix: Add missing vite.config.js to supradough package 2025-11-02 09:21:25 -03:00
Jade (Rose) Rowland 91e1cc136e working 2025-11-01 23:34:10 -04:00
Switch Angel AKA Jade Rose 92e3a80e65 Merge pull request 'FIX: Loading local samples uses too much memory' (#1706) from jade/fix_sample_loading into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1706
2025-11-01 07:50:39 +01:00
Jade (Rose) Rowland 4f046ae883 cf 2025-11-01 02:40:40 -04:00
Jade (Rose) Rowland 2b575d50b9 temporarily disable sample autocomplete 2025-11-01 02:39:43 -04:00
Jade (Rose) Rowland 7b92874909 fix broken array 2025-11-01 01:55:05 -04:00
Jade (Rose) Rowland 10bfe32a2d rm console 2025-11-01 01:48:42 -04:00
Jade (Rose) Rowland a41b07c67a fixed build 2025-11-01 01:44:32 -04:00
Jade (Rose) Rowland 20027d7208 Merge branch 'jade/fix_sample_loading' of ssh://codeberg.org/uzu/strudel into jade/fix_sample_loading 2025-11-01 01:32:35 -04:00
Jade (Rose) Rowland f38792c569 format 2025-11-01 01:32:24 -04:00
Switch Angel AKA Jade Rose 2c2bfd7070 Merge branch 'main' into jade/fix_sample_loading 2025-11-01 06:31:25 +01:00
Jade (Rose) Rowland 484c5e6cdd working 2025-11-01 01:26:25 -04:00
Jason Dufair a036c4f921 Merge remote-tracking branch 'upstream/main' 2025-10-30 18:57:36 -04:00
froos 4c35f6a77b Merge pull request 'Bug Fix: Handle scale-for-notes when n also supplied' (#1625) from glossing/strudel:glossing/scale-with-note-and-n into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1625
2025-10-28 22:58:24 +01:00
froos e92f4938cd Merge branch 'main' into glossing/scale-with-note-and-n 2025-10-28 22:54:34 +01:00
froos 12204c9053 Merge pull request 'Repurpose vim shortcuts for usability' (#1624) from dtricks/strudel:feat/vim-shortcuts into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1624
2025-10-28 22:51:15 +01:00
froos 980bf17059 Merge branch 'main' into feat/vim-shortcuts 2025-10-28 22:38:04 +01:00
froos f2009a33aa Merge pull request 'Add support for euclidian in mondo with bd&3:8' (#1630) from TristanCacqueray/strudel:mondian into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1630
2025-10-28 22:35:19 +01:00
froos a65a68b828 Merge branch 'main' into mondian 2025-10-28 22:20:41 +01:00
alex cbe7aaacfb Publish
- @strudel/codemirror@1.2.6
 - @strudel/core@1.2.5
 - @strudel/csound@1.2.6
 - @strudel/draw@1.2.5
 - @strudel/gamepad@1.2.5
 - @strudel/hydra@1.2.5
 - @strudel/midi@1.2.6
 - @strudel/mini@1.2.5
 - @strudel/mondo@1.1.5
 - @strudel/motion@1.2.5
 - @strudel/mqtt@1.2.5
 - @strudel/osc@1.3.0
 - @strudel/repl@1.2.7
 - @strudel/serial@1.2.5
 - @strudel/soundfonts@1.2.6
 - superdough@1.2.6
 - supradough@1.2.4
 - @strudel/tonal@1.2.5
 - @strudel/transpiler@1.2.5
 - @strudel/web@1.2.6
 - @strudel/webaudio@1.2.6
 - @strudel/xen@1.2.5
2025-10-27 15:38:32 +00:00
Jason Dufair c0e7971be7 Merge remote-tracking branch 'upstream/main' 2025-10-27 09:26:47 -04:00
Alex McLean 35014b5190 Merge pull request 'Use bunny cdn for all samples and json files' (#1701) from bunny-cdn into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1701
2025-10-27 10:42:06 +01:00
alex 9ff0449ca3 use json files in dough-samples via bunny cdn 2025-10-27 09:24:17 +00:00
kaosuryoko b491c42af5 Merge branch 'main' into gamepad-stick-button 2025-10-27 00:36:50 +01:00
alex 8398385dcd piano via bunnycdn 2025-10-26 22:18:37 +00:00
Alex McLean 629c2b9301 Merge pull request 'degithub - switch some samples to bunnycdn' (#1697) from bunny-cdn into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1697
2025-10-26 22:52:53 +01:00
alex 5cc93996ce degithub 2025-10-26 21:49:10 +00:00
Greg Ervin ee43d85f29 Add stick button mappings to gamepad implementation and improve docs 2025-10-26 13:31:13 -05:00
Alex McLean 7cef558440 Merge pull request 'Fix ZZFX example' (#1685) from moumar/strudel:fix-zzfx-example into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1685
2025-10-26 17:09:20 +01:00
Alex McLean c261fb6dd6 Merge branch 'main' into fix-zzfx-example 2025-10-26 17:08:56 +01:00
moumar fcd83cce55 Re-evaluate pattern if activated 2025-10-26 14:07:36 +01:00
moumar fddf2ff2a7 Set pattern auto-start to true by default 2025-10-26 14:06:31 +01:00
Jason Dufair 6d4787ee33 Test snapshot 2025-10-24 09:21:04 -04:00
Jason Dufair 1a362bd8ea Fix example 2025-10-24 09:19:02 -04:00
moumar 28056deb66 Fix ZZFX example
ZZFX example did not produce any sound out of the box, fixed by updating
tremolo param.
2025-10-23 23:15:53 +02:00
moumar 20bbda30f8 Add setting to toggle pattern auto-start on pattern change 2025-10-23 23:15:21 +02:00
Ignacio Ortega a8c613b7eb Refactor sound stopping and triggering logic in SoundsTab component 2025-10-23 19:04:29 +02:00
Jason Dufair fd0a910bfb Voicings JSDoc 2025-10-23 11:11:21 -04:00
yaxu 7071dec775 revert 39e2221c29
revert add _processParts()
2025-10-23 16:07:42 +02:00
Alex McLean 3d55567445 Merge branch 'main' of ssh://codeberg.org/uzu/strudel 2025-10-23 15:05:50 +01:00
Alex McLean 39e2221c29 add _processParts() 2025-10-23 15:05:39 +01:00
Alex McLean b8ce920f50 Merge pull request 'Make osc port and host configurable. Changes dependencies.' (#1682) from multiosc into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1682
2025-10-23 15:56:00 +02:00
yaxu ccb5ec1685 Add action to deploy 'beta' version to warm.strudel.cc 2025-10-23 10:41:29 +02:00
yaxu b09230fe90 Update .forgejo/workflows/deploy.yml 2025-10-23 10:40:08 +02:00
yaxu 8f7b439dce Move deploy location to deploy/strudel.cc 2025-10-23 10:39:26 +02:00
Aria c823a37c93 Merge pull request 'Adds back shape to superdough' (#1672) from glossing/strudel:glossing/return-shape-to-sd into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1672
2025-10-23 03:47:30 +02:00
Aria 0c9f535ff5 Merge branch 'main' into glossing/return-shape-to-sd 2025-10-23 03:44:44 +02:00
froos fb5884ab81 Merge pull request 'Fix sampler.mjs githubPath' (#1684) from jeromew/strudel:fix-githubpath into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1684
2025-10-22 22:15:17 +02:00
jeromew ea91d30ce4 Add semicolon 2025-10-22 21:52:53 +02:00
jeromew f0bd5bffaa Fix sampler.mjs githubPath
PR https://codeberg.org/uzu/strudel/pulls/1679 introduced a subtle bug when both other and subpath are empty. The url should not end with '//'
2025-10-22 21:39:08 +02:00
Alex McLean ac37b6a029 Merge pull request 'Fix onPaint for widgets' (#1658) from fix-onpaint into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1658
2025-10-22 16:20:49 +02:00
froos a986688874 Merge pull request 'Fix a bug introduced by #4e17cfbdd6' (#1679) from milliganf/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1679
2025-10-22 15:19:11 +02:00
froos 4ab8c256b2 Merge branch 'main' into main 2025-10-22 15:18:53 +02:00
alex 06ea5a36e2 Make osc port and host configurable. Changes dependencies. 2025-10-22 10:15:48 +01:00
froos 98f5a56ef6 Merge pull request 'feature/autocomplete-sound-names' (#1564) from drdozer/strudel:feature/autocomplete-sound-names into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1564
2025-10-20 22:37:14 +02:00
milliganf 46af6ed9ef Fix a bug introduced by #4e17cfbdd6
When there's no subpath for a Github path, the URL should end with a '/'. Otherwise when we concatenate values on from a sample map it doesn't separate the base URL from the filename.

Signed-off-by: milliganf <m.h.freeson@outlook.com>
2025-10-20 22:30:58 +02:00
froos 752940b3ab Merge branch 'main' into feature/autocomplete-sound-names 2025-10-20 22:17:45 +02:00
froos c727534d29 Merge pull request 'add replicate + use it for ! in mondo' (#1436) from JoStro/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1436
2025-10-20 21:58:11 +02:00
Felix Roos 20e5fdedfb fix: use replicate for ! in mondo 2025-10-20 21:38:43 +02:00
Felix Roos 188f0e9098 snapshot 2025-10-20 21:35:28 +02:00
Felix Roos c2a5562bad add new flavour under replicate 2025-10-20 21:24:13 +02:00
froos 57f2a97b7b Merge branch 'main' into main 2025-10-20 21:21:08 +02:00
Aria 72423c3a25 Adds back shape to superdough 2025-10-18 10:41:33 -05:00
Felix Roos 105db2a4ca fix: tonal import in autocomplete + add tonal to codemirror package dependencies 2025-10-18 08:49:31 +02:00
froos be61d08cc6 Merge branch 'main' into feature/autocomplete-sound-names 2025-10-18 08:41:15 +02:00
Switch Angel AKA Jade Rose 6e58c973af Merge pull request 'Bug Fix: Wavetable: phase wrapping at 1 and detune' (#1620) from glossing/strudel:glossing/wrap-phase-typo into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1620
2025-10-18 04:29:19 +02:00
Switch Angel AKA Jade Rose 2473a5391d Merge branch 'main' into glossing/wrap-phase-typo 2025-10-17 04:55:25 +02:00
Aria c02def03b6 Don't tag when scale fails, allow mixed sharps and flats, simplify scale function 2025-10-16 16:26:33 -05:00
froos 4e17cfbdd6 Merge pull request 'github samples: default to "samples" if repository is not specified' (#1644) from prezmop/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1644
2025-10-16 20:26:26 +02:00
prezmop 83c7e63432 update docs 2025-10-16 18:20:22 +02:00
prezmop 909e0154fe default to samples if repo not found 2025-10-16 18:20:13 +02:00
froos ef4e21ac40 Merge pull request 'Docs: add example of custom chained function' (#1642) from dariusk/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1642
2025-10-16 17:33:39 +02:00
froos 857c82e260 Merge branch 'main' into main 2025-10-16 17:25:07 +02:00
froos 0d5dc1f8c1 Merge branch 'main' into glossing/scale-with-note-and-n 2025-10-16 17:19:07 +02:00
jeromew 47c85f8540 fix interoperability of all with hydra
Hydra add methods on Array.prototype which breaks the Array enumeration with for...in.
Use for...of instead
cf https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of#difference_between_for...of_and_for...in for details on the issue

without this fix, it is impossible to use both `await initHydra()` and `all` in the same strudel code. Strudel errors with "Error: got "undefined" instead of pattern."

example code: https://strudel.cc/#YXdhaXQgaW5pdEh5ZHJhKCkKCmFsbChwaWFub3JvbGwpCgokOiBzKCJiZCIp
2025-10-16 14:06:33 +02:00
Aria fd03d1fdfb Update conditional to allow to control user waveforms 2025-10-15 15:14:31 -05:00
Aria 35016b5847 Mention range in docstring 2025-10-15 14:56:20 -05:00
Aria 0fe7edc157 Some examples 2025-10-15 14:53:07 -05:00
Aria d6de160224 Merge branch 'main' into glossing/partials 2025-10-15 13:19:57 -05:00
alex ba93bd1bc5 Fix onPaint for widgets 2025-10-15 18:54:02 +01:00
Aria 3c1a8c8bdb Format 2025-10-14 11:41:11 -05:00
Aria b7e941c649 Merge branch 'main' into glossing/wrap-phase-typo 2025-10-14 11:40:29 -05:00
Aria ac7e9c6978 Typos on envelope values 2025-10-14 11:37:41 -05:00
Aria c13a726129 Merge branch 'main' into glossing/filter-lfo 2025-10-14 11:24:49 -05:00
Switch Angel AKA Jade Rose ded91fab7d Merge pull request 'Feature: Distortion Modes' (#1561) from glossing/strudel:glossing/distortion-modes into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1561
2025-10-13 07:26:50 +02:00
Switch Angel AKA Jade Rose f40d079543 Merge pull request 'Feature: Wavetable FM' (#1623) from glossing/strudel:glossing/wavetable-fm into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1623
2025-10-13 07:05:56 +02:00
Switch Angel AKA Jade Rose d5833dd475 Merge branch 'main' into glossing/wavetable-fm 2025-10-13 07:05:24 +02:00
Switch Angel AKA Jade Rose 99b3c04a76 Merge branch 'main' into glossing/filter-lfo 2025-10-13 07:04:51 +02:00
Switch Angel AKA Jade Rose e3658ec30e Merge pull request 'fix: repair REPL sample sources and URL concat (#1640)' (#1646) from erikfox/strudel:fix-repl-sample-sources-1640 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1646
2025-10-12 20:43:41 +02:00
Erik Fox 72eaf96b4f fix: repair REPL sample sources and URL concat (#1640) 2025-10-12 12:30:36 -04:00
Darius Kazemi 9c5c71c31a Removing semicolon to be more idiomatic;;; 2025-10-11 17:52:26 -07:00
Darius Kazemi af53bab259 Fixing some syntax and a typo 2025-10-11 17:50:58 -07:00
Darius Kazemi 0065db8569 Docs: add example of custom chained function
This adds a subsection to the "Understand/Coding Syntax" documentation that shows a simple example of converting the previous chain of effects into a custom, reusable chained function. There's also a prompt for the reader to experiment.
2025-10-11 17:40:55 -07:00
Alex McLean 7eebf3a466 Merge pull request 'Add control-metadata' (#1634) from control-metadata into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1634
2025-10-10 11:25:00 +02:00
Aria 57fac57677 Remove todo 2025-10-09 22:23:28 -05:00
Aria a3b803c3f3 First pass 2025-10-09 18:28:59 -05:00
Aria 6f703e48e7 Merge branch 'main' into glossing/wrap-phase-typo 2025-10-08 16:44:35 -05:00
Alex McLean 64f16c4f33 placate format gods 2025-10-08 15:35:34 +01:00
Alex McLean ffbbc43c89 add cyclist version to the query metadata 2025-10-08 15:34:51 +01:00
Alex McLean 086596c47c fix setControls to do union with existing controls 2025-10-08 14:52:00 +01:00
Alex McLean fdde05e751 add pattern id to query state controls 2025-10-08 14:51:21 +01:00
Alex McLean 694162a7b1 fix purity of 'withState' so it returns a new pattern 2025-10-08 14:13:33 +01:00
Felix Roos 319a2d7289 fix: tests 2025-10-07 23:05:00 +02:00
Felix Roos 3a7d50924d refactor: add op_precedence to express & with desugar_ops 2025-10-07 22:59:50 +02:00
Alex McLean cbcb25b063 Merge pull request 'Fix MQTT: change the trigger handler to match new hap' (#1629) from jurrchen/strudel:bugfix/mqtt-hap-sig into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1629
2025-10-07 22:59:38 +02:00
Felix Roos f33db5f07f - refactor bjork -> bjorklund
- refactor e -> bjork
- flip & desugared arguments
2025-10-07 22:35:29 +02:00
Tristan de Cacqueray b7fa440bda mondough: interpret & 2025-10-07 18:10:24 +02:00
Tristan de Cacqueray 3dc9b3524c mondo: add & sugar 2025-10-07 17:19:27 +02:00
dtricks f48f8e46e5 fix empty catches 2025-10-06 15:53:44 +02:00
dtricks a79fc3db5b remove unnecessary comments 2025-10-06 15:53:44 +02:00
dtricks 30a2e14959 weird permission change 2025-10-06 15:53:44 +02:00
dtricks cfe17fcba5 wrong unicode char 2025-10-06 15:53:44 +02:00
dtricks 46ba165c7e remove gcc 2025-10-06 15:53:44 +02:00
dtricks 73a9834959 codeformat 2025-10-06 15:53:44 +02:00
dtricks be9368e8a2 feat(codemirror): add Vim :w evaluate, :q stop, and gc/gcc toggle comment via custom events; docs: add Vim shortcuts page and link from REPL manual 2025-10-06 15:53:44 +02:00
Aria 1ae773acc4 Merge branch 'main' into glossing/wrap-phase-typo 2025-10-05 18:21:05 -05:00
Aria 39a73f2499 Merge branch 'main' into glossing/scale-with-note-and-n 2025-10-05 18:20:30 -05:00
froos 53c504f260 Merge branch 'main' into feature/autocomplete-sound-names 2025-10-05 21:50:45 +02:00
Jieren Chen 0d08225983 change the trigger handler to match new hap 2025-10-05 21:45:23 +02:00
froos 00872bd1de Merge pull request 'Fix case sensitivity for synonym search' (#1618) from vvolhejn/strudel:vv/fix-case-sensitivity into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1618
2025-10-05 21:42:18 +02:00
Aria cc4452ce58 Working version of partials and phases 2025-10-04 20:33:25 -05:00
Aria 11dd8a318a Cleanup 2025-10-04 02:13:42 -05:00
Aria 3ef6c7c921 Handle scale-for-notes when n also supplied 2025-10-04 02:07:35 -05:00
Aria 120f89d57f Handle detune in the presence of pitch envelope 2025-10-04 01:37:57 -05:00
Aria d496919fe5 Import 2025-10-02 22:48:41 -05:00
Aria f369de13d7 Hook up FM to wavetables 2025-10-02 22:45:48 -05:00
Dr Mathew R Pocock 908a703b7f Merge branch 'main' into feature/autocomplete-sound-names 2025-10-02 10:32:16 +02:00
Aria 99e14dae5c Consistency 2025-10-02 01:12:42 -05:00
Aria ac582b4d40 Wrap properly when phase === 1 2025-10-02 01:06:29 -05:00
Václav Volhejn b7d98dd370 Fix case sensitivity for synonym search 2025-10-01 21:53:28 +02:00
Alex McLean 0ecacae71e format 2025-10-01 12:02:09 +01:00
yaxu 8cd497abb2 Update website/src/pages/technical-manual/project-start.mdx 2025-10-01 12:54:58 +02:00
Switch Angel AKA Jade Rose 070055d2ef Merge pull request 'Optimize wavetable synth' (#1613) from glossing/strudel:glossing/wavetable-optimizations into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1613
2025-10-01 08:56:33 +02:00
Switch Angel AKA Jade Rose e500ebe268 Merge branch 'main' into glossing/wavetable-optimizations 2025-10-01 08:54:53 +02:00
Aria 72298c5f03 Docstring cleanup 2025-10-01 00:45:26 -05:00
Aria c885d84785 Add comment 2025-10-01 00:39:44 -05:00
Aria a14bf4d97b Add aliases for distort types 2025-10-01 00:36:59 -05:00
Aria c5f1085bd9 WIP 2025-09-30 23:51:59 -05:00
Aria ccbf28f2db Merge branch 'main' into glossing/distortion-modes 2025-09-30 23:25:57 -05:00
Aria 3a7ec18d46 Move invsr to constructor 2025-09-30 23:19:55 -05:00
Aria a2b8407fbb Add back mipmaps; add caching 2025-09-30 23:08:28 -05:00
Switch Angel AKA Jade Rose ac1fd88d8d Merge pull request 'fix: pattern switching' (#1616) from fix_pattern_Switching into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1616
2025-10-01 02:02:30 +02:00
Jade (Rose) Rowland 9f8143e062 format 2025-09-30 19:46:53 -04:00
Jade (Rose) Rowland cdc5e5f3f3 working 2025-09-30 19:45:30 -04:00
Aria bad498a1e5 Remove unused var 2025-09-30 16:21:28 -07:00
Switch Angel AKA Jade Rose f4cf77f5c6 Merge branch 'main' into solo 2025-09-30 08:09:37 +02:00
Aria 4de0c11f8c Remove mipmaps, fix clamping of parameters 2025-09-29 14:15:50 -07:00
Dr Mathew R Pocock 97af50222f Merge branch 'main' into feature/autocomplete-sound-names 2025-09-29 18:10:12 +02:00
Switch Angel AKA Jade Rose f360d394a7 Merge pull request 'fix wavetable JSON' (#1611) from fix_JSON into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1611
2025-09-29 02:10:53 +02:00
Jade (Rose) Rowland 48718daea8 add_vgame 2025-09-28 19:57:19 -04:00
Jade (Rose) Rowland 8d45016ccf working 2025-09-28 19:11:13 -04:00
Switch Angel AKA Jade Rose ec69f6fd77 Merge pull request 'rename wavetable controls to match existing naming conventions' (#1610) from wavetable_controls into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1610
2025-09-28 21:28:15 +02:00
Jade (Rose) Rowland d94f145649 fix vib 2025-09-28 15:23:58 -04:00
Jade (Rose) Rowland bf3fe605d9 fix unison gain 2025-09-28 15:21:06 -04:00
Jade (Rose) Rowland 54ebc97ddc fix tests 2025-09-28 15:07:35 -04:00
Jade (Rose) Rowland c689874bb4 simplify modulators 2025-09-28 14:57:12 -04:00
Jade (Rose) Rowland 23d4bfa9d9 rename controls 2025-09-28 09:19:39 -04:00
Switch Angel AKA Jade Rose 91eb50f1c1 Merge pull request '<Warn: Don't merge until tables uploaded> Feature: Wavetable synth improvements' (#1607) from glossing/strudel:glossing/wavetable-synth into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1607
2025-09-27 23:28:12 +02:00
Aria 5838ade740 Merge branch 'main' into glossing/wavetable-synth 2025-09-27 13:17:05 -07:00
Aria 917d01c47d Add LFOs and envelopes 2025-09-27 12:48:37 -07:00
Switch Angel AKA Jade Rose 0a51718df3 Merge pull request 'wavetable improvements' (#1608) from wt_improvements into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1608
2025-09-27 21:43:09 +02:00
Jade (Rose) Rowland 774372a339 format 2025-09-27 15:29:55 -04:00
Jade (Rose) Rowland 626a99ba5b supports old documented way of loading wavetables: 2025-09-27 15:22:54 -04:00
Aria 8c3ee6db6b Missed deletion 2025-09-27 11:07:04 -07:00
Aria c5c05ebadc Clean up to align with other PR 2025-09-27 11:02:57 -07:00
Aria a8918d55fb Actually update normalizer 2025-09-27 10:45:30 -07:00
Aria 07e3ddbe40 Merge branch 'main' into glossing/distortion-modes 2025-09-27 10:25:51 -07:00
Jade (Rose) Rowland a9957b45e5 suppoert existing wt_ api 2025-09-27 13:22:21 -04:00
Jade (Rose) Rowland 4bd103e612 gain adj 2025-09-27 11:31:43 -04:00
Aria 466234bad6 Merge branch 'main' into glossing/wavetable-synth 2025-09-26 21:22:45 -07:00
Aria 97beaec25a Added examples, fixed samplerate issue on import, added to settings tab, fixed spread, added default wavetables, change default phaserand to 0 2025-09-26 20:46:10 -07:00
Jade (Rose) Rowland 2b3b638943 sample source for WT 2025-09-26 21:47:45 -05:00
Switch Angel AKA Jade Rose e1629e8383 Merge pull request 'create orbit based DJ filter' (#1603) from djf into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1603
2025-09-26 08:48:45 +02:00
Jade (Rose) Rowland 266cb614a8 add better example 2025-09-25 23:44:32 -07:00
Jade (Rose) Rowland dfc4a0818c res adjust 2025-09-25 23:39:08 -07:00
Jade (Rose) Rowland 236ff577a6 Merge branch 'main' into djf 2025-09-25 23:18:22 -07:00
Switch Angel AKA Jade Rose 66a32ac45d Merge pull request 'Feature: Wavetable synth' (#1525) from glossing/strudel:glossing/wavetable-synth into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1525
2025-09-26 08:15:48 +02:00
Aria 8b2c35b7a3 Cleanup 2025-09-25 01:10:41 -07:00
Aria f8f42565ae Typo - add back slight delay in cleanup 2025-09-25 00:40:25 -07:00
Aria 96981c3c1d A bit more cleanup 2025-09-25 00:36:28 -07:00
Aria bd8d207a3d Typo 2025-09-25 00:17:13 -07:00
Aria e3741ae8fa Add control over phase randomization and fix a bug with supersaw 2025-09-25 00:05:50 -07:00
Switch Angel AKA Jade Rose ef87f65bef Merge branch 'main' into glossing/wavetable-synth 2025-09-25 07:21:17 +02:00
Aria b3537a9acb Some cleanup 2025-09-24 22:17:30 -07:00
Aria d3d9f23c3c Remove unnecessary defaulting 2025-09-24 22:06:53 -07:00
Jade (Rose) Rowland c474b8c92e rm comment 2025-09-24 20:33:51 -07:00
Jade (Rose) Rowland da81ba00fb adjust control range 2025-09-24 20:32:45 -07:00
Aria f0669ce3ba Move algorithm into constructor 2025-09-24 17:32:03 -07:00
Jade (Rose) Rowland 28c94efaa9 format 2025-09-24 00:24:25 -07:00
Jade (Rose) Rowland 0633954e24 working 2025-09-24 00:23:05 -07:00
Switch Angel AKA Jade Rose cedf09168f Merge pull request 'fix: time signal' (#1583) from fix_time into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1583
2025-09-23 13:28:19 +02:00
Switch Angel AKA Jade Rose 3373225f53 Merge branch 'main' into fix_time 2025-09-23 13:13:51 +02:00
Jade (Rose) Rowland 1eb9dc73fa fix duck 2025-09-22 00:34:57 -04:00
Jade (Rose) Rowland fdcdc3aaa0 rm deadcode 2025-09-22 00:13:59 -04:00
Jade (Rose) Rowland da926805b3 rm dead code 2025-09-22 00:12:48 -04:00
Jade (Rose) Rowland 82893ffc22 fix import 2025-09-22 00:11:02 -04:00
Jade (Rose) Rowland beafd9c700 format 2025-09-22 00:07:06 -04:00
Jade (Rose) Rowland c5bd2a7487 working 2025-09-21 23:54:04 -04:00
Kissaki 4d39f36868 Fix web README sample code
Looks like the code was copied, and then the wrong string was replaced when creating this code sample.
2025-09-21 18:31:59 +02:00
Kissaki c64aa3bf01 Fix link syntax in project-start 2025-09-21 18:27:05 +02:00
Dr Mathew R Pocock b4d57f12fe Merge branch 'main' into feature/autocomplete-sound-names 2025-09-21 18:13:40 +02:00
froos 118086aff5 Merge pull request 'mondo: fix all + setcpm + setcps' (#1600) from mondo-fixes into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1600
2025-09-21 16:16:05 +02:00
Felix Roos 452827630b move unpure up 2025-09-21 16:13:09 +02:00
Felix Roos 86248328cc fix: mondo setcps / setcpm 2025-09-21 16:09:10 +02:00
Felix Roos 8833d623f7 fix: all function in mondo 2025-09-21 16:06:28 +02:00
froos 3fff4609a0 Merge pull request 'fix: osc error message' (#1597) from osc-debug-flag into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1597
2025-09-19 21:59:42 +02:00
Dr Mathew R Pocock 98a1573e27 Merge branch 'main' into feature/autocomplete-sound-names 2025-09-18 22:54:41 +02:00
Matthew Pocock 7080490c68 Merge handlers and cache regexes in autocomplete
- Merged scalePreColonHandler and scaleAfterColonHandler into single scaleHandler
- Merged modePreColonHandler and modeAfterColonHandler into single modeHandler
- Cached all regex patterns as constants above their respective handler functions
- Eliminated regex compilation overhead on every keystroke/navigation event
- Improved code maintainability by reducing duplication between similar handlers
- Preserved exact same functionality while improving performance

Each handler now follows a consistent pattern of checking after-colon context
first (more specific) then pre-colon context, with all regex patterns
pre-compiled for better performance during intensive autocomplete sessions.
2025-09-18 21:52:40 +01:00
froos e2b5b92a0c Merge pull request 'osc: --debug flag to see incoming messages' (#1595) from osc-debug-flag into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1595
2025-09-18 21:09:55 +02:00
Felix Roos 1c3e07afd3 fix: rephrase error message + bump to 1.2.10 2025-09-18 20:40:25 +02:00
Felix Roos c54fa7d266 chore: bump osc to 1.2.8 2025-09-18 20:39:05 +02:00
Felix Roos a25f763796 docs: add --debug flag to readme 2025-09-18 20:37:19 +02:00
Felix Roos 91ad3d729f feat: osc add --debug flag to log messages + log errors 2025-09-18 20:36:40 +02:00
Felix Roos a0c2edad30 fix: add clamp function 2025-09-18 10:54:41 +02:00
Felix Roos 82c9047195 fix: unstable filter 2025-09-18 10:47:23 +02:00
Matthew Pocock f51ad2cb7e Merge branch 'main' into feature/autocomplete-sound-names 2025-09-17 23:08:06 +01:00
froos 4cb0488839 Merge pull request 'simplify osc usage' (#1588) from osc-server-via-npx into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1588
2025-09-17 14:12:09 +02:00
Felix Roos 663229ecde bump osc to 1.2.8 2025-09-17 13:50:02 +02:00
Felix Roos cdb623cb66 overhaul osc readme 2025-09-17 13:39:35 +02:00
Felix Roos 28d7e0e489 export osc function to be able to do all(osc) 2025-09-17 13:39:28 +02:00
Felix Roos aba594b72a fix: bin field 2025-09-17 12:52:42 +02:00
Felix Roos 5d7c6c3e4b fix: add node shebang + bump osc to 1.2.6 2025-09-17 12:47:41 +02:00
Felix Roos 820744dc27 bump osc to 1.2.5 2025-09-17 12:42:11 +02:00
Felix Roos 5445f0812f configurable port for osc bridge + add bin field 2025-09-17 12:41:46 +02:00
Dr Mathew R Pocock 49868fc119 Merge branch 'main' into feature/autocomplete-sound-names 2025-09-16 21:00:40 +02:00
Jade (Rose) Rowland a68c784753 rm unessecary change 2025-09-16 11:55:31 -04:00
Jade (Rose) Rowland f2b2f9f959 format 2025-09-16 11:53:19 -04:00
Jade (Rose) Rowland 53dd53553a Merge branch 'main' into fix_time 2025-09-16 11:52:54 -04:00
Jade (Rose) Rowland 89b7eac789 fix test 2025-09-16 11:52:36 -04:00
Jade (Rose) Rowland 9cdba1a50e Working 2025-09-16 11:36:21 -04:00
Switch Angel AKA Jade Rose 98b7003504 Merge pull request 'dev: logger errors to console in dev mode' (#1585) from devlog into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1585
2025-09-16 16:59:57 +02:00
Matthew Pocock cb2dc6304f Merge branch 'feature/autocomplete-sound-names' of ssh://codeberg.org/drdozer/strudel into feature/autocomplete-sound-names 2025-09-16 14:51:50 +01:00
Matthew Pocock edb6c0db2e Reordered handlers to be a bit more organised. 2025-09-16 14:45:22 +01:00
Dr Mathew R Pocock a46a055246 Merge branch 'main' into feature/autocomplete-sound-names 2025-09-16 15:42:35 +02:00
Jade (Rose) Rowland 9a8f8a051c rm comment 2025-09-16 01:48:37 -04:00
Jade (Rose) Rowland 3eb40ee7d3 working in dev 2025-09-16 01:44:01 -04:00
Jade (Rose) Rowland 4cc453c640 fix test 2025-09-15 22:09:05 -04:00
Jade (Rose) Rowland 74e27ca94f coerce in correct place 2025-09-15 21:57:56 -04:00
Jade (Rose) Rowland 7dfd7dbcdc rmconsole 2025-09-15 21:47:40 -04:00
Jade (Rose) Rowland 2cba1dcb31 working 2025-09-15 21:46:37 -04:00
Switch Angel AKA Jade Rose 48a6a12648 Merge pull request 'Bug Fix / Feature: Updates to duck to avoid clicks and allow configuration over release/attack' (#1514) from glossing/strudel:glossing/ducking-click into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1514
2025-09-16 03:19:36 +02:00
Dr Mathew R Pocock 71dd45cd4b Merge branch 'main' into feature/autocomplete-sound-names 2025-09-16 02:13:26 +02:00
froos 6bbf7cbe32 Merge pull request 'Feat: Enhance scale function to quantize notes to a named scale' (#1492) from glossing/strudel:glossing/scale-for-notes into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1492
2025-09-15 23:36:30 +02:00
froos 9f36b73081 Merge pull request 'add tic80 font' (#1579) from tic80font into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1579
2025-09-15 22:52:13 +02:00
Matthew Pocock 6ebe84e096 Added completion for chord. 2025-09-15 21:34:15 +01:00
Felix Roos a7e485992e add tic80 font 2025-09-15 20:54:39 +02:00
Matthew Pocock cb011c8bb0 Add autocomplete for mode function
- Added modePreColonHandler for autocomplete of mode values (below, above, duck, root)
- Added modeAfterColonHandler for pitch name completion after colon in mode:anchor syntax
- Follows same pattern as sound/bank/scale handlers for complex expressions
- Supports expressions like '<above [below root]>' with proper fragment matching
- Uses regex pattern (?:[\s\[\{\()<])([\w:]*)$ to handle delimited expressions
2025-09-15 18:44:30 +01:00
drdozer c8875b29c1 Improve autocompletion behavior
- Refactored handlers to use consistent early return pattern for better readability
- Added blocking behavior for unquoted contexts (sound(, scale(, bank()) to guide users toward correct syntax instead of showing irrelevant fallback completions
- Fixed soundHandler to show all sounds when fragment is empty (e.g., sound(""))
- Simplified bankHandler by removing complex quote handling logic in favor of consistent blocking pattern
- Made scale handler regex patterns consistent (removed leading dots)
- All handlers now follow same structure: block unquoted contexts, provide completions for quoted contexts
2025-09-15 14:27:59 +01:00
drdozer 978616833a Merged autocomplete.mjs conflict. 2025-09-15 13:01:16 +01:00
Aria ab74ccfe7c Merge branch 'main' into glossing/ducking-click 2025-09-14 18:28:00 -05:00
Aria 92b2013cf6 Correctly handle silences for non-notes 2025-09-14 17:04:39 -05: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
drdozer c265595536 Merge branch 'feature/autocomplete-sound-names' of ssh://codeberg.org/drdozer/strudel into feature/autocomplete-sound-names 2025-09-13 21:36:48 +01:00
drdozer 1670aebad8 Updated the gitignore rules for vscode. 2025-09-13 21:36:24 +01:00
drdozer 39460eb100 Commiting lockfile. 2025-09-13 21:36:24 +01:00
drdozer cd659ac2a1 feat: sound autocomplete matches anywhere in name using includes() 2025-09-13 21:36:24 +01:00
drdozer ed182d4b3f Updated the codemirror packages to be able to access additional imports. 2025-09-13 21:36:24 +01:00
drdozer 90614612e2 feat: add E# and B# to pitchNames for complete enharmonic coverage 2025-09-13 21:36:24 +01:00
drdozer 6b6d4b86a6 refactor: modularize autocomplete context handlers using strategy array 2025-09-13 21:36:24 +01:00
drdozer 0b67473bd3 refactor: modularize autocomplete context handlers using strategy array 2025-09-13 21:36:24 +01:00
drdozer d87ce960d2 feat: implement context-aware autocomplete
Adds context-sensitive autocompletion for:
- Sound names in s("...") and sound("...") after the opening quote or after whitespace/bracket
- Bank names in bank("...") with quote handling and live filtering
- Scale types in .scale("...") after the colon, inserting colon-form for multi-word names
- Pitch names (C, C#, Db, etc.) for scale keys on explicit completion (Ctrl+Space) before the colon
- Blocks fallback completions in scale context before colon to avoid function list cross-talk
2025-09-13 21:36:24 +01:00
Aria 69b034349a Codeformat 2025-09-13 14:30:05 -05:00
Aria ed00686804 Codeformat 2025-09-13 14:29:28 -05:00
Aria e10c469749 Merge branch 'main' into glossing/ducking-click 2025-09-13 14:28:35 -05:00
Aria 5f7e10733a Merge branch 'main' into glossing/distortion-modes 2025-09-13 14:23:17 -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
drdozer c85a4d4fc1 Merge branch 'feature/autocomplete-sound-names' of ssh://codeberg.org/drdozer/strudel into feature/autocomplete-sound-names 2025-09-12 12:12:02 +01:00
drdozer 7676641c9e Commiting lockfile. 2025-09-12 12:11:36 +01:00
Dr Mathew R Pocock 260d2e499e Merge branch 'main' into feature/autocomplete-sound-names 2025-09-12 13:04:11 +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
drdozer e3409e52d0 Merge branch 'feature/autocomplete-sound-names' of ssh://codeberg.org/drdozer/strudel into feature/autocomplete-sound-names 2025-09-11 21:38:53 +01:00
drdozer 41d6f1b5da feat: sound autocomplete matches anywhere in name using includes() 2025-09-11 21:37:35 +01:00
Dr Mathew R Pocock aca4601734 Merge branch 'main' into feature/autocomplete-sound-names 2025-09-11 21:22:16 +02:00
drdozer 2a00fcdf5d Updated the codemirror packages to be able to access additional imports. 2025-09-11 19:01:22 +01:00
drdozer 9200a67a60 feat: add E# and B# to pitchNames for complete enharmonic coverage 2025-09-11 19:00:05 +01:00
drdozer 80fe5b8140 refactor: modularize autocomplete context handlers using strategy array 2025-09-11 18:59:50 +01:00
drdozer 085e839bfe refactor: modularize autocomplete context handlers using strategy array 2025-09-11 18:59:26 +01:00
drdozer 7ff7e8852b feat: implement context-aware autocomplete
Adds context-sensitive autocompletion for:
- Sound names in s("...") and sound("...") after the opening quote or after whitespace/bracket
- Bank names in bank("...") with quote handling and live filtering
- Scale types in .scale("...") after the colon, inserting colon-form for multi-word names
- Pitch names (C, C#, Db, etc.) for scale keys on explicit completion (Ctrl+Space) before the colon
- Blocks fallback completions in scale context before colon to avoid function list cross-talk
2025-09-11 18:42:23 +01: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
Aria 9848740e18 Formatting and tests 2025-09-10 21:39:02 -05:00
Aria 51b150b829 Optimize folding for audio rate 2025-09-10 21:27:56 -05: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
Aria 798eb22d9a Use currentTime for exact scheduling 2025-09-06 10:09:44 -05:00
Aria 9c108862ef Corrected piping 2025-09-05 22:09:50 -05:00
Aria 46a45b4596 Some missing cleanup 2025-09-04 20:51:06 -05:00
Aria 1205573bc6 Merge branch 'main' into glossing/wavetable-synth 2025-09-04 20:31:01 -05:00
Aria 9b1c23d49f Some tuning experimentally and adding back chebyshev 2025-09-04 20:05:28 -05:00
Aria 42ab65abdb Move things around to allow selection by name and avoid circular imports; fixed chebyshev 2025-09-04 11:47:01 -05:00
Aria bb982e6b9b First pass at modes 2025-09-03 21:55:48 -05: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
Aria 4e2e790864 Set delay and reverb nodes properly 2025-09-01 20:45:53 -05:00
Aria 4f3b40bd7b Merge branch 'main' into glossing/ducking-click 2025-09-01 20:45:00 -05:00
Jade (Rose) Rowland b8e1b8d7ac Merge branch 'main' into supradough 2025-08-31 22:42:07 -04:00
Aria e4070b86f8 More descriptive variables and comments 2025-08-29 14:33:14 -05:00
Aria fe46e1da53 Update docstring to include new warp modes; save sample server for a separate PR 2025-08-28 16:28:33 -05:00
Aria d1869c18ba Remove internal LFO 2025-08-28 16:25:10 -05:00
Aria 02cd79a6d9 Add wavetable oscillator with scanning, warps, and detune 2025-08-28 16:25:10 -05:00
Aria 9eeb6da3e6 Codeformat 2025-08-28 14:54:09 -05:00
Aria 869141b0a3 Parity with old adsr 2025-08-28 00:31:37 -05:00
Aria ea017e7622 Working version 2025-08-27 20:11:00 -05:00
Aria 763f900c69 First pass at envelopes with curves 2025-08-27 19:19:54 -05:00
Aria 022ac95bbd Typo 2025-08-26 12:09:12 -05:00
Aria b3e25cfc52 Merge branch 'main' into glossing/ducking-click 2025-08-26 12:08:58 -05:00
Aria 3d25aa1b91 Rename params, more examples 2025-08-26 12:06:10 -05:00
Aria b8c46d6b26 Added some description and examples of multi-accidentals and negative octaves 2025-08-22 13:23:41 -05:00
Aria 79453ac2c3 Allow negatives and multi-accidentals 2025-08-22 13:17:13 -05:00
Aria b08890bd9e Working version with note names 2025-08-22 11:25:13 -05:00
Aria 938c92fda2 Merge branch 'main' into glossing/scale-for-notes 2025-08-21 15:58:43 -05:00
Aria 9a9fe83f9c Example tests 2025-08-21 14:13:01 -05:00
Aria bf5d9917ab Codeformat and examples 2025-08-21 13:20:12 -05:00
Aria 4f15d681b0 Add release parameter, avoid clicks, some cleanup 2025-08-21 13:12:36 -05:00
Aria c8c6a2ce08 Add example test and update fastChunk test (which was previously broken) 2025-08-15 13:34:43 -05:00
Aria a4c040e101 Add new tests, organize old tests, fix issue with note 2025-08-06 12:37:55 -05:00
Aria 7046c1f546 Cleanup and simplification; improved docstrings 2025-08-06 10:17:48 -05:00
Aria a4ab8e1b06 First pass at extending scale function to include notes 2025-08-06 00:45:01 -05:00
Felix Roos 7f103e77e3 prefix "S" for solo 2025-07-28 04:05:21 +02: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
JoStro f5ae50c3ce Modify extend to better match behavior of ! operator 2025-07-01 22:07:22 +01: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
146 changed files with 14564 additions and 7646 deletions
+37
View File
@@ -0,0 +1,37 @@
name: Build and Deploy to beta (warm.strudel.cc)
on: [workflow_dispatch]
# Allow one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
build:
runs-on: docker
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9.12.2
- uses: actions/setup-node@v4
with:
node-version: 20
# cache: "pnpm"
- name: Install Dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Deploy
run: |
eval $(ssh-agent -s)
echo "$SSH_PRIVATE_KEY" | ssh-add -
apt update && apt install -y rsync
mkdir ~/.ssh
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/warm.strudel.cc
+2 -2
View File
@@ -1,4 +1,4 @@
name: Build and Deploy
name: Build and Deploy to live (strudel.cc)
on: [workflow_dispatch]
@@ -34,4 +34,4 @@ jobs:
apt update && apt install -y rsync
mkdir ~/.ssh
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
+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
+14
View File
@@ -45,6 +45,20 @@ tidal-drum-machines
webaudiofontdata
src-tauri/target
### BEGIN Visual Studio Code ###
# Blanket, recursive exclude for .vscode directory and files
.vscode/**/*
# Unexclude specific files and directories within .vscode
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
### END Visual Studio Code ###
# BEGIN JetBrains -> END JetBrains
# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+1
View File
@@ -0,0 +1 @@
22
+88
View File
@@ -0,0 +1,88 @@
# changelog
NOTE: you can generate this with `node warm.js`. it might still not be perfectly sorted...
## november 2025
- 2025-11-27T22:03:53+01:00 [perf] in `noise`, let noiseMix do the disconnect when it exists by jeromew in: [#1783](https://codeberg.org/uzu/strudel/pulls/1783)
- 2025-11-27T22:01:01+01:00 Bug Fix: Retries for sounds tab by glossing in: [#1754](https://codeberg.org/uzu/strudel/pulls/1754)
- 2025-11-27T20:36:46+01:00 [hydra] return the hydra object when await initHydra(..) is called by jeromew in: [#1784](https://codeberg.org/uzu/strudel/pulls/1784)
- 2025-11-27T20:36:14+01:00 Use errorLogger for query and tonal errors by glossing in: [#1782](https://codeberg.org/uzu/strudel/pulls/1782)
- 2025-11-27T20:27:10+01:00 [perf] level 5 `connect-leak` on `vowel` by jeromew in: [#1779](https://codeberg.org/uzu/strudel/pulls/1779)
- 2025-11-26T08:23:38+01:00 feat: add prebake script import button for loading .strudel files by daslyfe in: [#1774](https://codeberg.org/uzu/strudel/pulls/1774)
- 2025-11-26T07:39:28+01:00 Fix Sampler port trampling by Dayglo in: [#1717](https://codeberg.org/uzu/strudel/pulls/1717)
- 2025-11-25T20:59:09+01:00 Feat: Hook up octave and fix example by glossing in: [#1773](https://codeberg.org/uzu/strudel/pulls/1773)
- 2025-11-25T20:48:09+01:00 [perf] fix `connect-leak` in `tremolo` param by jeromew in: [#1780](https://codeberg.org/uzu/strudel/pulls/1780)
- 2025-11-24T17:51:20+01:00 filter modulation improvements! by daslyfe in: [#1769](https://codeberg.org/uzu/strudel/pulls/1769)
- 2025-11-24T01:20:53+01:00 Add o as a synonym for orbit by daslyfe in: [#1766](https://codeberg.org/uzu/strudel/pulls/1766)
- 2025-11-23T22:23:26+01:00 [perf] fix `connect-leak` in `fm` modulation by jeromew in: [#1758](https://codeberg.org/uzu/strudel/pulls/1758)
- 2025-11-23T09:47:24+01:00 Fix interoperability issue between `all` and `await initHydra()` by jeromew in: [#1663](https://codeberg.org/uzu/strudel/pulls/1663)
- 2025-11-23T04:23:06+01:00 Bug fix: Swap l/r gains with temp variable by glossing in: [#1768](https://codeberg.org/uzu/strudel/pulls/1768)
- 2025-11-23T00:52:28+01:00 FIX: prevent LFO filter modulation from popping from negative values by daslyfe in: [#1767](https://codeberg.org/uzu/strudel/pulls/1767)
- 2025-11-21T01:56:11+01:00 Bug Fix: Use frac due to negative frequencies from FM by glossing in: [#1759](https://codeberg.org/uzu/strudel/pulls/1759)
- 2025-11-20T19:48:16+01:00 [perf] fix `connect-leak` added by #1742 when noise() is not used by jeromew in: [#1757](https://codeberg.org/uzu/strudel/pulls/1757)
- 2025-11-20T14:51:00+01:00 [perf] fix `connect-leak` in `delay` effect by jeromew in: [#1755](https://codeberg.org/uzu/strudel/pulls/1755)
- 2025-11-20T14:49:41+01:00 [perf] fix `connect leak` when .noise() is in the mix by jeromew in: [#1742](https://codeberg.org/uzu/strudel/pulls/1742)
- 2025-11-18T23:52:45+01:00 wchooseCycles has now notes in an example by scrappy_fiddler in: [#1748](https://codeberg.org/uzu/strudel/pulls/1748)
- 2025-11-17T05:31:54+01:00 Feature: Partials by glossing in: [#1659](https://codeberg.org/uzu/strudel/pulls/1659)
- 2025-11-16T22:06:14+01:00 README: update superdough documentation by TristanMlct in: [#1741](https://codeberg.org/uzu/strudel/pulls/1741)
- 2025-11-16T21:08:54+01:00 Worklet optimizations by glossing in: [#1730](https://codeberg.org/uzu/strudel/pulls/1730)
- 2025-11-15T14:59:30+01:00 [perf] disconnect Offline AudioNode connections in generateReverb by jeromew in: [#1740](https://codeberg.org/uzu/strudel/pulls/1740)
- 2025-11-15T14:57:47+01:00 [perf] disconnect `send` effect AudioNode when `room` is used by jeromew in: [#1736](https://codeberg.org/uzu/strudel/pulls/1736)
- 2025-11-12T21:22:34+01:00 [supradough] fix: unstable filter by froos in: [#1593](https://codeberg.org/uzu/strudel/pulls/1593)
- 2025-11-12T21:12:07+01:00 Fix link syntax in `project-start` by Kissaki in: [#1724](https://codeberg.org/uzu/strudel/pulls/1724)
- 2025-11-12T21:06:13+01:00 Fix web README sample code by Kissaki in: [#1725](https://codeberg.org/uzu/strudel/pulls/1725)
- 2025-11-12T20:59:39+01:00 Fix typo: 'studel' -> 'strudel' by drhayes in: [#1726](https://codeberg.org/uzu/strudel/pulls/1726)
- 2025-11-12T15:23:41+01:00 fix for node 24 support in tests - #1718 by ausav in: [#1719](https://codeberg.org/uzu/strudel/pulls/1719)
- 2025-11-11T09:00:49+01:00 soundAlias example fix by PepsiiMan in: [#1720](https://codeberg.org/uzu/strudel/pulls/1720)
- 2025-11-10T08:18:36+01:00 fix: Make Supradough package build by munshkr in: [#1711](https://codeberg.org/uzu/strudel/pulls/1711)
- 2025-11-10T08:18:00+01:00 added docs for spaces in scale names by ondras in: [#1715](https://codeberg.org/uzu/strudel/pulls/1715)
- 2025-11-10T02:44:01+01:00 Feature: LFOs for Filters by glossing in: [#1636](https://codeberg.org/uzu/strudel/pulls/1636)
- 2025-11-05T17:07:00+01:00 improvement: sync midi messages to audio context clock by daslyfe in: [#1708](https://codeberg.org/uzu/strudel/pulls/1708)
- 2025-11-04T19:36:14+01:00 Add stick button mappings to gamepad implementation and improve docs by kaosuryoko in: [#1696](https://codeberg.org/uzu/strudel/pulls/1696)
- 2025-11-04T19:29:35+01:00 Refactor sound stopping and triggering logic in SoundsTab component by IJOL in: [#1688](https://codeberg.org/uzu/strudel/pulls/1688)
- 2025-11-04T19:13:21+01:00 Add setting to toggle pattern auto-start on pattern change by moumar in: [#1690](https://codeberg.org/uzu/strudel/pulls/1690)
- 2025-11-04T18:58:50+01:00 Voicings JSDoc by sharkeys_lunchbox in: [#1686](https://codeberg.org/uzu/strudel/pulls/1686)
- 2025-11-01T07:50:40+01:00 FIX: Loading local samples uses too much memory by daslyfe in: [#1706](https://codeberg.org/uzu/strudel/pulls/1706)
## october 2025
- 2025-10-28T22:58:24+01:00 Bug Fix: Handle scale-for-notes when n also supplied by glossing in: [#1625](https://codeberg.org/uzu/strudel/pulls/1625)
- 2025-10-28T22:51:16+01:00 Repurpose vim shortcuts for usability by dtricks in: [#1624](https://codeberg.org/uzu/strudel/pulls/1624)
- 2025-10-28T22:35:21+01:00 Add support for euclidian in mondo with `bd&3:8` by TristanCacqueray in: [#1630](https://codeberg.org/uzu/strudel/pulls/1630)
- 2025-10-27T10:42:07+01:00 Use bunny cdn for all samples and json files by yaxu in: [#1701](https://codeberg.org/uzu/strudel/pulls/1701)
- 2025-10-26T22:52:54+01:00 degithub - switch some samples to bunnycdn by yaxu in: [#1697](https://codeberg.org/uzu/strudel/pulls/1697)
- 2025-10-26T17:09:22+01:00 Fix ZZFX example by moumar in: [#1685](https://codeberg.org/uzu/strudel/pulls/1685)
- 2025-10-23T15:56:04+02:00 Make osc port and host configurable. Changes dependencies. by yaxu in: [#1682](https://codeberg.org/uzu/strudel/pulls/1682)
- 2025-10-23T03:47:31+02:00 Adds back shape to superdough by glossing in: [#1672](https://codeberg.org/uzu/strudel/pulls/1672)
- 2025-10-22T22:15:19+02:00 Fix sampler.mjs githubPath by jeromew in: [#1684](https://codeberg.org/uzu/strudel/pulls/1684)
- 2025-10-22T16:20:50+02:00 Fix onPaint for widgets by yaxu in: [#1658](https://codeberg.org/uzu/strudel/pulls/1658)
- 2025-10-22T15:19:12+02:00 Fix a bug introduced by #4e17cfbdd6 by milliganf in: [#1679](https://codeberg.org/uzu/strudel/pulls/1679)
- 2025-10-18T04:29:20+02:00 Bug Fix: Wavetable: phase wrapping at 1 and detune by glossing in: [#1620](https://codeberg.org/uzu/strudel/pulls/1620)
- 2025-10-16T20:26:28+02:00 github samples: default to "samples" if repository is not specified by prezmop in: [#1644](https://codeberg.org/uzu/strudel/pulls/1644)
- 2025-10-16T17:33:39+02:00 Docs: add example of custom chained function by dariusk in: [#1642](https://codeberg.org/uzu/strudel/pulls/1642)
- 2025-10-14T12:39:48+02:00 textbox by yaxu in: [#1650](https://codeberg.org/uzu/strudel/pulls/1650)
- 2025-10-13T07:05:57+02:00 Feature: Wavetable FM by glossing in: [#1623](https://codeberg.org/uzu/strudel/pulls/1623)
- 2025-10-12T20:43:42+02:00 fix: repair REPL sample sources and URL concat (#1640) by erikfox in: [#1646](https://codeberg.org/uzu/strudel/pulls/1646)
- 2025-10-10T11:25:02+02:00 Add control-metadata by yaxu in: [#1634](https://codeberg.org/uzu/strudel/pulls/1634)
- 2025-10-07T22:59:39+02:00 Fix MQTT: change the trigger handler to match new hap by jurrchen in: [#1629](https://codeberg.org/uzu/strudel/pulls/1629)
- 2025-10-05T21:42:19+02:00 Fix case sensitivity for synonym search by vvolhejn in: [#1618](https://codeberg.org/uzu/strudel/pulls/1618)
- 2025-10-01T08:56:35+02:00 Optimize wavetable synth by glossing in: [#1613](https://codeberg.org/uzu/strudel/pulls/1613)
- 2025-10-01T02:02:33+02:00 fix: pattern switching by daslyfe in: [#1616](https://codeberg.org/uzu/strudel/pulls/1616)
## September 2025
- 2025-09-29T02:10:54+02:00 fix wavetable JSON by daslyfe in: [#1611](https://codeberg.org/uzu/strudel/pulls/1611)
- 2025-09-28T21:28:16+02:00 rename wavetable controls to match existing naming conventions by daslyfe in: [#1610](https://codeberg.org/uzu/strudel/pulls/1610)
- 2025-09-27T23:28:13+02:00 Feature: Wavetable synth improvements by glossing in: [#1607](https://codeberg.org/uzu/strudel/pulls/1607)
- 2025-09-27T21:43:10+02:00 wavetable improvements by daslyfe in: [#1608](https://codeberg.org/uzu/strudel/pulls/1608)
- 2025-09-26T08:48:46+02:00 create orbit based DJ filter by daslyfe in: [#1603](https://codeberg.org/uzu/strudel/pulls/1603)
- 2025-09-23T13:28:20+02:00 fix: time signal by daslyfe in: [#1583](https://codeberg.org/uzu/strudel/pulls/1583)
- 2025-09-21T16:16:06+02:00 mondo: fix all + setcpm + setcps by froos in: [#1600](https://codeberg.org/uzu/strudel/pulls/1600)
- 2025-09-19T21:59:43+02:00 fix: osc error message by froos in: [#1597](https://codeberg.org/uzu/strudel/pulls/1597)
- 2025-09-18T21:09:56+02:00 osc: --debug flag to see incoming messages by froos in: [#1595](https://codeberg.org/uzu/strudel/pulls/1595)
- 2025-09-17T14:12:10+02:00 simplify osc usage by froos in: [#1588](https://codeberg.org/uzu/strudel/pulls/1588)
- 2025-09-16T16:59:58+02:00 dev: logger errors to console in dev mode by daslyfe in: [#1585](https://codeberg.org/uzu/strudel/pulls/1585)
- 2025-09-15T22:52:14+02:00 add tic80 font by froos in: [#1579](https://codeberg.org/uzu/strudel/pulls/1579)
- 2025-09-15T00:00:19+02:00 supradough: fix delay + add some jsdoc types by froos in: [#1578](https://codeberg.org/uzu/strudel/pulls/1578)
+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"]
+1 -1
View File
@@ -13,7 +13,7 @@ https://strudel.cc/
After cloning the project, you can run the REPL locally:
1. Install [Node.js](https://nodejs.org/)
1. Install [Node.js](https://nodejs.org/) 18 or newer
2. Install [pnpm](https://pnpm.io/installation)
3. Install dependencies by running the following command:
```bash
+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',
],
},
+3
View File
@@ -20,5 +20,8 @@
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -14,5 +14,8 @@
},
"dependencies": {
"@strudel/web": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -18,5 +18,8 @@
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*",
"@strudel/tonal": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -13,5 +13,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -32,5 +32,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+4 -1
View File
@@ -73,5 +73,8 @@
"prettier": "^3.4.2",
"vitest": "^3.0.4",
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
}
+384 -23
View File
@@ -1,6 +1,11 @@
import jsdoc from '../../doc.json';
import { autocompletion } from '@codemirror/autocomplete';
import { h } from './html';
//TODO: fix tonal scale import
// import { Scale } from '@tonaljs/tonal';
// import { soundMap } from '@strudel/webaudio';
let soundMap = undefined;
import { complex } from '@strudel/tonal';
const escapeHtml = (str) => {
const div = document.createElement('div');
@@ -54,11 +59,12 @@ const buildExamples = (examples) =>
`
: '';
export const Autocomplete = ({ doc, label }) =>
export const Autocomplete = (doc) =>
h`
<div class="autocomplete-info-container">
<div class="autocomplete-info-tooltip">
<h3 class="autocomplete-info-function-name">${label || getDocLabel(doc)}</h3>
<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)}
@@ -74,29 +80,384 @@ const isValidDoc = (doc) => {
const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
const jsdocCompletions = jsdoc.docs
.filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc))
// https://codemirror.net/docs/ref/#autocomplete.Completion
.map((doc) => ({
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
}));
export function bankCompletions() {
// TODO: FIX IMPORT
const soundDict = soundMap?.get() ?? {};
const banks = new Set();
for (const key of Object.keys(soundDict)) {
const [bank, suffix] = key.split('_');
if (suffix && bank) banks.add(bank);
}
return Array.from(banks)
.sort()
.map((name) => ({ label: name, type: 'bank' }));
}
// Attempt to get all scale names from Tonal TODO: FIX IMPORT
let scaleCompletions = [];
// try {
// scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
// } catch (e) {
// console.warn('[autocomplete] Could not load scale names from Tonal:', e);
// }
// Valid mode values for voicing
const modeCompletions = [
{ label: 'below', type: 'mode' },
{ label: 'above', type: 'mode' },
{ label: 'duck', type: 'mode' },
{ label: 'root', type: 'mode' },
];
// Valid chord symbols from ireal dictionary plus empty string for major triads
const chordSymbols = ['', ...Object.keys(complex)].sort();
const chordSymbolCompletions = chordSymbols.map((symbol) => {
if (symbol === '') {
return {
label: 'major',
apply: '',
type: 'chord-symbol',
};
}
return {
label: symbol,
apply: symbol,
type: 'chord-symbol',
};
});
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;
})();
// --- Handler functions for each context ---
const pitchNames = [
'C',
'C#',
'Db',
'D',
'D#',
'Eb',
'E',
'E#',
'Fb',
'F',
'F#',
'Gb',
'G',
'G#',
'Ab',
'A',
'A#',
'Bb',
'B',
'B#',
'Cb',
];
// Cached regex patterns for scaleHandler
const SCALE_NO_QUOTES_REGEX = /scale\(\s*$/;
const SCALE_AFTER_COLON_REGEX = /scale\(\s*['"][^'"]*:[^'"]*$/;
const SCALE_PRE_COLON_REGEX = /scale\(\s*['"][^'"]*$/;
const SCALE_PITCH_MATCH_REGEX = /([A-Ga-g][#b]*)?$/;
const SCALE_SPACES_TO_COLON_REGEX = /\s+/g;
function scaleHandler(context) {
// First check for scale context without quotes - block with empty completions
let scaleNoQuotesContext = context.matchBefore(SCALE_NO_QUOTES_REGEX);
if (scaleNoQuotesContext) {
return {
from: scaleNoQuotesContext.to,
options: [],
};
}
// Check for after-colon context first (more specific)
let scaleAfterColonContext = context.matchBefore(SCALE_AFTER_COLON_REGEX);
if (scaleAfterColonContext) {
const text = scaleAfterColonContext.text;
const colonIdx = text.lastIndexOf(':');
if (colonIdx !== -1) {
const fragment = text.slice(colonIdx + 1);
const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment));
const options = filteredScales.map((s) => ({
...s,
apply: s.label.replace(SCALE_SPACES_TO_COLON_REGEX, ':'),
}));
const from = scaleAfterColonContext.from + colonIdx + 1;
return {
from,
options,
};
}
}
// Then check for pre-colon context
let scalePreColonContext = context.matchBefore(SCALE_PRE_COLON_REGEX);
if (scalePreColonContext) {
if (!scalePreColonContext.text.includes(':')) {
if (context.explicit) {
const text = scalePreColonContext.text;
const match = text.match(SCALE_PITCH_MATCH_REGEX);
const fragment = match ? match[0] : '';
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const from = scalePreColonContext.to - fragment.length;
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
return { from, options };
} else {
return { from: scalePreColonContext.to, options: [] };
}
}
}
return null;
}
// Cached regex patterns for soundHandler
const SOUND_NO_QUOTES_REGEX = /(s|sound)\(\s*$/;
const SOUND_WITH_QUOTES_REGEX = /(s|sound)\(\s*['"][^'"]*$/;
const SOUND_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w]*)$/;
function soundHandler(context) {
// First check for sound context without quotes - block with empty completions
let soundNoQuotesContext = context.matchBefore(SOUND_NO_QUOTES_REGEX);
if (soundNoQuotesContext) {
return {
from: soundNoQuotesContext.to,
options: [],
};
}
// Then check for sound context with quotes - provide completions
let soundContext = context.matchBefore(SOUND_WITH_QUOTES_REGEX);
if (!soundContext) return null;
const text = soundContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
const soundNames = Object.keys(soundMap?.get() ?? {}).sort();
const filteredSounds = soundNames.filter((name) => name.includes(fragment));
let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
const from = soundContext.to - fragment.length;
return {
from,
options,
};
}
// Cached regex patterns for bankHandler
const BANK_NO_QUOTES_REGEX = /bank\(\s*$/;
const BANK_WITH_QUOTES_REGEX = /bank\(\s*['"][^'"]*$/;
function bankHandler(context) {
// First check for bank context without quotes - block with empty completions
let bankNoQuotesContext = context.matchBefore(BANK_NO_QUOTES_REGEX);
if (bankNoQuotesContext) {
return {
from: bankNoQuotesContext.to,
options: [],
};
}
// Then check for bank context with quotes - provide completions
let bankMatch = context.matchBefore(BANK_WITH_QUOTES_REGEX);
if (!bankMatch) return null;
const text = bankMatch.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragment = inside;
let banks = bankCompletions();
const filteredBanks = banks.filter((b) => b.label.startsWith(fragment));
const from = bankMatch.to - fragment.length;
return {
from,
options: filteredBanks,
};
}
// Cached regex patterns for modeHandler
const MODE_NO_QUOTES_REGEX = /mode\(\s*$/;
const MODE_AFTER_COLON_REGEX = /mode\(\s*['"][^'"]*:[^'"]*$/;
const MODE_PRE_COLON_REGEX = /mode\(\s*['"][^'"]*$/;
const MODE_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w:]*)$/;
function modeHandler(context) {
// First check for mode context without quotes - block with empty completions
let modeNoQuotesContext = context.matchBefore(MODE_NO_QUOTES_REGEX);
if (modeNoQuotesContext) {
return {
from: modeNoQuotesContext.to,
options: [],
};
}
// Check for after-colon context first (more specific)
let modeAfterColonContext = context.matchBefore(MODE_AFTER_COLON_REGEX);
if (modeAfterColonContext) {
const text = modeAfterColonContext.text;
const colonIdx = text.lastIndexOf(':');
if (colonIdx !== -1) {
const fragment = text.slice(colonIdx + 1);
// For anchor after colon, we can suggest pitch names
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
const from = modeAfterColonContext.from + colonIdx + 1;
return {
from,
options,
};
}
}
// Then check for pre-colon context
let modeContext = context.matchBefore(MODE_PRE_COLON_REGEX);
if (!modeContext) return null;
const text = modeContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragMatch = inside.match(MODE_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
const filteredModes = modeCompletions.filter((m) => m.label.startsWith(fragment));
const from = modeContext.to - fragment.length;
return {
from,
options: filteredModes,
};
}
// Cached regex patterns for chordHandler
const CHORD_NO_QUOTES_REGEX = /chord\(\s*$/;
const CHORD_WITH_QUOTES_REGEX = /chord\(\s*['"][^'"]*$/;
const CHORD_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w#b+^:-]*)$/;
function chordHandler(context) {
// First check for chord context without quotes - block with empty completions
let chordNoQuotesContext = context.matchBefore(CHORD_NO_QUOTES_REGEX);
if (chordNoQuotesContext) {
return {
from: chordNoQuotesContext.to,
options: [],
};
}
// Then check for chord context with quotes - provide completions
let chordContext = context.matchBefore(CHORD_WITH_QUOTES_REGEX);
if (!chordContext) return null;
const text = chordContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
// Use same fragment matching as sound/mode for expressions like "<G Am>"
const fragMatch = inside.match(CHORD_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
// Check if fragment contains any pitch name at start (for root + symbol)
let rootMatch = null;
let symbolFragment = fragment;
for (const pitch of pitchNames) {
if (fragment.toLowerCase().startsWith(pitch.toLowerCase())) {
rootMatch = pitch;
symbolFragment = fragment.slice(pitch.length);
break;
}
}
if (rootMatch) {
// We have a root, now complete chord symbols
const filteredSymbols = chordSymbolCompletions.filter((s) =>
s.label.toLowerCase().startsWith(symbolFragment.toLowerCase()),
);
// Create completions that replace the entire chord, not just the symbol part
const options = filteredSymbols;
const from = chordContext.to - symbolFragment.length;
return { from, options };
} else {
// No root yet, complete with pitch names
const filteredPitches = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const options = filteredPitches.map((p) => ({ label: p, type: 'pitch' }));
const from = chordContext.to - fragment.length;
return { from, options };
}
}
// Cached regex patterns for fallbackHandler
const FALLBACK_WORD_REGEX = /\w*/;
function fallbackHandler(context) {
const word = context.matchBefore(FALLBACK_WORD_REGEX);
if (word && word.from === word.to && !context.explicit) return null;
if (word) {
return {
from: word.from,
options: jsdocCompletions,
};
}
return null;
}
const handlers = [
soundHandler,
bankHandler,
chordHandler,
scaleHandler,
modeHandler,
// this handler *must* be last
fallbackHandler,
];
export const strudelAutocomplete = (context) => {
const word = context.matchBefore(/\w*/);
if (word.from === word.to && !context.explicit) return null;
return {
from: word.from,
options: jsdocCompletions,
/* options: [
{ label: 'match', type: 'keyword' },
{ label: 'hello', type: 'variable', info: '(World)' },
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
], */
};
for (const handler of handlers) {
const result = handler(context);
if (result) {
return result;
}
}
return null;
};
export const isAutoCompletionEnabled = (enabled) =>
+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]),
])();
+76 -25
View File
@@ -1,29 +1,31 @@
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 { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
import { indentWithTab, toggleLineComment } from '@codemirror/commands';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
import {
drawSelection,
EditorView,
highlightActiveLineGutter,
highlightActiveLine,
highlightActiveLineGutter,
keymap,
lineNumbers,
drawSelection,
} from '@codemirror/view';
import { repl, registerControl } from '@strudel/core';
import { Drawer, cleanupDraw } from '@strudel/draw';
import { persistentAtom } from '@nanostores/persistent';
import { logger, registerControl, repl } from '@strudel/core';
import { cleanupDraw, Drawer } from '@strudel/draw';
import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
import { basicSetup } from './basicSetup.mjs';
import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs';
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 { activateTheme, initTheme, theme } from './themes.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
import { updateWidgets, widgetPlugin } from './widget.mjs';
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
@@ -85,13 +87,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
// 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(
@@ -114,13 +120,13 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
run: () => onStop?.(),
},
/* {
key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()),
},
{
key: 'Ctrl-Shift-Enter',
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
}, */
key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()),
},
{
key: 'Ctrl-Shift-Enter',
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
}, */
]),
),
],
@@ -201,7 +207,8 @@ export class StrudelMirror {
updateWidgets(this.editor, widgets);
updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options);
// if no painters are set (.onPaint was not called), then we only need the present moment (for highlighting)
// if no painters are set (.onPaint was not called), then we only need
// the present moment (for highlighting)
const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0];
this.drawer.setDrawTime(drawTime);
// invalidate drawer after we've set the appropriate drawTime
@@ -240,6 +247,33 @@ export class StrudelMirror {
}
};
document.addEventListener('start-repl', this.onStartRepl);
// Handle global evaluation requests (e.g., from Vim :w)
this.onEvaluateRequest = (e) => {
try {
// Evaluate current editor on repl-evaluate
logger('[repl] evaluate via event');
this.evaluate();
e?.cancelable && e.preventDefault?.();
} catch (err) {
console.error('Error handling repl-evaluate event', err);
}
};
document.addEventListener('repl-evaluate', this.onEvaluateRequest);
document.addEventListener('repl-stop', this.onStopRequest);
// Toggle comments requested from Vim (gc)
this.onToggleComment = (e) => {
try {
// Honor selections; toggleLineComment handles both selections and
// single line
toggleLineComment(this.editor);
e?.cancelable && e.preventDefault?.();
} catch (err) {
console.error('Error handling repl-toggle-comment event', err);
}
};
document.addEventListener('repl-toggle-comment', this.onToggleComment);
}
draw(haps, time, painters) {
painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
@@ -266,6 +300,16 @@ export class StrudelMirror {
async stop() {
this.repl.scheduler.stop();
}
// Listen for global stop requests (e.g., from Vim :q)
onStopRequest = (e) => {
try {
this.stop();
e?.cancelable && e.preventDefault?.();
} catch (err) {
console.error('Error handling repl-stop event', err);
}
};
async toggle() {
if (this.repl.scheduler.started) {
this.repl.stop();
@@ -341,11 +385,18 @@ export class StrudelMirror {
}
}
setCode(code) {
const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
const changes = {
from: 0,
to: this.editor.state.doc.length,
insert: code,
};
this.editor.dispatch({ changes });
}
clear() {
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest);
this.onStopRequest && document.removeEventListener('repl-stop', this.onStopRequest);
this.onToggleComment && document.removeEventListener('repl-toggle-comment', this.onToggleComment);
}
getCursorLocation() {
return this.editor.state.selection.main.head;
+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('');
+109 -3
View File
@@ -1,10 +1,12 @@
import { defaultKeymap } from '@codemirror/commands';
import { Prec } from '@codemirror/state';
import { keymap, ViewPlugin } from '@codemirror/view';
// import { searchKeymap } from '@codemirror/search';
import { emacs } from '@replit/codemirror-emacs';
import { vim } from '@replit/codemirror-vim';
import { vim, 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 { logger } from '@strudel/core';
const vscodePlugin = ViewPlugin.fromClass(
class {
@@ -18,6 +20,110 @@ const vscodePlugin = ViewPlugin.fromClass(
);
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
// Map Vim :w to trigger the same action as evaluation. We dispatch a custom
// event 'repl-evaluate' that the editor listens for, and also simulate
// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
// appears in the Console panel.
try {
if (Vim && typeof Vim.defineEx === 'function') {
// Map gc to toggle line comments by dispatching a custom event that our
// CodeMirror integration listens to. This avoids depending on Vim's
// internal actions and works with current selections/visual mode.
try {
Vim.defineAction('strudelToggleComment', (cm) => {
const view = cm?.view || cm;
try {
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
document.dispatchEvent(ev);
} catch (e) {
console.error('strudelToggleComment dispatch failed', e);
}
});
Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'normal' });
Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'visual' });
} catch (e) {
console.error('Vim gc mapping failed', e);
}
// :q to pause/stop
Vim.defineEx('quit', 'q', (cm) => {
try {
const view = cm?.view || cm;
// First try dispatching our custom stop event, then fallback to Alt+.
let handled = false;
try {
const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true });
handled = document.dispatchEvent(ev) === false;
} catch (e) {
console.error('Error dispatching repl-stop event', e);
}
if (!handled) {
const altDot = new KeyboardEvent('keydown', {
key: '.',
code: 'Period',
altKey: true,
bubbles: true,
cancelable: true,
});
view?.dom?.dispatchEvent?.(altDot);
}
} catch (e) {
console.error('Error dispatching :q stop event', e);
}
});
// :w to evaluate
Vim.defineEx('write', 'w', (cm) => {
const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself
try {
view?.focus?.();
// Let the app know this came from Vim :w
try {
logger('[vim] :w — evaluating code');
} catch (e) {
console.error('Error logging Vim :w evaluation', e);
}
// Dispatch a dedicated evaluate event first
let handled = false;
try {
const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true });
handled = document.dispatchEvent(ev) === false; // false means preventDefault was called
} catch (e) {
console.error('Error dispatching repl-evaluate event', e);
}
if (handled) {
return;
}
// Try Ctrl+Enter first if not handled by custom event
const ctrlEnter = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
ctrlKey: true,
bubbles: true,
cancelable: true,
});
view?.dom?.dispatchEvent?.(ctrlEnter);
// If not handled (no handler called preventDefault), try Alt+Enter as
// fallback
if (!ctrlEnter.defaultPrevented) {
const altEnter = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
altKey: true,
bubbles: true,
cancelable: true,
});
view?.dom?.dispatchEvent?.(altEnter);
}
} catch (e) {
console.error('Error dispatching :w evaluation event', e);
}
});
}
} catch (e) {
console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e);
}
const keymaps = {
vim,
emacs,
@@ -27,5 +133,5 @@ const keymaps = {
export function keybindings(name) {
const active = keymaps[name];
return [active ? active() : [], keymap.of(historyKeymap)];
return [active ? Prec.high(active()) : []];
}
+9 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "1.2.3",
"version": "1.2.6",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
@@ -42,14 +42,20 @@
"@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:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"nanostores": "^0.11.3"
"@tonaljs/tonal": "^4.10.0",
"nanostores": "^0.11.3",
"superdough": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+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 };
},
+645 -45
View File
@@ -87,10 +87,244 @@ export function registerControl(names, ...aliases) {
*/
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
/**
* Position in the wavetable of the wavetable oscillator
*
* @name wt
* @param {number | Pattern} position Position in the wavetable from 0 to 1
* @synonyms wavetablePosition
* @example
* s("squelch").bank("wt_digital").seg(8).note("F1").wt("0 0.25 0.5 0.75 1")
*/
export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePosition');
/**
* Amount of envelope applied wavetable oscillator's position envelope
*
* @name wtenv
* @param {number | Pattern} amount between 0 and 1
*/
export const { wtenv } = registerControl('wtenv');
/**
* Attack time of the wavetable oscillator's position envelope
*
* @name wtattack
* @synonyms wtatt
* @param {number | Pattern} time attack time in seconds
*/
export const { wtattack, wtatt } = registerControl('wtattack', 'wtatt');
/**
* Decay time of the wavetable oscillator's position envelope
*
* @name wtdecay
* @synonyms wtdec
* @param {number | Pattern} time decay time in seconds
*/
export const { wtdecay, wtdec } = registerControl('wtdecay', 'wtdec');
/**
* Sustain time of the wavetable oscillator's position envelope
*
* @name wtsustain
* @synonyms wtsus
* @param {number | Pattern} gain sustain level (0 to 1)
*/
export const { wtsustain, wtsus } = registerControl('wtsustain', 'wtsus');
/**
* Release time of the wavetable oscillator's position envelope
*
* @name wtrelease
* @synonyms wtrel
* @param {number | Pattern} time release time in seconds
*/
export const { wtrelease, wtrel } = registerControl('wtrelease', 'wtrel');
/**
* Rate of the LFO for the wavetable oscillator's position
*
* @name wtrate
* @param {number | Pattern} rate rate in hertz
*/
export const { wtrate } = registerControl('wtrate');
/**
* cycle synced rate of the LFO for the wavetable oscillator's position
*
* @name wtsync
* @param {number | Pattern} rate rate in cycles
*/
export const { wtsync } = registerControl('wtsync');
/**
* Depth of the LFO for the wavetable oscillator's position
*
* @name wtdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { wtdepth } = registerControl('wtdepth');
/**
* Shape of the LFO for the wavetable oscillator's position
*
* @name wtshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { wtshape } = registerControl('wtshape');
/**
* DC offset of the LFO for the wavetable oscillator's position
*
* @name wtdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { wtdc } = registerControl('wtdc');
/**
* Skew of the LFO for the wavetable oscillator's position
*
* @name wtskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { wtskew } = registerControl('wtskew');
/**
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator
*
* @name warp
* @param {number | Pattern} amount Warp of the wavetable from 0 to 1
* @synonyms wavetableWarp
* @example
* s("basique").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
* .warpmode("spin")
*/
export const { warp, wavetableWarp } = registerControl('warp', 'wavetableWarp');
/**
* Attack time of the wavetable oscillator's warp envelope
*
* @name warpattack
* @synonyms warpatt
* @param {number | Pattern} time attack time in seconds
*/
export const { warpattack, warpatt } = registerControl('warpattack', 'warpatt');
/**
* Decay time of the wavetable oscillator's warp envelope
*
* @name warpdecay
* @synonyms warpdec
* @param {number | Pattern} time decay time in seconds
*/
export const { warpdecay, warpdec } = registerControl('warpdecay', 'warpdec');
/**
* Sustain time of the wavetable oscillator's warp envelope
*
* @name warpsustain
* @synonyms warpsus
* @param {number | Pattern} gain sustain level (0 to 1)
*/
export const { warpsustain, warpsus } = registerControl('warpsustain', 'warpsus');
/**
* Release time of the wavetable oscillator's warp envelope
*
* @name warprelease
* @synonyms warprel
* @param {number | Pattern} time release time in seconds
*/
export const { warprelease, warprel } = registerControl('warprelease', 'warprel');
/**
* Rate of the LFO for the wavetable oscillator's warp
*
* @name warprate
* @param {number | Pattern} rate rate in hertz
*/
export const { warprate } = registerControl('warprate');
/**
* Depth of the LFO for the wavetable oscillator's warp
*
* @name warpdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { warpdepth } = registerControl('warpdepth');
/**
* Shape of the LFO for the wavetable oscillator's warp
*
* @name warpshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { warpshape } = registerControl('warpshape');
/**
* DC offset of the LFO for the wavetable oscillator's warp
*
* @name warpdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { warpdc } = registerControl('warpdc');
/**
* Skew of the LFO for the wavetable oscillator's warp
*
* @name warpskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { warpskew } = registerControl('warpskew');
/**
* Type of warp (alteration of the waveform) to apply to the wavetable oscillator.
*
* The current options are: none, asym, bendp, bendm, bendmp, sync, quant, fold, pwm, orbit,
* spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip
*
* @name warpmode
* @param {number | string | Pattern} mode Warp mode
* @synonyms wavetableWarpMode
* @example
* s("morgana").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
* .warpmode("<asym bendp spin logistic sync wormhole brownian>*2")
*
*/
export const { warpmode, wavetableWarpMode } = registerControl('warpmode', 'wavetableWarpMode');
/**
* Amount of randomness of the initial phase of the wavetable oscillator.
*
* @name wtphaserand
* @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random)
* @synonyms wavetablePhaseRand
* @example
* s("basique").bank("wt_digital").seg(16).wtphaserand("<0 1>")
*
*/
export const { wtphaserand, wavetablePhaseRand } = registerControl('wtphaserand', 'wavetablePhaseRand');
/**
* Amount of envelope applied wavetable oscillator's position envelope
*
* @name warpenv
* @param {number | Pattern} amount between 0 and 1
*/
export const { warpenv } = registerControl('warpenv');
/**
* cycle synced rate of the LFO for the wavetable warp position
*
* @name warpsync
* @param {number | Pattern} rate rate in cycles
*/
export const { warpsync } = registerControl('warpsync');
/**
* Define a custom webaudio node to use as a sound source.
*
* @name source
* @synonyms src
* @param {function} getSource
* @synonyms src
*
@@ -113,7 +347,7 @@ export const { n } = registerControl('n');
*
* - a letter (a-g or A-G)
* - optional accidentals (b or #)
* - optional octave number (0-9). Defaults to 3
* - optional (possibly negative) octave number (0-9). Defaults to 3
*
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
*
@@ -126,6 +360,8 @@ export const { n } = registerControl('n');
* note("c4 a4 f4 e4")
* @example
* note("60 69 65 64")
* @example
* note("fbb1 a#0 cbbb-1 e##-2").sound("saw")
*/
export const { note } = registerControl(['note', 'n']);
@@ -141,8 +377,8 @@ export const { note } = registerControl(['note', 'n']);
*/
export const { accelerate } = registerControl('accelerate');
/**
*
* Sets the velocity from 0 to 1. Is multiplied together with gain.
*
* @name velocity
* @example
* s("hh*8")
@@ -254,7 +490,7 @@ export const { fmenv } = registerControl('fmenv');
export const { fmattack } = registerControl('fmattack');
/**
* waveform of the fm modulator
* Waveform of the fm modulator
*
* @name fmwave
* @param {number | Pattern} wave waveform
@@ -309,6 +545,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
@@ -377,7 +624,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
// ['bpq'],
export const { bandq, bpq } = registerControl('bandq', 'bpq');
/**
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
* A pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
*
* @memberof Pattern
* @name begin
@@ -438,7 +685,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb');
*/
export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
/**
* bit crusher effect.
* Bit crusher effect.
*
* @name crush
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
@@ -449,7 +696,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
// ['clhatdecay'],
export const { crush } = registerControl('crush');
/**
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
* Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
*
* @name coarse
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
@@ -460,7 +707,7 @@ export const { crush } = registerControl('crush');
export const { coarse } = registerControl('coarse');
/**
* modulate the amplitude of a sound with a continuous waveform
* Modulate the amplitude of a sound with a continuous waveform
*
* @name tremolo
* @synonyms trem
@@ -472,7 +719,7 @@ export const { coarse } = registerControl('coarse');
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
/**
* modulate the amplitude of a sound with a continuous waveform
* Modulate the amplitude of a sound with a continuous waveform
*
* @name tremolosync
* @synonyms tremsync
@@ -487,7 +734,7 @@ export const { tremolosync } = registerControl(
);
/**
* depth of amplitude modulation
* Depth of amplitude modulation
*
* @name tremolodepth
* @synonyms tremdepth
@@ -498,7 +745,7 @@ export const { tremolosync } = registerControl(
*/
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
/**
* alter the shape of the modulation waveform
* Alter the shape of the modulation waveform
*
* @name tremoloskew
* @synonyms tremskew
@@ -510,7 +757,7 @@ export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
/**
* alter the phase of the modulation waveform
* Alter the phase of the modulation waveform
*
* @name tremolophase
* @synonyms tremphase
@@ -522,9 +769,10 @@ export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
/**
* shape of amplitude modulation
* 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")
@@ -532,7 +780,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
*/
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
/**
* filter overdrive for supported filter types
* Filter overdrive for supported filter types
*
* @name drive
* @param {number | Pattern} amount
@@ -540,44 +788,94 @@ 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
* Modulate the amplitude of an orbit to create a "sidechain" like effect.
*
* Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")`
*
* @name duckorbit
* @synonyms duck
* @param {number | Pattern} orbit target orbit
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
* $: s("hh*16").orbit(3)
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth(1)
*
*/
export const { duck } = registerControl('duckorbit', 'duck');
/**
* the amount of ducking applied to target orbit
* The amount of ducking applied to target orbit
*
* Can vary across orbits with the ':' mininotation, e.g. `duckdepth("0.3:0.1")`.
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckdepth
* @param {number | Pattern} depth depth of modulation from 0 to 1
* @example
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>"))
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
* $: s("hh*16").orbit(3)
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth("1:0.5")
*
*/
export const { duckdepth } = registerControl('duckdepth');
/**
* the attack time of the duck effect
* The time required for the ducked signal(s) to reach their lowest volume.
* Can be used to prevent clicking or for creative rhythmic effects.
*
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckonset
* @synonyms duckons
*
* @param {number | Pattern} time The onset time in seconds
* @example
* // Clicks
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
* duckerWithClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0).postgain(0)
* @example
* // No clicks
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
* duckerWithoutClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0.01).postgain(0)
* @example
* // Rhythmic
* noise: s("pink").distort("2:1").orbit(4) // used rhythmically with 0.3 onset below
* hhat: s("hh*16").orbit(7)
* ducker: s("bd*4").bank("tr909").duckorbit("4:7").duckonset("0.3:0.003").duckattack(0.25)
*
*/
export const { duckonset } = registerControl('duckonset', 'duckons');
/**
* The time required for the ducked signal(s) to return to their normal volume.
*
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckattack
* @param {number | Pattern} time
* @synonyms duckatt
*
* @param {number | Pattern} time The attack time in seconds
* @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))
* sound: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1)
* @example
* moreduck: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
* lessduck: s("hh*16").orbit(5)
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1")
*
*/
export const { duckattack } = registerControl('duckattack', 'duckatt');
export const { drive } = registerControl('drive');
/**
* Create byte beats with custom expressions
*
@@ -618,7 +916,7 @@ export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime',
export const { channels, ch } = registerControl('channels', 'ch');
/**
* controls the pulsewidth of the pulse oscillator
* Controls the pulsewidth of the pulse oscillator
*
* @name pw
* @param {number | Pattern} pulsewidth
@@ -630,7 +928,7 @@ export const { channels, ch } = registerControl('channels', 'ch');
export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
/**
* controls the lfo rate for the pulsewidth of the pulse oscillator
* Controls the lfo rate for the pulsewidth of the pulse oscillator
*
* @name pwrate
* @param {number | Pattern} rate
@@ -642,7 +940,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
export const { pwrate } = registerControl('pwrate');
/**
* controls the lfo sweep for the pulsewidth of the pulse oscillator
* Controls the lfo sweep for the pulsewidth of the pulse oscillator
*
* @name pwsweep
* @param {number | Pattern} sweep
@@ -683,7 +981,7 @@ export const { phaserrate, ph, phaser } = registerControl(
export const { phasersweep, phs } = registerControl('phasersweep', 'phs');
/**
* The center frequency of the phaser in HZ. Defaults to 1000
* The center frequency of the phaser in HZ. Defaults to 1000
*
* @name phasercenter
* @synonyms phc
@@ -700,7 +998,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)
@@ -711,7 +1009,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp');
/**
* choose the channel the pattern is sent to in superdirt
* Choose the channel the pattern is sent to in superdirt
*
* @name channel
* @param {number | Pattern} channel channel number
@@ -990,6 +1288,196 @@ export const { fanchor } = registerControl('fanchor');
*/
// currently an alias of 'hcutoff' https://codeberg.org/uzu/strudel/issues/496
// ['hpf'],
/**
* Rate of the LFO for the lowpass filter
*
* @name lprate
* @param {number | Pattern} rate rate in hertz
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lprate("<4 8 2 1>")
*/
export const { lprate } = registerControl('lprate');
/**
* Cycle-synced rate of the LFO for the lowpass filter
*
* @name lpsync
* @param {number | Pattern} rate rate in cycles
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpsync("<4 8 2 1>")
*/
export const { lpsync } = registerControl('lpsync');
/**
* Depth of the LFO for the lowpass filter
*
* @name lpdepth
* @param {number | Pattern} depth depth of modulation
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpdepth("<1 .5 1.8 0>")
*/
export const { lpdepth } = registerControl('lpdepth');
/**
* Depth of the LFO for the lowpass filter, in HZ
*
* @name lpdepthfrequency
* @synonyms
* lpdethfreq
* @param {number | Pattern} depth depth of modulation
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpdepthfrequency("<200 500 100 0>")
*/
export const { lpdepthfrequency } = registerControl('lpdepthfrequency', 'lpdepthfreq');
/**
* Shape of the LFO for the lowpass filter
*
* @name lpshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { lpshape } = registerControl('lpshape');
/**
* DC offset of the LFO for the lowpass filter
*
* @name lpdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { lpdc } = registerControl('lpdc');
/**
* Skew of the LFO for the lowpass filter
*
* @name lpskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { lpskew } = registerControl('lpskew');
/**
* Rate of the LFO for the bandpass filter
*
* @name bprate
* @param {number | Pattern} rate rate in hertz
*/
export const { bprate } = registerControl('bprate');
/**
* Cycle-synced rate of the LFO for the bandpass filter
*
* @name bpsync
* @param {number | Pattern} rate rate in cycles
*/
export const { bpsync } = registerControl('bpsync');
/**
* Depth of the LFO for the bandpass filter
*
* @name bpdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { bpdepth } = registerControl('bpdepth');
/**
* Depth of the LFO for the bandpass filter, in HZ
*
* @name bpdepthfrequency
* @synonyms
* bpdethfreq
* @param {number | Pattern} depth depth of modulation
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).bpdepthfrequency("<200 500 100 0>")
*/
export const { bpdepthfrequency } = registerControl('bpdepthfrequency', 'bpdepthfreq');
/**
* Shape of the LFO for the bandpass filter
*
* @name bpshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { bpshape } = registerControl('bpshape');
/**
* DC offset of the LFO for the bandpass filter
*
* @name bpdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { bpdc } = registerControl('bpdc');
/**
* Skew of the LFO for the bandpass filter
*
* @name bpskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { bpskew } = registerControl('bpskew');
/**
* Rate of the LFO for the highpass filter
*
* @name hprate
* @param {number | Pattern} rate rate in hertz
*/
export const { hprate } = registerControl('hprate');
/**
* Cycle-synced rate of the LFO for the highpass filter
*
* @name hpsync
* @param {number | Pattern} rate rate in cycles
*/
export const { hpsync } = registerControl('hpsync');
/**
* Depth of the LFO for the highpass filter
*
* @name hpdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { hpdepth, hpdepthfreq } = registerControl('hpdepth');
/**
* Depth of the LFO for the hipass filter, in hz
*
* @name hpdepthfrequency
* @synonyms
* hpdethfreq
* @param {number | Pattern} depth depth of modulation
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).hpdepthfrequency("<200 500 100 0>")
*/
export const { hpdepthfrequency } = registerControl('hpdepthfrequency', 'hpdepthfreq');
/**
* Shape of the LFO for the highpass filter
*
* @name hpshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { hpshape } = registerControl('hpshape');
/**
* DC offset of the LFO for the highpass filter
*
* @name hpdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { hpdc } = registerControl('hpdc');
/**
* Skew of the LFO for the highpass filter
*
* @name hpskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { hpskew } = registerControl('hpskew');
/**
* Applies a vibrato to the frequency of the oscillator.
*
@@ -1063,7 +1551,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq');
* @name djf
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
* @example
* n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc()
* n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>")
*
*/
export const { djf } = registerControl('djf');
@@ -1097,14 +1585,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 +1683,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()
@@ -1363,12 +1865,12 @@ export const { nudge } = registerControl('nudge');
* Sets the default octave of a synth.
*
* @name octave
* @synonyms oct
* @param {number | Pattern} octave octave number
* @example
* n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc()
* @superDirtOnly
* n("0,4,7").scale("F:minor").s('supersaw').octave("<0 1 2 3>")
*/
export const { octave } = registerControl('octave');
export const { octave, oct } = registerControl('octave', 'oct');
// ['ophatdecay'],
// TODO: example
@@ -1376,6 +1878,7 @@ export const { octave } = registerControl('octave');
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
*
* @name orbit
* @synonyms o
* @param {number | Pattern} number
* @example
* stack(
@@ -1383,7 +1886,7 @@ export const { octave } = registerControl('octave');
* s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2)
* )
*/
export const { orbit } = registerControl('orbit');
export const { orbit } = registerControl('orbit', 'o');
// TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that
export const { overgain } = registerControl('overgain');
// TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
@@ -1438,17 +1941,64 @@ export const { semitone } = registerControl('semitone');
// TODO: synth param
export const { voice } = registerControl('voice');
// voicings // https://codeberg.org/uzu/strudel/issues/506
// chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings
/**
* The chord to voice
* @name chord
* @param {string | Pattern} symbols chord symbols to voice e.g., C, Eb, Fm7, G7. The symbols can be defined via addVoicings
* @example
* chord("<Am C D F Am E Am E>").voicing()
**/
export const { chord } = registerControl('chord');
// which dictionary to use for the voicings
/**
* Which dictionary to use for the voicings. This falls back to the default dictionary if not provided
*
* @name dictionary
* @param {string} dictionaryName which dictionary (having been defined with `addVoicings`) to use
* @example
* addVoicings('house', {
'': ['7 12 16', '0 7 16', '4 7 12'],
'm': ['0 3 7']
})
chord("<Am C D F Am E Am E>")
.dict('house').anchor(66)
.voicing().room(.5)
**/
export const { dictionary, dict } = registerControl('dictionary', 'dict');
// the top note to align the voicing to, defaults to c5
/** The top note to align the voicing to. Defaults to c5
*
* @name anchor
* @param {string | Pattern} anchorNote the note to align the voicings to
* @example
* anchor("<c4 g4 c5 g5>").chord("C").voicing()
**/
export const { anchor } = registerControl('anchor');
// how the voicing is offset from the anchored position
/**
* Sets how the voicing is offset from the anchored position
*
* @name offset
* @param {number | Pattern} shift the amount to shift the voicing up or down
* @example
* chord("<Am C D F Am E Am E>").offset("<0 1 2 3 4 5>") // alter the voicing each time
**/
export const { offset } = registerControl('offset');
// how many octaves are voicing steps spread apart, defaults to 1
/**
* How many octaves are voicing steps spread apart, defaults to 1
*
* @name octaves
* @param {number | Pattern} count the number of octaves
* @example
* chord("<Am C D F Am E Am E>").octaves("<2 4>").voicing()
**/
export const { octaves } = registerControl('octaves');
// below = anchor note will be removed from the voicing, useful for melody harmonization
/**
* Remove anchor note from the voicing. Useful for melody harmonization
*
* @name mode
* @param {string | Pattern} modeName one of {below | above | duck | root}
* @example
* mode("<below above duck root>").chord("C").voicing()
*
**/
export const { mode } = registerControl(['mode', 'anchor']);
/**
@@ -1575,19 +2125,52 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size',
export const { shape } = registerControl(['shape', 'shapevol']);
/**
* Wave shaping distortion. CAUTION: it can get loud.
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output.
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. Third option sets the waveshaping type.
* Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)
*
* @name distort
* @synonyms dist
* @param {number | Pattern} distortion
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
* @param {number | string | Pattern} type type of distortion to apply
* @example
* s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>")
* @example
* note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4")
* @example
* s("bd:4*4").bank("tr808").distort("3:0.5:diode")
*
*/
export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dist');
export const { distort, dist } = registerControl(['distort', 'distortvol', 'distorttype'], 'dist');
/**
* Postgain for waveshaping distortion.
*
* @name distortvol
* @synonyms distvol
* @param {number | Pattern} volume linear postgain of the distortion
* @example
* s("bd*4").bank("tr909").distort(2).distortvol(0.8)
*/
export const { distortvol } = registerControl('distortvol', 'distvol');
/**
* Type of waveshaping distortion to apply.
*
* @name distorttype
* @synonyms disttype
* @param {number | string | Pattern} type type of distortion to apply
* @example
* s("bd*4").bank("tr909").distort(2).distorttype("<0 1 2>")
*
* @example
* s("sine").note("F1*2").release(1)
* .penv(24).pdecay(0.05)
* .distort(rand.range(1, 8))
* .distorttype("<fold chebyshev scurve diode asym sinefold>")
*/
export const { distorttype } = registerControl('distorttype', 'disttype');
/**
* Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")`
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
@@ -1727,7 +2310,6 @@ export const { tsdelay } = registerControl('tsdelay');
export const { real } = registerControl('real');
export const { imag } = registerControl('imag');
export const { enhance } = registerControl('enhance');
export const { partials } = registerControl('partials');
export const { comb } = registerControl('comb');
export const { smear } = registerControl('smear');
export const { scram } = registerControl('scram');
@@ -1975,6 +2557,24 @@ export const { miditouch } = registerControl('miditouch');
// TODO: what is this?
export const { polyTouch } = registerControl('polyTouch');
/**
* The host to send open sound control messages to. Requires running the OSC bridge.
* @name oschost
* @param {string | Pattern} oschost e.g. 'localhost'
* @example
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
*/
export const { oschost } = registerControl('oschost');
/**
* The port to send open sound control messages to. Requires running the OSC bridge.
* @name oscport
* @param {number | Pattern} oscport e.g. 57120
* @example
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
*/
export const { oscport } = registerControl('oscport');
export const getControlName = (alias) => {
if (controlAlias.has(alias)) {
return controlAlias.get(alias);
+1 -1
View File
@@ -57,7 +57,7 @@ export class Cyclist {
}
// query the pattern for events
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
haps.forEach((hap) => {
if (hap.hasOnset()) {
+7 -7
View File
@@ -35,18 +35,18 @@ const right = function (n, x) {
return result;
};
const _bjork = function (n, x) {
const _bjorklund = function (n, x) {
const [ons, offs] = n;
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
return Math.min(ons, offs) <= 1 ? [n, x] : _bjorklund(...(ons > offs ? left(n, x) : right(n, x)));
};
export const bjork = function (ons, steps) {
export const bjorklund = function (ons, steps) {
const inverted = ons < 0;
const absOns = Math.abs(ons);
const offs = steps - absOns;
const ones = Array(absOns).fill([1]);
const zeros = Array(offs).fill([0]);
const result = _bjork([absOns, offs], [ones, zeros]);
const result = _bjorklund([absOns, offs], [ones, zeros]);
const pattern = flatten(result[1][0]).concat(flatten(result[1][1]));
return inverted ? pattern.map((x) => 1 - x) : pattern;
};
@@ -128,7 +128,7 @@ export const bjork = function (ons, steps) {
*/
const _euclidRot = function (pulses, steps, rotation) {
const b = bjork(pulses, steps);
const b = bjorklund(pulses, steps);
if (rotation) {
return rotate(b, -rotation);
}
@@ -139,7 +139,7 @@ export const euclid = register('euclid', function (pulses, steps, pat) {
return pat.struct(_euclidRot(pulses, steps, 0));
});
export const e = register('e', function (euc, pat) {
export const bjork = register('bjork', function (euc, pat) {
if (!Array.isArray(euc)) {
euc = [euc];
}
@@ -216,6 +216,6 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s
* .pan(sine.slow(8))
*/
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc);
const morphed = _morph(bjorklund(pulses, steps), new Array(pulses).fill(1), perc);
return pat.struct(morphed).setSteps(steps);
});
+2
View File
@@ -126,6 +126,8 @@ export const lcm = (...fractions) => {
);
};
export const isFraction = (x) => x instanceof Fraction;
fraction._original = Fraction;
export default fraction;
+3 -2
View File
@@ -5,8 +5,9 @@ let debounce = 1000,
lastTime;
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
if (process.env.NODE_ENV === 'development') {
console.error(e);
}
logger(`[${origin}] error: ${e.message}`);
}
+1 -2
View File
@@ -38,8 +38,7 @@ export class NeoCyclist {
if (this.started === false) {
return;
}
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' });
haps.forEach((hap) => {
if (hap.hasOnset()) {
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "1.2.3",
"version": "1.2.5",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
@@ -37,5 +37,8 @@
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
+218 -14
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import TimeSpan from './timespan.mjs';
import Fraction, { lcm } from './fraction.mjs';
import Fraction, { isFraction, lcm } from './fraction.mjs';
import Hap from './hap.mjs';
import State from './state.mjs';
import { unionWithObj } from './value.mjs';
@@ -25,7 +25,7 @@ import {
stringifyValues,
} from './util.mjs';
import drawLine from './drawLine.mjs';
import { logger } from './logger.mjs';
import { errorLogger, logger } from './logger.mjs';
let stringParser;
@@ -98,10 +98,7 @@ export class Pattern {
// runs func on query state
withState(func) {
return this.withHaps((haps, state) => {
func(state);
return haps;
});
return new Pattern((state) => this.query(func(state)));
}
/**
@@ -417,7 +414,7 @@ export class Pattern {
try {
return this.query(new State(new TimeSpan(begin, end), controls));
} catch (err) {
logger(`[query]: ${err.message}`, 'error');
errorLogger(err, 'query');
return [];
}
}
@@ -999,7 +996,7 @@ addToPrototype('weaveWith', function (t, ...funcs) {
// compose matrix functions
function _nonArrayObject(x) {
return !Array.isArray(x) && typeof x === 'object';
return !Array.isArray(x) && typeof x === 'object' && !isFraction(x);
}
function _composeOp(a, b, func) {
if (_nonArrayObject(a) || _nonArrayObject(b)) {
@@ -1246,7 +1243,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 +1297,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 +1381,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"])
*
@@ -2397,6 +2396,57 @@ export const stut = register('stut', function (times, feedback, time, pat) {
return pat._echoWith(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;
});
/**
* Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played.
* @name iter
@@ -2524,8 +2574,8 @@ export const { chunkBack, chunkback } = register(
* @returns Pattern
* @example
* "<0 8> 1 2 3 4 5 6 7"
* .fastChunk(4, x => x.color('red')).slow(2)
* .scale("C2:major").note()
* .fastChunk(4, x => x.color('red')).slow(2)
*/
export const { fastchunk, fastChunk } = register(
['fastchunk', 'fastChunk'],
@@ -2948,6 +2998,24 @@ export const extend = stepRegister('extend', function (factor, pat) {
return pat.fast(factor).expand(factor);
});
/**
* *Experimental*
*
* `replicate` is similar to `fast` in that it increases its density, but it also increases the step count
* accordingly. So `stepcat("a b".replicate(2), "c d")` would be the same as `"a b a b c d"`, whereas
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
*
* TODO: find out how this function differs from extend
* @example
* stepcat(
* sound("bd bd - cp").replicate(2),
* sound("bd - sd -")
* ).pace(8)
*/
export const replicate = stepRegister('replicate', function (factor, pat) {
return pat.repeatCycles(factor).fast(factor).expand(factor);
});
/**
* *Experimental*
*
@@ -3483,3 +3551,139 @@ export const morph = (frompat, topat, bypat) => {
bypat = reify(bypat);
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
};
/**
* Soft-clipping distortion
*
* @name soft
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Hard-clipping distortion
*
* @name hard
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Cubic polynomial distortion
*
* @name cubic
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Diode-emulating distortion
*
* @name diode
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Asymmetrical diode distortion
*
* @name asym
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Wavefolding distortion
*
* @name fold
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Wavefolding distortion composed with sinusoid
*
* @name sinefold
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Distortion via Chebyshev polynomials
*
* @name chebyshev
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev'];
for (const name of distAlgoNames) {
// Add aliases for distortion algorithms
Pattern.prototype[name] = function (args) {
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
return this.distort(argsPat);
};
}
/**
* Turns a list of patterns into a single pattern which outputs list-values
*
* @name parray
* @returns Pattern
*/
export const parray = (pats) => {
const pack = (...xs) => xs;
let acc = pure(curry(pack, null, pats.length));
for (const p of pats) acc = acc.appBoth(reify(p));
return acc;
};
const _ensureListPattern = (list) => {
if (Array.isArray(list)) {
return parray(list);
}
return reify(list);
};
/**
* Scale the magnitude of the harmonics of one of the core synths ('sine', 'tri', 'saw', ..)
*
* Can also be used to create a new synth via `s('user').partials(...)`
*
* @name partials
* @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped)
* @example
* s("user").seg(16).n(irand(8)).scale("A:major")
* .partials([1, 0, 1, 0, 0, 1])
* @example
* s("saw").seg(8).n(irand(12)).scale("G#:minor")
* .partials(binaryL(irand(256).add("1")))
*/
Pattern.prototype.partials = function (list) {
return this.withValue((v) => (l) => ({ ...v, partials: l })).appLeft(_ensureListPattern(list));
};
// Also create a top-level function
export const partials = (list) => {
return _ensureListPattern(list).as('partials');
};
/**
* Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases
*
* @name phases
* @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped)
* @example
* // Phase cancellation
* s("saw").seg(8).n(irand(12)).scale("G#1:minor")
* .partials(partials([1, 1, 1]))
* .superimpose(x => x.phases([0.5, 0.5, 0.5]))
*/
Pattern.prototype.phases = function (list) {
return this.withValue((v) => (l) => ({ ...v, phases: l })).appLeft(_ensureListPattern(list));
};
// Also create a top-level function
export const phases = (list) => {
return _ensureListPattern(list).as('phases');
};
+37 -9
View File
@@ -74,6 +74,14 @@ export function repl({
return silence;
};
// helper to get a patternified pure value out
function unpure(pat) {
if (pat._Pattern) {
return pat.__pure;
}
return pat;
}
const setPattern = async (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
await scheduler.setPattern(pattern, autostart);
@@ -85,7 +93,10 @@ export function repl({
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
const setCps = (cps) => scheduler.setCps(cps);
const setCps = (cps) => {
scheduler.setCps(unpure(cps));
return silence;
};
/**
* Changes the global tempo to the given cycles per minute
@@ -97,7 +108,10 @@ export function repl({
* setcpm(140/4) // =140 bpm in 4/4
* $: s("bd*4,[- sd]*2").bank('tr707')
*/
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
const setCpm = (cpm) => {
scheduler.setCps(unpure(cpm) / 60);
return silence;
};
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
@@ -138,9 +152,9 @@ export function repl({
// allows muting a pattern x with x_ or _x
return silence;
}
if (id === '$') {
if (id.includes('$')) {
// allows adding anonymous patterns with $:
id = `$${anonymousIndex}`;
id = `${id}${anonymousIndex}`;
anonymousIndex++;
}
pPatterns[id] = this;
@@ -200,7 +214,21 @@ export function repl({
}
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
if (Object.keys(pPatterns).length) {
let patterns = Object.values(pPatterns);
let patterns = [];
let soloActive = false;
for (const [key, value] of Object.entries(pPatterns)) {
// handle soloed patterns ex: S$: s("bd!4")
const isSolod = key.length > 1 && key.startsWith('S');
if (isSolod && soloActive === false) {
// first time we see a soloed pattern, clear existing patterns
patterns = [];
soloActive = true;
}
if (!soloActive || (soloActive && isSolod)) {
const valWithState = value.withState((state) => state.setControls({ id: key }));
patterns.push(valWithState);
}
}
if (eachTransform) {
// Explicit lambda so only element (not index and array) are passed
patterns = patterns.map((x) => eachTransform(x));
@@ -210,13 +238,13 @@ export function repl({
pattern = eachTransform(pattern);
}
if (allTransforms.length) {
for (let i in allTransforms) {
pattern = allTransforms[i](pattern);
for (const transform of allTransforms) {
pattern = transform(pattern);
}
}
if (!isPattern(pattern)) {
const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
pattern = silence;
}
logger(`[eval] code updated`);
pattern = await setPattern(pattern, autostart);
+48 -3
View File
@@ -228,7 +228,7 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
export const run = (n) => saw.range(0, n).round().segment(n);
/**
* Creates a pattern from a binary number.
* Creates a binary pattern from a number.
*
* @name binary
* @param {number} n - input number to convert to binary
@@ -242,7 +242,7 @@ export const binary = (n) => {
};
/**
* Creates a pattern from a binary number, padded to n bits long.
* Creates a binary pattern from a number, padded to n bits long.
*
* @name binaryN
* @param {number} n - input number to convert to binary
@@ -258,6 +258,51 @@ export const binaryN = (n, nBits = 16) => {
return reify(n).segment(nBits).brshift(bitPos).band(pure(1));
};
/**
* Creates a binary list pattern from a number.
*
* @name binaryL
* @param {number} n - input number to convert to binary
* s("saw").seg(8)
* .partials(binaryL(irand(4096).add(1)))
*/
export const binaryL = (n) => {
const nBits = reify(n).log2(0).floor().add(1);
return binaryNL(n, nBits);
};
/**
* Creates a binary list pattern from a number, padded to n bits long.
*
* @name binaryNL
* @param {number} n - input number to convert to binary
* @param {number} nBits - pattern length, defaults to 16
*/
export const binaryNL = (n, nBits = 16) => {
return reify(n)
.withValue((v) => (bits) => {
const bList = [];
for (let i = bits - 1; i >= 0; i--) {
bList.push((v >> i) & 1);
}
return bList;
})
.appLeft(reify(nBits));
};
/**
* Creates a list of random numbers of the given length
*
* @name randL
* @param {number} n Number of random numbers to sample
* @example
* s("saw").seg(16).n(irand(12)).scale("F1:minor")
* .partials(randL(8))
*/
export const randL = (n) => {
return signal((t) => (nVal) => timeToRands(t, nVal).map(Math.abs)).appLeft(reify(n));
};
export const randrun = (n) => {
return signal((t) => {
// Without adding 0.5, the first cycle is always 0,1,2,3,...
@@ -479,7 +524,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
* @example
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
* @example
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
* wchooseCycles(["c c c",5], ["a a a",3], ["f f f",1]).fast(4).note()
* @example
* // The probability can itself be a pattern
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
+2 -2
View File
@@ -19,9 +19,9 @@ export class State {
return this.setSpan(func(this.span));
}
// Returns new State with different controls
// Returns new State with added controls.
setControls(controls) {
return new State(this.span, controls);
return new State(this.span, { ...this.controls, ...controls });
}
}
+8 -8
View File
@@ -1,14 +1,14 @@
import { bjork } from '../euclid.mjs';
import { bjorklund } from '../euclid.mjs';
import { describe, expect, it } from 'vitest';
import { fastcat } from '../pattern.mjs';
describe('bjork', () => {
it('should apply bjorklund to ons and steps', () => {
expect(bjork(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
expect(bjork(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
expect(bjork(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
expect(bjork(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
expect(bjork(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
describe('bjorklund', () => {
it('should apply bjorklundlund to ons and steps', () => {
expect(bjorklund(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
expect(bjorklund(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
expect(bjorklund(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
expect(bjorklund(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
expect(bjorklund(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
});
});
+8 -4
View File
@@ -7,13 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
import { logger } from './logger.mjs';
// returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
export const isNoteWithOctave = (name) => /^[a-gA-G][#bsf]*[0-9]*$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]*$/.test(name);
export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
if (!pc) {
return [];
}
@@ -23,6 +23,10 @@ export const tokenizeNote = (note) => {
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
const accs = { '#': 1, b: -1, s: 1, f: -1 };
export const getAccidentalsOffset = (accidentals) => {
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
};
// turns the given note into its midi number representation
export const noteToMidi = (note, defaultOctave = 3) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
@@ -30,7 +34,7 @@ export const noteToMidi = (note, defaultOctave = 3) => {
throw new Error('not a note: "' + note + '"');
}
const chroma = chromas[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
const offset = getAccidentalsOffset(acc);
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const midiToFreq = (n) => {
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "1.2.4",
"version": "1.2.6",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
@@ -38,5 +38,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+4 -1
View File
@@ -25,5 +25,8 @@
"@strudel/core": "workspace:*",
"@tauri-apps/api": "^2.2.0"
},
"homepage": "https://codeberg.org/uzu/strudel#readme"
"homepage": "https://codeberg.org/uzu/strudel#readme",
"engines": {
"node": ">=18.0.0"
}
}
+9
View File
@@ -84,9 +84,18 @@ Pattern.prototype.onPaint = function (painter) {
state.controls.painters = [];
}
state.controls.painters.push(painter);
return state;
});
};
// TODO - Why isn't this pure deep copy not working?
// Pattern.prototype.onPaint = function (painter) {
// return this.withState((state) => {
// const painters = state.controls.painters ? [...state.controls.painters, painter] : [painter];
// return new State(state.span, { ...state.controls, painters });
// });
// };
Pattern.prototype.getPainters = function () {
let painters = [];
this.queryArc(0, 0, { painters });
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
"version": "1.2.3",
"version": "1.2.5",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
@@ -33,5 +33,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+4 -1
View File
@@ -20,5 +20,8 @@
"bugs": {
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://codeberg.org/uzu/strudel#readme"
"homepage": "https://codeberg.org/uzu/strudel#readme",
"engines": {
"node": ">=18.0.0"
}
}
+6
View File
@@ -40,6 +40,12 @@ const pattern = sequence([
- D-Pad
- `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase)
- Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight`(or `tglU`, `tglD`, `tglL`, `tglR`)
- Stick Buttons
- `l3`, `r3` (or `ls`, `rs`)
- Toggle versions: `tglL3`, `tglR3` (or `tglLS`, `tglRS`)
- System Buttons
- `start`, `back` (or uppercase `START`, `BACK`)
- Toggle versions: `tglStart`, `tglBack` (or `tglSTART`, `tglBACK`)
### Analog Sticks
- Left Stick
+4
View File
@@ -29,6 +29,10 @@ The gamepad module provides access to buttons and analog sticks as normalized si
| | Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT` |
| D-Pad | `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) |
| | Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight` (or `tglU`, `tglD`, `tglL`, `tglR`) |
| Stick Buttons | `l3`, 'r3' (or `ls`, `rs`) |
| | Toggle versions: `tglL3`, 'tglR3' (or `tglLs`, `tglRs`) |
| System Buttons | `start`, `back` (or uppercase `START`, `BACK`) |
| | Toggle versions: `tglStart`, `tglBack` (or `tglSTART`, `tglBACK`) |
### Analog Sticks
+4
View File
@@ -14,6 +14,10 @@ export const buttonMap = {
rt: 7,
back: 8,
start: 9,
l3: 10,
ls: 10,
r3: 11,
rs: 11,
u: 12,
up: 12,
d: 13,
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/gamepad",
"version": "1.2.3",
"version": "1.2.5",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
@@ -33,5 +33,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -34,5 +34,8 @@
"devDependencies": {
"tree-sitter-haskell": "^0.23.1",
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+1
View File
@@ -35,6 +35,7 @@ export async function initHydra(options = {}) {
hydra.synth.s0.init({ src: canvas });
}
}
return hydra;
}
export function clearHydra() {
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
"version": "1.2.3",
"version": "1.2.5",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
@@ -40,5 +40,8 @@
"devDependencies": {
"pkg": "^5.8.1",
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+59 -42
View File
@@ -5,9 +5,10 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
import { Pattern, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi, getControlName } from '@strudel/core';
import { Note } from 'webmidi';
import { scheduleAtTime } from '../superdough/helpers.mjs';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -190,7 +191,7 @@ function mapCC(mapping, value) {
}
// sends a cc message to the given device on the given channel
function sendCC(ccn, ccv, device, midichan, timeOffsetString) {
function sendCC(ccn, ccv, device, midichan, targetTime) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1');
}
@@ -198,19 +199,23 @@ function sendCC(ccn, ccv, device, midichan, timeOffsetString) {
throw new Error('expected ccn to be a number or a string');
}
const scaled = Math.round(ccv * 127);
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
scheduleAtTime(() => {
device.sendControlChange(ccn, scaled, midichan);
}, targetTime);
}
// sends a program change message to the given device on the given channel
function sendProgramChange(progNum, device, midichan, timeOffsetString) {
function sendProgramChange(progNum, device, midichan, targetTime) {
if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) {
throw new Error('expected progNum (program change) to be a number between 0 and 127');
}
device.sendProgramChange(progNum, midichan, { time: timeOffsetString });
scheduleAtTime(() => {
device.sendProgramChange(progNum, midichan);
}, targetTime);
}
// sends a sysex message to the given device on the given channel
function sendSysex(sysexid, sysexdata, device, timeOffsetString) {
function sendSysex(sysexid, sysexdata, device, targetTime) {
if (Array.isArray(sysexid)) {
if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all sysexid bytes must be integers between 0 and 255');
@@ -225,11 +230,13 @@ function sendSysex(sysexid, sysexdata, device, timeOffsetString) {
if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all sysex bytes must be integers between 0 and 255');
}
device.sendSysex(sysexid, sysexdata, { time: timeOffsetString });
scheduleAtTime(() => {
device.sendSysex(sysexid, sysexdata);
}, targetTime);
}
// sends a NRPN message to the given device on the given channel
function sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString) {
function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) {
if (Array.isArray(nrpnn)) {
if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all nrpnn bytes must be integers between 0 and 255');
@@ -237,28 +244,34 @@ function sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString) {
} else if (!Number.isInteger(nrpv) || nrpv < 0 || nrpv > 255) {
throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers');
}
device.sendNRPN(nrpnn, nrpv, midichan, { time: timeOffsetString });
scheduleAtTime(() => {
device.sendNRPN(nrpnn, nrpv, midichan);
}, targetTime);
}
// sends a pitch bend message to the given device on the given channel
function sendPitchBend(midibend, device, midichan, timeOffsetString) {
function sendPitchBend(midibend, device, midichan, targetTime) {
if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) {
throw new Error('expected midibend to be a number between -1 and 1');
}
device.sendPitchBend(midibend, midichan, { time: timeOffsetString });
scheduleAtTime(() => {
device.sendPitchBend(midibend, midichan);
}, targetTime);
}
// sends a channel aftertouch message to the given device on the given channel
function sendAftertouch(miditouch, device, midichan, timeOffsetString) {
function sendAftertouch(miditouch, device, midichan, targetTime) {
if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) {
throw new Error('expected miditouch to be a number between 0 and 1');
}
device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString });
scheduleAtTime(() => {
device.sendChannelAftertouch(miditouch, midichan);
}, targetTime);
}
// sends a note message to the given device on the given channel
function sendNote(note, velocity, duration, device, midichan, timeOffsetString) {
function sendNote(note, velocity, duration, device, midichan, targetTime) {
if (note == null || note === '') {
throw new Error('note cannot be null or empty');
}
@@ -268,12 +281,12 @@ function sendNote(note, velocity, duration, device, midichan, timeOffsetString)
if (duration != null && (typeof duration !== 'number' || duration < 0)) {
throw new Error('duration must be a positive number');
}
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
const midiNote = new Note(midiNumber, { attack: velocity, duration });
device.playNote(midiNote, midichan, {
time: timeOffsetString,
});
scheduleAtTime(() => {
device.playNote(midiNote, midichan);
}, targetTime);
}
/**
@@ -309,7 +322,6 @@ Pattern.prototype.midi = function (midiport, options = {}) {
let midiConfig = {
// Default configuration values
isController: false, // Disable sending notes for midi controllers
latencyMs: 34, // Default latency to get audio engine to line up in ms
noteOffsetMs: 10, // Default note-off offset to prevent glitching in ms
midichannel: 1, // Default MIDI channel
velocity: 0.9, // Default velocity
@@ -333,18 +345,13 @@ Pattern.prototype.midi = function (midiport, options = {}) {
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});
return this.onTrigger((hap, currentTime, cps, targetTime) => {
return this.onTrigger((hap, _currentTime, cps, targetTime) => {
if (!WebMidi.enabled) {
logger('Midi not enabled');
return;
}
hap.ensureObjectValue();
//magic number to get audio engine to line up, can probably be calculated somehow
const latencyMs = midiConfig.latencyMs;
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
// midi event values from hap with configurable defaults
let {
note,
@@ -380,7 +387,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// if midimap is set, send a cc messages from defined controls
if (midicontrolMap.has(midimap)) {
const ccs = mapCC(midicontrolMap.get(midimap), hap.value);
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, targetTime));
} else if (midimap !== 'default') {
// Add warning when a non-existent midimap is specified
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
@@ -392,12 +399,12 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// try to prevent glitching by subtracting noteOffsetMs from the duration length
const duration = (hap.duration.valueOf() / cps) * 1000 - midiConfig.noteOffsetMs;
sendNote(note, velocity, duration, device, midichan, timeOffsetString);
sendNote(note, velocity, duration, device, midichan, targetTime);
}
// Handle program change
if (progNum !== undefined) {
sendProgramChange(progNum, device, midichan, timeOffsetString);
sendProgramChange(progNum, device, midichan, targetTime);
}
// Handle sysex
@@ -407,53 +414,63 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// if sysexid is an array the first byte is 0x00
if (sysexid !== undefined && sysexdata !== undefined) {
sendSysex(sysexid, sysexdata, device, timeOffsetString);
sendSysex(sysexid, sysexdata, device, targetTime);
}
// Handle control change
if (ccv !== undefined && ccn !== undefined) {
sendCC(ccn, ccv, device, midichan, timeOffsetString);
sendCC(ccn, ccv, device, midichan, targetTime);
}
// Handle NRPN non-registered parameter number
if (nrpnn !== undefined && nrpv !== undefined) {
sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString);
sendNRPN(nrpnn, nrpv, device, midichan, targetTime);
}
// Handle midibend
if (midibend !== undefined) {
sendPitchBend(midibend, device, midichan, timeOffsetString);
sendPitchBend(midibend, device, midichan, targetTime);
}
// Handle miditouch
if (miditouch !== undefined) {
sendAftertouch(miditouch, device, midichan, timeOffsetString);
sendAftertouch(miditouch, device, midichan, targetTime);
}
// Handle midicmd
if (hap.whole.begin + 0 === 0) {
// we need to start here because we have the timing info
device.sendStart({ time: timeOffsetString });
scheduleAtTime(() => {
device.sendStart();
}, targetTime);
}
if (['clock', 'midiClock'].includes(midicmd)) {
device.sendClock({ time: timeOffsetString });
scheduleAtTime(() => {
device.sendClock();
}, targetTime);
} else if (['start'].includes(midicmd)) {
device.sendStart({ time: timeOffsetString });
scheduleAtTime(() => {
device.sendStart();
}, targetTime);
} else if (['stop'].includes(midicmd)) {
device.sendStop({ time: timeOffsetString });
scheduleAtTime(() => {
device.sendStop();
}, targetTime);
} else if (['continue'].includes(midicmd)) {
device.sendContinue({ time: timeOffsetString });
scheduleAtTime(() => {
device.sendContinue();
}, targetTime);
} else if (Array.isArray(midicmd)) {
if (midicmd[0] === 'progNum') {
sendProgramChange(midicmd[1], device, midichan, timeOffsetString);
sendProgramChange(midicmd[1], device, midichan, targetTime);
} else if (midicmd[0] === 'cc') {
if (midicmd.length === 2) {
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, timeOffsetString);
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, targetTime);
}
} else if (midicmd[0] === 'sysex') {
if (midicmd.length === 3) {
const [_, id, data] = midicmd;
sendSysex(id, data, device, timeOffsetString);
sendSysex(id, data, device, targetTime);
}
}
}
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "1.2.4",
"version": "1.2.6",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
@@ -35,5 +35,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
"version": "1.2.3",
"version": "1.2.5",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
@@ -38,5 +38,8 @@
"peggy": "^4.2.0",
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
+8 -5
View File
@@ -21,13 +21,14 @@ export class MondoParser {
close_curly: /^\}/,
number: /^-?[0-9]*\.?[0-9]+/, // before pipe!
// TODO: better error handling when "-" is used as rest, e.g "s [- bd]"
op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? ..
op: /^[*/:!@%?+\-&]|^\.{2}/, // * / : ! @ % ? ..
// dollar: /^\$/,
pipe: /^#/,
stack: /^[,$]/,
or: /^[|]/,
plain: /^[a-zA-Z0-9-~_^#]+/,
};
op_precedence = [['*', '/', ':', '!', '@', '%', '?', '+', '-', '..'], ['&']];
// matches next token
next_token(code, offset = 0) {
for (let type in this.token_types) {
@@ -150,9 +151,9 @@ export class MondoParser {
}
return children;
}
desugar_ops(children) {
desugar_ops(children, types) {
while (true) {
let opIndex = children.findIndex((child) => child.type === 'op');
let opIndex = children.findIndex((child) => child.type === 'op' && types.includes(child.value));
if (opIndex === -1) break;
const op = { type: 'plain', value: children[opIndex].value };
if (opIndex === children.length - 1) {
@@ -263,8 +264,10 @@ export class MondoParser {
// the type we've removed before splitting needs to be added back
children = [{ type: 'plain', value: type }, ...children];
}
children = this.desugar_ops(children);
// children = this.desugar_pipes(children, (children) => this.desugar_dollars(children));
// for each precendence group, call desugar_ops once
this.op_precedence.forEach((ops) => {
children = this.desugar_ops(children, ops);
});
children = this.desugar_pipes(children);
return children;
}),
+3
View File
@@ -33,5 +33,8 @@
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
+1
View File
@@ -117,6 +117,7 @@ describe('mondo sugar', () => {
it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)'));
it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))'));
it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))'));
it('should desugar x&y:z', () => expect(desguar('bd&3:8')).toEqual('(& (: 8 3) bd)'));
it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)'));
/* it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)'));
it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))'));
+6 -4
View File
@@ -5,12 +5,13 @@ import {
slow,
seq,
stepcat,
extend,
replicate,
expand,
pace,
chooseIn,
degradeBy,
silence,
bjork,
} from '@strudel/core';
import { registerLanguage } from '@strudel/transpiler';
import { MondoRunner } from 'mondolang';
@@ -36,10 +37,11 @@ lib.square = (...args) => stepcat(...args).setSteps(1);
lib.angle = (...args) => stepcat(...args).pace(1);
lib['*'] = fast;
lib['/'] = slow;
lib['!'] = extend;
lib['!'] = replicate;
lib['@'] = expand;
lib['%'] = pace;
lib['?'] = degradeBy; // todo: default 0.5 not working..
lib['&'] = bjork;
lib[':'] = tail;
lib['..'] = range;
lib['def'] = () => silence;
@@ -85,7 +87,7 @@ function evaluator(node, scope) {
let pat;
if (type === 'plain' && typeof variable !== 'undefined') {
// some function names are not patternable, so we skip reification here
if (['!', 'extend', '@', 'expand', 'square', 'angle'].includes(value)) {
if (['!', 'extend', '@', 'expand', 'square', 'angle', 'all', 'setcpm', 'setcps'].includes(value)) {
return variable;
}
pat = reify(variable);
@@ -108,7 +110,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}]`;
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mondo",
"version": "1.1.1",
"version": "1.1.5",
"description": "mondo notation for strudel",
"main": "mondough.mjs",
"type": "module",
@@ -40,5 +40,8 @@
"mondo": "*",
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
+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',
},
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/motion",
"version": "1.2.3",
"version": "1.2.5",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
@@ -33,5 +33,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+1 -1
View File
@@ -82,7 +82,7 @@ Pattern.prototype.mqtt = function (
cx.connect(props);
}
return this.withHap((hap) => {
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
const onTrigger = (hap, currentTime, cps, targetTime) => {
let msg_topic = topic;
if (!cx || !cx.isConnected()) {
return;
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mqtt",
"version": "1.2.3",
"version": "1.2.5",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
@@ -34,5 +34,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+27 -17
View File
@@ -4,21 +4,13 @@ OSC output for strudel patterns! Currently only tested with super collider / sup
## Usage
OSC will only work if you run the REPL locally + the OSC server besides it:
Assuming you have [node.js](https://nodejs.org/) installed, you can run the osc bridge server via:
From the project root:
```js
npm run repl
```sh
npx @strudel/osc
```
and in a seperate shell:
```js
npm run osc
```
This should give you
You should see something like:
```log
osc client running on port 57120
@@ -26,14 +18,32 @@ osc server running on port 57121
websocket server running on port 8080
```
Now open Supercollider (with the super dirt startup file)
### --port
Now open the REPL and type:
By default it will use port 57120 for the osc client, which is what [superdirt](https://github.com/musikinformatik/SuperDirt) uses. You can change it via the `--port` option:
```js
s("<bd sd> hh").osc()
```sh
npx @strudel/osc --port 7771 # classic dirt
```
or just [click here](https://strudel.cc/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
### --debug
To log all incoming osc messages, add the `--debug` flag:
```sh
npx @strudel/osc --debug
```
## Usage in Strudel
To test it in strudel, you have can use `all(osc)` to send all events through osc:
```js
$: s("bd*4")
all(osc)
```
[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D)
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
+19 -20
View File
@@ -4,28 +4,25 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import OSC from 'osc-js';
import { logger, parseNumeral, Pattern, isNote, noteToMidi, ClockCollator } from '@strudel/core';
import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core';
let connection; // Promise<OSC>
function connect() {
if (!connection) {
// make sure this runs only once
connection = new Promise((resolve, reject) => {
const osc = new OSC();
osc.open();
osc.on('open', () => {
const url = osc.options?.plugin?.socket?.url;
logger(`[osc] connected${url ? ` to ${url}` : ''}`);
resolve(osc);
const ws = new WebSocket('ws://localhost:8080');
ws.addEventListener('open', (event) => {
logger(`[osc] websocket connected`);
resolve(ws);
});
osc.on('close', () => {
ws.addEventListener('close', (event) => {
logger(`[osc] websocket closed`);
connection = undefined; // allows new connection afterwards
console.log('[osc] disconnected');
reject('OSC connection closed');
});
osc.on('error', (err) => reject(err));
ws.addEventListener('error', (err) => reject(err));
}).catch((err) => {
connection = undefined;
throw new Error('Could not connect to OSC server. Is it running?');
@@ -61,15 +58,19 @@ export function parseControlsFromHap(hap, cps) {
const collator = new ClockCollator({});
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
const osc = await connect();
const ws = await connect();
const controls = parseControlsFromHap(hap, cps);
const keyvals = Object.entries(controls).flat();
const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000;
const msg = { address: '/dirt/play', args: keyvals, timestamp: ts };
const ts = Math.round(collator.calculateTimestamp(currentTime, targetTime) * 1000);
const message = new OSC.Message('/dirt/play', ...keyvals);
const bundle = new OSC.Bundle([message], ts);
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
osc.send(bundle);
if ('oschost' in hap.value) {
msg['host'] = hap.value['oschost'];
}
if ('oscport' in hap.value) {
msg['port'] = hap.value['oscport'];
}
ws.send(JSON.stringify(msg));
}
/**
@@ -81,6 +82,4 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
* @memberof Pattern
* @returns Pattern
*/
Pattern.prototype.osc = function () {
return this.onTrigger(oscTrigger);
};
export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger));
+7 -2
View File
@@ -1,8 +1,9 @@
{
"name": "@strudel/osc",
"version": "1.2.3",
"version": "1.3.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"bin": "./server.js",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
@@ -37,10 +38,14 @@
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"osc-js": "^2.4.1"
"osc": "^2.4.5",
"ws": "^8.18.3"
},
"devDependencies": {
"pkg": "^5.8.1",
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
Regular → Executable
+54 -24
View File
@@ -1,34 +1,64 @@
#!/usr/bin/env node
/*
server.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/server.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import OSC from 'osc-js';
// import OSC from 'osc-js';
const config = {
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
udpServer: {
host: 'localhost', // @param {string} Hostname of udp server to bind to
port: 57121, // @param {number} Port of udp client for messaging
// enabling the following line will receive tidal messages:
// port: 57120, // @param {number} Port of udp client for messaging
exclusive: false, // @param {boolean} Exclusive flag
},
udpClient: {
host: 'localhost', // @param {string} Hostname of udp client for messaging
port: 57120, // @param {number} Port of udp client for messaging
},
wsServer: {
host: 'localhost', // @param {string} Hostname of WebSocket server
port: 8080, // @param {number} Port of WebSocket server
},
};
import { WebSocketServer } from 'ws';
import osc from 'osc';
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
const WS_PORT = 8080; // WebSocket server port
const OSC_REMOTE_IP = '127.0.0.1';
const OSC_REMOTE_PORT = 57120;
osc.open(); // start a WebSocket server on port 8080
const udpPort = new osc.UDPPort({
localAddress: '0.0.0.0',
localPort: 0,
remoteAddress: OSC_REMOTE_IP,
remotePort: OSC_REMOTE_PORT,
});
console.log('osc client running on port', config.udpClient.port);
console.log('osc server running on port', config.udpServer.port);
console.log('websocket server running on port', config.wsServer.port);
udpPort.open();
console.log(`[Sending OSC] ${OSC_REMOTE_IP}:${OSC_REMOTE_PORT}`);
udpPort.on('error', (e) => {
console.log('Error: ', e);
});
const wss = new WebSocketServer({ port: WS_PORT });
console.log(`[Listening WS] ws://localhost:${WS_PORT}`);
wss.on('connection', (ws) => {
console.log('New WebSocket connection');
ws.on('message', (message) => {
let osc_host = '127.0.0.1';
let osc_port = 57120;
try {
const data = JSON.parse(message);
if ('host' in data) {
osc_host = data['host'];
}
if ('port' in data) {
osc_port = data['port'];
}
let msg = { address: data['address'], args: data['args'] };
if ('timestamp' in data) {
msg = { timeTag: osc.timeTag(0, data['timestamp']), packets: [msg] };
}
udpPort.send(msg, osc_host, osc_port);
} catch (err) {
console.error('Error parsing message:', err);
}
});
ws.on('close', () => {
console.log('WebSocket connection closed');
});
});
+3
View File
@@ -33,5 +33,8 @@
"homepage": "https://codeberg.org/uzu/strudel#readme",
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
"version": "1.2.4",
"version": "1.2.7",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
@@ -48,5 +48,8 @@
"@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
+6 -3
View File
@@ -20,10 +20,13 @@ export async function prebake() {
// import('@strudel/osc'),
);
// load samples
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/';
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main';
// TODO: move this onto the strudel repo
const ts = 'https://raw.githubusercontent.com/todepond/samples/main/';
const ts = 'https://raw.githubusercontent.com/todepond/samples/main';
const tc = 'https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main';
await Promise.all([
modulesLoading,
registerSynthSounds(),
@@ -36,9 +39,9 @@ export async function prebake() {
samples(`${ds}/tidal-drum-machines.json`),
samples(`${ds}/piano.json`),
samples(`${ds}/Dirt-Samples.json`),
samples(`${ds}/uzu-drumkit.json`),
samples(`${ds}/vcsl.json`),
samples(`${ds}/mridangam.json`),
samples(`${tc}/strudel.json`),
]);
aliasBank(`${ts}/tidal-drum-machines-alias.json`);
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/sampler",
"version": "0.2.2",
"version": "0.2.3",
"description": "",
"keywords": [
"tidalcycles",
@@ -15,5 +15,8 @@
"type": "module",
"dependencies": {
"cowsay": "^1.6.0"
},
"engines": {
"node": ">=18.0.0"
}
}
+1 -3
View File
@@ -9,6 +9,7 @@ import readline from 'readline';
import os from 'os';
const LOG = !!process.env.LOG || false;
const PORT = process.env.PORT || 5432;
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
const isAudioFile = (f) => {
@@ -54,7 +55,6 @@ async function getBanks(directory, flat = false) {
banks[bank].push(subDir);
return subDir;
});
banks._base = `http://localhost:5432`;
return { banks, files };
}
@@ -134,8 +134,6 @@ const server = http.createServer(async (req, res) => {
readStream.pipe(res);
});
// eslint-disable-next-line
const PORT = process.env.PORT || 5432;
const IP_ADDRESS = '0.0.0.0';
let IP;
const networkInterfaces = os.networkInterfaces();
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
"version": "1.2.3",
"version": "1.2.5",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
@@ -33,5 +33,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
"version": "1.2.4",
"version": "1.2.6",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -37,5 +37,8 @@
"devDependencies": {
"node-fetch": "^3.3.2",
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+2 -1
View File
@@ -89,7 +89,7 @@ superdough({ s: 'bd', delay: 0.5 }, 0, 1);
- `decay`: seconds of decay phase
- `sustain`: gain of sustain phase
- `release`: seconds of release phase
- `deadline`: seconds until the sound should play (0 = immediate)
- `deadline`: seconds from audio context initialization before playing the sound (getAudioContextCurrentTime() = immediate)
- `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds
### registerSynthSounds()
@@ -153,6 +153,7 @@ samples('github:tidalcycles/dirt-samples')
The format is `github:<user>/<repo>/<branch>`.
If `<repo>` and `<branch>` are not specified, they will default to `samples` and `main` respectively.
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
The format is also expected to be the same as explained above.
+18
View File
@@ -0,0 +1,18 @@
let audioContext;
export const setDefaultAudioContext = () => {
audioContext = new AudioContext();
return audioContext;
};
export const getAudioContext = () => {
if (!audioContext) {
return setDefaultAudioContext();
}
return audioContext;
};
export function getAudioContextCurrentTime() {
return getAudioContext().currentTime;
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { getAudioContext } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs';
let worklet;
export async function dspWorklet(ac, code) {
+278 -25
View File
@@ -1,6 +1,7 @@
import { getAudioContext } from './superdough.mjs';
import { clamp, nanFallback } from './util.mjs';
import { getAudioContext } from './audioContext.mjs';
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
import { getNoiseBuffer } from './noise.mjs';
import { logger } from './logger.mjs';
export const noises = ['pink', 'white', 'brown', 'crackle'];
@@ -10,6 +11,13 @@ export function gainNode(value) {
return node;
}
export function effectSend(input, effect, wet) {
const send = gainNode(wet);
input.connect(send);
send.connect(effect);
return send;
}
const getSlope = (y1, y2, x1, x2) => {
const denom = x2 - x1;
if (denom === 0) {
@@ -21,7 +29,9 @@ const getSlope = (y1, y2, x1, x2) => {
export function getWorklet(ac, processor, params, config) {
const node = new AudioWorkletNode(ac, processor, config);
Object.entries(params).forEach(([key, value]) => {
node.parameters.get(key).value = value;
if (value !== undefined) {
node.parameters.get(key).value = value;
}
});
return node;
}
@@ -88,6 +98,35 @@ export const getParamADSR = (
param[ramp](min, end + release);
};
function getModulationShapeInput(val) {
if (typeof val === 'number') {
return val % 5;
}
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
}
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
const lfoprops = {
frequency: 1,
depth,
skew: 0.5,
phaseoffset: 0,
time: begin,
begin,
end,
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
}
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
const options = {
threshold: threshold ?? -3,
@@ -115,36 +154,110 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
};
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) {
const curve = 'exponential';
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
let filter;
let frequencyParam;
export function getParamLfo(audioContext, param, start, end, lfoValues) {
let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues;
if (depth == null) {
const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null);
depth = hasLFOParams ? defaultDepth : 0;
}
let lfo;
if (depth) {
lfo = getLfo(audioContext, start, end, {
depth,
dcoffset,
...getLfoInputs,
});
lfo.connect(param);
}
return lfo;
}
// helper utility for applying standard modulators to a parameter
export function applyParameterModulators(audioContext, param, start, end, envelopeValues, lfoValues) {
let { amount, offset, defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues;
if (amount == null) {
const hasADSRParams = values.some((p) => p != null);
amount = hasADSRParams ? defaultAmount : 0;
}
const min = offset ?? 0;
const max = amount + min;
const diff = Math.abs(max - min);
if (diff) {
const [attack, decay, sustain, release] = getADSRValues(values, curve, defaultValues);
getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve);
}
const lfo = getParamLfo(audioContext, param, start, end, lfoValues);
return { lfo, disconnect: () => lfo?.disconnect() };
}
export function createFilter(context, start, end, params, cps, cycle) {
let {
frequency,
anchor,
env,
type,
model,
q = 1,
drive = 0.69,
depth,
depthfrequency,
dcoffset = -0.5,
skew,
shape,
rate,
sync,
} = params;
let frequencyParam, filter;
if (model === 'ladder') {
filter = getWorklet(context, 'ladder-processor', { frequency, q: Q, drive });
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
frequencyParam = filter.parameters.get('frequency');
} else {
filter = context.createBiquadFilter();
filter.type = type;
filter.Q.value = Q;
filter.Q.value = q;
filter.frequency.value = frequency;
frequencyParam = filter.frequency;
}
const envelopeValues = [params.attack, params.decay, params.sustain, params.release];
const [attack, decay, sustain, release] = getADSRValues(envelopeValues, 'exponential', [0.005, 0.14, 0, 0.1]);
// envelope is active when any of these values is set
const hasEnvelope = att ?? dec ?? sus ?? rel ?? fenv;
const hasEnvelope = [...envelopeValues, env].some((v) => v !== undefined);
// Apply ADSR to filter frequency
if (hasEnvelope !== undefined) {
fenv = nanFallback(fenv, 1, true);
fanchor = nanFallback(fanchor, 0, true);
const fenvAbs = Math.abs(fenv);
const offset = fenvAbs * fanchor;
if (hasEnvelope) {
env = nanFallback(env, 1, true);
anchor = nanFallback(anchor, 0, true);
const envAbs = Math.abs(env);
const offset = envAbs * anchor;
let min = clamp(2 ** -offset * frequency, 0, 20000);
let max = clamp(2 ** (fenvAbs - offset) * frequency, 0, 20000);
if (fenv < 0) [min, max] = [max, min];
getParamADSR(frequencyParam, attack, decay, sustain, release, min, max, start, end, curve);
return filter;
let max = clamp(2 ** (envAbs - offset) * frequency, 0, 20000);
if (env < 0) [min, max] = [max, min];
getParamADSR(frequencyParam, attack, decay, sustain, release, min, max, start, end, 'exponential');
}
if (sync != null) {
rate = cps * sync;
}
const hasLFO = [depth, depthfrequency, skew, shape, rate].some((v) => v !== undefined);
if (hasLFO) {
depth = depth ?? 1;
const time = cycle / cps;
const modDepth = depthfrequency ?? (depth ?? 1) * frequency;
const lfoValues = {
depth: modDepth,
dcoffset,
skew,
shape,
frequency: rate ?? cps,
min: -frequency + 30,
max: 20000 - frequency,
time,
curve: 1,
};
getParamLfo(context, frequencyParam, start, end, lfoValues);
}
return filter;
}
@@ -167,14 +280,22 @@ export function drywet(dry, wet, wetAmount = 0) {
let mix = ac.createGain();
dry_gain.connect(mix);
wet_gain.connect(mix);
return mix;
return {
node: mix,
onended: () => {
dry_gain.disconnect(mix);
wet_gain.disconnect(mix);
dry.disconnect(dry_gain);
wet.disconnect(wet_gain);
},
};
}
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);
@@ -202,10 +323,19 @@ export function getVibratoOscillator(param, value, t) {
gain.gain.value = vibmod * 100;
vibratoOscillator.connect(gain);
gain.connect(param);
vibratoOscillator.onended = () => {
gain.disconnect(param);
vibratoOscillator.disconnect(gain);
};
vibratoOscillator.start(t);
return vibratoOscillator;
}
}
export function scheduleAtTime(callback, targetTime, audioContext = getAudioContext()) {
const currentTime = audioContext.currentTime;
webAudioTimeout(audioContext, callback, currentTime, targetTime);
}
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
// a bit of a hack, but it works very well :)
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
@@ -250,9 +380,9 @@ const mod = (freq, range = 1, type = 'sine') => {
}
osc.start();
const g = new GainNode(ctx, { gain: range });
const g = gainNode(range);
osc.connect(g); // -range, range
return { node: g, stop: (t) => osc.stop(t) };
return { node: g, stop: (t) => osc.stop(t), osc: osc };
};
const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => {
const carrfreq = frequencyparam.value;
@@ -304,6 +434,129 @@ export function applyFM(param, value, begin) {
modulator.connect(envGain);
envGain.connect(param);
}
fmmod.osc.onended = () => {
envGain.disconnect();
modulator.disconnect();
fmmod.osc.disconnect();
};
}
return { stop };
}
// Saturation curves
const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1)
const _mod = (n, m) => ((n % m) + m) % m;
const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x));
const _soft = (x, k) => Math.tanh(x * (1 + k));
const _hard = (x, k) => clamp((1 + k) * x, -1, 1);
const _fold = (x, k) => {
// Closed form folding for audio rate
let y = (1 + 0.5 * k) * x;
const window = _mod(y + 1, 4);
return 1 - Math.abs(window - 2);
};
const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k));
const _cubic = (x, k) => {
const t = __squash(Math.log1p(k));
const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1)
return _soft(cubic, k);
};
const _diode = (x, k, asym = false) => {
const g = 1 + 2 * k; // gain
const t = __squash(Math.log1p(k));
const bias = 0.07 * t;
const pos = _soft(x + bias, 2 * k);
const neg = _soft(asym ? bias : -x + bias, 2 * k);
const y = pos - neg;
// We divide by the derivative at 0 so that the distortion is roughly
// the identity map near 0 => small values are preserved and undistorted
const sech = 1 / Math.cosh(g * bias);
const sech2 = sech * sech; // derivative of soft (i.e. tanh) is sech^2
const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x
return _soft(y / denom, k);
};
const _asym = (x, k) => _diode(x, k, true);
const _chebyshev = (x, k) => {
const kl = 10 * Math.log1p(k);
let tnm1 = 1;
let tnm2 = x;
let tn;
let y = 0;
for (let i = 1; i < 64; i++) {
if (i < 2) {
// Already set inital conditions
y += i == 0 ? tnm1 : tnm2;
continue;
}
tn = 2 * x * tnm1 - tnm2; // https://en.wikipedia.org/wiki/Chebyshev_polynomials#Recurrence_definition
tnm2 = tnm1;
tnm1 = tn;
if (i % 2 === 0) {
y += Math.min((1.3 * kl) / i, 2) * tn;
}
}
// Soft clip
return _soft(y, kl / 20);
};
export const distortionAlgorithms = {
scurve: _scurve,
soft: _soft,
hard: _hard,
cubic: _cubic,
diode: _diode,
asym: _asym,
fold: _fold,
sinefold: _sineFold,
chebyshev: _chebyshev,
};
const _algoNames = Object.freeze(Object.keys(distortionAlgorithms));
export const getDistortionAlgorithm = (algo) => {
let index = algo;
if (typeof algo === 'string') {
index = _algoNames.indexOf(algo);
if (index === -1) {
logger(`[superdough] Could not find waveshaping algorithm ${algo}.
Available options are ${_algoNames.join(', ')}.
Defaulting to ${_algoNames[0]}.`);
index = 0;
}
}
const name = _algoNames[index % _algoNames.length]; // allow for wrapping if algo was a number
return distortionAlgorithms[name];
};
export const getDistortion = (distort, postgain, algorithm) => {
return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } });
};
export const getFrequencyFromValue = (value, defaultNote = 36) => {
let { note, freq, octave = 0 } = value;
note = note || defaultNote;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
freq *= Math.pow(2, octave);
return Number(freq);
};
export const destroyAudioWorkletNode = (node) => {
if (node == null) {
return;
}
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
};
+2
View File
@@ -11,3 +11,5 @@ export * from './synth.mjs';
export * from './zzfx.mjs';
export * from './logger.mjs';
export * from './dspworklet.mjs';
export * from './audioContext.mjs';
export * from './wavetable.mjs';
+4 -3
View File
@@ -1,8 +1,9 @@
let log = (msg) => console.log(msg);
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
export function errorLogger(e, origin = 'superdough') {
if (process.env.NODE_ENV === 'development') {
console.error(e);
}
logger(`[${origin}] error: ${e.message}`);
}
+3 -2
View File
@@ -1,5 +1,5 @@
import { drywet } from './helpers.mjs';
import { getAudioContext } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs';
let noiseCache = {};
@@ -65,8 +65,9 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) {
export function getNoiseMix(inputNode, wet, t) {
const noiseOscillator = getNoiseOscillator('pink', t);
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
noiseOscillator.node.onended = () => noiseMix.onended();
return {
node: noiseMix,
node: noiseMix.node,
stop: (time) => noiseOscillator?.stop(time),
};
}
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "1.2.4",
"version": "1.2.6",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
@@ -37,5 +37,8 @@
},
"dependencies": {
"nanostores": "^0.11.3"
},
"engines": {
"node": ">=18.0.0"
}
}
+2
View File
@@ -104,6 +104,8 @@ var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt,
player.start();
context.oncomplete = function (event) {
callback(event.renderedBuffer);
filter.disconnect();
player.disconnect();
};
context.startRendering();
+89 -88
View File
@@ -1,5 +1,6 @@
import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs';
import { getAudioContext, registerSound } from './index.mjs';
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
import { registerSound, registerWaveTable } from './index.mjs';
import { getAudioContext } from './audioContext.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { logger } from './logger.mjs';
@@ -22,39 +23,16 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u];
}
// deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
export function getSampleInfo(hapValue, bank) {
const { s, n = 0, speed = 1.0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
let sampleUrl;
let index = 0;
if (Array.isArray(bank)) {
index = getSoundIndex(n, bank.length);
sampleUrl = bank[index];
} else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
const closest = Object.keys(bank)
.filter((k) => !k.startsWith('_'))
.reduce(
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
null,
);
transpose = -midiDiff(closest); // semitones to repitch
index = getSoundIndex(n, bank[closest].length);
sampleUrl = bank[closest][index];
}
const label = `${s}:${index}`;
const { speed = 1.0 } = hapValue;
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
return { transpose, sampleUrl, index, midi, label, playbackRate };
return { transpose, url, index, midi, label, playbackRate };
}
// takes hapValue and returns buffer + playbackRate.
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
if (resolveUrl) {
sampleUrl = await resolveUrl(sampleUrl);
}
@@ -79,14 +57,14 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
bufferSource.buffer = buffer;
bufferSource.playbackRate.value = playbackRate;
const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferSource.buffer.duration;
const loop = s.startsWith('wt_') ? 1 : hapValue.loop;
const loop = hapValue.loop;
if (loop) {
bufferSource.loop = true;
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
@@ -143,13 +121,18 @@ function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl');
}
let [_, path] = base.split('github:');
let path = base.slice('github:'.length);
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}`;
let components = path.split('/');
let user = components[0];
let repo = components.length >= 2 ? components[1] : 'samples';
let branch = components.length >= 3 ? components[2] : 'main';
let other = components.slice(3);
other.push(subpath ? subpath : '');
other = other.join('/');
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
}
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
@@ -196,6 +179,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 = getBaseURL(url);
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,61 +246,16 @@ 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(
sampleMap,
(key, bank) =>
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
type: 'sample',
samples: bank,
baseUrl,
prebake,
tag,
}),
(key, bank) => {
registerSampleSource(key, bank, { baseUrl, prebake, tag });
},
baseUrl,
);
};
@@ -361,3 +345,20 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
return handle;
}
function registerSample(key, bank, params) {
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
type: 'sample',
samples: bank,
...params,
});
}
export function registerSampleSource(key, bank, params) {
const isWavetable = key.startsWith('wt_');
if (isWavetable) {
registerWaveTable(key, bank, params);
} else {
registerSample(key, bank, params);
}
}
+139 -278
View File
@@ -7,12 +7,14 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs';
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
import { map } from 'nanostores';
import { logger, errorLogger } from './logger.mjs';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
import { getAudioContext } from './audioContext.mjs';
import { SuperdoughAudioController } from './superdoughoutput.mjs';
export const DEFAULT_MAX_POLYPHONY = 128;
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
@@ -28,13 +30,6 @@ export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
function getModulationShapeInput(val) {
if (typeof val === 'number') {
return val % 5;
}
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
}
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
@@ -113,6 +108,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`);
@@ -138,15 +146,11 @@ let defaultDefaultValues = {
gain: 0.8,
postgain: 1,
density: '.03',
ftype: '12db',
fanchor: 0,
resonance: 1,
hresonance: 1,
bandq: 1,
channels: [1, 2],
phaserdepth: 0.75,
shapevol: 1,
distortvol: 1,
distorttype: 0,
delay: 0,
byteBeatExpression: '0',
delayfeedback: 0.5,
@@ -193,30 +197,17 @@ export function setVersionDefaults(version) {
export const resetLoadedSounds = () => soundMap.set({});
let audioContext;
export const setDefaultAudioContext = () => {
audioContext = new AudioContext();
return audioContext;
};
export const getAudioContext = () => {
if (!audioContext) {
return setDefaultAudioContext();
}
return audioContext;
};
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;
@@ -272,8 +263,8 @@ let audioReady;
export async function initAudioOnFirstClick(options) {
if (!audioReady) {
audioReady = new Promise((resolve) => {
document.addEventListener('click', async function listener() {
document.removeEventListener('click', listener);
document.addEventListener('mousedown', async function listener() {
document.removeEventListener('mousedown', listener);
await initAudio(options);
resolve();
});
@@ -282,88 +273,16 @@ export async function initAudioOnFirstClick(options) {
return audioReady;
}
const maxfeedback = 0.98;
let channelMerger, destinationGain;
//update the output channel configuration to match user's audio device
export function initializeAudioOutput() {
const audioContext = getAudioContext();
const maxChannelCount = audioContext.destination.maxChannelCount;
audioContext.destination.channelCount = maxChannelCount;
channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
destinationGain = new GainNode(audioContext);
channelMerger.connect(destinationGain);
destinationGain.connect(audioContext.destination);
let controller;
function getSuperdoughAudioController() {
if (controller == null) {
controller = new SuperdoughAudioController(getAudioContext());
}
return controller;
}
// input: AudioNode, channels: ?Array<int>
export const connectToDestination = (input, channels = [0, 1]) => {
const ctx = getAudioContext();
if (channelMerger == null) {
initializeAudioOutput();
}
//This upmix can be removed if correct channel counts are set throughout the app,
// and then strudel could theoretically support surround sound audio files
const stereoMix = new StereoPannerNode(ctx);
input.connect(stereoMix);
const splitter = new ChannelSplitterNode(ctx, {
numberOfOutputs: stereoMix.channelCount,
});
stereoMix.connect(splitter);
channels.forEach((ch, i) => {
splitter.connect(channelMerger, i % stereoMix.channelCount, ch % ctx.destination.channelCount);
});
};
export const panic = () => {
if (destinationGain == null) {
return;
}
destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
destinationGain = null;
channelMerger == null;
};
function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = clamp(delayfeedback, 0, 0.98);
if (!orbits[orbit].delayNode) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
connectToOrbit(dly, orbit);
orbits[orbit].delayNode = dly;
}
orbits[orbit].delayNode.delayTime.value !== delaytime &&
orbits[orbit].delayNode.delayTime.setValueAtTime(delaytime, t);
orbits[orbit].delayNode.feedback.value !== delayfeedback &&
orbits[orbit].delayNode.feedback.setValueAtTime(delayfeedback, t);
return orbits[orbit].delayNode;
}
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
const lfoprops = {
frequency: 1,
depth,
skew: 0.5,
phaseoffset: 0,
time: begin,
begin,
end,
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
export function connectToDestination(input, channels) {
const controller = getSuperdoughAudioController();
controller.output.connectToDestination(input, channels);
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
@@ -397,78 +316,6 @@ function getFilterType(ftype) {
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
}
//type orbit {
// gain: number,
// reverbNode: reverbNode
// delayNode: delayNode
//}
let orbits = {};
function connectToOrbit(node, orbit) {
if (orbits[orbit] == null) {
errorLogger(new Error('target orbit does not exist'), 'superdough');
}
node.connect(orbits[orbit].gain);
}
function setOrbit(audioContext, orbit, channels) {
if (orbits[orbit] == null) {
orbits[orbit] = {
gain: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }),
};
connectToDestination(orbits[orbit].gain, channels);
}
}
function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1) {
const targetArr = [targetOrbit].flat();
targetArr.forEach((target) => {
if (orbits[target] == null) {
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
return;
}
webAudioTimeout(
audioContext,
() => {
orbits[target].gain.gain.cancelScheduledValues(t);
const currVal = orbits[target].gain.gain.value;
orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t);
orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime));
},
0,
t - 0.01,
);
});
}
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir, 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, 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
// avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
}
return orbits[orbit].reverbNode;
}
export let analysers = {},
analysersData = {};
@@ -501,15 +348,8 @@ export function getAnalyzerData(type = 'time', id = 1) {
return analysersData[id];
}
function effectSend(input, effect, wet) {
const send = gainNode(wet);
input.connect(send);
send.connect(effect);
return send;
}
export function resetGlobalEffects() {
orbits = {};
controller?.reset();
analysers = {};
analysersData = {};
}
@@ -524,6 +364,7 @@ function mapChannelNumbers(channels) {
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
// new: t is always expected to be the absolute target onset time
const ac = getAudioContext();
const audioController = getSuperdoughAudioController();
let { stretch } = value;
if (stretch != null) {
@@ -563,36 +404,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
postgain = getDefaultValue('postgain'),
density = getDefaultValue('density'),
duckorbit,
duckonset,
duckattack,
duckdepth,
djf,
// filters
fanchor = getDefaultValue('fanchor'),
drive = 0.69,
release = 0,
// low pass
cutoff,
lpenv,
lpattack,
lpdecay,
lpsustain,
lprelease,
resonance = getDefaultValue('resonance'),
// high pass
hpenv,
hcutoff,
hpattack,
hpdecay,
hpsustain,
hprelease,
hresonance = getDefaultValue('hresonance'),
// band pass
bpenv,
bandf,
bpattack,
bpdecay,
bpsustain,
bprelease,
bandq = getDefaultValue('bandq'),
//phaser
phaserrate: phaser,
@@ -608,6 +426,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
shapevol = getDefaultValue('shapevol'),
distort,
distortvol = getDefaultValue('distortvol'),
distorttype = getDefaultValue('distorttype'),
pan,
vowel,
delay = getDefaultValue('delay'),
@@ -641,10 +460,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
);
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
setOrbit(ac, orbit, channels, t, cycle, cps);
const orbitBus = audioController.getOrbit(orbit, channels);
if (duckorbit != null) {
duckOrbit(ac, duckorbit, t, duckattack, duckdepth);
audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth);
}
gain = applyGainCurve(nanFallback(gain, 1));
@@ -663,7 +481,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) {
const ch = activeSoundSources.entries().next();
const source = ch.value[1];
const source = ch.value[1].deref();
const chainID = ch.value[0];
const endTime = t + 0.25;
source?.node?.gain?.linearRampToValueAtTime(0, endTime);
@@ -691,11 +509,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
audioNodes.forEach((n) => n?.disconnect());
activeSoundSources.delete(chainID);
};
const soundHandle = await onTrigger(t, value, onEnded);
const soundHandle = await onTrigger(t, value, onEnded, cps);
if (soundHandle) {
sourceNode = soundHandle.node;
activeSoundSources.set(chainID, soundHandle);
activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC
}
} else {
throw new Error(`sound ${s} not found! Is it loaded?`);
@@ -717,57 +535,90 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// gain stage
chain.push(gainNode(gain));
//filter
// filter
const ftype = getFilterType(value.ftype);
if (cutoff !== undefined) {
let lp = () =>
createFilter(
ac,
'lowpass',
cutoff,
resonance,
lpattack,
lpdecay,
lpsustain,
lprelease,
lpenv,
t,
end,
fanchor,
ftype,
drive,
);
if (value.cutoff !== undefined) {
const lpMap = {
frequency: 'cutoff',
q: 'resonance',
attack: 'lpattack',
decay: 'lpdecay',
sustain: 'lpsustain',
release: 'lprelease',
env: 'lpenv',
anchor: 'fanchor',
model: 'ftype',
drive: 'drive',
rate: 'lprate',
sync: 'lpsync',
depth: 'lpdepth',
depthfrequency: 'lpdepthfrequency',
shape: 'lpshape',
dcoffset: 'lpdc',
skew: 'lpskew',
};
const lpParams = pickAndRename(value, lpMap);
lpParams.type = 'lowpass';
let lp = () => createFilter(ac, t, end, lpParams, cps, cycle);
chain.push(lp());
if (ftype === '24db') {
chain.push(lp());
}
}
if (hcutoff !== undefined) {
let hp = () =>
createFilter(
ac,
'highpass',
hcutoff,
hresonance,
hpattack,
hpdecay,
hpsustain,
hprelease,
hpenv,
t,
end,
fanchor,
);
if (value.hcutoff !== undefined) {
const hpMap = {
frequency: 'hcutoff',
q: 'hresonance',
attack: 'hpattack',
decay: 'hpdecay',
sustain: 'hpsustain',
release: 'hprelease',
env: 'hpenv',
anchor: 'fanchor',
model: 'ftype',
drive: 'drive',
rate: 'hprate',
sync: 'hpsync',
depth: 'hpdepth',
depthfrequency: 'hpdepthfrequency',
shape: 'hpshape',
dcoffset: 'hpdc',
skew: 'hpskew',
};
const hpParams = pickAndRename(value, hpMap);
hpParams.type = 'highpass';
let hp = () => createFilter(ac, t, end, hpParams, cps, cycle);
chain.push(hp());
if (ftype === '24db') {
chain.push(hp());
}
}
if (bandf !== undefined) {
let bp = () =>
createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor);
if (value.bandf !== undefined) {
const bpMap = {
frequency: 'bandf',
q: 'bandq',
attack: 'bpattack',
decay: 'bpdecay',
sustain: 'bpsustain',
release: 'bprelease',
env: 'bpenv',
anchor: 'fanchor',
model: 'ftype',
drive: 'drive',
rate: 'bprate',
sync: 'bpsync',
depth: 'bpdepth',
depthfrequency: 'bpdepthfrequency',
shape: 'bpshape',
dcoffset: 'bpdc',
skew: 'bpskew',
};
const bpParams = pickAndRename(value, bpMap);
bpParams.type = 'bandpass';
let bp = () => createFilter(ac, t, end, bpParams, cps, cycle);
chain.push(bp());
if (ftype === '24db') {
chain.push(bp());
@@ -783,12 +634,20 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
if (tremolosync != null) {
tremolo = cps * tremolosync;
}
if (value.wtPosSynced != null) {
value.wtPosRate /= cps;
}
if (value.wtWarpSynced != null) {
value.wtWarpRate /= cps;
}
if (tremolo !== undefined) {
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
@@ -809,6 +668,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
curve: 1.5,
});
lfo.connect(amGain.gain);
audioNodes.push(lfo);
chain.push(amGain);
}
@@ -834,14 +694,12 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
chain.push(post);
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delayNode = getDelay(orbit, delaytime, delayfeedback, t);
delaySend = effectSend(post, delayNode, delay);
audioNodes.push(delaySend);
orbitBus.getDelay(delaytime, delayfeedback, t);
const send = orbitBus.sendDelay(post, delay);
audioNodes.push(send);
}
// reverb
let reverbSend;
if (room > 0) {
let roomIR;
if (ir !== undefined) {
@@ -854,25 +712,28 @@ 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, irspeed, irbegin);
reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend);
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
const send = orbitBus.sendReverb(post, room);
audioNodes.push(send);
}
if (djf != null) {
orbitBus.getDjf(djf, t);
}
// analyser
let analyserSend;
if (analyze) {
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
analyserSend = effectSend(post, analyserNode, 1);
const analyserSend = effectSend(post, analyserNode, 1);
audioNodes.push(analyserSend);
}
if (dry != null) {
dry = applyGainCurve(dry);
const dryGain = new GainNode(ac, { gain: dry });
chain.push(dryGain);
connectToOrbit(dryGain, orbit);
orbitBus.connectToOutput(dryGain);
} else {
connectToOrbit(post, orbit);
orbitBus.connectToOutput(post);
}
// connect chain elements together
+209
View File
@@ -0,0 +1,209 @@
import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
import { errorLogger } from './logger.mjs';
import { clamp } from './util.mjs';
let hasChanged = (now, before) => now !== undefined && now !== before;
export class Orbit {
reverbNode;
delayNode;
output;
summingNode;
djfNode;
audioContext;
constructor(audioContext) {
this.audioContext = audioContext;
this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
this.summingNode.connect(this.output);
}
disconnect() {
this.output.disconnect();
this.summingNode.disconnect();
this.delayNode?.disconnect();
this.reverbNode?.disconnect();
}
getDjf(value, t = 0) {
if (this.djfNode == null) {
this.djfNode = getWorklet(this.audioContext, 'djf-processor', { value });
this.summingNode.disconnect();
this.summingNode.connect(this.djfNode);
this.djfNode.connect(this.output);
}
const val = this.djfNode.parameters.get('value');
val.setValueAtTime(value, t);
}
getDelay(delaytime = 0, feedback = 0.5, t) {
const maxfeedback = 0.98;
if (feedback > maxfeedback) {
//logger(`feedback was clamped to ${maxfeedback} to save your ears`);
}
feedback = clamp(feedback, 0, 0.98);
if (this.delayNode == null) {
this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback);
this.delayNode.connect(this.summingNode);
this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed..
}
this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t);
this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t);
return this.delayNode;
}
getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) {
// If no reverb has been created for a given orbit, create one
if (this.reverbNode == null) {
this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
this.reverbNode.connect(this.summingNode);
}
if (
hasChanged(duration, this.reverbNode.duration) ||
hasChanged(fade, this.reverbNode.fade) ||
hasChanged(lp, this.reverbNode.lp) ||
hasChanged(dim, this.reverbNode.dim) ||
hasChanged(irspeed, this.reverbNode.irspeed) ||
hasChanged(irbegin, this.reverbNode.irbegin) ||
this.reverbNode.ir !== ir
) {
// only regenerate when something has changed
// avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
}
return this.reverbNode;
}
sendReverb(node, amount) {
return effectSend(node, this.reverbNode, amount);
}
sendDelay(node, amount) {
return effectSend(node, this.delayNode, amount);
}
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
const onset = onsettime;
const attack = Math.max(attacktime, 0.002);
const gainParam = this.output.gain;
webAudioTimeout(
this.audioContext,
() => {
const now = this.audioContext.currentTime;
// cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime
// on browsers which lack that method
const currVal = gainParam.value;
gainParam.cancelScheduledValues(now);
gainParam.setValueAtTime(currVal, now);
const t0 = Math.max(t, now); // guard against now > t
const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal);
gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset);
gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack);
},
0,
t - 0.01,
);
}
connectToOutput(node) {
node.connect(this.summingNode);
}
}
export class SuperdoughOutput {
channelMerger;
destinationGain;
constructor(audioContext) {
this.audioContext = audioContext;
this.initializeAudio();
}
initializeAudio() {
const audioContext = this.audioContext;
const maxChannelCount = audioContext.destination.maxChannelCount;
this.audioContext.destination.channelCount = maxChannelCount;
this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
this.destinationGain = new GainNode(audioContext);
this.channelMerger.connect(this.destinationGain);
this.destinationGain.connect(audioContext.destination);
}
reset() {
this.disconnect();
this.initializeAudio();
}
disconnect() {
this.channelMerger.disconnect();
this.destinationGain.disconnect();
this.destinationGain = null;
this.channelMerger = null;
}
connectToDestination = (input, channels = [0, 1]) => {
//This upmix can be removed if correct channel counts are set throughout the app,
// and then strudel could theoretically support surround sound audio files
const stereoMix = new StereoPannerNode(this.audioContext);
input.connect(stereoMix);
const splitter = new ChannelSplitterNode(this.audioContext, {
numberOfOutputs: stereoMix.channelCount,
});
stereoMix.connect(splitter);
channels.forEach((ch, i) => {
splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount);
});
};
}
export class SuperdoughAudioController {
audioContext;
output;
nodes = {};
constructor(audioContext) {
this.audioContext = audioContext;
this.output = new SuperdoughOutput(audioContext);
}
reset() {
Array.from(this.nodes).forEach((node) => {
node.disconnect();
});
this.nodes = {};
this.output.reset();
}
duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) {
const targetArr = [targetOrbits].flat();
const onsetArr = [onsettime].flat();
const attackArr = [attacktime].flat();
const depthArr = [depth].flat();
targetArr.forEach((target, idx) => {
const orbit = this.nodes[target];
if (orbit == null) {
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
return;
}
const onset = onsetArr[idx] ?? onsetArr[0];
const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002);
const depth = depthArr[idx] ?? depthArr[0];
orbit.duck(t, onset, attack, depth);
});
}
getOrbit(orbitNum, channels) {
if (this.nodes[orbitNum] == null) {
this.nodes[orbitNum] = new Orbit(this.audioContext);
this.output.connectToDestination(this.nodes[orbitNum].output, channels);
}
return this.nodes[orbitNum];
}
}
+58 -49
View File
@@ -1,40 +1,24 @@
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
import { clamp } from './util.mjs';
import { registerSound, soundMap } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs';
import {
applyFM,
destroyAudioWorkletNode,
gainNode,
getADSRValues,
getFrequencyFromValue,
getLfo,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
webAudioTimeout,
getWorklet,
noises,
webAudioTimeout,
} from './helpers.mjs';
import { logger } from './logger.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const getFrequencyFromValue = (value, defaultNote = 36) => {
let { note, freq } = value;
note = note || defaultNote;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
return Number(freq);
};
function destroyAudioWorkletNode(node) {
if (node == null) {
return;
}
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
}
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
const waveformAliases = [
['tri', 'triangle'],
['sqr', 'square'],
@@ -64,19 +48,17 @@ export function registerSynthSounds() {
[0.001, 0.05, 0.6, 0.01],
);
let sound = getOscillator(s, t, value);
let { node: o, stop, triggerRelease } = sound;
// turn down
const g = gainNode(0.3);
const { duration } = value;
o.onended = () => {
o.disconnect();
let sound = getOscillator(s, t, value, () => {
g.disconnect();
onended();
};
});
let { node: o, stop, triggerRelease } = sound;
const { duration } = value;
const envGain = gainNode(1);
let node = o.connect(g).connect(envGain);
@@ -431,9 +413,13 @@ export function registerSynthSounds() {
waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] }));
}
export function waveformN(partials, type) {
const real = new Float32Array(partials + 1);
const imag = new Float32Array(partials + 1);
const PI2 = 2 * Math.PI;
export function waveformN(partials, phases, type) {
const isList = typeof partials === 'object';
partials = isList ? partials : new Float32Array(partials).fill(1);
const len = partials.length;
const real = new Float32Array(len + 1);
const imag = new Float32Array(len + 1);
const ac = getAudioContext();
const osc = ac.createOscillator();
@@ -441,20 +427,29 @@ export function waveformN(partials, type) {
sawtooth: (n) => [0, -1 / n],
square: (n) => [0, n % 2 === 0 ? 0 : 1 / n],
triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0],
user: (_n) => [0, 1],
};
if (!terms[type]) {
throw new Error(`unknown wave type ${type}`);
}
real[0] = 0; // dc offset
imag[0] = 0;
let n = 1;
while (n <= partials) {
const [r, i] = terms[type](n);
real[n] = r;
imag[n] = i;
n++;
for (let n = 0; n < len; n++) {
const mag = partials[n];
const [r, i] = terms[type](n + 1); // we skip n === 0 as this is dc offset
const phase = phases?.[n] ?? 0;
// Scale by `partials`
let R = r * mag;
let I = i * mag;
// Apply rotation by the phase
if (phase !== 0) {
const c = Math.cos(PI2 * phase);
const s = Math.sin(PI2 * phase);
R = c * R - s * I;
I = s * R + c * I;
}
real[n + 1] = R;
imag[n + 1] = I;
}
const wave = ac.createPeriodicWave(real, imag);
@@ -463,21 +458,28 @@ export function waveformN(partials, type) {
}
// expects one of waveforms as s
export function getOscillator(s, t, value) {
let { n: partials, duration, noise = 0 } = value;
export function getOscillator(s, t, value, onended) {
const { duration, noise = 0 } = value;
const partials = value.partials ?? value.n;
let o;
if (s === 'user' && !partials) {
logger(
`[superdough] Synth 'user' was selected, but partials not specified. Defaulting to triangle. Use pat.partials to setup custom waveform`,
);
s = 'triangle';
}
s = s === 'user' && !partials ? 'triangle' : s;
// If no partials are given, use stock waveforms
if (!partials || s === 'sine') {
if (!partials || partials?.length === 0 || s === 'sine') {
o = getAudioContext().createOscillator();
o.type = s || 'triangle';
}
// generate custom waveform if partials are given
else {
o = waveformN(partials, s);
o = waveformN(partials, value.phases, s);
}
// set frequency
o.frequency.value = getFrequencyFromValue(value);
o.start(t);
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
@@ -490,6 +492,13 @@ export function getOscillator(s, t, value) {
noiseMix = getNoiseMix(o, noise, t);
}
o.onended = () => {
noiseMix || o.disconnect();
noiseMix?.node.disconnect();
onended();
};
o.start(t);
return {
node: noiseMix?.node || o,
stop: (time) => {
+50 -2
View File
@@ -7,7 +7,7 @@ export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
if (!pc) {
return [];
}
@@ -16,13 +16,17 @@ export const tokenizeNote = (note) => {
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
const accs = { '#': 1, b: -1, s: 1, f: -1 };
export const getAccidentalsOffset = (accidentals) => {
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
};
export const noteToMidi = (note, defaultOctave = 3) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
}
const chroma = chromas[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
const offset = getAccidentalsOffset(acc);
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const midiToFreq = (n) => {
@@ -76,3 +80,47 @@ export function cycleToSeconds(cycle, cps) {
export function secondsToCycle(t, cps) {
return t * cps;
}
// deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
export function getCommonSampleInfo(hapValue, bank) {
const { s, n = 0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
let url;
let index = 0;
if (Array.isArray(bank)) {
index = getSoundIndex(n, bank.length);
url = bank[index];
} else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
const closest = Object.keys(bank)
.filter((k) => !k.startsWith('_'))
.reduce(
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
null,
);
transpose = -midiDiff(closest); // semitones to repitch
index = getSoundIndex(n, bank[closest].length);
url = bank[closest][index];
}
const label = `${s}:${index}`;
return { transpose, url, index, midi, label };
}
/** Selects entries from `source` and renames them via `map` */
export const pickAndRename = (source, map) => {
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
};
export const getBaseURL = (url) => {
try {
// For real URLs
return new URL('.', new URL(url)).href.replace(/\/$/, ''); // removes trailing slash
} catch {
// For pseudo URLS
return url.split('/').slice(0, -1).join('/');
}
};
+17 -5
View File
@@ -45,7 +45,8 @@ if (typeof GainNode !== 'undefined') {
throw new Error('vowel: unknown vowel ' + letter);
}
const { gains, qs, freqs } = vowelFormant[letter];
const makeupGain = ac.createGain();
this.makeupGain = ac.createGain();
this.audioNodes = [];
for (let i = 0; i < 5; i++) {
const gain = ac.createGain();
gain.gain.value = gains[i];
@@ -53,14 +54,25 @@ if (typeof GainNode !== 'undefined') {
filter.type = 'bandpass';
filter.Q.value = qs[i];
filter.frequency.value = freqs[i];
this.connect(filter);
super.connect(filter);
filter.connect(gain);
gain.connect(makeupGain);
this.audioNodes.push(filter);
gain.connect(this.makeupGain);
this.audioNodes.push(gain);
}
makeupGain.gain.value = 8; // how much makeup gain to add?
this.connect = (target) => makeupGain.connect(target);
this.makeupGain.gain.value = 8; // how much makeup gain to add?
return this;
}
connect(target) {
this.makeupGain.connect(target);
}
disconnect() {
this.makeupGain.disconnect();
this.audioNodes.forEach((n) => n.disconnect());
super.disconnect();
this.makeupGain = null;
this.audioNodes = null;
}
}
AudioContext.prototype.createVowelFilter = function (letter) {
+337
View File
@@ -0,0 +1,337 @@
import { getAudioContext, registerSound } from './index.mjs';
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
import {
applyFM,
applyParameterModulators,
destroyAudioWorkletNode,
getADSRValues,
getFrequencyFromValue,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
getWorklet,
webAudioTimeout,
} from './helpers.mjs';
import { logger } from './logger.mjs';
export const Warpmode = Object.freeze({
NONE: 0,
ASYM: 1,
MIRROR: 2,
BENDP: 3,
BENDM: 4,
BENDMP: 5,
SYNC: 6,
QUANT: 7,
FOLD: 8,
PWM: 9,
ORBIT: 10,
SPIN: 11,
CHAOS: 12,
PRIMES: 13,
BINARY: 14,
BROWNIAN: 15,
RECIPROCAL: 16,
WORMHOLE: 17,
LOGISTIC: 18,
SIGMOID: 19,
FRACTAL: 20,
FLIP: 21,
});
const seenKeys = new Set();
async function getPayload(url, label, frameLen = 2048) {
const key = `${url},${frameLen}`;
if (!seenKeys.has(key)) {
const buf = await loadBuffer(url, label);
const ch0 = buf.getChannelData(0);
const total = ch0.length;
const numFrames = Math.max(1, Math.floor(total / frameLen));
const frames = new Array(numFrames);
for (let i = 0; i < numFrames; i++) {
const start = i * frameLen;
frames[i] = ch0.subarray(start, start + frameLen);
}
seenKeys.add(key);
return { frames, frameLen, numFrames, key };
}
return { frameLen, key }; // worklet will use the cached version
}
function humanFileSize(bytes, si) {
var thresh = si ? 1000 : 1024;
if (bytes < thresh) return bytes + ' B';
var units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
var u = -1;
do {
bytes /= thresh;
++u;
} while (bytes >= thresh);
return bytes.toFixed(1) + ' ' + units[u];
}
// Extract the sample rate of a .wav file
function parseWavSampleRate(arrBuf) {
const dv = new DataView(arrBuf);
// Header is "RIFF<chunk size (4 bytes)>WAVE", so 12 bytes
let p = 12;
// Look through chunks for the format header
// (they will always have an 8 byte header (id and size) followed by a payload)
while (p + 8 <= dv.byteLength) {
// Parse id
const id = String.fromCharCode(dv.getUint8(p), dv.getUint8(p + 1), dv.getUint8(p + 2), dv.getUint8(p + 3));
// Parse chunk size
const size = dv.getUint32(p + 4, true);
if (id === 'fmt ') {
// The format chunk contains the sample rate after
// 8 bytes of header, 2 bytes of format tag, 2 bytes of num channels
// (for a total of 12)
return dv.getUint32(p + 12, true);
}
// Advance to next chunk
p += 8 + size + (size & 1);
}
return null;
}
async function decodeAtNativeRate(arr) {
const sr = parseWavSampleRate(arr) || 44100;
const tempAC = new OfflineAudioContext(1, 1, sr);
return await tempAC.decodeAudioData(arr);
}
const loadCache = {};
const loadBuffer = (url, label) => {
url = url.replace('#', '%23');
if (!loadCache[url]) {
logger(`[wavetable] load table ${label}..`, 'load-table', { url });
const timestamp = Date.now();
loadCache[url] = fetch(url)
.then((res) => res.arrayBuffer())
.then(async (res) => {
const took = Date.now() - timestamp;
const size = humanFileSize(res.byteLength);
logger(`[wavetable] load table ${label}... done! loaded ${size} in ${took}ms`, 'loaded-table', { url });
const decoded = await decodeAtNativeRate(res);
return decoded;
});
}
return loadCache[url];
};
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}`;
}
const _processTables = (json, baseUrl, frameLen, options = {}) => {
baseUrl = json._base || baseUrl;
return Object.entries(json).forEach(([key, tables]) => {
if (key === '_base') return false;
if (typeof tables === 'string') {
tables = [tables];
}
if (typeof tables !== 'object') {
throw new Error('wrong json format for ' + key);
}
let resolvedUrl = baseUrl;
if (resolvedUrl.startsWith('github:')) {
resolvedUrl = githubPath(resolvedUrl, '');
}
tables = tables
.map((t) => resolvedUrl + t)
.filter((t) => {
if (!t.toLowerCase().endsWith('.wav')) {
logger(`[wavetable] skipping ${t} -- wavetables must be ".wav" format`);
return false;
}
return true;
});
if (tables.length) {
registerWaveTable(key, tables, { baseUrl, frameLen });
}
});
};
export function registerWaveTable(key, tables, params) {
registerSound(
key,
(t, hapValue, onended, cps) => {
return onTriggerSynth(t, hapValue, onended, tables, cps, params?.frameLen ?? 2048);
},
{
type: 'wavetable',
tables,
...params,
},
);
}
/**
* Loads a collection of wavetables to use with `s`
*
* @name tables
*/
export const tables = async (url, frameLen, json, options = {}) => {
if (json !== undefined) return _processTables(json, url, frameLen);
if (url.startsWith('github:')) {
url = githubPath(url, 'strudel.json');
}
if (url.startsWith('local:')) {
url = `http://localhost:5432`;
}
const base = getBaseURL(url);
if (typeof fetch !== 'function') {
// not a browser
return;
}
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
return fetch(url)
.then((res) => res.json())
.then((json) => _processTables(json, base, frameLen, options))
.catch((error) => {
console.error(error);
throw new Error(`error loading "${url}"`);
});
};
export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
const { s, n = 0, duration, clip } = value;
const ac = getAudioContext();
const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
let { warpmode } = value;
if (typeof warpmode === 'string') {
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
}
const frequency = getFrequencyFromValue(value);
const { url, label } = getCommonSampleInfo(value, tables);
const payload = await getPayload(url, label, frameLen);
let holdEnd = t + duration;
if (clip !== undefined) {
holdEnd = Math.min(t + clip * duration, holdEnd);
}
const endWithRelease = holdEnd + release;
const envEnd = endWithRelease + 0.01;
const source = getWorklet(
ac,
'wavetable-oscillator-processor',
{
begin: t,
end: envEnd,
frequency,
freqspread: value.detune,
position: value.wt,
warp: value.warp,
warpMode: warpmode,
voices: Math.max(value.unison ?? 1, 1),
panspread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
},
{ outputChannelCount: [2] },
);
source.port.postMessage({ type: 'table', payload });
if (ac.currentTime > t) {
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
return;
}
const posADSRParams = [value.wtattack, value.wtdecay, value.wtsustain, value.wtrelease];
const warpADSRParams = [value.warpattack, value.warpdecay, value.warpsustain, value.warprelease];
const wtParams = source.parameters;
const positionParam = wtParams.get('position');
const warpParam = wtParams.get('warp');
let wtrate = value.wtrate;
if (value.wtsync != null) {
wtrate = cps * value.wtsync;
}
const wtPosModulators = applyParameterModulators(
ac,
positionParam,
t,
endWithRelease,
{
offset: value.wt,
amount: value.wtenv,
defaultAmount: 0.5,
shape: 'linear',
values: posADSRParams,
holdEnd,
defaultValues: [0, 0.5, 0, 0.1],
},
{
frequency: wtrate,
depth: value.wtdepth,
defaultDepth: 0.5,
shape: value.wtshape,
skew: value.wtskew,
dcoffset: value.wtdc ?? 0,
},
);
let warprate = value.warprate;
if (value.warpsync != null) {
warprate = warprate = cps * value.warpsync;
}
const wtWarpModulators = applyParameterModulators(
ac,
warpParam,
t,
endWithRelease,
{
offset: value.warp,
amount: value.warpenv,
defaultAmount: 0.5,
shape: 'linear',
values: warpADSRParams,
holdEnd,
defaultValues: [0, 0.5, 0, 0.1],
},
{
frequency: warprate,
depth: value.warpdepth,
defaultDepth: 0.5,
shape: value.warpshape,
skew: value.warpskew,
dcoffset: value.warpdc ?? 0,
},
);
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
const fm = applyFM(source.parameters.get('frequency'), value, t);
const envGain = ac.createGain();
const node = source.connect(envGain);
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
const handle = { node, source };
const timeoutNode = webAudioTimeout(
ac,
() => {
destroyAudioWorkletNode(source);
vibratoOscillator?.stop();
fm?.stop();
node.disconnect();
wtPosModulators?.disconnect();
wtWarpModulators?.disconnect();
onended();
},
t,
envEnd,
);
handle.stop = (time) => {
timeoutNode.stop(time);
};
return handle;
}
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -1,6 +1,7 @@
//import { ZZFX } from 'zzfx';
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { registerSound } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs';
import { buildSamples } from './zzfx_fork.mjs';
export const getZZFX = (value, t) => {
+1 -1
View File
@@ -1,4 +1,4 @@
import { getAudioContext } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs';
// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6
// changes: replaced this.volume with 1 + using sampleRate from getAudioContext()
+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;
+39
View File
@@ -0,0 +1,39 @@
{
"name": "supradough",
"version": "1.2.4",
"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://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://codeberg.org/uzu/strudel#readme",
"devDependencies": {
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*",
"wav-encoder": "^1.3.0"
},
"engines": {
"node": ">=18.0.0"
}
}
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [bundleAudioWorkletPlugin()],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
target: 'esnext',
},
});
+3
View File
@@ -25,5 +25,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+2 -2
View File
@@ -1,9 +1,9 @@
import './tonal.mjs';
import './voicings.mjs';
import './ireal.mjs';
export * from './tonal.mjs';
export * from './voicings.mjs';
import './ireal.mjs';
export * from './ireal.mjs';
export const packageName = '@strudel/tonal';
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
"version": "1.2.3",
"version": "1.2.5",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -38,5 +38,8 @@
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}

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