Compare commits

..

188 Commits

Author SHA1 Message Date
Aria 48231ea12a Merge branch 'main' into glossing/midin-channels 2025-12-07 21:23:57 +01:00
Aria c8a442234e Merge pull request 'Feat: Transient shaper' (#1777) from glossing/transient-processor into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1777
2025-12-07 21:23:42 +01:00
Aria efda32eedb Merge branch 'main' into glossing/transient-processor 2025-12-07 20:29:50 +01:00
Aria 95d04d1149 Merge branch 'main' into glossing/midin-channels 2025-12-07 20:29:39 +01:00
Aria 358b1e3f56 Make sure processor turns off; fix array sizes for JIT 2025-12-07 13:28:57 -06:00
Aria fe7e8f7add Clearer docstring 2025-12-07 13:21:06 -06:00
Aria 45d329d3d1 Merge pull request 'Add vel as a synonym for velocity & update a few docstrings' (#1781) from glossing/controls-various into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1781
2025-12-07 19:15:42 +01:00
Aria 5e99393f5a Merge branch 'main' into glossing/controls-various 2025-12-07 18:54:18 +01:00
Aria 9650c8eb50 Merge pull request '[perf] fix phaser leak of unused biquads' (#1800) from jeromew/strudel:fix-perf10 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1800
Reviewed-by: Aria <glossing@noreply.codeberg.org>
2025-12-07 18:53:03 +01:00
Aria fab8d2193d Merge branch 'main' into glossing/midin-channels 2025-12-07 18:45:16 +01:00
Aria 150835aa4d Merge branch 'main' into glossing/transient-processor 2025-12-07 18:45:11 +01:00
Aria 76237016a3 Merge branch 'main' into glossing/controls-various 2025-12-07 18:45:02 +01:00
Aria 6f4d8ebacf Merge branch 'main' into fix-perf10 2025-12-07 18:44:47 +01:00
Aria 3cecf4a50e Merge pull request 'Bug fix: Remove failing tests due to shabda removal' (#1820) from glossing/shabda-test-fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1820
2025-12-07 18:42:49 +01:00
Aria 16b5faf333 Removing failing tests due to shabda removal 2025-12-07 11:23:55 -06:00
Aria c4826b7dfb Merge branch 'main' into glossing/midin-channels 2025-12-07 18:11:59 +01:00
Aria bd61c1dc7e Merge branch 'main' into glossing/transient-processor 2025-12-07 18:11:50 +01:00
Aria ceb3aa797d Merge branch 'main' into glossing/controls-various 2025-12-07 18:11:39 +01:00
Aria 9b3277da1c Merge branch 'main' into fix-perf10 2025-12-07 18:09:00 +01:00
jeromew 0b711e879c Improve naming & backward compat 2025-12-04 08:23:36 +00:00
Aria e01621d0cc Merge pull request 'Feature: Eight FMs' (#1628) from glossing/strudel:glossing/fmsss into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1628
2025-12-03 18:27:27 +01:00
Aria 196f70ab6b Formatting 2025-12-03 11:13:21 -06:00
Aria 1d4f2b81d8 Switch back to explicit exports and use new release mechanism 2025-12-03 11:12:30 -06:00
Aria d28c8dac8e Merge branch 'main' into glossing/fmsss 2025-12-03 10:42:35 -06:00
Aria eb18648a2a Merge pull request '[perf] release unused AudioBufferSourceNode + releaseAudioNode' (#1805) from jeromew/strudel:fix-perf11 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1805
Reviewed-by: Aria <glossing@noreply.codeberg.org>
2025-12-03 17:35:03 +01:00
jeromew 0ec9826de5 onceEnded mechanism to clean audioNode.onended callbacks 2025-12-02 09:29:50 +00:00
jeromew af7855402f Accept non-instanciated nodes for better developer experience 2025-12-01 20:43:55 +00:00
jeromew 68b1cb87ca [perf] release unused AudioBufferSourceNode + releaseAudioNode 2025-12-01 15:29:19 +00:00
jeromew b65cc2128f [perf] fix phaser leak of unused biquads 2025-11-29 14:47:03 +00:00
froos 7a3e0fd3fe Merge pull request 'added export to getSuperdoughAudioController() so that its possible to route superdough audio through other webaudio applications.' (#1796) from ndr0n/strudel:exportSuperdoughAudioController into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1796
2025-11-29 01:00:41 +01:00
Alex McLean 4061dd4cf8 Merge pull request 'add revv() for reversing whole patterns' (#1791) from revv into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1791
2025-11-28 23:26:19 +01:00
alex b523205590 Merge branch 'revv' of ssh://codeberg.org/uzu/strudel into revv 2025-11-28 22:03:32 +00:00
alex 956598da18 snapshot 2025-11-28 22:03:21 +00:00
Alex McLean e011f3b982 Merge branch 'main' into revv 2025-11-28 22:37:03 +01:00
alex cb591815ed fix copy paste errors 2025-11-28 21:35:35 +00:00
Alex McLean 6a4fd27f5b Merge branch 'main' into exportSuperdoughAudioController 2025-11-28 22:32:10 +01:00
Aria d30df8d4cf Merge pull request '[perf] Disconnect lfos for phaser and filters' (#1787) from glossing/perf-lfos into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1787
2025-11-28 20:19:14 +01:00
Aria e10cd22c26 Merge branch 'main' into glossing/perf-lfos 2025-11-28 19:59:51 +01:00
Aria 18dc4944bf Typos and cleanup 2025-11-28 12:36:23 -06:00
ndr0n a6008e2700 added export to getSuperdoughAudioController() so that its possible to route superdough audio on external application integrations. 2025-11-28 04:03:48 +00:00
froos 2f8cabbb6b Merge pull request 'fix: return silence when no pattern is returned' (#1795) from fix-empty-pattern into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1795
2025-11-27 23:08:50 +01:00
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
Alex McLean f4b1baac87 add revv() for reversing whole patterns 2025-11-27 15:34:18 +00: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 692359309f Typo and formatting 2025-11-25 17:31:11 -06:00
Aria d65b908bda Disconnect lfos for phaser and filters 2025-11-25 17:27:37 -06: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
Aria 39e7f70079 Remove synonym 2025-11-24 18:44:29 -06:00
Aria 7e5a907ce5 Add a few more functions to autocomplete 2025-11-24 18:41:25 -06:00
Aria d7afb8a6c0 Add register example 2025-11-24 18:01:20 -06:00
Aria a278f21f1b Allow register in autocomplete, add vel as synonym for velocity, fix some controls 2025-11-24 17:52:00 -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
Aria 36587ae747 Switch back to original version 2025-11-23 22:55:36 -06:00
Aria 6c01c16a0c Fix typo 2025-11-23 22:39:21 -06:00
Aria be36898803 Formatting and examples 2025-11-23 22:29:33 -06:00
Aria df047fd48d Optimizations 2025-11-23 22:28:10 -06:00
Aria 68840de188 Working version 2025-11-23 20:58:28 -06:00
Aria 020a6bc75f First pass at transient processor 2025-11-23 20:31:31 -06:00
Jade (Rose) Rowland 20016fd9aa update hash 2025-11-23 20:25:13 -05:00
Aria 7bc6fcbf2a Add channel support to midi in 2025-11-23 18:24:48 -06: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
Aria 654c1a86f0 Simplify declarations via globalThis; fix cleanup/stop 2025-11-18 16:02:33 -06: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 ac3f477f30 Merge branch 'main' into glossing/fmsss 2025-11-16 22:32:38 -06: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 573cd1ae41 Merge branch 'main' into glossing/fmsss 2025-11-16 22:23:31 -06: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
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
Aria 4c49cd9297 More examples 2025-10-18 11:24:56 -05:00
Aria 70d2da81d8 Merge branch 'main' into glossing/fmsss 2025-10-18 11:01:23 -05: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
Aria cc4452ce58 Working version of partials and phases 2025-10-04 20:33:25 -05:00
Aria bd73d96847 Cleanup 2025-10-03 18:49:49 -05:00
Aria 7cf9db4872 Big matrix refactor 2025-10-03 18:20:41 -05:00
Aria c9381a11f9 Working version 2025-10-03 17:21:05 -05:00
Switch Angel AKA Jade Rose f4cf77f5c6 Merge branch 'main' into solo 2025-09-30 08:09:37 +02:00
Aria e4070b86f8 More descriptive variables and comments 2025-08-29 14:33:14 -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
Felix Roos 7f103e77e3 prefix "S" for solo 2025-07-28 04:05:21 +02:00
43 changed files with 2561 additions and 718 deletions
+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)
+1 -1
View File
@@ -1,4 +1,4 @@
<script src="https://unpkg.com/@strudel/web@1.2.6"></script> <script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="play">PLAY</button> <button id="play">PLAY</button>
<script> <script>
initStrudel({ initStrudel({
+8 -8
View File
@@ -12,14 +12,14 @@
"vite": "^6.0.11" "vite": "^6.0.11"
}, },
"dependencies": { "dependencies": {
"@strudel/codemirror": "1.2.6", "@strudel/codemirror": "workspace:*",
"@strudel/core": "1.2.5", "@strudel/core": "workspace:*",
"@strudel/draw": "1.2.5", "@strudel/draw": "workspace:*",
"@strudel/mini": "1.2.5", "@strudel/mini": "workspace:*",
"@strudel/soundfonts": "1.2.6", "@strudel/soundfonts": "workspace:*",
"@strudel/tonal": "1.2.5", "@strudel/tonal": "workspace:*",
"@strudel/transpiler": "1.2.5", "@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "1.2.6" "@strudel/webaudio": "workspace:*"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
+8 -5
View File
@@ -1,6 +1,6 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0 export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
samples('github:felixroos/samples') samples('github:felixroos/samples')
samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/') samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2" "<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
// make it 22 edo // make it 22 edo
@@ -28,12 +28,13 @@ samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/ti
.mask("<0@4 1@12>/4") .mask("<0@4 1@12>/4")
.reset("<x@15 [x(3,8) x*[4 8]]>") .reset("<x@15 [x(3,8) x*[4 8]]>")
// wait for it... // wait for it...
).fast(4/3) ).fast(2/3)
//.crush(6) // remove "//" if you dare`; //.crush(6) // remove "//" if you dare`;
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0 export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/') samples('github:felixroos/samples')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', { addVoicings('hip', {
m11: ['2M 3m 4P 7m'], m11: ['2M 3m 4P 7m'],
@@ -60,14 +61,16 @@ stack(
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2) s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
) )
) )
.late("[0 .05]*2").late(12).fast(2) .late("[0 .05]*2").late(12)
`; `;
export const funk42 = `// froos - how to funk in 42 lines of code export const funk42 = `// froos - how to funk in 42 lines of code
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg // adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4 // thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/') samples('github:felixroos/samples')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5) setcps(.5)
+1 -1
View File
@@ -13,7 +13,7 @@
<button id="c">C</button> <button id="c">C</button>
<button id="stop">stop</button> <button id="stop">stop</button>
<script type="module"> <script type="module">
import { initStrudel, hush } from '@strudel/web'; import { initStrudel } from '@strudel/web';
initStrudel({ initStrudel({
prebake: () => samples('github:tidalcycles/dirt-samples'), prebake: () => samples('github:tidalcycles/dirt-samples'),
}); });
+1 -1
View File
@@ -13,7 +13,7 @@
"vite": "^6.0.11" "vite": "^6.0.11"
}, },
"dependencies": { "dependencies": {
"@strudel/web": "1.2.6" "@strudel/web": "workspace:*"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
+234 -18
View File
@@ -72,6 +72,27 @@ export function registerControl(names, ...aliases) {
return bag; return bag;
} }
export function registerMultiControl(names, maxControls, ...aliases) {
names = Array.isArray(names) ? names : [names];
let bag = {};
for (let i = 1; i <= maxControls; i++) {
let theseAliases = [...aliases];
let theseNames = [...names];
if (i === 1) {
// adds e.g. fm1 as an alias for fm
const aliases1 = theseAliases.map((a) => `${a}1`);
const names1 = theseNames.map((n) => `${n}1`);
theseAliases = theseAliases.concat(aliases1).concat(names1);
} else {
theseAliases = theseAliases.map((a) => `${a}${i}`);
theseNames = theseNames.map((n) => `${n}${i}`);
}
const subBag = registerControl(theseNames, ...theseAliases);
bag = { ...bag, ...subBag };
}
return bag;
}
/** /**
* Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters * Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters
* separated by ':'. * separated by ':'.
@@ -380,12 +401,13 @@ export const { accelerate } = registerControl('accelerate');
* Sets the velocity from 0 to 1. Is multiplied together with gain. * Sets the velocity from 0 to 1. Is multiplied together with gain.
* *
* @name velocity * @name velocity
* @synonyms vel
* @example * @example
* s("hh*8") * s("hh*8")
* .gain(".4!2 1 .4!2 1 .4 1") * .gain(".4!2 1 .4!2 1 .4 1")
* .velocity(".4 1") * .velocity(".4 1")
*/ */
export const { velocity } = registerControl('velocity'); export const { velocity, vel } = registerControl('velocity', 'vel');
/** /**
* Controls the gain by an exponential amount. * Controls the gain by an exponential amount.
* *
@@ -435,6 +457,9 @@ export const { attack, att } = registerControl('attack', 'att');
* Whole numbers and simple ratios sound more natural, * Whole numbers and simple ratios sound more natural,
* while decimal numbers and complex ratios sound metallic. * while decimal numbers and complex ratios sound metallic.
* *
* A number may be added afterwards to control the harmonicity of
* any of the 8 individual FMs (e.g. `fmh2`)
*
* @name fmh * @name fmh
* @param {number | Pattern} harmonicity * @param {number | Pattern} harmonicity
* @example * @example
@@ -444,25 +469,40 @@ export const { attack, att } = registerControl('attack', 'att');
* ._scope() * ._scope()
* *
*/ */
export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh'); export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerMultiControl(['fmh', 'fmi'], 8, 'fmh');
/** /**
* Sets the Frequency Modulation of the synth. * Sets the Frequency Modulation of the synth.
* Controls the modulation index, which defines the brightness of the sound. * Controls the modulation index, which defines the brightness of the sound.
* *
* @name fm * A number may be added afterwards to control the modulation index of
* any of the 8 individual FMs (e.g. `fm3`). Also, FMs may be routed into
* each other with matrix commands like `fm13`, which would send `fm1` back into
* `fm3`
*
* @name fmi
* @param {number | Pattern} brightness modulation index * @param {number | Pattern} brightness modulation index
* @synonyms fmi * @synonyms fm
* @example * @example
* note("c e g b g e") * note("c e g b g e")
* .fm("<0 1 2 8 32>") * .fm("<0 1 2 8 32>")
* ._scope() * ._scope()
* @example
* s("sine").note("F1").seg(8)
* .fm(4).fm2(rand.mul(4)).fm3(saw.mul(8).slow(8))
* .fmh(1.06).fmh2(10).fmh3(0.1)
* *
*/ */
export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm'); export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8 } =
registerMultiControl(['fmi', 'fmh'], 8, 'fm');
// fm envelope // fm envelope
/** /**
* Ramp type of fm envelope. Exp might be a bit broken.. * Ramp type of fm envelope. Exp might be a bit broken..
* *
* A number may be added afterwards to control the envelope of
* any of the 8 individual FMs (e.g. `fmenv4`)
*
* @name fmenv * @name fmenv
* @param {number | Pattern} type lin | exp * @param {number | Pattern} type lin | exp
* @example * @example
@@ -474,11 +514,19 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
* ._scope() * ._scope()
* *
*/ */
export const { fmenv } = registerControl('fmenv'); export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fmenv8 } = registerMultiControl(
'fmenv',
8,
);
/** /**
* Attack time for the FM envelope: time it takes to reach maximum modulation * Attack time for the FM envelope: time it takes to reach maximum modulation
* *
* A number may be added afterwards to control the attack of the envelope of
* any of the 8 individual FMs (e.g. `fmatt5`)
*
* @name fmattack * @name fmattack
* @synonyms fmatt
* @param {number | Pattern} time attack time * @param {number | Pattern} time attack time
* @example * @example
* note("c e g b g e") * note("c e g b g e")
@@ -487,11 +535,33 @@ export const { fmenv } = registerControl('fmenv');
* ._scope() * ._scope()
* *
*/ */
export const { fmattack } = registerControl('fmattack'); export const {
fmattack,
fmattack1,
fmattack2,
fmattack3,
fmattack4,
fmattack5,
fmattack6,
fmattack7,
fmattack8,
fmatt,
fmatt1,
fmatt2,
fmatt3,
fmatt4,
fmatt5,
fmatt6,
fmatt7,
fmatt8,
} = registerMultiControl('fmattack', 8, 'fmatt');
/** /**
* Waveform of the fm modulator * Waveform of the fm modulator
* *
* A number may be added afterwards to control the waveform
* any of the 8 individual FMs (e.g. `fmwave6`)
*
* @name fmwave * @name fmwave
* @param {number | Pattern} wave waveform * @param {number | Pattern} wave waveform
* @example * @example
@@ -500,12 +570,19 @@ export const { fmattack } = registerControl('fmattack');
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6) * n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6)
* *
*/ */
export const { fmwave } = registerControl('fmwave'); export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmwave7, fmwave8 } = registerMultiControl(
'fmwave',
8,
);
/** /**
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase. * Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
* *
* A number may be added afterwards to control the decay of the envelope of
* any of the 8 individual FMs (e.g. `fmdec6`)
*
* @name fmdecay * @name fmdecay
* @synonyms fmdec
* @param {number | Pattern} time decay time * @param {number | Pattern} time decay time
* @example * @example
* note("c e g b g e") * note("c e g b g e")
@@ -515,11 +592,35 @@ export const { fmwave } = registerControl('fmwave');
* ._scope() * ._scope()
* *
*/ */
export const { fmdecay } = registerControl('fmdecay'); export const {
fmdecay,
fmdecay1,
fmdecay2,
fmdecay3,
fmdecay4,
fmdecay5,
fmdecay6,
fmdecay7,
fmdecay8,
fmdec,
fmdec1,
fmdec2,
fmdec3,
fmdec4,
fmdec5,
fmdec6,
fmdec7,
fmdec8,
} = registerMultiControl('fmdecay', 8, 'fmdec');
/** /**
* Sustain level for the FM envelope: how much modulation is applied after the decay phase * Sustain level for the FM envelope: how much modulation is applied after the decay phase
* *
* A number may be added afterwards to control the sustain of the envelope of
* any of the 8 individual FMs (e.g. `fmsus7`)
*
* @name fmsustain * @name fmsustain
* @synonyms fmsus
* @param {number | Pattern} level sustain level * @param {number | Pattern} level sustain level
* @example * @example
* note("c e g b g e") * note("c e g b g e")
@@ -529,10 +630,69 @@ export const { fmdecay } = registerControl('fmdecay');
* ._scope() * ._scope()
* *
*/ */
export const { fmsustain } = registerControl('fmsustain'); export const {
// these are not really useful... skipping for now fmsustain,
export const { fmrelease } = registerControl('fmrelease'); fmsustain1,
export const { fmvelocity } = registerControl('fmvelocity'); fmsustain2,
fmsustain3,
fmsustain4,
fmsustain5,
fmsustain6,
fmsustain7,
fmsustain8,
fmsus,
fmsus1,
fmsus2,
fmsus3,
fmsus4,
fmsus5,
fmsus6,
fmsus7,
fmsus8,
} = registerMultiControl('fmsustain', 8, 'fmsus');
/**
* Release time for the FM envelope: how much modulation is applied after the note is released
*
* A number may be added afterwards to control the release of the envelope of
* any of the 8 individual FMs (e.g. `fmrel8`)
*
* @name fmrelease
* @synonyms fmrel
* @param {number | Pattern} time release time
*
*/
export const {
fmrelease,
fmrelease1,
fmrelease2,
fmrelease3,
fmrelease4,
fmrelease5,
fmrelease6,
fmrelease7,
fmrelease8,
fmrel,
fmrel1,
fmrel2,
fmrel3,
fmrel4,
fmrel5,
fmrel6,
fmrel7,
fmrel8,
} = registerMultiControl('fmrelease', 8, 'fmrel');
// FM Matrix
// Note: we do not declare top-level exports here since it would add
// ~162 more explicit exports. This is likely fine as the most common use-case would be to at least
// declare one other FM prior to utilizing the matrix functionality, but if we ever decide we need it,
// TODO to add it explicitly / go with the globalThis approach
for (let i = 0; i <= 8; i++) {
for (let j = 0; j <= 8; j++) {
registerControl(`fmi${i}${j}`, `fm${i}${j}`);
}
}
/** /**
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`. * Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
@@ -1294,6 +1454,8 @@ export const { fanchor } = registerControl('fanchor');
* *
* @name lprate * @name lprate
* @param {number | Pattern} rate rate in hertz * @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'); export const { lprate } = registerControl('lprate');
@@ -1302,6 +1464,8 @@ export const { lprate } = registerControl('lprate');
* *
* @name lpsync * @name lpsync
* @param {number | Pattern} rate rate in cycles * @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'); export const { lpsync } = registerControl('lpsync');
@@ -1310,8 +1474,22 @@ export const { lpsync } = registerControl('lpsync');
* *
* @name lpdepth * @name lpdepth
* @param {number | Pattern} depth depth of modulation * @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'); export const { lpdepth } = registerControl('lpdepth');
/**
* Depth of the LFO for the lowpass filter, in HZ
*
* @name lpdepthfrequency
* @synonyms lpdepthfreq
* @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, lpdepthfreq } = registerControl('lpdepthfrequency', 'lpdepthfreq');
/** /**
* Shape of the LFO for the lowpass filter * Shape of the LFO for the lowpass filter
@@ -1361,6 +1539,18 @@ export const { bpsync } = registerControl('bpsync');
*/ */
export const { bpdepth } = registerControl('bpdepth'); export const { bpdepth } = registerControl('bpdepth');
/**
* Depth of the LFO for the bandpass filter, in HZ
*
* @name bpdepthfrequency
* @synonyms bpdepthfreq
* @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, bpdepthfreq } = registerControl('bpdepthfrequency', 'bpdepthfreq');
/** /**
* Shape of the LFO for the bandpass filter * Shape of the LFO for the bandpass filter
* *
@@ -1409,6 +1599,18 @@ export const { hpsync } = registerControl('hpsync');
*/ */
export const { hpdepth } = registerControl('hpdepth'); export const { hpdepth } = registerControl('hpdepth');
/**
* Depth of the LFO for the hipass filter, in hz
*
* @name hpdepthfrequency
* @synonyms hpdepthfreq
* @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, hpdepthfreq } = registerControl('hpdepthfrequency', 'hpdepthfreq');
/** /**
* Shape of the LFO for the highpass filter * Shape of the LFO for the highpass filter
* *
@@ -1820,12 +2022,12 @@ export const { nudge } = registerControl('nudge');
* Sets the default octave of a synth. * Sets the default octave of a synth.
* *
* @name octave * @name octave
* @synonyms oct
* @param {number | Pattern} octave octave number * @param {number | Pattern} octave octave number
* @example * @example
* n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc() * n("0,4,7").scale("F:minor").s('supersaw').octave("<0 1 2 3>")
* @superDirtOnly
*/ */
export const { octave } = registerControl('octave'); export const { octave, oct } = registerControl('octave', 'oct');
// ['ophatdecay'], // ['ophatdecay'],
// TODO: example // TODO: example
@@ -1833,6 +2035,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. * An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
* *
* @name orbit * @name orbit
* @synonyms o
* @param {number | Pattern} number * @param {number | Pattern} number
* @example * @example
* stack( * stack(
@@ -1840,7 +2043,7 @@ export const { octave } = registerControl('octave');
* s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2) * 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 // 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'); export const { overgain } = registerControl('overgain');
// TODO: what is this? not found in tidal doc. Similar to above, but limited to 1 // TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
@@ -2264,7 +2467,6 @@ export const { tsdelay } = registerControl('tsdelay');
export const { real } = registerControl('real'); export const { real } = registerControl('real');
export const { imag } = registerControl('imag'); export const { imag } = registerControl('imag');
export const { enhance } = registerControl('enhance'); export const { enhance } = registerControl('enhance');
export const { partials } = registerControl('partials');
export const { comb } = registerControl('comb'); export const { comb } = registerControl('comb');
export const { smear } = registerControl('smear'); export const { smear } = registerControl('smear');
export const { scram } = registerControl('scram'); export const { scram } = registerControl('scram');
@@ -2584,3 +2786,17 @@ export const scrub = register(
}, },
false, false,
); );
/**
* Transient shaper. Gives independent control over the emphasis on transients
* and sustains
*
* @name transient
* @param {number | Pattern} attack Emphasis on transients; between -1 (deaccentuate) and 1 (accentuate)
* @param {number | Pattern} sustain Emphasis on the sustains; between -1 (deaccentuate) and 1 (accentuate)
* @example
* s("bd").transient("<-1 -0.5 0 0.5 1>")
* @example
* s("hh*16").bank("tr909").transient("<-1:1 1:-1>")
*/
export const { transient } = registerControl(['transient', 'transsustain']);
+106 -9
View File
@@ -25,7 +25,7 @@ import {
stringifyValues, stringifyValues,
} from './util.mjs'; } from './util.mjs';
import drawLine from './drawLine.mjs'; import drawLine from './drawLine.mjs';
import { logger } from './logger.mjs'; import { errorLogger, logger } from './logger.mjs';
let stringParser; let stringParser;
@@ -414,7 +414,7 @@ export class Pattern {
try { try {
return this.query(new State(new TimeSpan(begin, end), controls)); return this.query(new State(new TimeSpan(begin, end), controls));
} catch (err) { } catch (err) {
logger(`[query]: ${err.message}`, 'error'); errorLogger(err, 'query');
return []; return [];
} }
} }
@@ -574,7 +574,8 @@ export class Pattern {
* Returns a new Pattern, which only returns haps that meet the given test. * Returns a new Pattern, which only returns haps that meet the given test.
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern * @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
* @returns Pattern * @returns Pattern
* @noAutocomplete * @example
* s("bd*8").velocity(rand).filterHaps((h) => (h.whole.begin % 1) < h.value.velocity)
*/ */
filterHaps(hap_test) { filterHaps(hap_test) {
return new Pattern((state) => this.query(state).filter(hap_test)); return new Pattern((state) => this.query(state).filter(hap_test));
@@ -585,7 +586,11 @@ export class Pattern {
* inside haps. * inside haps.
* @param {Function} value_test * @param {Function} value_test
* @returns Pattern * @returns Pattern
* @noAutocomplete * @example
* const drums = s("bd sd bd sd")
* kick: drums.filterValues((v) => v.s === 'bd').duck(2)
* snare: drums.filterValues((v) => v.s === 'sd')
* bass: s("saw!4").note("G#1").lpf(80).lpenv(4).orbit(2)
*/ */
filterValues(value_test) { filterValues(value_test) {
return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))).setSteps(this._steps); return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))).setSteps(this._steps);
@@ -1587,7 +1592,13 @@ export const func = curry((a, b) => reify(b).func(a));
* *
* @param {string | string[]} name name of the function, or an array of names to be used as synonyms * @param {string | string[]} name name of the function, or an array of names to be used as synonyms
* @param {function} func function with 1 or more params, where last is the current pattern * @param {function} func function with 1 or more params, where last is the current pattern
* @noAutocomplete * @param {bool} patternify defaults to true; if set to false, you will have more control over the arguments to `func` as they will be
* in their raw form and it will be up to you to patternify them and/or query them for values
* @example
* const vlpf = register('vlpf', (freq, pat) => {
* return pat.fmap((v) => ({...v, cutoff: freq * (v.velocity ?? 1) }));
* })
* s("saw").seg(8).velocity(rand).vlpf(800)
* *
*/ */
export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) { export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) {
@@ -2234,7 +2245,7 @@ export const brak = register('brak', function (pat) {
}); });
/** /**
* Reverse all haps in a pattern * Reverse all cycles in a pattern. See also `revv` for reversing a whole pattern.
* *
* @name rev * @name rev
* @memberof Pattern * @memberof Pattern
@@ -2266,6 +2277,23 @@ export const rev = register(
true, true,
); );
/**
* Reverse a whole pattern. See also `rev` for reversing each cycle.
*
* @name revv
* @memberof Pattern
* @returns Pattern
* @example
* // This is the same as `<[g e] [d c]>`. If `rev()` is used, you get
* // the same as `<[d c] [g e]>`, where each cycle reverses, but the order of
* // cycles stays the same.
* note("<[c d] [e g]>").revv()
*/
export const revv = register('revv', function (pat) {
const negateSpan = (span) => new TimeSpan(Fraction(0).sub(span.end), Fraction(0).sub(span.begin));
return pat.withQuerySpan(negateSpan).withHapSpan(negateSpan);
});
/** Like press, but allows you to specify the amount by which each /** Like press, but allows you to specify the amount by which each
* event is shifted. pressBy(0.5) is the same as press, while * event is shifted. pressBy(0.5) is the same as press, while
* pressBy(1/3) shifts each event by a third of its timespan. * pressBy(1/3) shifts each event by a third of its timespan.
@@ -2659,8 +2687,13 @@ export const hsl = register('hsl', (h, s, l, pat) => {
/** /**
* Tags each Hap with an identifier. Good for filtering. The function populates Hap.context.tags (Array). * Tags each Hap with an identifier. Good for filtering. The function populates Hap.context.tags (Array).
* @name tag * @name tag
* @noAutocomplete
* @param {string} tag anything unique * @param {string} tag anything unique
* @example
* s("saw!16").note("F1")
* .lpf(tri.range(40, 80).slow(4)).lpenv(5).lpq(4).lpd(0.15)
* .when(rand.late(0.1).gte(0.5), x => x.transpose("12").tag('altered'))
* .when(rand.late(0.2).gte(0.5), x => x.s("square").tag('altered'))
* .when("<0 1>", x => x.filter((hap) => hap.hasTag('altered')))
*/ */
Pattern.prototype.tag = function (tag) { Pattern.prototype.tag = function (tag) {
return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) })); return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) }));
@@ -2671,15 +2704,16 @@ Pattern.prototype.tag = function (tag) {
* @name filter * @name filter
* @param {Function} test function to test Hap * @param {Function} test function to test Hap
* @example * @example
* s("hh!7 oh").filter(hap => hap.value.s==='hh') * s("hh!7 oh").filter(hap => hap.value.s === 'hh')
*/ */
export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test))); export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test)));
/** /**
* Filters haps by their begin time * Filters haps by their begin time
* @name filterWhen * @name filterWhen
* @noAutocomplete
* @param {Function} test function to test Hap.whole.begin * @param {Function} test function to test Hap.whole.begin
* @example
* oneCycle: s("bd*4").filterWhen((t) => t < 1)
*/ */
export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin))); export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin)));
@@ -3624,3 +3658,66 @@ for (const name of distAlgoNames) {
return this.distort(argsPat); 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');
};
+17 -7
View File
@@ -152,9 +152,9 @@ export function repl({
// allows muting a pattern x with x_ or _x // allows muting a pattern x with x_ or _x
return silence; return silence;
} }
if (id === '$') { if (id.includes('$')) {
// allows adding anonymous patterns with $: // allows adding anonymous patterns with $:
id = `$${anonymousIndex}`; id = `${id}${anonymousIndex}`;
anonymousIndex++; anonymousIndex++;
} }
pPatterns[id] = this; pPatterns[id] = this;
@@ -215,8 +215,19 @@ export function repl({
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
if (Object.keys(pPatterns).length) { if (Object.keys(pPatterns).length) {
let patterns = []; let patterns = [];
let soloActive = false;
for (const [key, value] of Object.entries(pPatterns)) { for (const [key, value] of Object.entries(pPatterns)) {
patterns.push(value.withState((state) => state.setControls({ id: key }))); // 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) { if (eachTransform) {
// Explicit lambda so only element (not index and array) are passed // Explicit lambda so only element (not index and array) are passed
@@ -227,14 +238,13 @@ export function repl({
pattern = eachTransform(pattern); pattern = eachTransform(pattern);
} }
if (allTransforms.length) { if (allTransforms.length) {
for (let i in allTransforms) { for (const transform of allTransforms) {
pattern = allTransforms[i](pattern); pattern = transform(pattern);
} }
} }
if (!isPattern(pattern)) { if (!isPattern(pattern)) {
const message = `got "${typeof evaluated}" instead of pattern`; pattern = silence;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
} }
logger(`[eval] code updated`); logger(`[eval] code updated`);
pattern = await setPattern(pattern, autostart); 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); 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 * @name binary
* @param {number} n - input number to convert to 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 * @name binaryN
* @param {number} n - input number to convert to binary * @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)); 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) => { export const randrun = (n) => {
return signal((t) => { return signal((t) => {
// Without adding 0.5, the first cycle is always 0,1,2,3,... // 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 * @example
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8) * wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
* @example * @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 * @example
* // The probability can itself be a pattern * // The probability can itself be a pattern
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s() * wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
+12
View File
@@ -586,6 +586,18 @@ describe('Pattern', () => {
.map((a) => a.value), .map((a) => a.value),
).toStrictEqual(['c', 'b', 'a']); ).toStrictEqual(['c', 'b', 'a']);
}); });
it('Does not reverse the order of cycles', () => {
expect(fastcat('a', 'b', 'c', 'd').slow(2).rev().fast(2).sortHapsByPart().firstCycle()).toStrictEqual(
fastcat('b', 'a', 'd', 'c').firstCycle(),
);
});
});
describe('revv()', () => {
it('Does reverse the order of cycles', () => {
expect(fastcat('a', 'b', 'c', 'd').slow(2).revv().fast(2).sortHapsByPart().firstCycle()).toStrictEqual(
fastcat('d', 'c', 'b', 'a').firstCycle(),
);
});
}); });
describe('sequence()', () => { describe('sequence()', () => {
it('Can work like fastcat', () => { it('Can work like fastcat', () => {
+2 -2
View File
@@ -1,9 +1,9 @@
# @strudel/draw # @strudel/canvas
Helpers for drawing with the Canvas API and Strudel Helpers for drawing with the Canvas API and Strudel
## Install ## Install
```sh ```sh
npm i @strudel/draw --save npm i @strudel/canvas --save
``` ```
+1
View File
@@ -35,6 +35,7 @@ export async function initHydra(options = {}) {
hydra.synth.s0.init({ src: canvas }); hydra.synth.s0.init({ src: canvas });
} }
} }
return hydra;
} }
export function clearHydra() { export function clearHydra() {
+26 -9
View File
@@ -479,14 +479,24 @@ Pattern.prototype.midi = function (midiport, options = {}) {
let listeners = {}; let listeners = {};
const refs = {}; const refs = {};
const refsByChan = {};
/** /**
* MIDI input: Opens a MIDI input port to receive MIDI control change messages. * MIDI input: Opens a MIDI input port to receive MIDI control change messages.
*
* The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel
* @param {string | number} input MIDI device name or index defaulting to 0 * @param {string | number} input MIDI device name or index defaulting to 0
* @returns {Function} * @returns {function(number, number=): Pattern} A function from (cc, channel?) to a pattern.
* When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1)
* that came through that cc number (and channel, if provided)
* @example * @example
* let cc = await midin('IAC Driver Bus 1') * const cc = await midin('IAC Driver Bus 1')
* note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth") * note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth")
* @example
* const allCC = await midin('IAC Driver Bus 1')
* const cc = (ccNum) => allCC(ccNum, 2) // just channel 2
* note("c a f e").s("saw")
* .when(cc(0).gt(0), x => x.postgain(0))
*/ */
export async function midin(input) { export async function midin(input) {
if (isPattern(input)) { if (isPattern(input)) {
@@ -511,17 +521,24 @@ export async function midin(input) {
}`, }`,
); );
} }
// ensure refs for this input are initialized refs[input] ??= {};
if (!refs[input]) { refsByChan[input] ??= {};
refs[input] = {}; const cc = (cc, chan) => {
} if (chan !== undefined) {
const cc = (cc) => ref(() => refs[input][cc] || 0); return ref(() => refsByChan[input][cc]?.[chan] || 0);
}
return ref(() => refs[input][cc] || 0);
};
listeners[input] && device.removeListener('midimessage', listeners[input]); listeners[input] && device.removeListener('midimessage', listeners[input]);
listeners[input] = (e) => { listeners[input] = (e) => {
const cc = e.dataBytes[0]; const ccNum = e.dataBytes[0];
const v = e.dataBytes[1]; const v = e.dataBytes[1];
refs[input] && (refs[input][cc] = v / 127); const chan = e.message.channel;
const scaled = v / 127;
refsByChan[input][ccNum] ??= {};
refsByChan[input][ccNum][chan] = scaled;
refs[input][ccNum] = scaled;
}; };
device.addListener('midimessage', listeners[input]); device.addListener('midimessage', listeners[input]);
return cc; return cc;
+1 -3
View File
@@ -9,6 +9,7 @@ import readline from 'readline';
import os from 'os'; import os from 'os';
const LOG = !!process.env.LOG || false; const LOG = !!process.env.LOG || false;
const PORT = process.env.PORT || 5432;
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg']; const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
const isAudioFile = (f) => { const isAudioFile = (f) => {
@@ -54,7 +55,6 @@ async function getBanks(directory, flat = false) {
banks[bank].push(subDir); banks[bank].push(subDir);
return subDir; return subDir;
}); });
banks._base = `http://localhost:5432`;
return { banks, files }; return { banks, files };
} }
@@ -134,8 +134,6 @@ const server = http.createServer(async (req, res) => {
readStream.pipe(res); readStream.pipe(res);
}); });
// eslint-disable-next-line
const PORT = process.env.PORT || 5432;
const IP_ADDRESS = '0.0.0.0'; const IP_ADDRESS = '0.0.0.0';
let IP; let IP;
const networkInterfaces = os.networkInterfaces(); const networkInterfaces = os.networkInterfaces();
+3 -2
View File
@@ -6,6 +6,7 @@ import {
getADSRValues, getADSRValues,
getPitchEnvelope, getPitchEnvelope,
getVibratoOscillator, getVibratoOscillator,
onceEnded,
} from '@strudel/webaudio'; } from '@strudel/webaudio';
import gm from './gm.mjs'; import gm from './gm.mjs';
@@ -170,12 +171,12 @@ export function registerSoundfonts() {
bufferSource.stop(envEnd); bufferSource.stop(envEnd);
const stop = (releaseTime) => {}; const stop = (releaseTime) => {};
bufferSource.onended = () => { onceEnded(bufferSource, () => {
bufferSource.disconnect(); bufferSource.disconnect();
vibratoOscillator?.stop(); vibratoOscillator?.stop();
node.disconnect(); node.disconnect();
onended(); onended();
}; });
return { node, stop }; return { node, stop };
}, },
{ type: 'soundfont', prebake: true, fonts }, { type: 'soundfont', prebake: true, fonts },
+1 -1
View File
@@ -89,7 +89,7 @@ superdough({ s: 'bd', delay: 0.5 }, 0, 1);
- `decay`: seconds of decay phase - `decay`: seconds of decay phase
- `sustain`: gain of sustain phase - `sustain`: gain of sustain phase
- `release`: seconds of release 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 - `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds
### registerSynthSounds() ### registerSynthSounds()
+176 -80
View File
@@ -1,7 +1,7 @@
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
import { getNoiseBuffer } from './noise.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
import { getNoiseBuffer } from './noise.mjs';
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
export const noises = ['pink', 'white', 'brown', 'crackle']; export const noises = ['pink', 'white', 'brown', 'crackle'];
@@ -191,8 +191,7 @@ export function applyParameterModulators(audioContext, param, start, end, envelo
const lfo = getParamLfo(audioContext, param, start, end, lfoValues); const lfo = getParamLfo(audioContext, param, start, end, lfoValues);
return { lfo, disconnect: () => lfo?.disconnect() }; return { lfo, disconnect: () => lfo?.disconnect() };
} }
export function createFilter(context, start, end, params, cps, cycle) {
export function createFilter(context, start, end, params, cps) {
let { let {
frequency, frequency,
anchor, anchor,
@@ -202,12 +201,14 @@ export function createFilter(context, start, end, params, cps) {
q = 1, q = 1,
drive = 0.69, drive = 0.69,
depth, depth,
depthfrequency,
dcoffset = -0.5, dcoffset = -0.5,
skew, skew,
shape, shape,
rate, rate,
sync, sync,
} = params; } = params;
let frequencyParam, filter; let frequencyParam, filter;
if (model === 'ladder') { if (model === 'ladder') {
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive }); filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
@@ -238,9 +239,27 @@ export function createFilter(context, start, end, params, cps) {
if (sync != null) { if (sync != null) {
rate = cps * sync; rate = cps * sync;
} }
const lfoValues = { depth, dcoffset, skew, shape, frequency: rate }; const hasLFO = [depth, depthfrequency, skew, shape, rate].some((v) => v !== undefined);
getParamLfo(context, frequencyParam, start, end, lfoValues); let lfo;
return filter; 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,
};
lfo = getParamLfo(context, frequencyParam, start, end, lfoValues);
}
return { filter, lfo };
} }
// stays 1 until .5, then fades out // stays 1 until .5, then fades out
@@ -262,7 +281,15 @@ export function drywet(dry, wet, wetAmount = 0) {
let mix = ac.createGain(); let mix = ac.createGain();
dry_gain.connect(mix); dry_gain.connect(mix);
wet_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']; let curves = ['linear', 'exponential'];
@@ -297,6 +324,10 @@ export function getVibratoOscillator(param, value, t) {
gain.gain.value = vibmod * 100; gain.gain.value = vibmod * 100;
vibratoOscillator.connect(gain); vibratoOscillator.connect(gain);
gain.connect(param); gain.connect(param);
vibratoOscillator.onended = () => {
gain.disconnect(param);
vibratoOscillator.disconnect(gain);
};
vibratoOscillator.start(t); vibratoOscillator.start(t);
return vibratoOscillator; return vibratoOscillator;
} }
@@ -318,25 +349,17 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
constantNode.connect(zeroGain); constantNode.connect(zeroGain);
// Schedule the `onComplete` callback to occur at `stopTime` // Schedule the `onComplete` callback to occur at `stopTime`
constantNode.onended = () => { onceEnded(constantNode, () => {
// Ensure garbage collection releaseAudioNode(zeroGain);
try { releaseAudioNode(constantNode);
zeroGain.disconnect();
} catch {
// pass
}
try {
constantNode.disconnect();
} catch {
// pass
}
onComplete(); onComplete();
}; });
constantNode.start(startTime); constantNode.start(startTime);
constantNode.stop(stopTime); constantNode.stop(stopTime);
return constantNode; return constantNode;
} }
const mod = (freq, range = 1, type = 'sine') => {
const mod = (freq, type = 'sine') => {
const ctx = getAudioContext(); const ctx = getAudioContext();
let osc; let osc;
if (noises.includes(type)) { if (noises.includes(type)) {
@@ -348,64 +371,89 @@ const mod = (freq, range = 1, type = 'sine') => {
osc.type = type; osc.type = type;
osc.frequency.value = freq; osc.frequency.value = freq;
} }
osc.start(); osc.start();
const g = new GainNode(ctx, { gain: range }); return osc;
osc.connect(g); // -range, range
return { node: g, stop: (t) => osc.stop(t) };
}; };
const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => {
const fm = (frequencyparam, harmonicityRatio, wave = 'sine') => {
const carrfreq = frequencyparam.value; const carrfreq = frequencyparam.value;
const modfreq = carrfreq * harmonicityRatio; const modfreq = carrfreq * harmonicityRatio;
const modgain = modfreq * modulationIndex; return { osc: mod(modfreq, wave), freq: modfreq };
return mod(modfreq, modgain, wave);
}; };
export function applyFM(param, value, begin) { export function applyFM(param, value, begin) {
const { const ac = getAudioContext();
fmh: fmHarmonicity = 1, const toStop = []; // fm oscillators we will expose `stop` for
fmi: fmModulationIndex, const fms = {};
fmenv: fmEnvelopeType = 'exp', // Matrix
fmattack: fmAttack, for (let i = 1; i <= 8; i++) {
fmdecay: fmDecay, for (let j = 0; j <= 8; j++) {
fmsustain: fmSustain, let control;
fmrelease: fmRelease, if (i === j + 1) {
fmvelocity: fmVelocity, // Standard fm3 -> fm2 -> fm1 -> param usage
fmwave: fmWaveform = 'sine', const iS = i === 1 ? '' : i;
duration, control = `fmi${iS}`;
} = value; } else {
let modulator; control = `fmi${i}${j}`;
let stop = () => {}; }
const amt = value[control];
if (fmModulationIndex) { if (!amt) continue;
const ac = getAudioContext(); let io = [];
const envGain = ac.createGain(); for (let [isMod, idx] of [
const fmmod = fm(param, fmHarmonicity, fmModulationIndex, fmWaveform); [true, i], // source
[false, j], // target
modulator = fmmod.node; ]) {
stop = fmmod.stop; if (idx === 0) {
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) { io.push(param);
// no envelope by default continue;
modulator.connect(param); }
} else { if (!fms[idx]) {
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]); const idxS = idx === 1 ? '' : idx;
const holdEnd = begin + duration; const { osc, freq } = fm(param, value[`fmh${idxS}`] ?? 1, value[`fmwave${idxS}`] ?? 'sine');
getParamADSR( toStop.push(osc);
envGain.gain, const toCleanup = [osc]; // nodes we want to cleanup after oscillator `stop`
attack, const adsr = ['attack', 'decay', 'sustain', 'release'].map((s) => value[`fm${s}${idxS}`]);
decay, let output = osc;
sustain, if (adsr.some((v) => v !== undefined)) {
release, const envGain = ac.createGain();
0, const [attack, decay, sustain, release] = getADSRValues(adsr);
1, const holdEnd = begin + value.duration;
begin, const fmEnvelopeType = value[`fmenv${idxS}`] ?? 'exp';
holdEnd, getParamADSR(
fmEnvelopeType === 'exp' ? 'exponential' : 'linear', envGain.gain,
); attack,
modulator.connect(envGain); decay,
envGain.connect(param); sustain,
release,
0,
1,
begin,
holdEnd,
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
);
toCleanup.push(envGain);
output = osc.connect(envGain);
}
fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup };
}
const { input, output, freq, osc, toCleanup } = fms[idx];
const g = gainNode(amt * freq);
io.push(isMod ? output.connect(g) : input);
cleanupOnEnd(osc, [...toCleanup, g]);
}
if (!io[1]) {
logger(
`[superdough] control ${control} failed to connect FM ${i} to target ${j} due to missing frequency parameter (likely because fm${j} is noise)`,
'warning',
);
continue;
}
io[0].connect(io[1]);
} }
} }
return { stop }; return {
stop: (t) => toStop.forEach((m) => m?.stop(t)),
};
} }
// Saturation curves // Saturation curves
@@ -505,7 +553,7 @@ export const getDistortion = (distort, postgain, algorithm) => {
}; };
export const getFrequencyFromValue = (value, defaultNote = 36) => { export const getFrequencyFromValue = (value, defaultNote = 36) => {
let { note, freq } = value; let { note, freq, octave = 0 } = value;
note = note || defaultNote; note = note || defaultNote;
if (typeof note === 'string') { if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48 note = noteToMidi(note); // e.g. c3 => 48
@@ -514,14 +562,62 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => {
if (!freq && typeof note === 'number') { if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48); freq = midiToFreq(note); // + 48);
} }
freq *= Math.pow(2, octave);
return Number(freq); return Number(freq);
}; };
export const destroyAudioWorkletNode = (node) => { // This helper should be used instead of the `node.onended = callback` pattern
if (node == null) { // It adds a mechanism to help minimize gc retention
return; export const onceEnded = (node, callback) => {
} const onended = callback;
node.disconnect(); node.onended = function cleanup() {
node.parameters.get('end')?.setValueAtTime(0, 0); onended && onended();
this.onended = null;
};
};
export const releaseAudioNode = (node) => {
if (node == null) return;
// check we received an AudioNode
if (!(node instanceof AudioNode)) {
throw new Error('releaseAudioNode can only release an AudioNode');
}
// https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect
node.disconnect();
// make sure all AudioScheduledSourceNodes are in a stopped state
// https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode
if (node instanceof AudioScheduledSourceNode) {
if (node.onended && node.onended.name !== 'cleanup') {
logger(
`[superdough] Deprecation warning: it seems your code path is setting 'node.onended = callback' instead of using the onceEnded helper`,
);
}
try {
node.stop();
} catch (e) {
// At the stage, `start` was not called on the node
// but an `onended` callback releasing resources may exist
// and we want it to fire :
// - we force a start/stop cycle so that `onended` gets called
// - we `lock` the node so that no-one can start it
node.start(node.context.currentTime + 5); // will never happen
node.stop();
}
}
// https://www.w3.org/TR/webaudio-1.1/#AudioNode-actively-processing
// An AudioWorkletNode is actively processing when its AudioWorkletProcessor's [[callable process]]
// returns true and either its active source flag is true or
// any AudioNode connected to one of its inputs is actively processing.
if (node instanceof AudioWorkletNode) {
node.parameters.get('end')?.setValueAtTime(0, 0);
}
};
// Once the `anchor` node has ended, release all nodes in `toCleanup`
export const cleanupOnEnd = (anchor, toCleanup) => {
onceEnded(anchor, () => toCleanup.forEach((n) => releaseAudioNode(n)));
}; };
+2 -1
View File
@@ -65,8 +65,9 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) {
export function getNoiseMix(inputNode, wet, t) { export function getNoiseMix(inputNode, wet, t) {
const noiseOscillator = getNoiseOscillator('pink', t); const noiseOscillator = getNoiseOscillator('pink', t);
const noiseMix = drywet(inputNode, noiseOscillator.node, wet); const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
noiseOscillator.node.onended = () => noiseMix.onended();
return { return {
node: noiseMix, node: noiseMix.node,
stop: (time) => noiseOscillator?.stop(time), stop: (time) => noiseOscillator?.stop(time),
}; };
} }
+2
View File
@@ -104,6 +104,8 @@ var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt,
player.start(); player.start();
context.oncomplete = function (event) { context.oncomplete = function (event) {
callback(event.renderedBuffer); callback(event.renderedBuffer);
filter.disconnect();
player.disconnect();
}; };
context.startRendering(); context.startRendering();
+20 -17
View File
@@ -1,7 +1,14 @@
import { getCommonSampleInfo } from './util.mjs'; import { getBaseURL, getCommonSampleInfo } from './util.mjs';
import { registerSound, registerWaveTable } from './index.mjs'; import { registerSound, registerWaveTable } from './index.mjs';
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import {
getADSRValues,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
onceEnded,
releaseAudioNode,
} from './helpers.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer> const bufferCache = {}; // string: Promise<ArrayBuffer>
@@ -211,7 +218,7 @@ export async function fetchSampleMap(url) {
// not a browser // not a browser
return; return;
} }
const base = url.split('/').slice(0, -1).join('/'); const base = getBaseURL(url);
if (typeof fetch === 'undefined') { if (typeof fetch === 'undefined') {
// skip fetch when in node / testing // skip fetch when in node / testing
return; return;
@@ -236,12 +243,6 @@ export async function fetchSampleMap(url) {
* sd: '808sd/SD0010.WAV' * sd: '808sd/SD0010.WAV'
* }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/'); * }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
* s("[bd ~]*2, [~ hh]*2, ~ sd") * s("[bd ~]*2, [~ hh]*2, ~ sd")
* @example
* samples('shabda:noise,chimp:2')
* s("noise <chimp:0*2 chimp:1>")
* @example
* samples('shabda/speech/fr-FR/f:chocolat')
* s("chocolat*4")
*/ */
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => { export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
@@ -286,17 +287,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl); const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
// asny stuff above took too long?
if (ac.currentTime > t) {
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
// console.warn('sample still loading:', s, n);
return;
}
if (!bufferSource) { if (!bufferSource) {
logger(`[sampler] could not load "${s}:${n}"`, 'error'); logger(`[sampler] could not load "${s}:${n}"`, 'error');
return; return;
} }
// async stuff above took too long?
if (ac.currentTime > t) {
logger(`[sampler] loading sound "${s}:${n}" took too long`, 'highlight');
// AudioBufferSourceNode will never be used. discard it
releaseAudioNode(bufferSource);
return;
}
// vibrato // vibrato
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
@@ -319,13 +322,13 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
node.connect(out); node.connect(out);
bufferSource.onended = function () { onceEnded(bufferSource, function () {
bufferSource.disconnect(); bufferSource.disconnect();
vibratoOscillator?.stop(); vibratoOscillator?.stop();
node.disconnect(); node.disconnect();
out.disconnect(); out.disconnect();
onended(); onended();
}; });
let envEnd = holdEnd + release + 0.01; let envEnd = holdEnd + release + 0.01;
bufferSource.stop(envEnd); bufferSource.stop(envEnd);
const stop = (endTime) => { const stop = (endTime) => {
+63 -28
View File
@@ -263,8 +263,8 @@ let audioReady;
export async function initAudioOnFirstClick(options) { export async function initAudioOnFirstClick(options) {
if (!audioReady) { if (!audioReady) {
audioReady = new Promise((resolve) => { audioReady = new Promise((resolve) => {
document.addEventListener('click', async function listener() { document.addEventListener('mousedown', async function listener() {
document.removeEventListener('click', listener); document.removeEventListener('mousedown', listener);
await initAudio(options); await initAudio(options);
resolve(); resolve();
}); });
@@ -274,7 +274,7 @@ export async function initAudioOnFirstClick(options) {
} }
let controller; let controller;
function getSuperdoughAudioController() { export function getSuperdoughAudioController() {
if (controller == null) { if (controller == null) {
controller = new SuperdoughAudioController(getAudioContext()); controller = new SuperdoughAudioController(getAudioContext());
} }
@@ -287,11 +287,11 @@ export function connectToDestination(input, channels) {
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) { function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
const ac = getAudioContext(); const ac = getAudioContext();
const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 }); const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
//filters //filters
const numStages = 2; //num of filters in series const numStages = 1; //num of filters in series
let fOffset = 0; let fOffset = 282; //for backward compat in #1800
const filterChain = []; const filterChain = [];
for (let i = 0; i < numStages; i++) { for (let i = 0; i < numStages; i++) {
const filter = ac.createBiquadFilter(); const filter = ac.createBiquadFilter();
@@ -300,14 +300,11 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000
filter.frequency.value = centerFrequency + fOffset; filter.frequency.value = centerFrequency + fOffset;
filter.Q.value = 2 - Math.min(Math.max(depth * 2, 0), 1.9); filter.Q.value = 2 - Math.min(Math.max(depth * 2, 0), 1.9);
lfoGain.connect(filter.detune); lfo.connect(filter.detune);
fOffset += 282; fOffset += 282;
if (i > 0) {
filterChain[i - 1].connect(filter);
}
filterChain.push(filter); filterChain.push(filter);
} }
return filterChain[filterChain.length - 1]; return { filterChain, lfo };
} }
function getFilterType(ftype) { function getFilterType(ftype) {
@@ -413,7 +410,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
release = 0, release = 0,
//phaser //phaser
phaserrate: phaser, phaserrate,
phaserdepth = getDefaultValue('phaserdepth'), phaserdepth = getDefaultValue('phaserdepth'),
phasersweep, phasersweep,
phasercenter, phasercenter,
@@ -451,6 +448,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
compressorKnee, compressorKnee,
compressorAttack, compressorAttack,
compressorRelease, compressorRelease,
transient,
transsustain,
} = value; } = value;
delaytime = delaytime ?? cycleToSeconds(delaysync, cps); delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
@@ -481,7 +480,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 // oldest audio nodes will be destroyed if maximum polyphony is exceeded
for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) { for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) {
const ch = activeSoundSources.entries().next(); const ch = activeSoundSources.entries().next();
const source = ch.value[1]; const source = ch.value[1].deref();
const chainID = ch.value[0]; const chainID = ch.value[0];
const endTime = t + 0.25; const endTime = t + 0.25;
source?.node?.gain?.linearRampToValueAtTime(0, endTime); source?.node?.gain?.linearRampToValueAtTime(0, endTime);
@@ -513,7 +512,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
if (soundHandle) { if (soundHandle) {
sourceNode = soundHandle.node; sourceNode = soundHandle.node;
activeSoundSources.set(chainID, soundHandle); activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC
} }
} else { } else {
throw new Error(`sound ${s} not found! Is it loaded?`); throw new Error(`sound ${s} not found! Is it loaded?`);
@@ -532,6 +531,23 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
chain.push(sourceNode); chain.push(sourceNode);
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
transient !== undefined &&
chain.push(
getWorklet(
ac,
'transient-processor',
{},
{
processorOptions: {
attack: transient,
sustain: transsustain,
begin: t,
end: endWithRelease,
},
},
),
);
// gain stage // gain stage
chain.push(gainNode(gain)); chain.push(gainNode(gain));
@@ -553,16 +569,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
rate: 'lprate', rate: 'lprate',
sync: 'lpsync', sync: 'lpsync',
depth: 'lpdepth', depth: 'lpdepth',
depthfrequency: 'lpdepthfrequency',
shape: 'lpshape', shape: 'lpshape',
dcoffset: 'lpdc', dcoffset: 'lpdc',
skew: 'lpskew', skew: 'lpskew',
}; };
const lpParams = pickAndRename(value, lpMap); const lpParams = pickAndRename(value, lpMap);
lpParams.type = 'lowpass'; lpParams.type = 'lowpass';
let lp = () => createFilter(ac, t, end, lpParams, cps); const lp = () => createFilter(ac, t, end, lpParams, cps, cycle);
chain.push(lp()); const { filter: lpf1, lfo: lfo1 } = lp();
chain.push(lpf1);
lfo1 && audioNodes.push(lfo1);
if (ftype === '24db') { if (ftype === '24db') {
chain.push(lp()); const { filter: lpf2, lfo: lfo2 } = lp();
chain.push(lpf2);
lfo2 && audioNodes.push(lfo2);
} }
} }
@@ -581,16 +602,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
rate: 'hprate', rate: 'hprate',
sync: 'hpsync', sync: 'hpsync',
depth: 'hpdepth', depth: 'hpdepth',
depthfrequency: 'hpdepthfrequency',
shape: 'hpshape', shape: 'hpshape',
dcoffset: 'hpdc', dcoffset: 'hpdc',
skew: 'hpskew', skew: 'hpskew',
}; };
const hpParams = pickAndRename(value, hpMap); const hpParams = pickAndRename(value, hpMap);
hpParams.type = 'highpass'; hpParams.type = 'highpass';
let hp = () => createFilter(ac, t, end, hpParams, cps); const hp = () => createFilter(ac, t, end, hpParams, cps, cycle);
chain.push(hp()); const { filter: hpf1, lfo: lfo1 } = hp();
chain.push(hpf1);
lfo1 && audioNodes.push(lfo1);
if (ftype === '24db') { if (ftype === '24db') {
chain.push(hp()); const { filter: hpf2, lfo: lfo2 } = hp();
chain.push(hpf2);
lfo2 && audioNodes.push(lfo2);
} }
} }
@@ -609,16 +635,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
rate: 'bprate', rate: 'bprate',
sync: 'bpsync', sync: 'bpsync',
depth: 'bpdepth', depth: 'bpdepth',
depthfrequency: 'bpdepthfrequency',
shape: 'bpshape', shape: 'bpshape',
dcoffset: 'bpdc', dcoffset: 'bpdc',
skew: 'bpskew', skew: 'bpskew',
}; };
const bpParams = pickAndRename(value, bpMap); const bpParams = pickAndRename(value, bpMap);
bpParams.type = 'bandpass'; bpParams.type = 'bandpass';
let bp = () => createFilter(ac, t, end, bpParams, cps); const bp = () => createFilter(ac, t, end, bpParams, cps, cycle);
chain.push(bp()); const { filter: bpf1, lfo: lfo1 } = bp();
chain.push(bpf1);
lfo1 && audioNodes.push(lfo1);
if (ftype === '24db') { if (ftype === '24db') {
chain.push(bp()); const { filter: bpf2, lfo: lfo2 } = bp();
chain.push(bpf2);
lfo2 && audioNodes.push(lfo2);
} }
} }
@@ -665,6 +696,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
curve: 1.5, curve: 1.5,
}); });
lfo.connect(amGain.gain); lfo.connect(amGain.gain);
audioNodes.push(lfo);
chain.push(amGain); chain.push(amGain);
} }
@@ -680,9 +712,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
chain.push(panner); chain.push(panner);
} }
// phaser // phaser
if (phaser !== undefined && phaserdepth > 0) { if (phaserrate !== undefined && phaserdepth > 0) {
const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep); const { filterChain, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep);
chain.push(phaserFX); audioNodes.push(lfo);
chain.push(...filterChain);
} }
// last gain // last gain
@@ -692,7 +725,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// delay // delay
if (delay > 0 && delaytime > 0 && delayfeedback > 0) { if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
orbitBus.getDelay(delaytime, delayfeedback, t); orbitBus.getDelay(delaytime, delayfeedback, t);
orbitBus.sendDelay(post, delay); const send = orbitBus.sendDelay(post, delay);
audioNodes.push(send);
} }
// reverb // reverb
if (room > 0) { if (room > 0) {
@@ -708,7 +742,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
roomIR = await loadBuffer(url, ac, ir, 0); roomIR = await loadBuffer(url, ac, ir, 0);
} }
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
orbitBus.sendReverb(post, room); const send = orbitBus.sendReverb(post, room);
audioNodes.push(send);
} }
if (djf != null) { if (djf != null) {
+2 -2
View File
@@ -78,11 +78,11 @@ export class Orbit {
return this.reverbNode; return this.reverbNode;
} }
sendReverb(node, amount) { sendReverb(node, amount) {
effectSend(node, this.reverbNode, amount); return effectSend(node, this.reverbNode, amount);
} }
sendDelay(node, amount) { sendDelay(node, amount) {
effectSend(node, this.delayNode, amount); return effectSend(node, this.delayNode, amount);
} }
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) { duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
+56 -33
View File
@@ -3,7 +3,6 @@ import { registerSound, soundMap } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { import {
applyFM, applyFM,
destroyAudioWorkletNode,
gainNode, gainNode,
getADSRValues, getADSRValues,
getFrequencyFromValue, getFrequencyFromValue,
@@ -13,11 +12,13 @@ import {
getVibratoOscillator, getVibratoOscillator,
getWorklet, getWorklet,
noises, noises,
releaseAudioNode,
webAudioTimeout, webAudioTimeout,
} from './helpers.mjs'; } from './helpers.mjs';
import { logger } from './logger.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
const waveformAliases = [ const waveformAliases = [
['tri', 'triangle'], ['tri', 'triangle'],
['sqr', 'square'], ['sqr', 'square'],
@@ -47,19 +48,17 @@ export function registerSynthSounds() {
[0.001, 0.05, 0.6, 0.01], [0.001, 0.05, 0.6, 0.01],
); );
let sound = getOscillator(s, t, value);
let { node: o, stop, triggerRelease } = sound;
// turn down // turn down
const g = gainNode(0.3); const g = gainNode(0.3);
const { duration } = value; let sound = getOscillator(s, t, value, () => {
o.onended = () => {
o.disconnect();
g.disconnect(); g.disconnect();
onended(); onended();
}; });
let { node: o, stop, triggerRelease } = sound;
const { duration } = value;
const envGain = gainNode(1); const envGain = gainNode(1);
let node = o.connect(g).connect(envGain); let node = o.connect(g).connect(envGain);
@@ -193,8 +192,7 @@ export function registerSynthSounds() {
let timeoutNode = webAudioTimeout( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); releaseAudioNode(o);
envGain.disconnect();
onended(); onended();
fm?.stop(); fm?.stop();
vibratoOscillator?.stop(); vibratoOscillator?.stop();
@@ -271,8 +269,7 @@ export function registerSynthSounds() {
let timeoutNode = webAudioTimeout( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); releaseAudioNode(o);
envGain.disconnect();
onended(); onended();
}, },
begin, begin,
@@ -345,9 +342,8 @@ export function registerSynthSounds() {
let timeoutNode = webAudioTimeout( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); releaseAudioNode(o);
destroyAudioWorkletNode(lfo); releaseAudioNode(lfo);
envGain.disconnect();
onended(); onended();
fm?.stop(); fm?.stop();
vibratoOscillator?.stop(); vibratoOscillator?.stop();
@@ -414,9 +410,13 @@ export function registerSynthSounds() {
waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] })); waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] }));
} }
export function waveformN(partials, type) { const PI2 = 2 * Math.PI;
const real = new Float32Array(partials + 1); export function waveformN(partials, phases, type) {
const imag = new Float32Array(partials + 1); 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 ac = getAudioContext();
const osc = ac.createOscillator(); const osc = ac.createOscillator();
@@ -424,20 +424,29 @@ export function waveformN(partials, type) {
sawtooth: (n) => [0, -1 / n], sawtooth: (n) => [0, -1 / n],
square: (n) => [0, n % 2 === 0 ? 0 : 1 / n], square: (n) => [0, n % 2 === 0 ? 0 : 1 / n],
triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0], triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0],
user: (_n) => [0, 1],
}; };
if (!terms[type]) { if (!terms[type]) {
throw new Error(`unknown wave type ${type}`); throw new Error(`unknown wave type ${type}`);
} }
real[0] = 0; // dc offset for (let n = 0; n < len; n++) {
imag[0] = 0; const mag = partials[n];
let n = 1; const [r, i] = terms[type](n + 1); // we skip n === 0 as this is dc offset
while (n <= partials) { const phase = phases?.[n] ?? 0;
const [r, i] = terms[type](n); // Scale by `partials`
real[n] = r; let R = r * mag;
imag[n] = i; let I = i * mag;
n++; // 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); const wave = ac.createPeriodicWave(real, imag);
@@ -446,21 +455,28 @@ export function waveformN(partials, type) {
} }
// expects one of waveforms as s // expects one of waveforms as s
export function getOscillator(s, t, value) { export function getOscillator(s, t, value, onended) {
let { n: partials, duration, noise = 0 } = value; const { duration, noise = 0 } = value;
const partials = value.partials ?? value.n;
let o; 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 no partials are given, use stock waveforms
if (!partials || s === 'sine') { if (!partials || partials?.length === 0 || s === 'sine') {
o = getAudioContext().createOscillator(); o = getAudioContext().createOscillator();
o.type = s || 'triangle'; o.type = s || 'triangle';
} }
// generate custom waveform if partials are given // generate custom waveform if partials are given
else { else {
o = waveformN(partials, s); o = waveformN(partials, value.phases, s);
} }
// set frequency // set frequency
o.frequency.value = getFrequencyFromValue(value); o.frequency.value = getFrequencyFromValue(value);
o.start(t);
let vibratoOscillator = getVibratoOscillator(o.detune, value, t); let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
@@ -473,6 +489,13 @@ export function getOscillator(s, t, value) {
noiseMix = getNoiseMix(o, noise, t); noiseMix = getNoiseMix(o, noise, t);
} }
o.onended = () => {
noiseMix || o.disconnect();
noiseMix?.node.disconnect();
onended();
};
o.start(t);
return { return {
node: noiseMix?.node || o, node: noiseMix?.node || o,
stop: (time) => { stop: (time) => {
+10
View File
@@ -114,3 +114,13 @@ export function getCommonSampleInfo(hapValue, bank) {
export const pickAndRename = (source, map) => { export const pickAndRename = (source, map) => {
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]])); 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); throw new Error('vowel: unknown vowel ' + letter);
} }
const { gains, qs, freqs } = vowelFormant[letter]; const { gains, qs, freqs } = vowelFormant[letter];
const makeupGain = ac.createGain(); this.makeupGain = ac.createGain();
this.audioNodes = [];
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
const gain = ac.createGain(); const gain = ac.createGain();
gain.gain.value = gains[i]; gain.gain.value = gains[i];
@@ -53,14 +54,25 @@ if (typeof GainNode !== 'undefined') {
filter.type = 'bandpass'; filter.type = 'bandpass';
filter.Q.value = qs[i]; filter.Q.value = qs[i];
filter.frequency.value = freqs[i]; filter.frequency.value = freqs[i];
this.connect(filter); super.connect(filter);
filter.connect(gain); 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.makeupGain.gain.value = 8; // how much makeup gain to add?
this.connect = (target) => makeupGain.connect(target);
return this; 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) { AudioContext.prototype.createVowelFilter = function (letter) {
+5 -5
View File
@@ -1,15 +1,15 @@
import { getAudioContext, registerSound } from './index.mjs'; import { getAudioContext, registerSound } from './index.mjs';
import { getCommonSampleInfo } from './util.mjs'; import { getBaseURL, getCommonSampleInfo } from './util.mjs';
import { import {
applyFM, applyFM,
applyParameterModulators, applyParameterModulators,
destroyAudioWorkletNode,
getADSRValues, getADSRValues,
getFrequencyFromValue, getFrequencyFromValue,
getParamADSR, getParamADSR,
getPitchEnvelope, getPitchEnvelope,
getVibratoOscillator, getVibratoOscillator,
getWorklet, getWorklet,
releaseAudioNode,
webAudioTimeout, webAudioTimeout,
} from './helpers.mjs'; } from './helpers.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
@@ -190,6 +190,7 @@ export const tables = async (url, frameLen, json, options = {}) => {
if (url.startsWith('local:')) { if (url.startsWith('local:')) {
url = `http://localhost:5432`; url = `http://localhost:5432`;
} }
const base = getBaseURL(url);
if (typeof fetch !== 'function') { if (typeof fetch !== 'function') {
// not a browser // not a browser
return; return;
@@ -200,7 +201,7 @@ export const tables = async (url, frameLen, json, options = {}) => {
} }
return fetch(url) return fetch(url)
.then((res) => res.json()) .then((res) => res.json())
.then((json) => _processTables(json, url, frameLen, options)) .then((json) => _processTables(json, base, frameLen, options))
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
throw new Error(`error loading "${url}"`); throw new Error(`error loading "${url}"`);
@@ -318,10 +319,9 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
const timeoutNode = webAudioTimeout( const timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(source); releaseAudioNode(source);
vibratoOscillator?.stop(); vibratoOscillator?.stop();
fm?.stop(); fm?.stop();
node.disconnect();
wtPosModulators?.disconnect(); wtPosModulators?.disconnect();
wtWarpModulators?.disconnect(); wtWarpModulators?.disconnect();
onended(); onended();
+326 -179
View File
@@ -6,51 +6,60 @@ import OLAProcessor from './ola-processor';
import FFT from './fft.js'; import FFT from './fft.js';
import { getDistortionAlgorithm } from './helpers.mjs'; import { getDistortionAlgorithm } from './helpers.mjs';
const blockSize = 128;
const PI = Math.PI;
const TWO_PI = 2 * PI;
const INVSR = 1 / sampleRate;
const timeToCoeff = (t) => 1 - Math.exp(-INVSR / t);
const dbToLin = (db) => Math.pow(10, db / 20);
const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const mod = (n, m) => ((n % m) + m) % m; const mod = (n, m) => ((n % m) + m) % m;
const lerp = (a, b, n) => n * (b - a) + a; const lerp = (a, b, n) => n * (b - a) + a;
const pv = (arr, n) => arr[n] ?? arr[0]; const pv = (arr, n) => arr[n] ?? arr[0];
const frac = (x) => x - Math.floor(x); const frac = (x) => x - Math.floor(x);
const ffloor = (x) => x | 0; // fast floor for non-negative
const getUnisonDetune = (unison, detune, voiceIndex) => { // Fast integer ops for non-negative values
if (unison < 2) { const ffloor = (x) => x | 0;
return 0; const fround = (x) => ffloor(x + 0.5);
} const fceil = (x) => ffloor(x + 1);
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1)); const ffrac = (x) => x - ffloor(x);
const fast_tanh = (x) => {
const x2 = x ** 2;
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
}; };
// Optimized per-voice detuner which precomputes constants
const getDetuner = (unison, detune) => {
if (unison < 2) {
return (_voiceIdx) => 0;
}
const scale = detune / (unison - 1);
const center = detune * 0.5;
return (voiceIdx) => voiceIdx * scale - center;
};
const applySemitoneDetuneToFrequency = (frequency, detune) => { const applySemitoneDetuneToFrequency = (frequency, detune) => {
return frequency * Math.pow(2, detune / 12); return frequency * Math.pow(2, detune / 12);
}; };
// Restrict phase to the range [0, maxPhase) via wrapping
function wrapPhase(phase, maxPhase = 1) {
if (phase >= maxPhase) {
phase -= maxPhase;
} else if (phase < 0) {
phase += maxPhase;
}
return phase;
}
const blockSize = 128;
// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing // Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517 // referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
function polyBlep(phase, dt) { function polyBlep(phase, dt) {
dt = Math.min(dt, 1 - dt); dt = Math.min(dt, 1 - dt);
const invdt = 1 / dt;
// Start of cycle // Start of cycle
if (phase < dt) { if (phase < dt) {
phase /= dt; phase *= invdt;
// 2 * (phase - phase^2/2 - 0.5) return 2 * phase - phase ** 2 - 1;
return phase + phase - phase * phase - 1;
} }
// End of cycle // End of cycle
else if (phase > 1 - dt) { else if (phase > 1 - dt) {
phase = (phase - 1) / dt; phase = (phase - 1) * invdt;
// 2 * (phase^2/2 + phase + 0.5) return phase ** 2 + 2 * phase + 1;
return phase * phase + phase + phase + 1;
} }
// 0 otherwise // 0 otherwise
else { else {
return 0; return 0;
@@ -66,7 +75,7 @@ const waveshapes = {
return phase / skew; return phase / skew;
}, },
sine(phase) { sine(phase) {
return Math.sin(Math.PI * 2 * phase) * 0.5 + 0.5; return Math.sin(TWO_PI * phase) * 0.5 + 0.5;
}, },
ramp(phase) { ramp(phase) {
return phase; return phase;
@@ -100,12 +109,6 @@ const waveshapes = {
return v - polyBlep(phase, dt); return v - polyBlep(phase, dt);
}, },
}; };
function getParamValue(block, param) {
if (param.length > 1) {
return param[block];
}
return param[0];
}
const waveShapeNames = Object.keys(waveshapes); const waveShapeNames = Object.keys(waveshapes);
class LFOProcessor extends AudioWorkletProcessor { class LFOProcessor extends AudioWorkletProcessor {
@@ -165,9 +168,9 @@ class LFOProcessor extends AudioWorkletProcessor {
const blockSize = output[0].length ?? 0; const blockSize = output[0].length ?? 0;
if (this.phase == null) { if (this.phase == null) {
this.phase = mod(time * frequency + phaseoffset, 1); this.phase = ffrac(time * frequency + phaseoffset);
} }
const dt = frequency / sampleRate; const dt = frequency * INVSR;
for (let n = 0; n < blockSize; n++) { for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < output.length; i++) { for (let i = 0; i < output.length; i++) {
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth; let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
@@ -293,8 +296,8 @@ class TwoPoleFilter {
// Out of bound values can produce NaNs // Out of bound values can produce NaNs
resonance = clamp(resonance, 0, 1); resonance = clamp(resonance, 0, 1);
cutoff = clamp(cutoff, 0, sampleRate / 2 - 1); cutoff = clamp(cutoff, 0, sampleRate / 2 - 1);
const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14); const c = clamp(2 * Math.sin(cutoff * PI * INVSR), 0, 1.14);
const r = Math.pow(0.5, (resonance + 0.125) / 0.125); const r = Math.pow(0.5, 8 * resonance + 1);
const mrc = 1 - r * c; const mrc = 1 - r * c;
this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
this.s1 = mrc * this.s1 + c * this.s0; // lpf this.s1 = mrc * this.s1 + c * this.s0; // lpf
@@ -353,11 +356,6 @@ class DJFProcessor extends AudioWorkletProcessor {
} }
registerProcessor('djf-processor', DJFProcessor); registerProcessor('djf-processor', DJFProcessor);
function fast_tanh(x) {
const x2 = x * x;
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
}
const _PI = 3.14159265359;
//adapted from https://github.com/TheBouteillacBear/webaudioworklet-wasm?tab=MIT-1-ov-file //adapted from https://github.com/TheBouteillacBear/webaudioworklet-wasm?tab=MIT-1-ov-file
class LadderProcessor extends AudioWorkletProcessor { class LadderProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() { static get parameterDescriptors() {
@@ -395,7 +393,7 @@ class LadderProcessor extends AudioWorkletProcessor {
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000); const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
let cutoff = parameters.frequency[0]; let cutoff = parameters.frequency[0];
cutoff = (cutoff * 2 * _PI) / sampleRate; cutoff = cutoff * TWO_PI * INVSR;
cutoff = cutoff > 1 ? 1 : cutoff; cutoff = cutoff > 1 ? 1 : cutoff;
const k = Math.min(8, resonance * 0.13); const k = Math.min(8, resonance * 0.13);
@@ -508,51 +506,50 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
name: 'voices', name: 'voices',
defaultValue: 5, defaultValue: 5,
min: 1, min: 1,
automationRate: 'k-rate',
}, },
]; ];
} }
process(_input, outputs, params) { process(_input, outputs, params) {
if (currentTime <= params.begin[0]) {
return true;
}
if (currentTime >= params.end[0]) { if (currentTime >= params.end[0]) {
// this.port.postMessage({ type: 'onended' }); // should terminate
return false; return false;
} }
if (currentTime <= params.begin[0]) {
// keep alive
return true;
}
const output = outputs[0]; const output = outputs[0];
const voices = params.voices[0]; // k-rate
for (let i = 0; i < output[0].length; i++) { for (let i = 0; i < output[0].length; i++) {
const detune = pv(params.detune, i); const detune = pv(params.detune, i);
const voices = pv(params.voices, i);
const freqspread = pv(params.freqspread, i); const freqspread = pv(params.freqspread, i);
const panspread = pv(params.panspread, i) * 0.5 + 0.5; const panspread = pv(params.panspread, i) * 0.5 + 0.5;
const gain1 = Math.sqrt(1 - panspread); let gainL = Math.sqrt(1 - panspread);
const gain2 = Math.sqrt(panspread); let gainR = Math.sqrt(panspread);
let freq = pv(params.frequency, i); let freq = pv(params.frequency, i);
// Main detuning // Main detuning
freq = applySemitoneDetuneToFrequency(freq, detune / 100); freq = applySemitoneDetuneToFrequency(freq, detune / 100);
const detuner = getDetuner(voices, freqspread);
for (let n = 0; n < voices; n++) { for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1;
let gainL = gain1;
let gainR = gain2;
// invert right and left gain
if (isOdd) {
gainL = gain2;
gainR = gain1;
}
// Individual voice detuning // Individual voice detuning
const freqVoice = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n)); const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
// We must wrap this here because it is passed into sawblep below which // We must wrap this here because it is passed into sawblep below which
// has domain [0, 1] // has domain [0, 1]
const dt = mod(freqVoice / sampleRate, 1); const dt = frac(freqVoice * INVSR);
this.phase[n] = this.phase[n] ?? Math.random(); this.phase[n] = this.phase[n] ?? Math.random();
const v = waveshapes.sawblep(this.phase[n], dt); const v = waveshapes.sawblep(this.phase[n], dt);
output[0][i] = output[0][i] + v * gainL; output[0][i] += v * gainL;
output[1][i] = output[1][i] + v * gainR; output[1][i] += v * gainR;
this.phase[n] = wrapPhase(this.phase[n] + dt); let pn = this.phase[n] + dt;
if (pn >= 1.0) pn -= 1.0;
this.phase[n] = pn;
// invert right and left gain
const tmp = gainL;
gainL = gainR;
gainR = tmp;
} }
} }
return true; return true;
@@ -564,12 +561,16 @@ registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file // Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
const BUFFERED_BLOCK_SIZE = 2048; const BUFFERED_BLOCK_SIZE = 2048;
const hannCache = new Map();
function genHannWindow(length) { function genHannWindow(length) {
let win = new Float32Array(length); if (!hannCache.has(length)) {
for (var i = 0; i < length; i++) { const win = new Float32Array(length);
win[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / length)); for (let i = 0; i < length; i++) {
win[i] = 0.5 * (1 - Math.cos((TWO_PI * i) / length));
}
hannCache.set(length, win);
} }
return win; return hannCache.get(length);
} }
class PhaseVocoderProcessor extends OLAProcessor { class PhaseVocoderProcessor extends OLAProcessor {
@@ -587,11 +588,10 @@ class PhaseVocoderProcessor extends OLAProcessor {
blockSize: BUFFERED_BLOCK_SIZE, blockSize: BUFFERED_BLOCK_SIZE,
}; };
super(options); super(options);
this.fftSize = this.blockSize;
this.timeCursor = 0; this.timeCursor = 0;
this.fftSize = this.blockSize;
this.hannWindow = genHannWindow(this.blockSize); this.invfftSize = 1 / this.fftSize;
this.hannWindow = genHannWindow(this.fftSize);
// prepare FFT and pre-allocate buffers // prepare FFT and pre-allocate buffers
this.fft = new FFT(this.fftSize); this.fft = new FFT(this.fftSize);
this.freqComplexBuffer = this.fft.createComplexArray(); this.freqComplexBuffer = this.fft.createComplexArray();
@@ -604,52 +604,43 @@ class PhaseVocoderProcessor extends OLAProcessor {
processOLA(inputs, outputs, parameters) { processOLA(inputs, outputs, parameters) {
// no automation, take last value // no automation, take last value
let pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1]; let pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1];
if (pitchFactor < 0) { if (pitchFactor < 0) {
pitchFactor = pitchFactor * 0.25; pitchFactor = pitchFactor * 0.25;
} }
pitchFactor = Math.max(0, pitchFactor + 1); pitchFactor = Math.max(0, pitchFactor + 1);
for (let i = 0; i < this.nbInputs; i++) {
for (var i = 0; i < this.nbInputs; i++) { for (let j = 0; j < inputs[i].length; j++) {
for (var j = 0; j < inputs[i].length; j++) { const input = inputs[i][j];
// big assumption here: output is symetric to input const output = outputs[i][j];
var input = inputs[i][j];
var output = outputs[i][j];
this.applyHannWindow(input); this.applyHannWindow(input);
this.fft.realTransform(this.freqComplexBuffer, input); this.fft.realTransform(this.freqComplexBuffer, input);
this.computeMagnitudes(); this.computeMagnitudes();
this.findPeaks(); this.findPeaks();
this.shiftPeaks(pitchFactor); this.shiftPeaks(pitchFactor);
this.fft.completeSpectrum(this.freqComplexBufferShifted); this.fft.completeSpectrum(this.freqComplexBufferShifted);
this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted); this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted);
this.fft.fromComplexArray(this.timeComplexBuffer, output); this.fft.fromComplexArray(this.timeComplexBuffer, output);
this.applyHannWindow(output); this.applyHannWindow(output);
} }
} }
this.timeCursor += this.hopSize; this.timeCursor += this.hopSize;
} }
/** Apply Hann window in-place */ /** Apply Hann window in-place */
applyHannWindow(input) { applyHannWindow(input) {
for (var i = 0; i < this.blockSize; i++) { for (let i = 0; i < this.blockSize; i++) {
input[i] = input[i] * this.hannWindow[i] * 1.62; input[i] *= this.hannWindow[i] * 1.62;
} }
} }
/** Compute squared magnitudes for peak finding **/ /** Compute squared magnitudes for peak finding **/
computeMagnitudes() { computeMagnitudes() {
var i = 0, let i = 0,
j = 0; j = 0;
while (i < this.magnitudes.length) { while (i < this.magnitudes.length) {
let real = this.freqComplexBuffer[j]; const real = this.freqComplexBuffer[j];
let imag = this.freqComplexBuffer[j + 1]; const imag = this.freqComplexBuffer[j + 1];
// no need to sqrt for peak finding // no need to sqrt for peak finding
this.magnitudes[i] = real ** 2 + imag ** 2; this.magnitudes[i] = real ** 2 + imag ** 2;
i += 1; i += 1;
@@ -660,12 +651,10 @@ class PhaseVocoderProcessor extends OLAProcessor {
/** Find peaks in spectrum magnitudes **/ /** Find peaks in spectrum magnitudes **/
findPeaks() { findPeaks() {
this.nbPeaks = 0; this.nbPeaks = 0;
var i = 2; let i = 2;
let end = this.magnitudes.length - 2; const end = this.magnitudes.length - 2;
while (i < end) { while (i < end) {
let mag = this.magnitudes[i]; const mag = this.magnitudes[i];
if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) { if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) {
i++; i++;
continue; continue;
@@ -674,7 +663,6 @@ class PhaseVocoderProcessor extends OLAProcessor {
i++; i++;
continue; continue;
} }
this.peakIndexes[this.nbPeaks] = i; this.peakIndexes[this.nbPeaks] = i;
this.nbPeaks++; this.nbPeaks++;
i += 2; i += 2;
@@ -685,53 +673,44 @@ class PhaseVocoderProcessor extends OLAProcessor {
shiftPeaks(pitchFactor) { shiftPeaks(pitchFactor) {
// zero-fill new spectrum // zero-fill new spectrum
this.freqComplexBufferShifted.fill(0); this.freqComplexBufferShifted.fill(0);
for (let i = 0; i < this.nbPeaks; i++) {
for (var i = 0; i < this.nbPeaks; i++) { const peakIndex = this.peakIndexes[i];
let peakIndex = this.peakIndexes[i]; const peakIndexShifted = fround(peakIndex * pitchFactor);
let peakIndexShifted = Math.round(peakIndex * pitchFactor);
if (peakIndexShifted > this.magnitudes.length) { if (peakIndexShifted > this.magnitudes.length) {
break; break;
} }
// find region of influence // find region of influence
var startIndex = 0; let startIndex = 0;
var endIndex = this.fftSize; let endIndex = this.fftSize;
if (i > 0) { if (i > 0) {
let peakIndexBefore = this.peakIndexes[i - 1]; startIndex = peakIndex - fround((peakIndex - this.peakIndexes[i - 1]) / 2);
startIndex = peakIndex - Math.floor((peakIndex - peakIndexBefore) / 2);
} }
if (i < this.nbPeaks - 1) { if (i < this.nbPeaks - 1) {
let peakIndexAfter = this.peakIndexes[i + 1]; endIndex = peakIndex + fceil((this.peakIndexes[i + 1] - peakIndex) / 2);
endIndex = peakIndex + Math.ceil((peakIndexAfter - peakIndex) / 2);
} }
// shift whole region of influence around peak to shifted peak // shift whole region of influence around peak to shifted peak
let startOffset = startIndex - peakIndex; const startOffset = startIndex - peakIndex;
let endOffset = endIndex - peakIndex; const endOffset = endIndex - peakIndex;
for (var j = startOffset; j < endOffset; j++) { const omegaDelta = TWO_PI * this.invfftSize * (peakIndexShifted - peakIndex);
let binIndex = peakIndex + j; const phaseShiftReal = Math.cos(omegaDelta * this.timeCursor);
let binIndexShifted = peakIndexShifted + j; const phaseShiftImag = Math.sin(omegaDelta * this.timeCursor);
for (let j = startOffset; j < endOffset; j++) {
const binIndex = peakIndex + j;
const binIndexShifted = peakIndexShifted + j;
if (binIndexShifted >= this.magnitudes.length) { if (binIndexShifted >= this.magnitudes.length) {
break; break;
} }
// apply phase correction // apply phase correction
let omegaDelta = (2 * Math.PI * (binIndexShifted - binIndex)) / this.fftSize; const indexReal = 2 * binIndex;
let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor); const indexImag = indexReal + 1;
let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor); const valueReal = this.freqComplexBuffer[indexReal];
const valueImag = this.freqComplexBuffer[indexImag];
let indexReal = binIndex * 2; const valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag;
let indexImag = indexReal + 1; const valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal;
let valueReal = this.freqComplexBuffer[indexReal];
let valueImag = this.freqComplexBuffer[indexImag];
let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag; const indexShiftedReal = 2 * binIndexShifted;
let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal; const indexShiftedImag = indexShiftedReal + 1;
let indexShiftedReal = binIndexShifted * 2;
let indexShiftedImag = indexShiftedReal + 1;
this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal; this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal;
this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag; this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag;
} }
@@ -745,11 +724,10 @@ registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
class PulseOscillatorProcessor extends AudioWorkletProcessor { class PulseOscillatorProcessor extends AudioWorkletProcessor {
constructor() { constructor() {
super(); super();
this.pi = _PI; this.phi = -PI; // phase
this.phi = -this.pi; // phase
this.Y0 = 0; // feedback memories this.Y0 = 0; // feedback memories
this.Y1 = 0; this.Y1 = 0;
this.PW = this.pi; // pulse width this.PW = PI; // pulse width
this.B = 2.3; // feedback coefficient this.B = 2.3; // feedback coefficient
this.dphif = 0; // filtered phase increment this.dphif = 0; // filtered phase increment
this.envf = 0; // filtered envelope this.envf = 0; // filtered envelope
@@ -806,11 +784,11 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
dphi; dphi;
for (let i = 0; i < (output[0].length ?? 0); i++) { for (let i = 0; i < (output[0].length ?? 0); i++) {
const pw = (1 - clamp(getParamValue(i, params.pulsewidth), -0.99, 0.99)) * this.pi; const pw = (1 - clamp(pv(params.pulsewidth, i), -0.99, 0.99)) * PI;
const detune = getParamValue(i, params.detune); const detune = pv(params.detune, i);
const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100); const freq = applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100);
dphi = freq * (this.pi / (sampleRate * 0.5)); // phase increment dphi = freq * TWO_PI * INVSR; // phase increment
this.dphif += 0.1 * (dphi - this.dphif); this.dphif += 0.1 * (dphi - this.dphif);
env *= 0.9998; // exponential decay envelope env *= 0.9998; // exponential decay envelope
@@ -822,7 +800,7 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
// Waveform generation (half-Tomisawa oscillators) // Waveform generation (half-Tomisawa oscillators)
this.phi += this.dphif; // phase increment this.phi += this.dphif; // phase increment
if (this.phi >= this.pi) this.phi -= 2 * this.pi; // phase wrapping if (this.phi >= PI) this.phi -= TWO_PI; // phase wrapping
// First half-Tomisawa generator // First half-Tomisawa generator
let out0 = Math.cos(this.phi + this.B * this.Y0); // self-phase modulation let out0 = Math.cos(this.phi + this.B * this.Y0); // self-phase modulation
@@ -852,24 +830,23 @@ const chyx = {
/*bit reverse*/ br: function (x, size = 8) { /*bit reverse*/ br: function (x, size = 8) {
if (size > 32) { if (size > 32) {
throw new Error('br() Size cannot be greater than 32'); throw new Error('br() Size cannot be greater than 32');
} else {
let result = 0;
for (let idx = 0; idx < size - 0; idx++) {
result += chyx.bitC(x, 2 ** idx, 2 ** (size - (idx + 1)));
}
return result;
} }
let result = 0;
for (let idx = 0; idx < size; idx++) {
result |= chyx.bitC(x, 1 << idx, 1 << (size - (idx + 1)));
}
return result;
}, },
/*sin that loops every 128 "steps", instead of every pi steps*/ sinf: function (x) { /*sin that loops every 128 "steps", instead of every pi steps*/ sinf: function (x) {
return Math.sin(x / (128 / Math.PI)); return Math.sin((x * PI) / 128);
}, },
/*cos that loops every 128 "steps", instead of every pi steps*/ cosf: function (x) { /*cos that loops every 128 "steps", instead of every pi steps*/ cosf: function (x) {
return Math.cos(x / (128 / Math.PI)); return Math.cos((x * PI) / 128);
}, },
/*tan that loops every 128 "steps", instead of every pi steps*/ tanf: function (x) { /*tan that loops every 128 "steps", instead of every pi steps*/ tanf: function (x) {
return Math.tan(x / (128 / Math.PI)); return Math.tan((x * PI) / 128);
}, },
/*converts t into a string composed of it's bits, regex's that*/ regG: function (t, X) { /*converts t into a string composed of its bits; regexes that*/ regG: function (t, X) {
return X.test(t.toString(2)); return X.test(t.toString(2));
}, },
}; };
@@ -877,7 +854,7 @@ const chyx = {
// Create shortened Math functions // Create shortened Math functions
let mathParams, byteBeatHelperFuncs; let mathParams, byteBeatHelperFuncs;
function getByteBeatFunc(codetext) { function getByteBeatFunc(codetext) {
if ((mathParams || byteBeatHelperFuncs) == null) { if (mathParams == null) {
mathParams = Object.getOwnPropertyNames(Math); mathParams = Object.getOwnPropertyNames(Math);
byteBeatHelperFuncs = mathParams.map((k) => Math[k]); byteBeatHelperFuncs = mathParams.map((k) => Math[k]);
const chyxNames = Object.getOwnPropertyNames(chyx); const chyxNames = Object.getOwnPropertyNames(chyx);
@@ -910,7 +887,7 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
this.func = getByteBeatFunc(codeText); this.func = getByteBeatFunc(codeText);
}; };
this.initialOffset = null; this.initialOffset = 0;
this.t = null; this.t = null;
this.func = null; this.func = null;
} }
@@ -957,18 +934,19 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
this.t = params.begin[0] * sampleRate; this.t = params.begin[0] * sampleRate;
} }
const output = outputs[0]; const output = outputs[0];
const scale = 256 * INVSR;
for (let i = 0; i < output[0].length; i++) { for (let i = 0; i < output[0].length; i++) {
const detune = getParamValue(i, params.detune); const detune = pv(params.detune, i);
const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100); const freq = applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100);
let local_t = (this.t / (sampleRate / 256)) * freq + this.initialOffset; const local_t = scale * freq * this.t + this.initialOffset;
const funcValue = this.func(local_t); const funcValue = this.func(local_t);
let signal = (funcValue & 255) / 127.5 - 1; const signal = (funcValue & 255) / 127.5 - 1;
const out = signal * 0.2; //prevent speaker blowout via clipping if threshold exceeds
const out = clamp(signal * 0.2, -0.4, 0.4);
for (let c = 0; c < output.length; c++) { for (let c = 0; c < output.length; c++) {
//prevent speaker blowout via clipping if threshold exceeds output[c][i] = out;
output[c][i] = clamp(out, -0.4, 0.4);
} }
this.t = this.t + 1; this.t++;
} }
return true; // keep the audio processing going return true; // keep the audio processing going
@@ -977,6 +955,102 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
registerProcessor('byte-beat-processor', ByteBeatProcessor); registerProcessor('byte-beat-processor', ByteBeatProcessor);
class EnvelopeProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{ name: 'begin', defaultValue: 0 },
{ name: 'end', defaultValue: 0 },
{ name: 'attack', defaultValue: 0.005, minValue: 0 },
{ name: 'decay', defaultValue: 0.14, minValue: 0 },
{ name: 'sustain', defaultValue: 0, minValue: 0, maxValue: 1 },
{ name: 'release', defaultValue: 0.1, minValue: 0 },
{ name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
{ name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
{ name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
{ name: 'peak', defaultValue: 1 },
{ name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 },
];
}
constructor() {
super();
this.val = 0;
this.segIdx = 0;
this.state = 0;
this.beginTime = 0;
this.endTime = 0;
this.attackStart = 0;
}
_warp(phase, curvature, strength = 8) {
if (phase === 0 || phase === 1) return phase; // fast exit
if (curvature > 0) {
// snappier
const exp = 1 + strength * curvature;
return 1 - Math.pow(1 - phase, exp);
} else {
// more calm
const exp = 1 - strength * curvature;
return Math.pow(phase, exp);
}
}
_advance(start, target, time, curvature) {
if (time === 0 || start === target) {
this.val = target;
} else {
// We compute our progress through this section of the envelope in time
// as a `phase` value, which is warped by the curvature, and then used
// to compute the value of the envelope at that time
const phase = Math.min(1, (currentTime - this.beginTime) / time);
const phaseWarped = this._warp(phase, curvature);
this.val = start + (target - start) * phaseWarped;
}
}
process(_inputs, outputs, params) {
const out = outputs[0][0];
if (!out) return true;
const begin = pv(params.begin, 0);
const retrigger = pv(params.retrigger, 0) >= 0.5; // convert to bool
if (begin !== this.beginTime && (this.state === 0 || retrigger)) {
// triggered
this.beginTime = begin;
this.state = 1;
this.endTime = pv(params.end, 0);
this.attackStart = this.val;
}
const susTime = this.endTime - this.beginTime;
for (let i = 0; i < out.length; i++) {
const attack = pv(params.attack, i);
const decay = pv(params.decay, i);
const sustain = pv(params.sustain, i);
const release = pv(params.release, i);
const aCurve = pv(params.attackCurve, i);
const dCurve = pv(params.decayCurve, i);
const rCurve = pv(params.releaseCurve, i);
const peak = pv(params.peak, i);
const states = [
{ time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle
{ time: attack, start: this.attackStart, target: 1, curve: aCurve },
{ time: attack + decay, start: 1, target: sustain, curve: dCurve },
{ time: susTime, start: sustain, target: sustain },
{ time: susTime + release, start: sustain, target: 0, curve: rCurve },
];
let { time, start, target, curve } = states[this.state];
this._advance(start, target, time, curve);
while (currentTime - this.beginTime >= time) {
this.state = (this.state + 1) % states.length;
time = states[this.state].time;
}
out[i] = this.val * peak;
}
return true;
}
}
registerProcessor('envelope-processor', EnvelopeProcessor);
export const WarpMode = Object.freeze({ export const WarpMode = Object.freeze({
NONE: 0, NONE: 0,
ASYM: 1, ASYM: 1,
@@ -1056,7 +1130,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
{ name: 'position', defaultValue: 0, min: 0, max: 1 }, { name: 'position', defaultValue: 0, min: 0, max: 1 },
{ name: 'warp', defaultValue: 0, min: 0, max: 1 }, { name: 'warp', defaultValue: 0, min: 0, max: 1 },
{ name: 'warpMode', defaultValue: 0 }, { name: 'warpMode', defaultValue: 0 },
{ name: 'voices', defaultValue: 1, min: 1 }, { name: 'voices', defaultValue: 1, min: 1, automationRate: 'k-rate' },
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 }, { name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 }, { name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
]; ];
@@ -1067,7 +1141,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
this.frameLen = 0; this.frameLen = 0;
this.numFrames = 0; this.numFrames = 0;
this.phase = []; this.phase = [];
this.invSR = 1 / sampleRate;
this.port.onmessage = (e) => { this.port.onmessage = (e) => {
const { type, payload } = e.data || {}; const { type, payload } = e.data || {};
@@ -1104,7 +1177,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
_toBits(amt, min = 2, max = 12) { _toBits(amt, min = 2, max = 12) {
const b = max + (min - max) * amt; const b = max + (min - max) * amt;
return { b, n: Math.round(Math.pow(2, b)) }; return { b, n: fround(Math.pow(2, b)) };
} }
_warpPhase(phase, amt, mode) { _warpPhase(phase, amt, mode) {
@@ -1130,7 +1203,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2); return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2);
} }
case WarpMode.SYNC: { case WarpMode.SYNC: {
const syncRatio = Math.pow(16, amt * amt); const syncRatio = Math.pow(16, amt ** 2);
return (phase * syncRatio) % 1; return (phase * syncRatio) % 1;
} }
case WarpMode.QUANT: { case WarpMode.QUANT: {
@@ -1139,8 +1212,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
} }
case WarpMode.FOLD: { case WarpMode.FOLD: {
const K = 7; const K = 7;
const k = 1 + Math.max(1, Math.round(K * amt)); const k = 1 + Math.max(1, fround(K * amt));
return Math.abs(frac(k * phase) - 0.5) * 2; return Math.abs(ffrac(k * phase) - 0.5) * 2;
} }
case WarpMode.PWM: { case WarpMode.PWM: {
const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1); const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1);
@@ -1150,12 +1223,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
case WarpMode.ORBIT: { case WarpMode.ORBIT: {
const depth = 0.5 * amt; const depth = 0.5 * amt;
const n = 3; const n = 3;
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase)); return frac(phase + depth * Math.sin(TWO_PI * n * phase));
} }
case WarpMode.SPIN: { case WarpMode.SPIN: {
const depth = 0.5 * amt; const depth = 0.5 * amt;
const { n } = this._toBits(amt, 1, 6); const { n } = this._toBits(amt, 1, 6);
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase)); return frac(phase + depth * Math.sin(TWO_PI * n * phase));
} }
case WarpMode.CHAOS: { case WarpMode.CHAOS: {
const r = 3.7 + 0.3 * amt; const r = 3.7 + 0.3 * amt;
@@ -1166,7 +1239,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
const isPrime = (n) => { const isPrime = (n) => {
if (n < 2) return false; if (n < 2) return false;
if (n % 2 === 0) return n === 2; if (n % 2 === 0) return n === 2;
for (let d = 3; d * d <= n; d += 2) if (n % d === 0) return false; for (let d = 3; d ** 2 <= n; d += 2) if (n % d === 0) return false;
return true; return true;
}; };
let { n } = this._toBits(amt, 3); let { n } = this._toBits(amt, 3);
@@ -1175,18 +1248,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
} }
case WarpMode.BINARY: { case WarpMode.BINARY: {
let { b } = this._toBits(amt, 3); let { b } = this._toBits(amt, 3);
b = Math.round(b); b = fround(b);
const n = 1 << b; const n = 1 << b;
const idx = ffloor(phase * n); const idx = ffloor(phase * n);
const ridx = bitReverse(idx, b); const ridx = bitReverse(idx, b);
return ridx / n; return ridx / n;
} }
case WarpMode.MODULAR: {
const { n } = this._toBits(amt);
const depth = 0.5 * amt;
const jump = frac(phase * n) / n;
return frac(phase + depth * jump);
}
case WarpMode.BROWNIAN: { case WarpMode.BROWNIAN: {
const disp = 0.25 * amt * brownian(64 * phase, 4); const disp = 0.25 * amt * brownian(64 * phase, 4);
return frac(phase + disp); return frac(phase + disp);
@@ -1209,7 +1276,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
case WarpMode.LOGISTIC: { case WarpMode.LOGISTIC: {
let x = phase; let x = phase;
const r = 3.6 + 0.4 * amt; const r = 3.6 + 0.4 * amt;
const iters = 1 + Math.round(2 * amt); const iters = 1 + fround(2 * amt);
for (let i = 0; i < iters; i++) x = r * x * (1 - x); for (let i = 0; i < iters; i++) x = r * x * (1 - x);
return clamp(x, 0, 1); return clamp(x, 0, 1);
} }
@@ -1222,7 +1289,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
return (y - y0) / (y1 - y0); return (y - y0) / (y1 - y0);
} }
case WarpMode.FRACTAL: { case WarpMode.FRACTAL: {
const d = 0.5 * Math.sin(2 * Math.PI * phase) * amt; const d = 0.5 * Math.sin(TWO_PI * phase) * amt;
return frac(phase + d); return frac(phase + d);
} }
case WarpMode.FLIP: { case WarpMode.FLIP: {
@@ -1269,16 +1336,16 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
if (outR !== outL) outR.set(outL); if (outR !== outL) outR.set(outL);
return true; return true;
} }
const voices = parameters.voices[0]; // k-rate
for (let i = 0; i < outL.length; i++) { for (let i = 0; i < outL.length; i++) {
const detune = pv(parameters.detune, i); const detune = pv(parameters.detune, i);
const freqspread = pv(parameters.freqspread, i); const freqspread = pv(parameters.freqspread, i);
const tablePos = clamp(pv(parameters.position, i), 0, 1); const tablePos = clamp(pv(parameters.position, i), 0, 1);
const idx = tablePos * (this.numFrames - 1); const idx = tablePos * (this.numFrames - 1);
const fIdx = idx | 0; const fIdx = idx | 0;
const frac = idx - fIdx; const interpT = idx - fIdx;
const warpAmount = clamp(pv(parameters.warp, i), 0, 1); const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
const warpMode = pv(parameters.warpMode, i); const warpMode = pv(parameters.warpMode, i);
const voices = pv(parameters.voices, i);
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1); const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0; const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
const gain1 = Math.sqrt(0.5 - 0.5 * panspread); const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
@@ -1286,6 +1353,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
let f = pv(parameters.frequency, i); let f = pv(parameters.frequency, i);
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const normalizer = 1 / Math.sqrt(voices); const normalizer = 1 / Math.sqrt(voices);
const detuner = getDetuner(voices, freqspread);
for (let n = 0; n < voices; n++) { for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1; const isOdd = (n & 1) == 1;
let gainL = gain1; let gainL = gain1;
@@ -1295,8 +1363,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
gainL = gain2; gainL = gain2;
gainR = gain1; gainR = gain1;
} }
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune
const dPhase = fVoice * this.invSR; const dPhase = fVoice * INVSR;
const level = this._chooseMip(dPhase); const level = this._chooseMip(dPhase);
const table = this.tables[level]; const table = this.tables[level];
@@ -1305,13 +1373,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode); const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
const s0 = this._sampleFrame(table[fIdx], ph); const s0 = this._sampleFrame(table[fIdx], ph);
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph); const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
let s = s0 + (s1 - s0) * frac; let s = lerp(s0, s1, interpT);
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
s = -s; s = -s;
} }
outL[i] += s * gainL * normalizer; outL[i] += s * gainL * normalizer;
outR[i] += s * gainR * normalizer; outR[i] += s * gainR * normalizer;
this.phase[n] = wrapPhase(this.phase[n] + dPhase); this.phase[n] = frac(this.phase[n] + dPhase);
} }
} }
return true; return true;
@@ -1319,3 +1387,82 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
} }
registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor); registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor);
class TransientProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [];
}
constructor(options) {
super();
this.gainCoeff = timeToCoeff(0.2);
this.avgGain = 1;
let {
attackTime = 0.003,
sustainTime = 0.08,
attack = 0,
sustain = 0,
sensitivity = 0.1,
mix = 1,
begin = 0,
end = 0,
} = options.processorOptions;
attackTime = clamp(attackTime, 0.0005, 0.05);
sustainTime = clamp(sustainTime, 0.01, 0.5);
this.attackCoeff = timeToCoeff(attackTime);
this.sustainCoeff = timeToCoeff(sustainTime);
this.attackAmt = clamp(attack, -1, 1);
this.sustainAmt = clamp(sustain, -1, 1);
this.scaling = 0.5 + 5 * clamp(sensitivity, 0, 1);
this.mix = clamp(mix, 0, 1);
this.begin = begin;
this.end = end;
this.attackEnv = new Float32Array(2); // assume stereo
this.sustainEnv = new Float32Array(2);
}
process(inputs, outputs, _params) {
const input = inputs[0];
const output = outputs[0];
if (currentTime >= this.end) {
return false;
}
if (currentTime <= this.begin) {
return true;
}
const channels = input.length;
if (channels > this.attackEnv.length) {
this.attackEnv = new Float32Array(channels);
this.sustainEnv = new Float32Array(channels);
}
let avgGain = this.avgGain;
for (let ch = 0; ch < channels; ch++) {
let attEnv = this.attackEnv[ch];
let susEnv = this.sustainEnv[ch];
for (let n = 0; n < blockSize; n++) {
const sample = input[ch][n];
const x = Math.abs(sample);
attEnv = lerp(attEnv, x, this.attackCoeff);
susEnv = lerp(susEnv, x, this.sustainCoeff);
const peakiness = clamp((this.scaling * (attEnv - susEnv)) / (susEnv + 1e-6), -1.5, 1.5);
const attScale = peakiness > 0 ? peakiness : 0;
const susScale = peakiness < 0 ? -peakiness : 0;
const attackGain = dbToLin(this.attackAmt * attScale * 18);
const sustainGain = dbToLin(this.sustainAmt * susScale * 36);
const gain = clamp(attackGain * sustainGain, 0, 8);
avgGain = lerp(avgGain, gain, this.gainCoeff);
const makeup = avgGain > 1e-3 ? 1 / avgGain : 1;
const wet = sample * gain * makeup;
let y = lerp(sample, wet, this.mix);
y /= 1 + Math.abs(y); // soft clip
output[ch][n] = y;
}
this.attackEnv[ch] = attEnv;
this.sustainEnv[ch] = susEnv;
}
this.avgGain = avgGain;
return true;
}
}
registerProcessor('transient-processor', TransientProcessor);
+4 -3
View File
@@ -3,6 +3,7 @@ import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound } from './superdough.mjs'; import { registerSound } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { buildSamples } from './zzfx_fork.mjs'; import { buildSamples } from './zzfx_fork.mjs';
import { onceEnded, releaseAudioNode } from './helpers.mjs';
export const getZZFX = (value, t) => { export const getZZFX = (value, t) => {
let { let {
@@ -83,10 +84,10 @@ export function registerZZFXSounds() {
wave, wave,
(t, value, onended) => { (t, value, onended) => {
const { node: o } = getZZFX({ s: wave, ...value }, t); const { node: o } = getZZFX({ s: wave, ...value }, t);
o.onended = () => { onceEnded(o, () => {
o.disconnect(); releaseAudioNode(o);
onended(); onended();
}; });
return { return {
node: o, node: o,
stop: () => {}, stop: () => {},
+11 -2
View File
@@ -5,7 +5,16 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import { Note, Interval, Scale } from '@tonaljs/tonal'; import { Note, Interval, Scale } from '@tonaljs/tonal';
import { register, _mod, logger, isNote, noteToMidi, removeUndefineds, getAccidentalsOffset } from '@strudel/core'; import {
_mod,
errorLogger,
getAccidentalsOffset,
isNote,
logger,
noteToMidi,
register,
removeUndefineds,
} from '@strudel/core';
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs'; import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P'; const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
@@ -294,7 +303,7 @@ export const scale = register(
} }
if (offset != 0) scaleNote = Note.transpose(scaleNote, Interval.fromSemitones(offset)); if (offset != 0) scaleNote = Note.transpose(scaleNote, Interval.fromSemitones(offset));
} catch (err) { } catch (err) {
logger(`[tonal] ${err.message}`, 'error'); errorLogger(err, 'tonal');
return; // will be removed return; // will be removed
} }
} }
+20 -132
View File
@@ -41,7 +41,7 @@ importers:
version: 2.2.7 version: 2.2.7
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 3.0.4 specifier: 3.0.4
version: 3.0.4(vitest@3.0.4) version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))
'@vitest/ui': '@vitest/ui':
specifier: ^3.0.4 specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4) version: 3.0.4(vitest@3.0.4)
@@ -85,29 +85,29 @@ importers:
examples/codemirror-repl: examples/codemirror-repl:
dependencies: dependencies:
'@strudel/codemirror': '@strudel/codemirror':
specifier: 1.2.6 specifier: workspace:*
version: 1.2.6(@codemirror/lint@6.8.4) version: link:../../packages/codemirror
'@strudel/core': '@strudel/core':
specifier: 1.2.5 specifier: workspace:*
version: 1.2.5 version: link:../../packages/core
'@strudel/draw': '@strudel/draw':
specifier: 1.2.5 specifier: workspace:*
version: 1.2.5 version: link:../../packages/draw
'@strudel/mini': '@strudel/mini':
specifier: 1.2.5 specifier: workspace:*
version: 1.2.5 version: link:../../packages/mini
'@strudel/soundfonts': '@strudel/soundfonts':
specifier: 1.2.6 specifier: workspace:*
version: 1.2.6 version: link:../../packages/soundfonts
'@strudel/tonal': '@strudel/tonal':
specifier: 1.2.5 specifier: workspace:*
version: 1.2.5 version: link:../../packages/tonal
'@strudel/transpiler': '@strudel/transpiler':
specifier: 1.2.5 specifier: workspace:*
version: 1.2.5 version: link:../../packages/transpiler
'@strudel/webaudio': '@strudel/webaudio':
specifier: 1.2.6 specifier: workspace:*
version: 1.2.6 version: link:../../packages/webaudio
devDependencies: devDependencies:
vite: vite:
specifier: ^6.0.11 specifier: ^6.0.11
@@ -116,8 +116,8 @@ importers:
examples/headless-repl: examples/headless-repl:
dependencies: dependencies:
'@strudel/web': '@strudel/web':
specifier: 1.2.6 specifier: workspace:*
version: 1.2.6 version: link:../../packages/web
devDependencies: devDependencies:
vite: vite:
specifier: ^6.0.11 specifier: ^6.0.11
@@ -2544,33 +2544,6 @@ packages:
'@sinclair/typebox@0.27.8': '@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
'@strudel/codemirror@1.2.6':
resolution: {integrity: sha512-zjKbnbnOY40dlIAQu1poq28rcngYliLsuDwXXVnJsaXzCZSVuXEOMq91LqY4IGz/2QUOIDGm663ziz6Nb4IXeg==}
'@strudel/core@1.2.5':
resolution: {integrity: sha512-XO6ICbfuGfvaQ4sZmfIFsPDcjFez/+AVpNgJnmks5gtMMQNXpr0reuee61tvMiu2k5QYKzxWgzDl2FF9pJeIgA==}
'@strudel/draw@1.2.5':
resolution: {integrity: sha512-ZI4WPxUsVsxgDoeZMHo6VfFf+S1tsj8ubKBeSLcf6lH29JqPp7Nz6tPmbomOV1fzazZ7+xpkC7z772MdtgWy2g==}
'@strudel/mini@1.2.5':
resolution: {integrity: sha512-GmTWSBt49C3ogBy0hjM3bmf6K12PaJ1JJLsEvQu7j5J+lUd2n7GmLYR1UL6fntY4yVqT8ZwMhfUf6vIgUCTNbw==}
'@strudel/soundfonts@1.2.6':
resolution: {integrity: sha512-F7eBhohkCXCMo8WiufSAhD3JCfL5GTgCh9RlOJVT+1swiFeIss1vUAWJR3WjonB6moL8BiuNpcYOqO6i6cFy4A==}
'@strudel/tonal@1.2.5':
resolution: {integrity: sha512-mX3uW0nWH8Sa52HTLwnxlR5BrejFX77abeaiMbp+kQCMrX9I+XRdf8rEr7gdeInB6f/y2TzTVFpnsNsB5jLdoA==}
'@strudel/transpiler@1.2.5':
resolution: {integrity: sha512-mBZWfQBzknvnIYsjjzR8D67A+uIZ1iZ60U2WoXT21q5luXKz2c95RcTpqdx3oOGpLK9K00lDzcKjhQDJtj4OKw==}
'@strudel/web@1.2.6':
resolution: {integrity: sha512-e6VltuX90zZz8JVe1FtN23DCaXA9p8zGDakp5OuUb5kZ5hC6cySFkkOsHdGC6dJ0EAsZSCKXMihRxgk9GhBwKA==}
'@strudel/webaudio@1.2.6':
resolution: {integrity: sha512-4LnyNvyeAXYNmXQGGw2f0chIkDr9Rbwo+SK6Y0p8C3W8juDiOfKq2ihSVph95yaBBPRYFJ5Kttj1kLknRHYsog==}
'@supabase/auth-js@2.67.3': '@supabase/auth-js@2.67.3':
resolution: {integrity: sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==} resolution: {integrity: sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==}
@@ -7121,9 +7094,6 @@ packages:
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
hasBin: true hasBin: true
superdough@1.2.6:
resolution: {integrity: sha512-zHEFfG+N4zhxJ9lUDf8p4Oj4sdGGQ5SrOPksdc0cKv5Yn0hzlNlMehEa5TkeIfDCiR27Y4H81ks7S8/CGSByhQ==}
supports-color@7.2.0: supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -7132,9 +7102,6 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
supradough@1.2.4:
resolution: {integrity: sha512-/v3cL9fq05QCQVwbttLHNZcjyyFFz5wdKvg3j6V6moRXpDlQG89l8dQtUIpQvHsxYI5sXNod9rh5bf+Y4NMGeA==}
tabbable@6.2.0: tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
@@ -10052,79 +10019,6 @@ snapshots:
'@sinclair/typebox@0.27.8': {} '@sinclair/typebox@0.27.8': {}
'@strudel/codemirror@1.2.6(@codemirror/lint@6.8.4)':
dependencies:
'@codemirror/autocomplete': 6.18.4
'@codemirror/commands': 6.8.0
'@codemirror/lang-javascript': 6.2.2
'@codemirror/language': 6.10.8
'@codemirror/search': 6.5.8
'@codemirror/state': 6.5.1
'@codemirror/view': 6.36.2
'@lezer/highlight': 1.2.1
'@nanostores/persistent': 0.10.2(nanostores@0.11.3)
'@replit/codemirror-emacs': 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@replit/codemirror-vim': 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@replit/codemirror-vscode-keymap': 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@strudel/core': 1.2.5
'@strudel/draw': 1.2.5
'@strudel/tonal': 1.2.5
'@strudel/transpiler': 1.2.5
'@tonaljs/tonal': 4.10.0
nanostores: 0.11.3
superdough: 1.2.6
transitivePeerDependencies:
- '@codemirror/lint'
'@strudel/core@1.2.5':
dependencies:
fraction.js: 5.2.1
'@strudel/draw@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@strudel/mini@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@strudel/soundfonts@1.2.6':
dependencies:
'@strudel/core': 1.2.5
'@strudel/webaudio': 1.2.6
sfumato: 0.1.2
soundfont2: 0.5.0
'@strudel/tonal@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@tonaljs/tonal': 4.10.0
chord-voicings: 0.0.1
webmidi: 3.1.12
'@strudel/transpiler@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@strudel/mini': 1.2.5
acorn: 8.14.0
escodegen: 2.1.0
estree-walker: 3.0.3
'@strudel/web@1.2.6':
dependencies:
'@strudel/core': 1.2.5
'@strudel/mini': 1.2.5
'@strudel/tonal': 1.2.5
'@strudel/transpiler': 1.2.5
'@strudel/webaudio': 1.2.6
'@strudel/webaudio@1.2.6':
dependencies:
'@strudel/core': 1.2.5
'@strudel/draw': 1.2.5
superdough: 1.2.6
supradough: 1.2.4
'@supabase/auth-js@2.67.3': '@supabase/auth-js@2.67.3':
dependencies: dependencies:
'@supabase/node-fetch': 2.6.15 '@supabase/node-fetch': 2.6.15
@@ -10660,7 +10554,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vitest/coverage-v8@3.0.4(vitest@3.0.4)': '@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
dependencies: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2 '@bcoe/v8-coverage': 1.0.2
@@ -15771,18 +15665,12 @@ snapshots:
pirates: 4.0.6 pirates: 4.0.6
ts-interface-checker: 0.1.13 ts-interface-checker: 0.1.13
superdough@1.2.6:
dependencies:
nanostores: 0.11.3
supports-color@7.2.0: supports-color@7.2.0:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
supradough@1.2.4: {}
tabbable@6.2.0: {} tabbable@6.2.0: {}
tailwindcss@3.4.17: tailwindcss@3.4.17:
File diff suppressed because it is too large Load Diff
+1
View File
@@ -1,4 +1,5 @@
{ {
"name": "dbpatch",
"dependencies": { "dependencies": {
"csv": "^6.3.11" "csv": "^6.3.11"
}, },
+14
View File
@@ -0,0 +1,14 @@
fetch('https://codeberg.org/api/v1/repos/uzu/strudel/pulls?state=closed&page=1')
.then((res) => res.json())
.then((pulls) => {
const r = pulls
.filter((pull) => pull.merged)
.sort((a, b) => new Date(b.closed_at) - new Date(a.closed_at))
.map((pull) => `${pull.closed_at} ${pull.title} by ${pull.user.login || '?'} in: [#${pull.number}](${pull.url}) `)
.join('\n');
console.log(r);
});
/*
*/
+73 -7
View File
@@ -48,28 +48,94 @@ You can also use the `crackle` type to play some subtle noise crackles. You can
### Additive Synthesis ### Additive Synthesis
To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform: Periodic waveforms are composed of several [harmonics](https://en.wikipedia.org/wiki/Harmonic) above a fundamental frequency, lying at integer multiples. These overtones combine to give a sound its unique timbral quality.
For the basic waveforms, we offer you control over these harmonics with the `partials` and `phases` functions.
#### Partials
`partials` refers to the magnitude of each harmonic relative to the fundamental frequency. They can thus be used to spectrally filter these waveforms and tame some of their harshness:
<MiniRepl <MiniRepl
client:idle client:idle
tune={`note("c2 <eb2 <g2 g1>>".fast(2)) tune={`note("c2 <eb2 <g2 g1>>".fast(2))
.sound("sawtooth") .sound("sawtooth")
.n("<32 16 8 4>") .partials([1, 1, "<1 0>", "<1 0>", "<1 0>", "<1 0>", "<1 0>"])
._scope()`} ._scope()`}
/> />
When the `n` control is used on a basic waveform, it defines the number of harmonic partials the sound is getting. `partials` can also be used to construct _new_ waveforms not present in our basic set with the 'user' sound source:
You can also set `n` directly in mini notation with `sound`:
<MiniRepl <MiniRepl
client:idle client:idle
tune={`note("c2 <eb2 <g2 g1>>".fast(2)) tune={`note("c2 <eb2 <g2 g1>>".fast(2))
.sound("sawtooth:<32 16 8 4>") .sound("user")
.partials([1, 0, 0.3, 0, 0.1, 0, 0, 0.3])
._scope()`} ._scope()`}
/> />
Note for tidal users: `n` in tidal is synonymous to `note` for synths only. We may algorithmically construct lists of magnitudes with Javascript code like:
In strudel, this is not the case, where `n` will always change timbre, be it though different samples or different waveforms.
<MiniRepl
client:idle
tune={`const numHarmonics = 22;
note("c2 <eb2 <g2 g1>>".fast(2))
.sound("saw")
.partials(new Array(numHarmonics).fill(1))
._scope()`}
/>
which acts as a spectral filter. Or:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>").fast(2)
.sound("user")
.partials(new Array(50).fill(0)
.map((_, idx) => ((-1) ** (idx + 1)) / (idx + 1))
)
._scope()`}
/>
which recovers a familiar waveform.
`partials` is also compatible with pattern functions designed to produce lists, like `randL` or `binaryL`:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>").fast(2)
.sound("user")
.partials(randL(10))
._scope()`}
/>
and with lists _of_ patterns:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>".fast(4))
.sound("user")
.partials([1, 0, "0 1", "0 1 0.3", rand])
._scope()`}
/>
Note that the first value in the `partials` array controls the magnitude of the fundamental harmonic rather than the DC offset, which is fixed at 0.
#### Phases
Earlier, we mentioned that periodic waveforms can be broken into a set of harmonics above a fundamental frequency. Each harmonic has two defining properties: its magnitude (how loud it is) and its phase, which determines where in its cycle that sine wave starts when the waveform is built.
These phases too can be declared in Strudel and can give your sounds interesting depth.
<MiniRepl
client:idle
tune={`s("saw").seg(16).n(irand(12)).scale("F1:minor")
.penv(48).panchor(0).pdec(0.05)
.delay(0.25).room(0.25)
.compressor(-20).vib(0.3)
.partials(randL(200))
.phases(randL(200))`}
/>
## Vibrato ## Vibrato
@@ -8,3 +8,34 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but
</button> </button>
); );
} }
export function SpecialActionButton(props) {
const { className, ...buttonProps } = props;
return (
<ActionButton
{...buttonProps}
className={cx('bg-background p-2 max-w-[300px] rounded-md hover:opacity-50', className)}
/>
);
}
export function ActionInput({ label, className, ...props }) {
return (
<label className={cx('inline-flex items-center cursor-pointer', className)}>
<input {...props} className="sr-only peer" />
<span className="inline-flex items-center peer-hover:opacity-50">{label}</span>
</label>
);
}
export function SpecialActionInput({ className, ...props }) {
return (
<ActionInput
{...props}
className={className}
label={<span className="bg-background p-2 max-w-[300px] rounded-md">{props.label}</span>}
/>
);
}
@@ -0,0 +1,29 @@
import { errorLogger } from '@strudel/core';
import { useSettings, storePrebakeScript } from '../../../settings.mjs';
import { SpecialActionInput } from '../button/action-button';
async function importScript(script) {
const reader = new FileReader();
reader.readAsText(script);
reader.onload = () => {
const text = reader.result;
storePrebakeScript(text);
};
reader.onerror = () => {
errorLogger(new Error('failed to import prebake script'), 'importScript');
};
}
export function ImportPrebakeScriptButton() {
const settings = useSettings();
return (
<SpecialActionInput
type="file"
label="import prebake script"
accept=".strudel"
onChange={(e) => importScript(e.target.files[0])}
/>
);
}
@@ -7,6 +7,8 @@ import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx'; import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
import { confirmDialog } from '../../util.mjs'; import { confirmDialog } from '../../util.mjs';
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio'; import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
import { ActionButton, SpecialActionButton } from '../button/action-button.jsx';
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
function Checkbox({ label, value, onChange, disabled = false }) { function Checkbox({ label, value, onChange, disabled = false }) {
return ( return (
@@ -113,6 +115,7 @@ export function SettingsTab({ started }) {
isTabIndentationEnabled, isTabIndentationEnabled,
isMultiCursorEnabled, isMultiCursorEnabled,
patternAutoStart, patternAutoStart,
includePrebakeScriptInShare,
} = useSettings(); } = useSettings();
const shouldAlwaysSync = isUdels(); const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
@@ -204,6 +207,15 @@ export function SettingsTab({ started }) {
/> />
</FormItem> </FormItem>
</div> </div>
<FormItem label="Prebake">
<ImportPrebakeScriptButton />
<Checkbox
label="Include prebake script in share"
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
value={includePrebakeScriptInShare}
/>
</FormItem>
<FormItem label="Keybindings"> <FormItem label="Keybindings">
<ButtonGroup <ButtonGroup
value={keybindings} value={keybindings}
@@ -313,8 +325,7 @@ export function SettingsTab({ started }) {
</FormItem> </FormItem>
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem> <FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
<FormItem label="Reset Settings"> <FormItem label="Reset Settings">
<button <SpecialActionButton
className="bg-background p-2 max-w-[300px] rounded-md hover:opacity-50"
onClick={() => { onClick={() => {
confirmDialog('Sure?').then((r) => { confirmDialog('Sure?').then((r) => {
if (r) { if (r) {
@@ -325,7 +336,7 @@ export function SettingsTab({ started }) {
}} }}
> >
restore default settings restore default settings
</button> </SpecialActionButton>
</FormItem> </FormItem>
</div> </div>
); );
+34 -15
View File
@@ -14,6 +14,10 @@ import { prebake } from '@src/repl/prebake.mjs';
const getSamples = (samples) => const getSamples = (samples) =>
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1; Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function SoundsTab() { export function SoundsTab() {
const sounds = useStore(soundMap); const sounds = useStore(soundMap);
@@ -56,9 +60,7 @@ export function SoundsTab() {
// holds mutable ref to current triggered sound // holds mutable ref to current triggered sound
const trigRef = useRef(); const trigRef = useRef();
const numRef = useRef(0);
// Used to cycle through sound previews on banks with multiple sounds
let soundPreviewIdx = 0;
// stop current sound on mouseup // stop current sound on mouseup
useEvent('mouseup', () => { useEvent('mouseup', () => {
@@ -66,6 +68,14 @@ export function SoundsTab() {
trigRef.current = undefined; trigRef.current = undefined;
ref?.stop?.(getAudioContext().currentTime + 0.01); ref?.stop?.(getAudioContext().currentTime + 0.01);
}); });
useEvent('keydown', (e) => {
if (!isNaN(Number(e.key))) {
numRef.current = Number(e.key);
}
});
useEvent('keyup', (e) => {
numRef.current = 0;
});
return ( return (
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground"> <div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} /> <Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
@@ -115,25 +125,34 @@ export function SoundsTab() {
const params = { const params = {
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined, note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
s: name, s: name,
n: soundPreviewIdx, n: numRef.current,
clip: 1, clip: 1,
release: 0.5, release: 0.5,
sustain: 1, sustain: 1,
duration: 0.5, duration: 0.5,
}; };
soundPreviewIdx++;
const onended = () => trigRef.current?.node?.disconnect(); const onended = () => trigRef.current?.node?.disconnect();
try { // Attempt to play the sample and retry every 200ms until 10 attempts have been reached
// Pre-load the sample by calling onTrigger with a future time let errMsg;
// This triggers the loading but schedules playback for later for (let attempt = 0; attempt < 10; attempt++) {
const time = ctx.currentTime + 0.5; // Give 500ms for loading try {
const ref = await onTrigger(time, params, onended); // Pre-load the sample by calling onTrigger with a future time
trigRef.current = ref; // This triggers the loading but schedules playback for later
if (ref?.node) { const time = ctx.currentTime + 0.05; // Give 50ms for loading
connectToDestination(ref.node); const ref = await onTrigger(time, params, onended);
trigRef.current = ref;
if (ref?.node) {
connectToDestination(ref.node);
break;
}
} catch (err) {
errMsg = err;
}
if (attempt == 9) {
console.warn('Failed to trigger sound after 10 attempts' + (errMsg ? `: ${errMsg}` : ''));
} else {
await wait(200);
} }
} catch (err) {
console.warn('Failed to trigger sound:', err);
} }
}} }}
> >
+2
View File
@@ -3,6 +3,8 @@ import { aliasBank, registerSynthSounds, registerZZFXSounds, samples } from '@st
import { registerSamplesFromDB } from './idbutils.mjs'; import { registerSamplesFromDB } from './idbutils.mjs';
import './piano.mjs'; import './piano.mjs';
import './files.mjs'; import './files.mjs';
import { settingsMap } from '@src/settings.mjs';
import { evaluate } from '@strudel/transpiler';
const { BASE_URL } = import.meta.env; const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
+15 -5
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core'; import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
import { getDrawContext } from '@strudel/draw'; import { getDrawContext } from '@strudel/draw';
import { transpiler } from '@strudel/transpiler'; import { evaluate, transpiler } from '@strudel/transpiler';
import { import {
getAudioContextCurrentTime, getAudioContextCurrentTime,
webaudioOutput, webaudioOutput,
@@ -63,11 +63,10 @@ async function getModule(name) {
const initialCode = `// LOADING`; const initialCode = `// LOADING`;
export function useReplContext() { export function useReplContext() {
const { isSyncEnabled, audioEngineTarget } = useSettings(); const { isSyncEnabled, audioEngineTarget, prebakeScript, includePrebakeScriptInShare } = useSettings();
const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc; const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc;
const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput; const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput;
const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds; const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds;
const init = useCallback(() => { const init = useCallback(() => {
const drawTime = [-2, 2]; const drawTime = [-2, 2];
const drawContext = getDrawContext(); const drawContext = getDrawContext();
@@ -84,7 +83,12 @@ export function useReplContext() {
pattern: silence, pattern: silence,
drawTime, drawTime,
drawContext, drawContext,
prebake: async () => Promise.all([modulesLoading, presets]), prebake: async () =>
Promise.all([modulesLoading, presets]).then(() => {
if (prebakeScript?.length) {
return evaluate(prebakeScript ?? '');
}
}),
onUpdateState: (state) => { onUpdateState: (state) => {
setReplState({ ...state }); setReplState({ ...state });
}, },
@@ -214,7 +218,13 @@ export function useReplContext() {
editorRef.current.repl.evaluate(code); editorRef.current.repl.evaluate(code);
}; };
const handleShare = async () => shareCode(replState.code); const handleShare = async () => {
let code = replState.code;
if (includePrebakeScriptInShare) {
code = prebakeScript + '\n' + code;
}
shareCode(code);
};
const context = { const context = {
started, started,
pending, pending,
+5 -6
View File
@@ -1,11 +1,10 @@
import { evalScope, hash2code, logger } from '@strudel/core'; import { code2hash, evalScope, hash2code, logger } from '@strudel/core';
import { settingPatterns } from '../settings.mjs'; import { settingPatterns } from '../settings.mjs';
import { setVersionDefaults } from '@strudel/webaudio'; import { setVersionDefaults } from '@strudel/webaudio';
import { getMetadata } from '../metadata_parser'; import { getMetadata } from '../metadata_parser';
import { isTauri } from '../tauri.mjs'; import { isTauri } from '../tauri.mjs';
import './Repl.css'; import './Repl.css';
import { createClient } from '@supabase/supabase-js'; import { createClient } from '@supabase/supabase-js';
import { nanoid } from 'nanoid';
import { writeText } from '@tauri-apps/plugin-clipboard-manager'; import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { $featuredPatterns /* , loadDBPatterns */ } from '@src/user_pattern_utils.mjs'; import { $featuredPatterns /* , loadDBPatterns */ } from '@src/user_pattern_utils.mjs';
@@ -109,9 +108,8 @@ export function confirmDialog(msg) {
}); });
} }
let lastShared;
//RIP due to SPAM //RIP due to SPAM
// let lastShared;
// export async function shareCode(codeToShare) { // export async function shareCode(codeToShare) {
// // const codeToShare = activeCode || code; // // const codeToShare = activeCode || code;
// if (lastShared === codeToShare) { // if (lastShared === codeToShare) {
@@ -146,9 +144,10 @@ let lastShared;
// }); // });
// } // }
export async function shareCode() { export async function shareCode(codeToShare) {
try { try {
const shareUrl = window.location.href; const hash = '#' + code2hash(codeToShare);
const shareUrl = window.location.origin + window.location.pathname + hash;
if (isTauri()) { if (isTauri()) {
await writeText(shareUrl); await writeText(shareUrl);
} else { } else {
+5
View File
@@ -45,11 +45,13 @@ export const defaultSettings = {
isPanelOpen: true, isPanelOpen: true,
togglePanelTrigger: 'click', //click | hover togglePanelTrigger: 'click', //click | hover
userPatterns: '{}', userPatterns: '{}',
prebakeScript: '',
audioEngineTarget: audioEngineTargets.webaudio, audioEngineTarget: audioEngineTargets.webaudio,
isButtonRowHidden: false, isButtonRowHidden: false,
isCSSAnimationDisabled: false, isCSSAnimationDisabled: false,
maxPolyphony: 128, maxPolyphony: 128,
multiChannelOrbits: false, multiChannelOrbits: false,
includePrebakeScriptInShare: true,
}; };
let search = null; let search = null;
@@ -96,6 +98,7 @@ export function useSettings() {
isPanelOpen: parseBoolean(state.isPanelOpen), isPanelOpen: parseBoolean(state.isPanelOpen),
userPatterns: userPatterns, userPatterns: userPatterns,
multiChannelOrbits: parseBoolean(state.multiChannelOrbits), multiChannelOrbits: parseBoolean(state.multiChannelOrbits),
includePrebakeScriptInShare: parseBoolean(state.includePrebakeScriptInShare),
patternAutoStart: isUdels() patternAutoStart: isUdels()
? false ? false
: state.patternAutoStart === undefined : state.patternAutoStart === undefined
@@ -108,6 +111,8 @@ export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool); export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool); export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script);
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active); export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
const patternSetting = (key) => const patternSetting = (key) =>