Compare commits

...

232 Commits

Author SHA1 Message Date
alex 8c8f6302b7 reinstate weave / weaveWith 2025-10-13 21:57:59 +01:00
Alex McLean 7eebf3a466 Merge pull request 'Add control-metadata' (#1634) from control-metadata into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1634
2025-10-10 11:25:00 +02:00
Alex McLean 64f16c4f33 placate format gods 2025-10-08 15:35:34 +01:00
Alex McLean ffbbc43c89 add cyclist version to the query metadata 2025-10-08 15:34:51 +01:00
Alex McLean 086596c47c fix setControls to do union with existing controls 2025-10-08 14:52:00 +01:00
Alex McLean fdde05e751 add pattern id to query state controls 2025-10-08 14:51:21 +01:00
Alex McLean 694162a7b1 fix purity of 'withState' so it returns a new pattern 2025-10-08 14:13:33 +01:00
Alex McLean cbcb25b063 Merge pull request 'Fix MQTT: change the trigger handler to match new hap' (#1629) from jurrchen/strudel:bugfix/mqtt-hap-sig into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1629
2025-10-07 22:59:38 +02:00
Jieren Chen 0d08225983 change the trigger handler to match new hap 2025-10-05 21:45:23 +02:00
froos 00872bd1de Merge pull request 'Fix case sensitivity for synonym search' (#1618) from vvolhejn/strudel:vv/fix-case-sensitivity into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1618
2025-10-05 21:42:18 +02:00
Václav Volhejn b7d98dd370 Fix case sensitivity for synonym search 2025-10-01 21:53:28 +02:00
Alex McLean 0ecacae71e format 2025-10-01 12:02:09 +01:00
yaxu 8cd497abb2 Update website/src/pages/technical-manual/project-start.mdx 2025-10-01 12:54:58 +02:00
Switch Angel AKA Jade Rose 070055d2ef Merge pull request 'Optimize wavetable synth' (#1613) from glossing/strudel:glossing/wavetable-optimizations into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1613
2025-10-01 08:56:33 +02:00
Switch Angel AKA Jade Rose e500ebe268 Merge branch 'main' into glossing/wavetable-optimizations 2025-10-01 08:54:53 +02:00
Aria 3a7ec18d46 Move invsr to constructor 2025-09-30 23:19:55 -05:00
Aria a2b8407fbb Add back mipmaps; add caching 2025-09-30 23:08:28 -05:00
Switch Angel AKA Jade Rose ac1fd88d8d Merge pull request 'fix: pattern switching' (#1616) from fix_pattern_Switching into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1616
2025-10-01 02:02:30 +02:00
Jade (Rose) Rowland 9f8143e062 format 2025-09-30 19:46:53 -04:00
Jade (Rose) Rowland cdc5e5f3f3 working 2025-09-30 19:45:30 -04:00
Aria bad498a1e5 Remove unused var 2025-09-30 16:21:28 -07:00
Aria 4de0c11f8c Remove mipmaps, fix clamping of parameters 2025-09-29 14:15:50 -07:00
Switch Angel AKA Jade Rose f360d394a7 Merge pull request 'fix wavetable JSON' (#1611) from fix_JSON into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1611
2025-09-29 02:10:53 +02:00
Jade (Rose) Rowland 48718daea8 add_vgame 2025-09-28 19:57:19 -04:00
Jade (Rose) Rowland 8d45016ccf working 2025-09-28 19:11:13 -04:00
Switch Angel AKA Jade Rose ec69f6fd77 Merge pull request 'rename wavetable controls to match existing naming conventions' (#1610) from wavetable_controls into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1610
2025-09-28 21:28:15 +02:00
Jade (Rose) Rowland d94f145649 fix vib 2025-09-28 15:23:58 -04:00
Jade (Rose) Rowland bf3fe605d9 fix unison gain 2025-09-28 15:21:06 -04:00
Jade (Rose) Rowland 54ebc97ddc fix tests 2025-09-28 15:07:35 -04:00
Jade (Rose) Rowland c689874bb4 simplify modulators 2025-09-28 14:57:12 -04:00
Jade (Rose) Rowland 23d4bfa9d9 rename controls 2025-09-28 09:19:39 -04:00
Switch Angel AKA Jade Rose 91eb50f1c1 Merge pull request '<Warn: Don't merge until tables uploaded> Feature: Wavetable synth improvements' (#1607) from glossing/strudel:glossing/wavetable-synth into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1607
2025-09-27 23:28:12 +02:00
Aria 5838ade740 Merge branch 'main' into glossing/wavetable-synth 2025-09-27 13:17:05 -07:00
Aria 917d01c47d Add LFOs and envelopes 2025-09-27 12:48:37 -07:00
Switch Angel AKA Jade Rose 0a51718df3 Merge pull request 'wavetable improvements' (#1608) from wt_improvements into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1608
2025-09-27 21:43:09 +02:00
Jade (Rose) Rowland 774372a339 format 2025-09-27 15:29:55 -04:00
Jade (Rose) Rowland 626a99ba5b supports old documented way of loading wavetables: 2025-09-27 15:22:54 -04:00
Aria 8c3ee6db6b Missed deletion 2025-09-27 11:07:04 -07:00
Aria c5c05ebadc Clean up to align with other PR 2025-09-27 11:02:57 -07:00
Aria a8918d55fb Actually update normalizer 2025-09-27 10:45:30 -07:00
Jade (Rose) Rowland a9957b45e5 suppoert existing wt_ api 2025-09-27 13:22:21 -04:00
Jade (Rose) Rowland 4bd103e612 gain adj 2025-09-27 11:31:43 -04:00
Aria 466234bad6 Merge branch 'main' into glossing/wavetable-synth 2025-09-26 21:22:45 -07:00
Aria 97beaec25a Added examples, fixed samplerate issue on import, added to settings tab, fixed spread, added default wavetables, change default phaserand to 0 2025-09-26 20:46:10 -07:00
Jade (Rose) Rowland 2b3b638943 sample source for WT 2025-09-26 21:47:45 -05:00
Switch Angel AKA Jade Rose e1629e8383 Merge pull request 'create orbit based DJ filter' (#1603) from djf into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1603
2025-09-26 08:48:45 +02:00
Jade (Rose) Rowland 266cb614a8 add better example 2025-09-25 23:44:32 -07:00
Jade (Rose) Rowland dfc4a0818c res adjust 2025-09-25 23:39:08 -07:00
Jade (Rose) Rowland 236ff577a6 Merge branch 'main' into djf 2025-09-25 23:18:22 -07:00
Switch Angel AKA Jade Rose 66a32ac45d Merge pull request 'Feature: Wavetable synth' (#1525) from glossing/strudel:glossing/wavetable-synth into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1525
2025-09-26 08:15:48 +02:00
Aria 8b2c35b7a3 Cleanup 2025-09-25 01:10:41 -07:00
Aria f8f42565ae Typo - add back slight delay in cleanup 2025-09-25 00:40:25 -07:00
Aria 96981c3c1d A bit more cleanup 2025-09-25 00:36:28 -07:00
Aria bd8d207a3d Typo 2025-09-25 00:17:13 -07:00
Aria e3741ae8fa Add control over phase randomization and fix a bug with supersaw 2025-09-25 00:05:50 -07:00
Switch Angel AKA Jade Rose ef87f65bef Merge branch 'main' into glossing/wavetable-synth 2025-09-25 07:21:17 +02:00
Aria d3d9f23c3c Remove unnecessary defaulting 2025-09-24 22:06:53 -07:00
Jade (Rose) Rowland c474b8c92e rm comment 2025-09-24 20:33:51 -07:00
Jade (Rose) Rowland da81ba00fb adjust control range 2025-09-24 20:32:45 -07:00
Jade (Rose) Rowland 28c94efaa9 format 2025-09-24 00:24:25 -07:00
Jade (Rose) Rowland 0633954e24 working 2025-09-24 00:23:05 -07:00
Switch Angel AKA Jade Rose cedf09168f Merge pull request 'fix: time signal' (#1583) from fix_time into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1583
2025-09-23 13:28:19 +02:00
Switch Angel AKA Jade Rose 3373225f53 Merge branch 'main' into fix_time 2025-09-23 13:13:51 +02:00
Jade (Rose) Rowland 1eb9dc73fa fix duck 2025-09-22 00:34:57 -04:00
Jade (Rose) Rowland fdcdc3aaa0 rm deadcode 2025-09-22 00:13:59 -04:00
Jade (Rose) Rowland da926805b3 rm dead code 2025-09-22 00:12:48 -04:00
Jade (Rose) Rowland 82893ffc22 fix import 2025-09-22 00:11:02 -04:00
Jade (Rose) Rowland beafd9c700 format 2025-09-22 00:07:06 -04:00
Jade (Rose) Rowland c5bd2a7487 working 2025-09-21 23:54:04 -04:00
froos 118086aff5 Merge pull request 'mondo: fix all + setcpm + setcps' (#1600) from mondo-fixes into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1600
2025-09-21 16:16:05 +02:00
Felix Roos 452827630b move unpure up 2025-09-21 16:13:09 +02:00
Felix Roos 86248328cc fix: mondo setcps / setcpm 2025-09-21 16:09:10 +02:00
Felix Roos 8833d623f7 fix: all function in mondo 2025-09-21 16:06:28 +02:00
froos 3fff4609a0 Merge pull request 'fix: osc error message' (#1597) from osc-debug-flag into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1597
2025-09-19 21:59:42 +02:00
froos e2b5b92a0c Merge pull request 'osc: --debug flag to see incoming messages' (#1595) from osc-debug-flag into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1595
2025-09-18 21:09:55 +02:00
Felix Roos 1c3e07afd3 fix: rephrase error message + bump to 1.2.10 2025-09-18 20:40:25 +02:00
Felix Roos c54fa7d266 chore: bump osc to 1.2.8 2025-09-18 20:39:05 +02:00
Felix Roos a25f763796 docs: add --debug flag to readme 2025-09-18 20:37:19 +02:00
Felix Roos 91ad3d729f feat: osc add --debug flag to log messages + log errors 2025-09-18 20:36:40 +02:00
froos 4cb0488839 Merge pull request 'simplify osc usage' (#1588) from osc-server-via-npx into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1588
2025-09-17 14:12:09 +02:00
Felix Roos 663229ecde bump osc to 1.2.8 2025-09-17 13:50:02 +02:00
Felix Roos cdb623cb66 overhaul osc readme 2025-09-17 13:39:35 +02:00
Felix Roos 28d7e0e489 export osc function to be able to do all(osc) 2025-09-17 13:39:28 +02:00
Felix Roos aba594b72a fix: bin field 2025-09-17 12:52:42 +02:00
Felix Roos 5d7c6c3e4b fix: add node shebang + bump osc to 1.2.6 2025-09-17 12:47:41 +02:00
Felix Roos 820744dc27 bump osc to 1.2.5 2025-09-17 12:42:11 +02:00
Felix Roos 5445f0812f configurable port for osc bridge + add bin field 2025-09-17 12:41:46 +02:00
Jade (Rose) Rowland a68c784753 rm unessecary change 2025-09-16 11:55:31 -04:00
Jade (Rose) Rowland f2b2f9f959 format 2025-09-16 11:53:19 -04:00
Jade (Rose) Rowland 53dd53553a Merge branch 'main' into fix_time 2025-09-16 11:52:54 -04:00
Jade (Rose) Rowland 89b7eac789 fix test 2025-09-16 11:52:36 -04:00
Jade (Rose) Rowland 9cdba1a50e Working 2025-09-16 11:36:21 -04:00
Switch Angel AKA Jade Rose 98b7003504 Merge pull request 'dev: logger errors to console in dev mode' (#1585) from devlog into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1585
2025-09-16 16:59:57 +02:00
Jade (Rose) Rowland 9a8f8a051c rm comment 2025-09-16 01:48:37 -04:00
Jade (Rose) Rowland 3eb40ee7d3 working in dev 2025-09-16 01:44:01 -04:00
Jade (Rose) Rowland 4cc453c640 fix test 2025-09-15 22:09:05 -04:00
Jade (Rose) Rowland 74e27ca94f coerce in correct place 2025-09-15 21:57:56 -04:00
Jade (Rose) Rowland 7dfd7dbcdc rmconsole 2025-09-15 21:47:40 -04:00
Jade (Rose) Rowland 2cba1dcb31 working 2025-09-15 21:46:37 -04:00
Switch Angel AKA Jade Rose 48a6a12648 Merge pull request 'Bug Fix / Feature: Updates to duck to avoid clicks and allow configuration over release/attack' (#1514) from glossing/strudel:glossing/ducking-click into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1514
2025-09-16 03:19:36 +02:00
froos 6bbf7cbe32 Merge pull request 'Feat: Enhance scale function to quantize notes to a named scale' (#1492) from glossing/strudel:glossing/scale-for-notes into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1492
2025-09-15 23:36:30 +02:00
froos 9f36b73081 Merge pull request 'add tic80 font' (#1579) from tic80font into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1579
2025-09-15 22:52:13 +02:00
Felix Roos a7e485992e add tic80 font 2025-09-15 20:54:39 +02:00
Aria ab74ccfe7c Merge branch 'main' into glossing/ducking-click 2025-09-14 18:28:00 -05:00
Aria 92b2013cf6 Correctly handle silences for non-notes 2025-09-14 17:04:39 -05:00
froos 2e97fb7be2 Merge pull request 'supradough: fix delay + add some jsdoc types' (#1578) from supradough-fixes into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1578
2025-09-15 00:00:18 +02:00
Felix Roos df1934f87b add some typechecking + fix delay 2025-09-14 23:54:51 +02:00
froos d3472feb5c Merge pull request 'added plyWith/plyWithClassic functions' (#1571) from Dsm0/strudel:add_plyWith into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1571
2025-09-14 12:42:57 +02:00
Dsm0 addb6db1c7 updated names + snapshot 2025-09-14 02:21:25 -07:00
froos 566d590453 Merge pull request 'feat: sound alias' (#1494) from Antipathie/strudel:feature/592-sound-alias into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1494
2025-09-14 10:39:11 +02:00
froos e57bd98da0 Merge branch 'main' into feature/592-sound-alias 2025-09-14 10:34:47 +02:00
Dsm0 f5c4373f99 added plyWith/plyWithClassic functions 2025-09-13 17:21:19 -07:00
froos e762291bb7 Merge pull request 'Bug Fix: Allow penv values to be falsy' (#1559) from glossing/strudel:glossing/penv-with-zeros into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1559
2025-09-14 01:20:14 +02:00
Felix Roos 9da027d377 hotfix: format 2025-09-14 01:11:34 +02:00
froos 96dccdc38b Merge pull request 'Update website/src/pages/workshop/first-sounds.mdx' (#1566) from fesmith/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1566
2025-09-14 01:10:47 +02:00
Felix Roos d6254294b0 fix: bring back tests on external PRs (push doesn't cut it for forks) 2025-09-14 01:09:30 +02:00
froos 339c1bb573 Merge pull request 'fix: autocomplete-styles + html rendering' (#1570) from autocomplete-styles into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1570
2025-09-14 01:07:47 +02:00
Felix Roos 9782795761 fix: autocomplete container style 2025-09-14 01:04:48 +02:00
Felix Roos 2d2b238da9 fix: use template element for string to html 2025-09-14 01:03:14 +02:00
froos f0839e02eb Merge pull request 'Feature: Synonyms in autocomplete and reference' (#1535) from glossing/strudel:glossing/synonyms-in-autocomplete into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1535
2025-09-14 00:49:16 +02:00
Felix Roos a9f7d825c8 hotfix: format 2025-09-14 00:40:54 +02:00
Aria 709b654ed3 Add synonyms to autocomplete 2025-09-13 16:21:09 -05:00
Aria ed00686804 Codeformat 2025-09-13 14:29:28 -05:00
Aria e10c469749 Merge branch 'main' into glossing/ducking-click 2025-09-13 14:28:35 -05:00
froos d81a214c74 Merge pull request 'Docs: add xen package examples' (#1446) from dudymas/strudel:docs/xen/added-tune into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1446
2025-09-13 20:01:47 +02:00
froos e57f240e70 Merge branch 'main' into docs/xen/added-tune 2025-09-13 20:00:35 +02:00
Felix Roos e18296e760 hotfix: word 2025-09-13 19:52:07 +02:00
froos c145355ccc Merge pull request 'Update website/src/pages/learn/code.mdx' (#1391) from anecondev/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1391
2025-09-13 19:51:09 +02:00
fesmith 352a3c39d7 Update website/src/pages/workshop/first-sounds.mdx 2025-09-13 00:00:36 +02:00
Felix Roos a46617f3f0 hotfix: use ?audioworklet for supradough worklet import 2025-09-12 10:16:37 +02:00
Felix Roos d164b5e7f5 Revert "Revert "Merge pull request 'supradough poc' (#1362) from supradough into main""
This reverts commit c0bf86d11e.
2025-09-12 10:08:55 +02:00
Felix Roos c0bf86d11e Revert "Merge pull request 'supradough poc' (#1362) from supradough into main"
This reverts commit 6f06683d70, reversing
changes made to 637d714ab2.
2025-09-11 23:19:22 +02:00
Switch Angel AKA Jade Rose 6f06683d70 Merge pull request 'supradough poc' (#1362) from supradough into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1362
2025-09-11 22:52:29 +02:00
Felix Roos 637d714ab2 Publish
- @strudel/mondo@1.1.4
2025-09-11 19:14:00 +02:00
Felix Roos c42d00f048 hotfix: export mondo getLocations 2025-09-11 19:11:45 +02:00
Felix Roos 702e558d1a Publish
- @strudel/mondo@1.1.3
2025-09-11 17:30:38 +02:00
froos bcc2e8f759 Merge pull request 'fix: exclude mondough dependencies' (#1563) from fix-mondo-package into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1563
2025-09-11 17:29:34 +02:00
Felix Roos 27339715b7 fix: exclude mondough dependencies 2025-09-11 17:22:06 +02:00
Aria 38ce593c60 Allow penv values to be falsy 2025-09-09 20:47:09 -05:00
Aria 798eb22d9a Use currentTime for exact scheduling 2025-09-06 10:09:44 -05:00
Aria 9c108862ef Corrected piping 2025-09-05 22:09:50 -05:00
Aria 46a45b4596 Some missing cleanup 2025-09-04 20:51:06 -05:00
Aria 1205573bc6 Merge branch 'main' into glossing/wavetable-synth 2025-09-04 20:31:01 -05:00
Antipathie 56b1ea3fa5 Add soundAlias function 2025-09-03 00:47:45 +02:00
Aria c199b51645 Update autocomplete name to be the label and update ref to use main name as a synonym 2025-09-02 16:29:01 -05:00
Aria 8d2a368da9 Add some more synonyms to controls docs 2025-09-02 16:14:50 -05:00
Aria 052d09e892 Add synonyms to reference 2025-09-02 16:14:38 -05:00
Aria 439a7dc5e6 Include synonyms in autocomplete 2025-09-02 16:05:40 -05:00
Aria 4e2e790864 Set delay and reverb nodes properly 2025-09-01 20:45:53 -05:00
Aria 4f3b40bd7b Merge branch 'main' into glossing/ducking-click 2025-09-01 20:45:00 -05:00
Jade (Rose) Rowland b8e1b8d7ac Merge branch 'main' into supradough 2025-08-31 22:42:07 -04:00
Aria fe46e1da53 Update docstring to include new warp modes; save sample server for a separate PR 2025-08-28 16:28:33 -05:00
Aria d1869c18ba Remove internal LFO 2025-08-28 16:25:10 -05:00
Aria 02cd79a6d9 Add wavetable oscillator with scanning, warps, and detune 2025-08-28 16:25:10 -05:00
Aria 022ac95bbd Typo 2025-08-26 12:09:12 -05:00
Aria b3e25cfc52 Merge branch 'main' into glossing/ducking-click 2025-08-26 12:08:58 -05:00
Aria 3d25aa1b91 Rename params, more examples 2025-08-26 12:06:10 -05:00
Aria b8c46d6b26 Added some description and examples of multi-accidentals and negative octaves 2025-08-22 13:23:41 -05:00
Aria 79453ac2c3 Allow negatives and multi-accidentals 2025-08-22 13:17:13 -05:00
Aria b08890bd9e Working version with note names 2025-08-22 11:25:13 -05:00
Aria 938c92fda2 Merge branch 'main' into glossing/scale-for-notes 2025-08-21 15:58:43 -05:00
Aria 9a9fe83f9c Example tests 2025-08-21 14:13:01 -05:00
Aria bf5d9917ab Codeformat and examples 2025-08-21 13:20:12 -05:00
Aria 4f15d681b0 Add release parameter, avoid clicks, some cleanup 2025-08-21 13:12:36 -05:00
Aria c8c6a2ce08 Add example test and update fastChunk test (which was previously broken) 2025-08-15 13:34:43 -05:00
Aria a4c040e101 Add new tests, organize old tests, fix issue with note 2025-08-06 12:37:55 -05:00
Aria 7046c1f546 Cleanup and simplification; improved docstrings 2025-08-06 10:17:48 -05:00
Aria a4ab8e1b06 First pass at extending scale function to include notes 2025-08-06 00:45:01 -05:00
daslyfe a26559fd3d Merge branch 'main' into supradough 2025-07-22 06:35:59 +02:00
Felix Roos 5632d83afb fix: supradough onTrigger after breaking change 2025-07-16 02:21:39 +02:00
Felix Roos a32473d570 Merge remote-tracking branch 'origin/main' into supradough 2025-07-15 22:26:46 +02:00
dudymas e87bb4376f Merge branch 'main' into docs/xen/added-tune 2025-07-05 20:28:09 +02:00
dudymas 0722cf7ded feat(xen/tunejs): support sending a custom tuning 2025-07-05 13:43:42 -04:00
dudymas 37fde8f3e8 docs(website/learn/xen): discussed strumming 2025-07-05 13:43:09 -04:00
dudymas 8655c5b73c Merge branch 'main' into docs/xen/added-tune 2025-07-05 00:50:57 +02:00
dudymas 31c1450edc docs(website/learn/xen): add tune() examples 2025-07-04 18:50:21 -04:00
dudymas cbb4207bab chore(docker): basic support added 2025-07-04 18:50:21 -04:00
anecondev 5efcd14f40 Update website/src/pages/learn/code.mdx
Correct "appended" to "prepended"
2025-06-18 17:07:13 +02:00
froos 8051e56d13 Merge branch 'main' into supradough 2025-06-14 00:35:52 +02:00
Felix Roos 239987af0b snapshot 2025-06-12 10:23:06 +02:00
Felix Roos 8363ba5a41 format 2025-06-12 10:22:35 +02:00
Felix Roos 30f7127a92 Merge remote-tracking branch 'origin/main' into supradough 2025-06-12 10:22:08 +02:00
Felix Roos 14193abe03 improve export: add stereo + measure performance + add bigger pattern for testing 2025-06-12 02:26:50 +02:00
Felix Roos 3caf08e199 up latency 2025-06-12 02:26:08 +02:00
Felix Roos 5cbd38a3e8 improve filter performance making it accept hz directly 2025-06-12 02:25:46 +02:00
Felix Roos bbd7ed0f27 lazy sample loading 2025-06-12 02:25:04 +02:00
Jade (Rose) Rowland 43504d16b6 add chorus 2025-06-10 16:03:42 +02:00
Jade (Rose) Rowland 7e8206dcbc pitched delay 2025-06-10 12:31:21 +02:00
Felix Roos 3963b807eb fix: calling doughsamples multiple times 2025-06-09 22:32:41 +02:00
Felix Roos e99adffc56 vibrato 2025-06-09 22:29:35 +02:00
Felix Roos c9f494d865 fix: node export 2025-06-09 22:10:09 +02:00
Felix Roos e8254735bb apply speed to synths and samples + add normalize flag (for fit) 2025-06-09 19:35:50 +02:00
Jade (Rose) Rowland 3d30007756 supersaw 2025-06-09 15:55:44 +02:00
Jade (Rose) Rowland aeaed50446 gaincurve 2025-06-09 15:18:47 +02:00
Felix Roos 3ac0fdf619 rudimentary sample map loading (doughsamples) 2025-06-09 11:07:57 +02:00
Felix Roos d1b6b6be85 fix: hpf 2025-06-09 09:54:24 +02:00
Felix Roos 1081fc69e9 refactor: stereo sample support + channelwise fx 2025-06-09 09:52:29 +02:00
Felix Roos 73ef43dd2c move things around in init
+ shorten things
2025-06-08 22:56:55 +02:00
Jade (Rose) Rowland 181e51ef0e adjustable envelope curves 2025-06-08 20:26:20 +02:00
Jade (Rose) Rowland 1167fdb910 Merge branch 'supradough' of https://github.com/tidalcycles/strudel into supradough 2025-06-08 19:25:47 +02:00
Jade (Rose) Rowland ec9109dd89 exponential lerp 2025-06-08 19:24:55 +02:00
Felix Roos 55aef18851 buffers can now be repitched (still with aliasing) 2025-06-08 14:43:23 +02:00
Felix Roos 356d4360ec fix: scheduling + sample playback 2025-06-08 09:05:14 +02:00
Felix Roos 0f2aa9569b half working samples 2025-06-08 02:04:49 +02:00
Felix Roos ac7ee376e5 pitch envelope 2025-06-08 00:43:58 +02:00
Felix Roos ab052ce1ac fix: polyblepped sawtooth + fm = death 2025-06-07 23:15:09 +02:00
Felix Roos 5988e3d3b4 fix: hot reloading for dough.mjs (see comment) 2025-06-07 22:42:48 +02:00
Felix Roos 2f2bf5bf29 fix: note2midi for notes without octave 2025-06-07 22:36:54 +02:00
Felix Roos b000f2297d fm 2025-06-07 20:31:47 +02:00
Felix Roos 77463a199a add square alias for pulse 2025-06-07 13:26:43 +02:00
Felix Roos e54449f5f7 white brown pink noise + crackle alias for dust 2025-06-07 13:24:06 +02:00
Felix Roos d743570546 set delay time and feedback from voice 2025-06-07 13:12:35 +02:00
Felix Roos 1dac69635d feedback delay (still hard coded feedback / time values) 2025-06-07 10:40:34 +02:00
Felix Roos 49f38aeddb remove log 2025-06-06 16:26:17 +02:00
Felix Roos 43c05cb504 move supradough to separate package 2025-06-06 16:17:05 +02:00
Felix Roos ccc90d547c ignore dough-export to fix checks 2025-06-06 15:47:19 +02:00
Felix Roos 998a010247 better filter envelopes 2025-06-06 11:21:01 +02:00
Felix Roos e4cd5dc552 rough filter envelope implementation 2025-06-06 10:55:38 +02:00
Felix Roos 6cf0b6f651 hpf + bpf 2025-06-06 10:12:49 +02:00
Felix Roos 794bf86904 distort 2025-06-06 09:56:41 +02:00
Felix Roos 9ed690ce45 pulse osc + velocity 2025-06-06 09:37:40 +02:00
Felix Roos 623bf93368 panning 2025-06-06 08:50:02 +02:00
Felix Roos 0cc18a6468 crush (might be wrong) 2025-06-06 08:27:47 +02:00
Felix Roos 87598e10d1 coarse 2025-06-06 08:17:20 +02:00
Felix Roos ccec7e725a poc: render pattern audio with node script 2025-06-05 11:04:55 +02:00
Felix Roos f0e6c5483c handle invalid spawns + remove logs 2025-06-05 10:30:53 +02:00
Felix Roos 2cbd35f6d2 refactor: make webaudio glue as minimal as possible 2025-06-05 10:26:14 +02:00
Felix Roos 930dabe4d6 dough as a singleton worklet with voice allocation 2025-06-05 09:54:44 +02:00
Jade (Rose) Rowland ff4b7ca171 fix volume 2025-06-04 01:08:36 +01:00
Jade (Rose) Rowland 5129fa6677 fix click 2025-06-04 00:48:32 +01:00
Felix Roos 5336569c25 proper lpf mapping + remove redundant worklet params, 2025-06-01 21:20:28 +02:00
Felix Roos 483843bd0a begin supradough 2025-06-01 18:06:36 +02:00
68 changed files with 4468 additions and 677 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
name: Strudel tests
on: [push]
on: [push, pull_request]
jobs:
build:
@@ -19,7 +19,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache: "pnpm"
- run: pnpm install
- run: pnpm run format-check
- run: pnpm run lint
+21
View File
@@ -0,0 +1,21 @@
FROM node:24
WORKDIR /app
RUN npm install pnpm --global
COPY pnpm-workspace.yaml ./
COPY package.json pnpm-lock.yaml ./
COPY packages/ ./packages/
COPY examples/ ./examples/
RUN mkdir -p website/public
COPY website/package.json ./website/
RUN pnpm install
COPY . .
EXPOSE 4321
CMD ["pnpm", "dev"]
+1
View File
@@ -42,6 +42,7 @@ export default [
'**/hydra.mjs',
'**/jsdoc-synonyms.js',
'packages/hs2js/src/hs2js.mjs',
'packages/supradough/dough-export.mjs',
'**/samples',
],
},
+40 -11
View File
@@ -54,11 +54,12 @@ const buildExamples = (examples) =>
`
: '';
export const Autocomplete = ({ doc, label }) =>
export const Autocomplete = (doc) =>
h`
<div class="autocomplete-info-container">
<div class="autocomplete-info-tooltip">
<h3 class="autocomplete-info-function-name">${label || getDocLabel(doc)}</h3>
<h3 class="autocomplete-info-function-name">${getDocLabel(doc)}</h3>
${doc.synonyms_text ? `<div class="autocomplete-info-function-synonyms">Synonyms: ${doc.synonyms_text}</div>` : ''}
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
${buildParamsList(doc.params)}
${buildExamples(doc.examples)}
@@ -74,15 +75,43 @@ const isValidDoc = (doc) => {
const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
const jsdocCompletions = jsdoc.docs
.filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc))
// https://codemirror.net/docs/ref/#autocomplete.Completion
.map((doc) => ({
label: getDocLabel(doc),
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
info: () => Autocomplete({ doc }),
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
}));
export const getSynonymDoc = (doc, synonym) => {
const synonyms = doc.synonyms || [];
const docLabel = getDocLabel(doc);
// Swap `doc.name` in for `s` in the list of synonyms
const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
return {
...doc,
name: synonym,
longname: synonym,
synonyms: synonymsWithDoc,
synonyms_text: synonymsWithDoc.join(', '),
};
};
const jsdocCompletions = (() => {
const seen = new Set(); // avoid repetition
const completions = [];
for (const doc of jsdoc.docs) {
if (!isValidDoc(doc) || hasExcludedTags(doc)) continue;
const docLabel = getDocLabel(doc);
// Remove duplicates
const synonyms = doc.synonyms || [];
let labels = [docLabel, ...synonyms];
for (const label of labels) {
// https://codemirror.net/docs/ref/#autocomplete.Completion
if (label && !seen.has(label)) {
seen.add(label);
completions.push({
label,
info: () => Autocomplete(getSynonymDoc(doc, label)),
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
});
}
}
}
return completions;
})();
export const strudelAutocomplete = (context) => {
const word = context.matchBefore(/\w*/);
+3 -2
View File
@@ -1,6 +1,7 @@
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
export let html = (string) => {
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
const template = document.createElement('template');
template.innerHTML = string.trim();
return template.content.childNodes;
};
let parseChunk = (chunk) => {
if (Array.isArray(chunk)) return chunk.flat().join('');
+5 -4
View File
@@ -1,6 +1,6 @@
import { hoverTooltip } from '@codemirror/view';
import jsdoc from '../../doc.json';
import { Autocomplete } from './autocomplete.mjs';
import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
const getDocLabel = (doc) => doc.name || doc.longname;
@@ -52,10 +52,11 @@ export const strudelTooltip = hoverTooltip(
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
if (!entry) {
// Try for synonyms
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
if (!entry) {
const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
if (!doc) {
return null;
}
entry = getSynonymDoc(doc, word);
}
return {
@@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip(
create(view) {
let dom = document.createElement('div');
dom.className = 'strudel-tooltip';
const ac = Autocomplete({ doc: entry, label: word });
const ac = Autocomplete(entry);
dom.appendChild(ac);
return { dom };
},
+343 -31
View File
@@ -87,10 +87,244 @@ export function registerControl(names, ...aliases) {
*/
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
/**
* Position in the wavetable of the wavetable oscillator
*
* @name wt
* @param {number | Pattern} position Position in the wavetable from 0 to 1
* @synonyms wavetablePosition
* @example
* s("squelch").bank("wt_digital").seg(8).note("F1").wt("0 0.25 0.5 0.75 1")
*/
export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePosition');
/**
* Amount of envelope applied wavetable oscillator's position envelope
*
* @name wtenv
* @param {number | Pattern} amount between 0 and 1
*/
export const { wtenv } = registerControl('wtenv');
/**
* Attack time of the wavetable oscillator's position envelope
*
* @name wtattack
* @synonyms wtatt
* @param {number | Pattern} time attack time in seconds
*/
export const { wtattack, wtatt } = registerControl('wtattack', 'wtatt');
/**
* Decay time of the wavetable oscillator's position envelope
*
* @name wtdecay
* @synonyms wtdec
* @param {number | Pattern} time decay time in seconds
*/
export const { wtdecay, wtdec } = registerControl('wtdecay', 'wtdec');
/**
* Sustain time of the wavetable oscillator's position envelope
*
* @name wtsustain
* @synonyms wtsus
* @param {number | Pattern} gain sustain level (0 to 1)
*/
export const { wtsustain, wtsus } = registerControl('wtsustain', 'wtsus');
/**
* Release time of the wavetable oscillator's position envelope
*
* @name wtrelease
* @synonyms wtrel
* @param {number | Pattern} time release time in seconds
*/
export const { wtrelease, wtrel } = registerControl('wtrelease', 'wtrel');
/**
* Rate of the LFO for the wavetable oscillator's position
*
* @name wtrate
* @param {number | Pattern} rate rate in hertz
*/
export const { wtrate } = registerControl('wtrate');
/**
* cycle synced rate of the LFO for the wavetable oscillator's position
*
* @name wtsync
* @param {number | Pattern} rate rate in cycles
*/
export const { wtsync } = registerControl('wtsync');
/**
* Depth of the LFO for the wavetable oscillator's position
*
* @name wtdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { wtdepth } = registerControl('wtdepth');
/**
* Shape of the LFO for the wavetable oscillator's position
*
* @name wtshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { wtshape } = registerControl('wtshape');
/**
* DC offset of the LFO for the wavetable oscillator's position
*
* @name wtdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { wtdc } = registerControl('wtdc');
/**
* Skew of the LFO for the wavetable oscillator's position
*
* @name wtskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { wtskew } = registerControl('wtskew');
/**
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator
*
* @name warp
* @param {number | Pattern} amount Warp of the wavetable from 0 to 1
* @synonyms wavetableWarp
* @example
* s("basique").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
* .warpmode("spin")
*/
export const { warp, wavetableWarp } = registerControl('warp', 'wavetableWarp');
/**
* Attack time of the wavetable oscillator's warp envelope
*
* @name warpattack
* @synonyms warpatt
* @param {number | Pattern} time attack time in seconds
*/
export const { warpattack, warpatt } = registerControl('warpattack', 'warpatt');
/**
* Decay time of the wavetable oscillator's warp envelope
*
* @name warpdecay
* @synonyms warpdec
* @param {number | Pattern} time decay time in seconds
*/
export const { warpdecay, warpdec } = registerControl('warpdecay', 'warpdec');
/**
* Sustain time of the wavetable oscillator's warp envelope
*
* @name warpsustain
* @synonyms warpsus
* @param {number | Pattern} gain sustain level (0 to 1)
*/
export const { warpsustain, warpsus } = registerControl('warpsustain', 'warpsus');
/**
* Release time of the wavetable oscillator's warp envelope
*
* @name warprelease
* @synonyms warprel
* @param {number | Pattern} time release time in seconds
*/
export const { warprelease, warprel } = registerControl('warprelease', 'warprel');
/**
* Rate of the LFO for the wavetable oscillator's warp
*
* @name warprate
* @param {number | Pattern} rate rate in hertz
*/
export const { warprate } = registerControl('warprate');
/**
* Depth of the LFO for the wavetable oscillator's warp
*
* @name warpdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { warpdepth } = registerControl('warpdepth');
/**
* Shape of the LFO for the wavetable oscillator's warp
*
* @name warpshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { warpshape } = registerControl('warpshape');
/**
* DC offset of the LFO for the wavetable oscillator's warp
*
* @name warpdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { warpdc } = registerControl('warpdc');
/**
* Skew of the LFO for the wavetable oscillator's warp
*
* @name warpskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { warpskew } = registerControl('warpskew');
/**
* Type of warp (alteration of the waveform) to apply to the wavetable oscillator.
*
* The current options are: none, asym, bendp, bendm, bendmp, sync, quant, fold, pwm, orbit,
* spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip
*
* @name warpmode
* @param {number | string | Pattern} mode Warp mode
* @synonyms wavetableWarpMode
* @example
* s("morgana").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
* .warpmode("<asym bendp spin logistic sync wormhole brownian>*2")
*
*/
export const { warpmode, wavetableWarpMode } = registerControl('warpmode', 'wavetableWarpMode');
/**
* Amount of randomness of the initial phase of the wavetable oscillator.
*
* @name wtphaserand
* @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random)
* @synonyms wavetablePhaseRand
* @example
* s("basique").bank("wt_digital").seg(16).wtphaserand("<0 1>")
*
*/
export const { wtphaserand, wavetablePhaseRand } = registerControl('wtphaserand', 'wavetablePhaseRand');
/**
* Amount of envelope applied wavetable oscillator's position envelope
*
* @name warpenv
* @param {number | Pattern} amount between 0 and 1
*/
export const { warpenv } = registerControl('warpenv');
/**
* cycle synced rate of the LFO for the wavetable warp position
*
* @name warpsync
* @param {number | Pattern} rate rate in cycles
*/
export const { warpsync } = registerControl('warpsync');
/**
* Define a custom webaudio node to use as a sound source.
*
* @name source
* @synonyms src
* @param {function} getSource
* @synonyms src
*
@@ -113,7 +347,7 @@ export const { n } = registerControl('n');
*
* - a letter (a-g or A-G)
* - optional accidentals (b or #)
* - optional octave number (0-9). Defaults to 3
* - optional (possibly negative) octave number (0-9). Defaults to 3
*
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
*
@@ -126,6 +360,8 @@ export const { n } = registerControl('n');
* note("c4 a4 f4 e4")
* @example
* note("60 69 65 64")
* @example
* note("fbb1 a#0 cbbb-1 e##-2").sound("saw")
*/
export const { note } = registerControl(['note', 'n']);
@@ -141,8 +377,8 @@ export const { note } = registerControl(['note', 'n']);
*/
export const { accelerate } = registerControl('accelerate');
/**
*
* Sets the velocity from 0 to 1. Is multiplied together with gain.
*
* @name velocity
* @example
* s("hh*8")
@@ -254,7 +490,7 @@ export const { fmenv } = registerControl('fmenv');
export const { fmattack } = registerControl('fmattack');
/**
* waveform of the fm modulator
* Waveform of the fm modulator
*
* @name fmwave
* @param {number | Pattern} wave waveform
@@ -309,6 +545,17 @@ export const { fmvelocity } = registerControl('fmvelocity');
*/
export const { bank } = registerControl('bank');
/**
* mix control for the chorus effect
*
* @name chorus
* @param {string | Pattern} chorus mix amount between 0 and 1
* @example
* note("d d a# a").s("sawtooth").chorus(.5)
*
*/
export const { chorus } = registerControl('chorus');
// analyser node send amount 0 - 1 (used by scope)
export const { analyze } = registerControl('analyze');
// fftSize of analyser
@@ -377,7 +624,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
// ['bpq'],
export const { bandq, bpq } = registerControl('bandq', 'bpq');
/**
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
* A pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
*
* @memberof Pattern
* @name begin
@@ -438,7 +685,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb');
*/
export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
/**
* bit crusher effect.
* Bit crusher effect.
*
* @name crush
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
@@ -449,7 +696,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
// ['clhatdecay'],
export const { crush } = registerControl('crush');
/**
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
* Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
*
* @name coarse
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
@@ -460,7 +707,7 @@ export const { crush } = registerControl('crush');
export const { coarse } = registerControl('coarse');
/**
* modulate the amplitude of a sound with a continuous waveform
* Modulate the amplitude of a sound with a continuous waveform
*
* @name tremolo
* @synonyms trem
@@ -472,7 +719,7 @@ export const { coarse } = registerControl('coarse');
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
/**
* modulate the amplitude of a sound with a continuous waveform
* Modulate the amplitude of a sound with a continuous waveform
*
* @name tremolosync
* @synonyms tremsync
@@ -487,7 +734,7 @@ export const { tremolosync } = registerControl(
);
/**
* depth of amplitude modulation
* Depth of amplitude modulation
*
* @name tremolodepth
* @synonyms tremdepth
@@ -498,7 +745,7 @@ export const { tremolosync } = registerControl(
*/
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
/**
* alter the shape of the modulation waveform
* Alter the shape of the modulation waveform
*
* @name tremoloskew
* @synonyms tremskew
@@ -510,7 +757,7 @@ export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
/**
* alter the phase of the modulation waveform
* Alter the phase of the modulation waveform
*
* @name tremolophase
* @synonyms tremphase
@@ -522,9 +769,10 @@ export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
/**
* shape of amplitude modulation
* Shape of amplitude modulation
*
* @name tremoloshape
* @synonyms tremshape
* @param {number | Pattern} shape tri | square | sine | saw | ramp
* @example
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
@@ -532,7 +780,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
*/
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
/**
* filter overdrive for supported filter types
* Filter overdrive for supported filter types
*
* @name drive
* @param {number | Pattern} amount
@@ -540,44 +788,94 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
*
*/
export const { drive } = registerControl('drive');
/**
* modulate the amplitude of an orbit to create a "sidechain" like effect
* Modulate the amplitude of an orbit to create a "sidechain" like effect.
*
* Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")`
*
* @name duckorbit
* @synonyms duck
* @param {number | Pattern} orbit target orbit
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
* $: s("hh*16").orbit(3)
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth(1)
*
*/
export const { duck } = registerControl('duckorbit', 'duck');
/**
* the amount of ducking applied to target orbit
* The amount of ducking applied to target orbit
*
* Can vary across orbits with the ':' mininotation, e.g. `duckdepth("0.3:0.1")`.
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckdepth
* @param {number | Pattern} depth depth of modulation from 0 to 1
* @example
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>"))
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
* $: s("hh*16").orbit(3)
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth("1:0.5")
*
*/
export const { duckdepth } = registerControl('duckdepth');
/**
* the attack time of the duck effect
* The time required for the ducked signal(s) to reach their lowest volume.
* Can be used to prevent clicking or for creative rhythmic effects.
*
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckonset
* @synonyms duckons
*
* @param {number | Pattern} time The onset time in seconds
* @example
* // Clicks
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
* duckerWithClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0).postgain(0)
* @example
* // No clicks
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
* duckerWithoutClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0.01).postgain(0)
* @example
* // Rhythmic
* noise: s("pink").distort("2:1").orbit(4) // used rhythmically with 0.3 onset below
* hhat: s("hh*16").orbit(7)
* ducker: s("bd*4").bank("tr909").duckorbit("4:7").duckonset("0.3:0.003").duckattack(0.25)
*
*/
export const { duckonset } = registerControl('duckonset', 'duckons');
/**
* The time required for the ducked signal(s) to return to their normal volume.
*
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckattack
* @param {number | Pattern} time
* @synonyms duckatt
*
* @param {number | Pattern} time The attack time in seconds
* @example
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1))
* sound: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1)
* @example
* moreduck: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
* lessduck: s("hh*16").orbit(5)
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1")
*
*/
export const { duckattack } = registerControl('duckattack', 'duckatt');
export const { drive } = registerControl('drive');
/**
* Create byte beats with custom expressions
*
@@ -618,7 +916,7 @@ export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime',
export const { channels, ch } = registerControl('channels', 'ch');
/**
* controls the pulsewidth of the pulse oscillator
* Controls the pulsewidth of the pulse oscillator
*
* @name pw
* @param {number | Pattern} pulsewidth
@@ -630,7 +928,7 @@ export const { channels, ch } = registerControl('channels', 'ch');
export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
/**
* controls the lfo rate for the pulsewidth of the pulse oscillator
* Controls the lfo rate for the pulsewidth of the pulse oscillator
*
* @name pwrate
* @param {number | Pattern} rate
@@ -642,7 +940,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
export const { pwrate } = registerControl('pwrate');
/**
* controls the lfo sweep for the pulsewidth of the pulse oscillator
* Controls the lfo sweep for the pulsewidth of the pulse oscillator
*
* @name pwsweep
* @param {number | Pattern} sweep
@@ -683,7 +981,7 @@ export const { phaserrate, ph, phaser } = registerControl(
export const { phasersweep, phs } = registerControl('phasersweep', 'phs');
/**
* The center frequency of the phaser in HZ. Defaults to 1000
* The center frequency of the phaser in HZ. Defaults to 1000
*
* @name phasercenter
* @synonyms phc
@@ -700,7 +998,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
* The amount the signal is affected by the phaser effect. Defaults to 0.75
*
* @name phaserdepth
* @synonyms phd
* @synonyms phd, phasdp
* @param {number | Pattern} depth number between 0 and 1
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
@@ -711,7 +1009,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp');
/**
* choose the channel the pattern is sent to in superdirt
* Choose the channel the pattern is sent to in superdirt
*
* @name channel
* @param {number | Pattern} channel channel number
@@ -1063,7 +1361,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq');
* @name djf
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
* @example
* n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc()
* n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>")
*
*/
export const { djf } = registerControl('djf');
@@ -1097,14 +1395,27 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
*
*/
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
/**
* Sets the level of the signal that is fed back into the delay.
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>")
*
*/
export const { delayspeed } = registerControl('delayspeed');
/**
* Sets the time of the delay effect.
*
* @name delaytime
* @param {number | Pattern} seconds between 0 and Infinity
* @name delayspeed
* @param {number | Pattern} delayspeed controls the pitch of the delay feedback
* @synonyms delayt, dt
* @example
* s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
*
*/
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
@@ -1182,6 +1493,7 @@ export const { dry } = registerControl('dry');
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
*
* @name fadeTime
* @synonyms fadeOutTime
* @param {number | Pattern} time between 0 and 1
* @example
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
+1 -1
View File
@@ -57,7 +57,7 @@ export class Cyclist {
}
// query the pattern for events
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
haps.forEach((hap) => {
if (hap.hasOnset()) {
+2
View File
@@ -126,6 +126,8 @@ export const lcm = (...fractions) => {
);
};
export const isFraction = (x) => x instanceof Fraction;
fraction._original = Fraction;
export default fraction;
+3 -2
View File
@@ -5,8 +5,9 @@ let debounce = 1000,
lastTime;
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
if (process.env.NODE_ENV === 'development') {
console.error(e);
}
logger(`[${origin}] error: ${e.message}`);
}
+1 -2
View File
@@ -38,8 +38,7 @@ export class NeoCyclist {
if (this.started === false) {
return;
}
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' });
haps.forEach((hap) => {
if (hap.hasOnset()) {
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
+78 -27
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import TimeSpan from './timespan.mjs';
import Fraction, { lcm } from './fraction.mjs';
import Fraction, { isFraction, lcm } from './fraction.mjs';
import Hap from './hap.mjs';
import State from './state.mjs';
import { unionWithObj } from './value.mjs';
@@ -98,10 +98,7 @@ export class Pattern {
// runs func on query state
withState(func) {
return this.withHaps((haps, state) => {
func(state);
return haps;
});
return new Pattern((state) => this.query(func(state)));
}
/**
@@ -966,26 +963,13 @@ export const arp = register(
false,
);
/*
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
* @name weave
* @memberof Pattern
* @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd"))
* @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd"))
addToPrototype('weave', function (t, ...pats) {
return this.weaveWith(t, ...pats.map((x) => set.out(x)));
});
*/
/*
* Like 'weave', but accepts functions rather than patterns, which are applied to the pattern.
* @name weaveWith
* @memberof Pattern
*/
addToPrototype('weaveWith', function (t, ...funcs) {
const pat = this;
export const weaveWith = register('weaveWith', function (t, funcs, pat) {
const l = funcs.length;
t = Fraction(t);
if (l == 0) {
@@ -993,13 +977,27 @@ addToPrototype('weaveWith', function (t, ...funcs) {
}
return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t);
});
*/
/*
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
* @name weave
* @memberof Pattern
* @example pan(saw).weave(4, [s("bd(3,8)"), s("~ sd")])
* @example n("0 1 2 3 4 5 6 7").weave(8, [s("bd(3,8)"), s("~ sd")])
*/
export const weave = register('weave', function (t, pats, pat) {
return pat.weaveWith(
t,
pats.map((x) => (y) => y.set.out(x)),
);
});
//////////////////////////////////////////////////////////////////////
// compose matrix functions
function _nonArrayObject(x) {
return !Array.isArray(x) && typeof x === 'object';
return !Array.isArray(x) && typeof x === 'object' && !isFraction(x);
}
function _composeOp(a, b, func) {
if (_nonArrayObject(a) || _nonArrayObject(b)) {
@@ -1246,7 +1244,8 @@ export const silence = gap(1);
/* Like silence, but with a 'steps' (relative duration) of 0 */
export const nothing = gap(0);
/** A discrete value that repeats once per cycle.
/**
* A discrete value that repeats once per cycle.
*
* @returns {Pattern}
* @example
@@ -1299,7 +1298,8 @@ export function sequenceP(pats) {
return result;
}
/** The given items are played at the same time at the same length.
/**
* The given items are played at the same time at the same length.
*
* @return {Pattern}
* @synonyms polyrhythm, pr
@@ -1382,11 +1382,11 @@ export function stackBy(by, ...pats) {
.setSteps(steps);
}
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
*
* synonyms: `cat`
/**
* Concatenation: combines a list of patterns, switching between them successively, one per cycle.
*
* @return {Pattern}
* @synonyms cat
* @example
* slowcat("e5", "b4", ["d5", "c5"])
*
@@ -2397,6 +2397,57 @@ export const stut = register('stut', function (times, feedback, time, pat) {
return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
});
export const applyN = register('applyN', function (n, func, p) {
let result = p;
for (let i = 0; i < n; i++) {
result = func(result);
}
return result;
});
/**
* The plyWith function repeats each event the given number of times, applying the given function to each event.\n
* @name plyWith
* @synonyms plywith
* @param {number} factor how many times to repeat
* @param {function} func function to apply, given the pattern
* @example
* "<0 [2 4]>"
* .plyWith(4, (p) => p.add(2))
* .scale("C:minor").note()
*/
export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) {
const result = pat
.fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor))
.squeezeJoin();
if (__steps) {
result._steps = Fraction(factor).mulmaybe(pat._steps);
}
return result;
});
/**
* The plyForEach function repeats each event the given number of times, applying the given function to each event.
* This version of ply uses the iteration index as an argument to the function, similar to echoWith.
* @name plyForEach
* @synonyms plyforeach
* @param {number} factor how many times to repeat
* @param {function} func function to apply, given the pattern and the iteration index
* @example
* "<0 [2 4]>"
* .plyForEach(4, (p,n) => p.add(n*2))
* .scale("C:minor").note()
*/
export const plyForEach = register(['plyForEach', 'plyforeach'], function (factor, func, pat) {
const result = pat
.fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor))
.squeezeJoin();
if (__steps) {
result._steps = Fraction(factor).mulmaybe(pat._steps);
}
return result;
});
/**
* Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played.
* @name iter
+21 -3
View File
@@ -74,6 +74,14 @@ export function repl({
return silence;
};
// helper to get a patternified pure value out
function unpure(pat) {
if (pat._Pattern) {
return pat.__pure;
}
return pat;
}
const setPattern = async (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
await scheduler.setPattern(pattern, autostart);
@@ -85,7 +93,10 @@ export function repl({
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
const setCps = (cps) => scheduler.setCps(cps);
const setCps = (cps) => {
scheduler.setCps(unpure(cps));
return silence;
};
/**
* Changes the global tempo to the given cycles per minute
@@ -97,7 +108,10 @@ export function repl({
* setcpm(140/4) // =140 bpm in 4/4
* $: s("bd*4,[- sd]*2").bank('tr707')
*/
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
const setCpm = (cpm) => {
scheduler.setCps(unpure(cpm) / 60);
return silence;
};
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
@@ -200,7 +214,10 @@ export function repl({
}
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
if (Object.keys(pPatterns).length) {
let patterns = Object.values(pPatterns);
let patterns = [];
for (const [key, value] of Object.entries(pPatterns)) {
patterns.push(value.withState((state) => state.setControls({ id: key })));
}
if (eachTransform) {
// Explicit lambda so only element (not index and array) are passed
patterns = patterns.map((x) => eachTransform(x));
@@ -214,6 +231,7 @@ export function repl({
pattern = allTransforms[i](pattern);
}
}
if (!isPattern(pattern)) {
const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
+2 -2
View File
@@ -19,9 +19,9 @@ export class State {
return this.setSpan(func(this.span));
}
// Returns new State with different controls
// Returns new State with added controls.
setControls(controls) {
return new State(this.span, controls);
return new State(this.span, { ...this.controls, ...controls });
}
}
+2 -2
View File
@@ -8,12 +8,12 @@ import { logger } from './logger.mjs';
// returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]?$/.test(name);
export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
if (!pc) {
return [];
}
+2 -2
View File
@@ -85,7 +85,7 @@ function evaluator(node, scope) {
let pat;
if (type === 'plain' && typeof variable !== 'undefined') {
// some function names are not patternable, so we skip reification here
if (['!', 'extend', '@', 'expand', 'square', 'angle'].includes(value)) {
if (['!', 'extend', '@', 'expand', 'square', 'angle', 'all', 'setcpm', 'setcps'].includes(value)) {
return variable;
}
pat = reify(variable);
@@ -108,7 +108,7 @@ export function mondo(code, offset = 0) {
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
}
let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
export let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
export const mondi = (str, offset) => {
const code = `[${str}]`;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mondo",
"version": "1.1.2",
"version": "1.1.4",
"description": "mondo notation for strudel",
"main": "mondough.mjs",
"type": "module",
+2 -2
View File
@@ -1,5 +1,5 @@
import { defineConfig } from 'vite';
//import { dependencies } from './package.json';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
@@ -12,7 +12,7 @@ export default defineConfig({
fileName: (ext) => ({ es: 'mondough.mjs' })[ext],
},
rollupOptions: {
// external: [...Object.keys(dependencies)],
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
+1 -1
View File
@@ -82,7 +82,7 @@ Pattern.prototype.mqtt = function (
cx.connect(props);
}
return this.withHap((hap) => {
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
const onTrigger = (hap, currentTime, cps, targetTime) => {
let msg_topic = topic;
if (!cx || !cx.isConnected()) {
return;
+27 -17
View File
@@ -4,21 +4,13 @@ OSC output for strudel patterns! Currently only tested with super collider / sup
## Usage
OSC will only work if you run the REPL locally + the OSC server besides it:
Assuming you have [node.js](https://nodejs.org/) installed, you can run the osc bridge server via:
From the project root:
```js
npm run repl
```sh
npx @strudel/osc
```
and in a seperate shell:
```js
npm run osc
```
This should give you
You should see something like:
```log
osc client running on port 57120
@@ -26,14 +18,32 @@ osc server running on port 57121
websocket server running on port 8080
```
Now open Supercollider (with the super dirt startup file)
### --port
Now open the REPL and type:
By default it will use port 57120 for the osc client, which is what [superdirt](https://github.com/musikinformatik/SuperDirt) uses. You can change it via the `--port` option:
```js
s("<bd sd> hh").osc()
```sh
npx @strudel/osc --port 7771 # classic dirt
```
or just [click here](https://strudel.cc/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
### --debug
To log all incoming osc messages, add the `--debug` flag:
```sh
npx @strudel/osc --debug
```
## Usage in Strudel
To test it in strudel, you have can use `all(osc)` to send all events through osc:
```js
$: s("bd*4")
all(osc)
```
[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D)
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
+2 -4
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import OSC from 'osc-js';
import { logger, parseNumeral, Pattern, isNote, noteToMidi, ClockCollator } from '@strudel/core';
import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core';
let connection; // Promise<OSC>
function connect() {
@@ -81,6 +81,4 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
* @memberof Pattern
* @returns Pattern
*/
Pattern.prototype.osc = function () {
return this.onTrigger(oscTrigger);
};
export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger));
+2 -1
View File
@@ -1,8 +1,9 @@
{
"name": "@strudel/osc",
"version": "1.2.4",
"version": "1.2.10",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"bin": "./server.js",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
+43 -2
View File
@@ -1,3 +1,5 @@
#!/usr/bin/env node
/*
server.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/server.js>
@@ -6,6 +8,19 @@ This program is free software: you can redistribute it and/or modify it under th
import OSC from 'osc-js';
const args = process.argv.slice(2);
function getArgValue(flag) {
const i = args.indexOf(flag);
if (i !== -1) {
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
if (nextIsFlag) return true;
return args[i + 1];
}
}
let udpClientPort = Number(getArgValue('--port')) || 57120;
let debug = Number(getArgValue('--debug')) || 0;
const config = {
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
udpServer: {
@@ -17,7 +32,7 @@ const config = {
},
udpClient: {
host: 'localhost', // @param {string} Hostname of udp client for messaging
port: 57120, // @param {number} Port of udp client for messaging
port: udpClientPort, // @param {number} Port of udp client for messaging
},
wsServer: {
host: 'localhost', // @param {string} Hostname of WebSocket server
@@ -27,8 +42,34 @@ const config = {
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
osc.open(); // start a WebSocket server on port 8080
if (debug) {
osc.on('*', (message) => {
const { address, args } = message;
let str = '';
for (let i = 0; i < args.length; i += 2) {
str += `${args[i]}: ${args[i + 1]} `;
}
console.log(`${address} ${str}`);
});
}
osc.on('error', (message) => {
if (message.toString().includes('EADDRINUSE')) {
console.log(`------ ERROR -------
a server is already running on port 57121! to stop it:
1. run "lsof -ti :57121 | xargs kill -9" (macos / linux)
2. re-run the osc server
`);
} else {
console.log(message);
}
});
osc.open();
console.log('osc client running on port', config.udpClient.port);
console.log('osc server running on port', config.udpServer.port);
console.log('websocket server running on port', config.wsServer.port);
if (debug) {
console.log('debug logs enabled. incoming messages will appear below');
}
+98 -3
View File
@@ -1,5 +1,5 @@
import { getAudioContext } from './superdough.mjs';
import { clamp, nanFallback } from './util.mjs';
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
import { getNoiseBuffer } from './noise.mjs';
export const noises = ['pink', 'white', 'brown', 'crackle'];
@@ -10,6 +10,13 @@ export function gainNode(value) {
return node;
}
export function effectSend(input, effect, wet) {
const send = gainNode(wet);
input.connect(send);
send.connect(effect);
return send;
}
const getSlope = (y1, y2, x1, x2) => {
const denom = x2 - x1;
if (denom === 0) {
@@ -21,7 +28,9 @@ const getSlope = (y1, y2, x1, x2) => {
export function getWorklet(ac, processor, params, config) {
const node = new AudioWorkletNode(ac, processor, config);
Object.entries(params).forEach(([key, value]) => {
node.parameters.get(key).value = value;
if (value !== undefined) {
node.parameters.get(key).value = value;
}
});
return node;
}
@@ -88,6 +97,35 @@ export const getParamADSR = (
param[ramp](min, end + release);
};
function getModulationShapeInput(val) {
if (typeof val === 'number') {
return val % 5;
}
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
}
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
const lfoprops = {
frequency: 1,
depth,
skew: 0.5,
phaseoffset: 0,
time: begin,
begin,
end,
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
}
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
const options = {
threshold: threshold ?? -3,
@@ -115,6 +153,41 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
};
// helper utility for applying standard modulators to a parameter
export function applyParameterModulators(audioContext, param, start, end, envelopeValues, lfoValues) {
let { amount, offset, defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues;
if (amount == null) {
const hasADSRParams = values.some((p) => p != null);
amount = hasADSRParams ? defaultAmount : 0;
}
const min = offset ?? 0;
const max = amount + min;
const diff = Math.abs(max - min);
if (diff) {
const [attack, decay, sustain, release] = getADSRValues(values, curve, defaultValues);
getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve);
}
let lfo;
let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues;
if (depth == null) {
const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null);
depth = hasLFOParams ? defaultDepth : 0;
}
if (depth) {
lfo = getLfo(audioContext, start, end, {
depth,
dcoffset,
...getLfoInputs,
});
lfo.connect(param);
}
return { lfo, disconnect: () => lfo?.disconnect() };
}
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) {
const curve = 'exponential';
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
@@ -174,7 +247,7 @@ let curves = ['linear', 'exponential'];
export function getPitchEnvelope(param, value, t, holdEnd) {
// envelope is active when any of these values is set
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
if (!hasEnvelope) {
if (hasEnvelope === undefined) {
return;
}
const penv = nanFallback(value.penv, 1, true);
@@ -307,3 +380,25 @@ export function applyFM(param, value, begin) {
}
return { stop };
}
export const getFrequencyFromValue = (value, defaultNote = 36) => {
let { note, freq } = value;
note = note || defaultNote;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
return Number(freq);
};
export const destroyAudioWorkletNode = (node) => {
if (node == null) {
return;
}
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
};
+1
View File
@@ -11,3 +11,4 @@ export * from './synth.mjs';
export * from './zzfx.mjs';
export * from './logger.mjs';
export * from './dspworklet.mjs';
export * from './wavetable.mjs';
+4 -3
View File
@@ -1,8 +1,9 @@
let log = (msg) => console.log(msg);
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
export function errorLogger(e, origin = 'superdough') {
if (process.env.NODE_ENV === 'development') {
console.error(e);
}
logger(`[${origin}] error: ${e.message}`);
}
+77 -82
View File
@@ -1,5 +1,5 @@
import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs';
import { getAudioContext, registerSound } from './index.mjs';
import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs';
import { getAudioContext, registerSound, registerWaveTable } from './index.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { logger } from './logger.mjs';
@@ -22,39 +22,16 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u];
}
// deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
export function getSampleInfo(hapValue, bank) {
const { s, n = 0, speed = 1.0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
let sampleUrl;
let index = 0;
if (Array.isArray(bank)) {
index = getSoundIndex(n, bank.length);
sampleUrl = bank[index];
} else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
const closest = Object.keys(bank)
.filter((k) => !k.startsWith('_'))
.reduce(
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
null,
);
transpose = -midiDiff(closest); // semitones to repitch
index = getSoundIndex(n, bank[closest].length);
sampleUrl = bank[closest][index];
}
const label = `${s}:${index}`;
const { speed = 1.0 } = hapValue;
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
return { transpose, sampleUrl, index, midi, label, playbackRate };
return { transpose, url, index, midi, label, playbackRate };
}
// takes hapValue and returns buffer + playbackRate.
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
if (resolveUrl) {
sampleUrl = await resolveUrl(sampleUrl);
}
@@ -79,14 +56,14 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
bufferSource.buffer = buffer;
bufferSource.playbackRate.value = playbackRate;
const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferSource.buffer.duration;
const loop = s.startsWith('wt_') ? 1 : hapValue.loop;
const loop = hapValue.loop;
if (loop) {
bufferSource.loop = true;
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
@@ -196,6 +173,52 @@ function getSamplesPrefixHandler(url) {
return;
}
export async function fetchSampleMap(url) {
// check if custom prefix handler
const handler = getSamplesPrefixHandler(url);
if (handler) {
return handler(url);
}
url = resolveSpecialPaths(url);
if (url.startsWith('github:')) {
url = githubPath(url, 'strudel.json');
}
if (url.startsWith('local:')) {
url = `http://localhost:5432`;
}
if (url.startsWith('shabda:')) {
let [_, path] = url.split('shabda:');
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
}
if (url.startsWith('shabda/speech')) {
let [_, path] = url.split('shabda/speech');
path = path.startsWith('/') ? path.substring(1) : path;
let [params, words] = path.split(':');
let gender = 'f';
let language = 'en-GB';
if (params) {
[language, gender] = params.split('/');
}
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
const base = url.split('/').slice(0, -1).join('/');
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
const json = await fetch(url)
.then((res) => res.json())
.catch((error) => {
console.error(error);
throw new Error(`error loading "${url}"`);
});
return [json, json._base || base];
}
/**
* Loads a collection of samples to use with `s`
* @example
@@ -217,61 +240,16 @@ function getSamplesPrefixHandler(url) {
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
if (typeof sampleMap === 'string') {
// check if custom prefix handler
const handler = getSamplesPrefixHandler(sampleMap);
if (handler) {
return handler(sampleMap);
}
sampleMap = resolveSpecialPaths(sampleMap);
if (sampleMap.startsWith('github:')) {
sampleMap = githubPath(sampleMap, 'strudel.json');
}
if (sampleMap.startsWith('local:')) {
sampleMap = `http://localhost:5432`;
}
if (sampleMap.startsWith('shabda:')) {
let [_, path] = sampleMap.split('shabda:');
sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`;
}
if (sampleMap.startsWith('shabda/speech')) {
let [_, path] = sampleMap.split('shabda/speech');
path = path.startsWith('/') ? path.substring(1) : path;
let [params, words] = path.split(':');
let gender = 'f';
let language = 'en-GB';
if (params) {
[language, gender] = params.split('/');
}
sampleMap = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
const base = sampleMap.split('/').slice(0, -1).join('/');
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
return fetch(sampleMap)
.then((res) => res.json())
.then((json) => samples(json, baseUrl || json._base || base, options))
.catch((error) => {
console.error(error);
throw new Error(`error loading "${sampleMap}"`);
});
const [json, base] = await fetchSampleMap(sampleMap);
return samples(json, baseUrl || base, options);
}
const { prebake, tag } = options;
processSampleMap(
sampleMap,
(key, bank) =>
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
type: 'sample',
samples: bank,
baseUrl,
prebake,
tag,
}),
(key, bank) => {
registerSampleSource(key, bank, { baseUrl, prebake, tag });
},
baseUrl,
);
};
@@ -361,3 +339,20 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
return handle;
}
function registerSample(key, bank, params) {
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
type: 'sample',
samples: bank,
...params,
});
}
export function registerSampleSource(key, bank, params) {
const isWavetable = key.startsWith('wt_');
if (isWavetable) {
registerWaveTable(key, bank, params);
} else {
registerSample(key, bank, params);
}
}
+60 -189
View File
@@ -7,12 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs';
import { createFilter, gainNode, getCompressor, getLfo, getWorklet, effectSend } from './helpers.mjs';
import { map } from 'nanostores';
import { logger, errorLogger } from './logger.mjs';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
import { SuperdoughAudioController } from './superdoughoutput.mjs';
export const DEFAULT_MAX_POLYPHONY = 128;
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
@@ -28,13 +29,6 @@ export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
function getModulationShapeInput(val) {
if (typeof val === 'number') {
return val % 5;
}
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
}
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
@@ -113,6 +107,19 @@ export async function aliasBank(...args) {
}
}
/**
* Register an alias for a sound.
* @param {string} original - The original sound name
* @param {string} alias - The alias to use for the sound
*/
export function soundAlias(original, alias) {
if (getSound(original) == null) {
logger('soundAlias: original sound not found');
return;
}
soundMap.setKey(alias, getSound(original));
}
export function getSound(s) {
if (typeof s !== 'string') {
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
@@ -196,7 +203,7 @@ export const resetLoadedSounds = () => soundMap.set({});
let audioContext;
export const setDefaultAudioContext = () => {
audioContext = new AudioContext();
audioContext = new AudioContext({ latencyHint: 'playback' });
return audioContext;
};
@@ -212,11 +219,17 @@ export function getAudioContextCurrentTime() {
return getAudioContext().currentTime;
}
let externalWorklets = [];
export function registerWorklet(url) {
externalWorklets.push(url);
}
let workletsLoading;
function loadWorklets() {
if (!workletsLoading) {
const audioCtx = getAudioContext();
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
const allWorkletURLs = externalWorklets.concat([workletsUrl]);
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL)));
}
return workletsLoading;
@@ -282,88 +295,16 @@ export async function initAudioOnFirstClick(options) {
return audioReady;
}
const maxfeedback = 0.98;
let channelMerger, destinationGain;
//update the output channel configuration to match user's audio device
export function initializeAudioOutput() {
const audioContext = getAudioContext();
const maxChannelCount = audioContext.destination.maxChannelCount;
audioContext.destination.channelCount = maxChannelCount;
channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
destinationGain = new GainNode(audioContext);
channelMerger.connect(destinationGain);
destinationGain.connect(audioContext.destination);
let controller;
function getSuperdoughAudioController() {
if (controller == null) {
controller = new SuperdoughAudioController(getAudioContext());
}
return controller;
}
// input: AudioNode, channels: ?Array<int>
export const connectToDestination = (input, channels = [0, 1]) => {
const ctx = getAudioContext();
if (channelMerger == null) {
initializeAudioOutput();
}
//This upmix can be removed if correct channel counts are set throughout the app,
// and then strudel could theoretically support surround sound audio files
const stereoMix = new StereoPannerNode(ctx);
input.connect(stereoMix);
const splitter = new ChannelSplitterNode(ctx, {
numberOfOutputs: stereoMix.channelCount,
});
stereoMix.connect(splitter);
channels.forEach((ch, i) => {
splitter.connect(channelMerger, i % stereoMix.channelCount, ch % ctx.destination.channelCount);
});
};
export const panic = () => {
if (destinationGain == null) {
return;
}
destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
destinationGain = null;
channelMerger == null;
};
function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = clamp(delayfeedback, 0, 0.98);
if (!orbits[orbit].delayNode) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
connectToOrbit(dly, orbit);
orbits[orbit].delayNode = dly;
}
orbits[orbit].delayNode.delayTime.value !== delaytime &&
orbits[orbit].delayNode.delayTime.setValueAtTime(delaytime, t);
orbits[orbit].delayNode.feedback.value !== delayfeedback &&
orbits[orbit].delayNode.feedback.setValueAtTime(delayfeedback, t);
return orbits[orbit].delayNode;
}
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
const lfoprops = {
frequency: 1,
depth,
skew: 0.5,
phaseoffset: 0,
time: begin,
begin,
end,
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
export function connectToDestination(input, channels) {
const controller = getSuperdoughAudioController();
controller.output.connectToDestination(input, channels);
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
@@ -397,78 +338,6 @@ function getFilterType(ftype) {
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
}
//type orbit {
// gain: number,
// reverbNode: reverbNode
// delayNode: delayNode
//}
let orbits = {};
function connectToOrbit(node, orbit) {
if (orbits[orbit] == null) {
errorLogger(new Error('target orbit does not exist'), 'superdough');
}
node.connect(orbits[orbit].gain);
}
function setOrbit(audioContext, orbit, channels) {
if (orbits[orbit] == null) {
orbits[orbit] = {
gain: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }),
};
connectToDestination(orbits[orbit].gain, channels);
}
}
function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1) {
const targetArr = [targetOrbit].flat();
targetArr.forEach((target) => {
if (orbits[target] == null) {
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
return;
}
webAudioTimeout(
audioContext,
() => {
orbits[target].gain.gain.cancelScheduledValues(t);
const currVal = orbits[target].gain.gain.value;
orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t);
orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime));
},
0,
t - 0.01,
);
});
}
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) {
// If no reverb has been created for a given orbit, create one
if (!orbits[orbit].reverbNode) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
connectToOrbit(reverb, orbit);
orbits[orbit].reverbNode = reverb;
}
if (
hasChanged(duration, orbits[orbit].reverbNode.duration) ||
hasChanged(fade, orbits[orbit].reverbNode.fade) ||
hasChanged(lp, orbits[orbit].reverbNode.lp) ||
hasChanged(dim, orbits[orbit].reverbNode.dim) ||
hasChanged(irspeed, orbits[orbit].reverbNode.irspeed) ||
hasChanged(irbegin, orbits[orbit].reverbNode.irbegin) ||
orbits[orbit].reverbNode.ir !== ir
) {
// only regenerate when something has changed
// avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
}
return orbits[orbit].reverbNode;
}
export let analysers = {},
analysersData = {};
@@ -501,15 +370,8 @@ export function getAnalyzerData(type = 'time', id = 1) {
return analysersData[id];
}
function effectSend(input, effect, wet) {
const send = gainNode(wet);
input.connect(send);
send.connect(effect);
return send;
}
export function resetGlobalEffects() {
orbits = {};
controller?.reset();
analysers = {};
analysersData = {};
}
@@ -524,6 +386,7 @@ function mapChannelNumbers(channels) {
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
// new: t is always expected to be the absolute target onset time
const ac = getAudioContext();
const audioController = getSuperdoughAudioController();
let { stretch } = value;
if (stretch != null) {
@@ -563,8 +426,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
postgain = getDefaultValue('postgain'),
density = getDefaultValue('density'),
duckorbit,
duckonset,
duckattack,
duckdepth,
djf,
// filters
fanchor = getDefaultValue('fanchor'),
drive = 0.69,
@@ -641,10 +506,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
);
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
setOrbit(ac, orbit, channels, t, cycle, cps);
const orbitBus = audioController.getOrbit(orbit, channels);
if (duckorbit != null) {
duckOrbit(ac, duckorbit, t, duckattack, duckdepth);
audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth);
}
gain = applyGainCurve(nanFallback(gain, 1));
@@ -691,7 +555,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
audioNodes.forEach((n) => n?.disconnect());
activeSoundSources.delete(chainID);
};
const soundHandle = await onTrigger(t, value, onEnded);
const soundHandle = await onTrigger(t, value, onEnded, cps);
if (soundHandle) {
sourceNode = soundHandle.node;
@@ -789,6 +653,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
tremolo = cps * tremolosync;
}
if (value.wtPosSynced != null) {
value.wtPosRate /= cps;
}
if (value.wtWarpSynced != null) {
value.wtWarpRate /= cps;
}
if (tremolo !== undefined) {
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
@@ -834,14 +706,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
chain.push(post);
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delayNode = getDelay(orbit, delaytime, delayfeedback, t);
delaySend = effectSend(post, delayNode, delay);
audioNodes.push(delaySend);
orbitBus.getDelay(delaytime, delayfeedback, t);
orbitBus.sendDelay(post, delay);
}
// reverb
let reverbSend;
if (room > 0) {
let roomIR;
if (ir !== undefined) {
@@ -854,25 +723,27 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
roomIR = await loadBuffer(url, ac, ir, 0);
}
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend);
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
orbitBus.sendReverb(post, room);
}
if (djf != null) {
orbitBus.getDjf(djf, t);
}
// analyser
let analyserSend;
if (analyze) {
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
analyserSend = effectSend(post, analyserNode, 1);
const analyserSend = effectSend(post, analyserNode, 1);
audioNodes.push(analyserSend);
}
if (dry != null) {
dry = applyGainCurve(dry);
const dryGain = new GainNode(ac, { gain: dry });
chain.push(dryGain);
connectToOrbit(dryGain, orbit);
orbitBus.connectToOutput(dryGain);
} else {
connectToOrbit(post, orbit);
orbitBus.connectToOutput(post);
}
// connect chain elements together
+209
View File
@@ -0,0 +1,209 @@
import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
import { errorLogger } from './logger.mjs';
import { clamp } from './util.mjs';
let hasChanged = (now, before) => now !== undefined && now !== before;
export class Orbit {
reverbNode;
delayNode;
output;
summingNode;
djfNode;
audioContext;
constructor(audioContext) {
this.audioContext = audioContext;
this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
this.summingNode.connect(this.output);
}
disconnect() {
this.output.disconnect();
this.summingNode.disconnect();
this.delayNode?.disconnect();
this.reverbNode?.disconnect();
}
getDjf(value, t = 0) {
if (this.djfNode == null) {
this.djfNode = getWorklet(this.audioContext, 'djf-processor', { value });
this.summingNode.disconnect();
this.summingNode.connect(this.djfNode);
this.djfNode.connect(this.output);
}
const val = this.djfNode.parameters.get('value');
val.setValueAtTime(value, t);
}
getDelay(delaytime = 0, feedback = 0.5, t) {
const maxfeedback = 0.98;
if (feedback > maxfeedback) {
//logger(`feedback was clamped to ${maxfeedback} to save your ears`);
}
feedback = clamp(feedback, 0, 0.98);
if (this.delayNode == null) {
this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback);
this.delayNode.connect(this.summingNode);
this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed..
}
this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t);
this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t);
return this.delayNode;
}
getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) {
// If no reverb has been created for a given orbit, create one
if (this.reverbNode == null) {
this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
this.reverbNode.connect(this.summingNode);
}
if (
hasChanged(duration, this.reverbNode.duration) ||
hasChanged(fade, this.reverbNode.fade) ||
hasChanged(lp, this.reverbNode.lp) ||
hasChanged(dim, this.reverbNode.dim) ||
hasChanged(irspeed, this.reverbNode.irspeed) ||
hasChanged(irbegin, this.reverbNode.irbegin) ||
this.reverbNode.ir !== ir
) {
// only regenerate when something has changed
// avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
}
return this.reverbNode;
}
sendReverb(node, amount) {
effectSend(node, this.reverbNode, amount);
}
sendDelay(node, amount) {
effectSend(node, this.delayNode, amount);
}
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
const onset = onsettime;
const attack = Math.max(attacktime, 0.002);
const gainParam = this.output.gain;
webAudioTimeout(
this.audioContext,
() => {
const now = this.audioContext.currentTime;
// cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime
// on browsers which lack that method
const currVal = gainParam.value;
gainParam.cancelScheduledValues(now);
gainParam.setValueAtTime(currVal, now);
const t0 = Math.max(t, now); // guard against now > t
const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal);
gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset);
gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack);
},
0,
t - 0.01,
);
}
connectToOutput(node) {
node.connect(this.summingNode);
}
}
export class SuperdoughOutput {
channelMerger;
destinationGain;
constructor(audioContext) {
this.audioContext = audioContext;
this.initializeAudio();
}
initializeAudio() {
const audioContext = this.audioContext;
const maxChannelCount = audioContext.destination.maxChannelCount;
this.audioContext.destination.channelCount = maxChannelCount;
this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
this.destinationGain = new GainNode(audioContext);
this.channelMerger.connect(this.destinationGain);
this.destinationGain.connect(audioContext.destination);
}
reset() {
this.disconnect();
this.initializeAudio();
}
disconnect() {
this.channelMerger.disconnect();
this.destinationGain.disconnect();
this.destinationGain = null;
this.channelMerger = null;
}
connectToDestination = (input, channels = [0, 1]) => {
//This upmix can be removed if correct channel counts are set throughout the app,
// and then strudel could theoretically support surround sound audio files
const stereoMix = new StereoPannerNode(this.audioContext);
input.connect(stereoMix);
const splitter = new ChannelSplitterNode(this.audioContext, {
numberOfOutputs: stereoMix.channelCount,
});
stereoMix.connect(splitter);
channels.forEach((ch, i) => {
splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount);
});
};
}
export class SuperdoughAudioController {
audioContext;
output;
nodes = {};
constructor(audioContext) {
this.audioContext = audioContext;
this.output = new SuperdoughOutput(audioContext);
}
reset() {
Array.from(this.nodes).forEach((node) => {
node.disconnect();
});
this.nodes = {};
this.output.reset();
}
duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) {
const targetArr = [targetOrbits].flat();
const onsetArr = [onsettime].flat();
const attackArr = [attacktime].flat();
const depthArr = [depth].flat();
targetArr.forEach((target, idx) => {
const orbit = this.nodes[target];
if (orbit == null) {
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
return;
}
const onset = onsetArr[idx] ?? onsetArr[0];
const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002);
const depth = depthArr[idx] ?? depthArr[0];
orbit.duck(t, onset, attack, depth);
});
}
getOrbit(orbitNum, channels) {
if (this.nodes[orbitNum] == null) {
this.nodes[orbitNum] = new Orbit(this.audioContext);
this.output.connectToDestination(this.nodes[orbitNum].output, channels);
}
return this.nodes[orbitNum];
}
}
+6 -24
View File
@@ -1,39 +1,21 @@
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
import { clamp } from './util.mjs';
import { registerSound, getAudioContext, soundMap } from './superdough.mjs';
import {
applyFM,
destroyAudioWorkletNode,
gainNode,
getADSRValues,
getFrequencyFromValue,
getLfo,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
webAudioTimeout,
getWorklet,
noises,
webAudioTimeout,
} from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const getFrequencyFromValue = (value, defaultNote = 36) => {
let { note, freq } = value;
note = note || defaultNote;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
return Number(freq);
};
function destroyAudioWorkletNode(node) {
if (node == null) {
return;
}
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
}
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const waveformAliases = [
['tri', 'triangle'],
+30 -1
View File
@@ -7,7 +7,7 @@ export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
if (!pc) {
return [];
}
@@ -76,3 +76,32 @@ export function cycleToSeconds(cycle, cps) {
export function secondsToCycle(t, cps) {
return t * cps;
}
// deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
export function getCommonSampleInfo(hapValue, bank) {
const { s, n = 0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
let url;
let index = 0;
if (Array.isArray(bank)) {
index = getSoundIndex(n, bank.length);
url = bank[index];
} else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
const closest = Object.keys(bank)
.filter((k) => !k.startsWith('_'))
.reduce(
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
null,
);
transpose = -midiDiff(closest); // semitones to repitch
index = getSoundIndex(n, bank[closest].length);
url = bank[closest][index];
}
const label = `${s}:${index}`;
return { transpose, url, index, midi, label };
}
+333
View File
@@ -0,0 +1,333 @@
import { getAudioContext, registerSound } from './index.mjs';
import { getCommonSampleInfo } from './util.mjs';
import {
applyParameterModulators,
destroyAudioWorkletNode,
getADSRValues,
getFrequencyFromValue,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
getWorklet,
webAudioTimeout,
} from './helpers.mjs';
import { logger } from './logger.mjs';
export const Warpmode = Object.freeze({
NONE: 0,
ASYM: 1,
MIRROR: 2,
BENDP: 3,
BENDM: 4,
BENDMP: 5,
SYNC: 6,
QUANT: 7,
FOLD: 8,
PWM: 9,
ORBIT: 10,
SPIN: 11,
CHAOS: 12,
PRIMES: 13,
BINARY: 14,
BROWNIAN: 15,
RECIPROCAL: 16,
WORMHOLE: 17,
LOGISTIC: 18,
SIGMOID: 19,
FRACTAL: 20,
FLIP: 21,
});
const seenKeys = new Set();
async function getPayload(url, label, frameLen = 2048) {
const key = `${url},${frameLen}`;
if (!seenKeys.has(key)) {
const buf = await loadBuffer(url, label);
const ch0 = buf.getChannelData(0);
const total = ch0.length;
const numFrames = Math.max(1, Math.floor(total / frameLen));
const frames = new Array(numFrames);
for (let i = 0; i < numFrames; i++) {
const start = i * frameLen;
frames[i] = ch0.subarray(start, start + frameLen);
}
seenKeys.add(key);
return { frames, frameLen, numFrames, key };
}
return { frameLen, key }; // worklet will use the cached version
}
function humanFileSize(bytes, si) {
var thresh = si ? 1000 : 1024;
if (bytes < thresh) return bytes + ' B';
var units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
var u = -1;
do {
bytes /= thresh;
++u;
} while (bytes >= thresh);
return bytes.toFixed(1) + ' ' + units[u];
}
// Extract the sample rate of a .wav file
function parseWavSampleRate(arrBuf) {
const dv = new DataView(arrBuf);
// Header is "RIFF<chunk size (4 bytes)>WAVE", so 12 bytes
let p = 12;
// Look through chunks for the format header
// (they will always have an 8 byte header (id and size) followed by a payload)
while (p + 8 <= dv.byteLength) {
// Parse id
const id = String.fromCharCode(dv.getUint8(p), dv.getUint8(p + 1), dv.getUint8(p + 2), dv.getUint8(p + 3));
// Parse chunk size
const size = dv.getUint32(p + 4, true);
if (id === 'fmt ') {
// The format chunk contains the sample rate after
// 8 bytes of header, 2 bytes of format tag, 2 bytes of num channels
// (for a total of 12)
return dv.getUint32(p + 12, true);
}
// Advance to next chunk
p += 8 + size + (size & 1);
}
return null;
}
async function decodeAtNativeRate(arr) {
const sr = parseWavSampleRate(arr) || 44100;
const tempAC = new OfflineAudioContext(1, 1, sr);
return await tempAC.decodeAudioData(arr);
}
const loadCache = {};
const loadBuffer = (url, label) => {
url = url.replace('#', '%23');
if (!loadCache[url]) {
logger(`[wavetable] load table ${label}..`, 'load-table', { url });
const timestamp = Date.now();
loadCache[url] = fetch(url)
.then((res) => res.arrayBuffer())
.then(async (res) => {
const took = Date.now() - timestamp;
const size = humanFileSize(res.byteLength);
logger(`[wavetable] load table ${label}... done! loaded ${size} in ${took}ms`, 'loaded-table', { url });
const decoded = await decodeAtNativeRate(res);
return decoded;
});
}
return loadCache[url];
};
function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl');
}
let [_, path] = base.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
if (path.split('/').length === 2) {
// assume main as default branch if none set
path += '/main';
}
return `https://raw.githubusercontent.com/${path}/${subpath}`;
}
const _processTables = (json, baseUrl, frameLen, options = {}) => {
baseUrl = json._base || baseUrl;
return Object.entries(json).forEach(([key, tables]) => {
if (key === '_base') return false;
if (typeof tables === 'string') {
tables = [tables];
}
if (typeof tables !== 'object') {
throw new Error('wrong json format for ' + key);
}
let resolvedUrl = baseUrl;
if (resolvedUrl.startsWith('github:')) {
resolvedUrl = githubPath(resolvedUrl, '');
}
tables = tables
.map((t) => resolvedUrl + t)
.filter((t) => {
if (!t.toLowerCase().endsWith('.wav')) {
logger(`[wavetable] skipping ${t} -- wavetables must be ".wav" format`);
return false;
}
return true;
});
if (tables.length) {
registerWaveTable(key, tables, { baseUrl, frameLen });
}
});
};
export function registerWaveTable(key, tables, params) {
registerSound(
key,
(t, hapValue, onended, cps) => {
return onTriggerSynth(t, hapValue, onended, tables, cps, params?.frameLen ?? 2048);
},
{
type: 'wavetable',
tables,
...params,
},
);
}
/**
* Loads a collection of wavetables to use with `s`
*
* @name tables
*/
export const tables = async (url, frameLen, json, options = {}) => {
if (json !== undefined) return _processTables(json, url, frameLen);
if (url.startsWith('github:')) {
url = githubPath(url, 'strudel.json');
}
if (url.startsWith('local:')) {
url = `http://localhost:5432`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
return fetch(url)
.then((res) => res.json())
.then((json) => _processTables(json, url, frameLen, options))
.catch((error) => {
console.error(error);
throw new Error(`error loading "${url}"`);
});
};
export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
const { s, n = 0, duration, clip } = value;
const ac = getAudioContext();
const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
let { warpmode } = value;
if (typeof warpmode === 'string') {
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
}
const frequency = getFrequencyFromValue(value);
const { url, label } = getCommonSampleInfo(value, tables);
const payload = await getPayload(url, label, frameLen);
let holdEnd = t + duration;
if (clip !== undefined) {
holdEnd = Math.min(t + clip * duration, holdEnd);
}
const endWithRelease = holdEnd + release;
const envEnd = endWithRelease + 0.01;
const source = getWorklet(
ac,
'wavetable-oscillator-processor',
{
begin: t,
end: envEnd,
frequency,
detune: value.detune,
position: value.wt,
warp: value.warp,
warpMode: warpmode,
voices: Math.max(value.unison ?? 1, 1),
spread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
},
{ outputChannelCount: [2] },
);
source.port.postMessage({ type: 'table', payload });
if (ac.currentTime > t) {
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
return;
}
const posADSRParams = [value.wtattack, value.wtdecay, value.wtsustain, value.wtrelease];
const warpADSRParams = [value.warpattack, value.warpdecay, value.warpsustain, value.warprelease];
const wtParams = source.parameters;
const positionParam = wtParams.get('position');
const warpParam = wtParams.get('warp');
let wtrate = value.wtrate;
if (value.wtsync != null) {
wtrate = cps * value.wtsync;
}
const wtPosModulators = applyParameterModulators(
ac,
positionParam,
t,
endWithRelease,
{
offset: value.wt,
amount: value.wtenv,
defaultAmount: 0.5,
shape: 'linear',
values: posADSRParams,
holdEnd,
defaultValues: [0, 0.5, 0, 0.1],
},
{
frequency: wtrate,
depth: value.wtdepth,
defaultDepth: 0.5,
shape: value.wtshape,
skew: value.wtskew,
dcoffset: value.wtdc ?? 0,
},
);
let warprate = value.warprate;
if (value.warpsync != null) {
warprate = warprate = cps * value.warpsync;
}
const wtWarpModulators = applyParameterModulators(
ac,
warpParam,
t,
endWithRelease,
{
offset: value.warp,
amount: value.warpenv,
defaultAmount: 0.5,
shape: 'linear',
values: warpADSRParams,
holdEnd,
defaultValues: [0, 0.5, 0, 0.1],
},
{
frequency: warprate,
depth: value.warpdepth,
defaultDepth: 0.5,
shape: value.warpshape,
skew: value.warpskew,
dcoffset: value.warpdc ?? 0,
},
);
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
const envGain = ac.createGain();
const node = source.connect(envGain);
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
const handle = { node, source };
const timeoutNode = webAudioTimeout(
ac,
() => {
destroyAudioWorkletNode(source);
vibratoOscillator?.stop();
node.disconnect();
wtPosModulators?.disconnect();
wtWarpModulators?.disconnect();
onended();
},
t,
envEnd,
);
handle.stop = (time) => {
timeoutNode.stop(time);
};
return handle;
}
+445 -38
View File
@@ -6,7 +6,21 @@ import OLAProcessor from './ola-processor';
import FFT from './fft.js';
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const _mod = (n, m) => ((n % m) + m) % m;
const mod = (n, m) => ((n % m) + m) % m;
const lerp = (a, b, n) => n * (b - a) + a;
const pv = (arr, n) => arr[n] ?? arr[0];
const frac = (x) => x - Math.floor(x);
const ffloor = (x) => x | 0; // fast floor for non-negative
const getUnisonDetune = (unison, detune, voiceIndex) => {
if (unison < 2) {
return 0;
}
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
};
const applySemitoneDetuneToFrequency = (frequency, detune) => {
return frequency * Math.pow(2, detune / 12);
};
// Restrict phase to the range [0, maxPhase) via wrapping
function wrapPhase(phase, maxPhase = 1) {
@@ -123,7 +137,7 @@ class LFOProcessor extends AudioWorkletProcessor {
}
}
process(inputs, outputs, parameters) {
process(_inputs, outputs, parameters) {
const begin = parameters['begin'][0];
if (currentTime >= parameters.end[0]) {
return false;
@@ -150,7 +164,7 @@ class LFOProcessor extends AudioWorkletProcessor {
const blockSize = output[0].length ?? 0;
if (this.phase == null) {
this.phase = _mod(time * frequency + phaseoffset, 1);
this.phase = mod(time * frequency + phaseoffset, 1);
}
const dt = frequency / sampleRate;
for (let n = 0; n < blockSize; n++) {
@@ -271,6 +285,73 @@ class ShapeProcessor extends AudioWorkletProcessor {
}
registerProcessor('shape-processor', ShapeProcessor);
class TwoPoleFilter {
s0 = 0;
s1 = 0;
update(s, cutoff, resonance = 0) {
// Out of bound values can produce NaNs
resonance = clamp(resonance, 0, 1);
cutoff = clamp(cutoff, 0, sampleRate / 2 - 1);
const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14);
const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
const mrc = 1 - r * c;
this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
this.s1 = mrc * this.s1 + c * this.s0; // lpf
return this.s1; // return lpf by default
}
}
class DJFProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [{ name: 'value', defaultValue: 0.5 }];
}
constructor() {
super();
this.filters = [new TwoPoleFilter(), new TwoPoleFilter()];
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const hasInput = !(input[0] === undefined);
this.started = hasInput;
const value = clamp(parameters.value[0], 0, 1);
let filterType = 'none';
let cutoff;
let v = 1;
if (value > 0.51) {
filterType = 'hipass';
v = (value - 0.5) * 2;
} else if (value < 0.49) {
filterType = 'lopass';
v = value * 2;
}
cutoff = Math.pow(v * 11, 4);
for (let i = 0; i < input.length; i++) {
for (let n = 0; n < blockSize; n++) {
if (filterType == 'none') {
output[i][n] = input[i][n];
} else {
this.filters[i].update(input[i][n], cutoff, 0.1);
if (filterType === 'lopass') {
output[i][n] = this.filters[i].s1;
} else if (filterType === 'hipass') {
output[i][n] = input[i][n] - this.filters[i].s1;
} else {
output[i][n] = input[i][n];
}
}
}
}
return true;
}
}
registerProcessor('djf-processor', DJFProcessor);
function fast_tanh(x) {
const x2 = x * x;
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
@@ -378,21 +459,6 @@ class DistortProcessor extends AudioWorkletProcessor {
registerProcessor('distort-processor', DistortProcessor);
// SUPERSAW
function lerp(a, b, n) {
return n * (b - a) + a;
}
function getUnisonDetune(unison, detune, voiceIndex) {
if (unison < 2) {
return 0;
}
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
}
function applySemitoneDetuneToFrequency(frequency, detune) {
return frequency * Math.pow(2, detune / 12);
}
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
constructor() {
super();
@@ -444,7 +510,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
},
];
}
process(input, outputs, params) {
process(_input, outputs, params) {
if (currentTime <= params.begin[0]) {
return true;
}
@@ -454,29 +520,31 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
}
const output = outputs[0];
const voices = params.voices[0];
const freqspread = params.freqspread[0];
const panspread = params.panspread[0] * 0.5 + 0.5;
const gain1 = Math.sqrt(1 - panspread);
const gain2 = Math.sqrt(panspread);
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;
}
for (let i = 0; i < output[0].length; i++) {
// Main detuning
let freq = applySemitoneDetuneToFrequency(params.frequency[i] ?? params.frequency[0], params.detune[0] / 100);
for (let i = 0; i < output[0].length; i++) {
const detune = pv(params.detune, i);
const voices = pv(params.voices, i);
const freqspread = pv(params.freqspread, i);
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
const gain1 = Math.sqrt(1 - panspread);
const gain2 = Math.sqrt(panspread);
let freq = pv(params.frequency, i);
// Main detuning
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
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
freq = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
const freqVoice = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
// We must wrap this here because it is passed into sawblep below which
// has domain [0, 1]
const dt = _mod(freq / sampleRate, 1);
const dt = mod(freqVoice / sampleRate, 1);
this.phase[n] = this.phase[n] ?? Math.random();
const v = waveshapes.sawblep(this.phase[n], dt);
@@ -907,3 +975,342 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
}
registerProcessor('byte-beat-processor', ByteBeatProcessor);
export const WarpMode = Object.freeze({
NONE: 0,
ASYM: 1,
MIRROR: 2,
BENDP: 3,
BENDM: 4,
BENDMP: 5,
SYNC: 6,
QUANT: 7,
FOLD: 8,
PWM: 9,
ORBIT: 10,
SPIN: 11,
CHAOS: 12,
PRIMES: 13,
BINARY: 14,
BROWNIAN: 15,
RECIPROCAL: 16,
WORMHOLE: 17,
LOGISTIC: 18,
SIGMOID: 19,
FRACTAL: 20,
FLIP: 21,
});
function hash32(u) {
u = u + 0x7ed55d16 + (u << 12);
u = u ^ 0xc761c23c ^ (u >>> 19);
u = u + 0x165667b1 + (u << 5);
u = (u + 0xd3a2646c) ^ (u << 9);
u = u + 0xfd7046c5 + (u << 3);
u = u ^ 0xb55a4f09 ^ (u >>> 16);
return u >>> 0;
}
const hash01 = (i) => (hash32(i) >>> 8) / 0x01000000;
function bitReverse(i, n) {
let r = 0;
for (let b = 0; b < n; b++) {
r = (r << 1) | (i & 1);
i >>>= 1;
}
return r;
}
function noise(x) {
const i = Math.floor(x),
f = x - i;
const a = hash01(i),
b = hash01(i + 1);
return a + (b - a) * f;
}
function brownian(x, oct = 4) {
let amp = 0.5,
sum = 0,
norm = 0,
freq = 1;
for (let o = 0; o < oct; o++) {
sum += amp * noise(x * freq);
norm += amp;
amp *= 0.5;
freq *= 2;
}
return (sum / norm) * 2 - 1;
}
const tablesCache = {};
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 },
{ name: 'detune', defaultValue: 0.18 },
{ name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 },
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
{ name: 'warpMode', defaultValue: 0 },
{ name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 },
{ name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 },
{ name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 },
];
}
constructor(options) {
super(options);
this.frameLen = 0;
this.numFrames = 0;
this.phase = [];
this.invSR = 1 / sampleRate;
this.port.onmessage = (e) => {
const { type, payload } = e.data || {};
if (type === 'table') {
const key = payload.key;
this.frameLen = payload.frameLen;
if (!tablesCache[key]) {
const tables = [payload.frames];
let table = tables[0];
for (let level = 1; level < 1; level++) {
const nextLen = table.length >> 1;
const nextTable = table.map((frame) => {
const avg = new Float32Array(nextLen);
for (let i = 0; i < nextLen; i++) {
avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2;
}
return avg;
});
tables.push(nextTable);
table = nextTable;
if (nextLen <= 32) break;
}
tablesCache[key] = tables;
}
this.tables = tablesCache[key];
this.numFrames = this.tables[0].length;
}
};
}
_mirror(x) {
return 1 - Math.abs(2 * x - 1);
}
_toBits(amt, min = 2, max = 12) {
const b = max + (min - max) * amt;
return { b, n: Math.round(Math.pow(2, b)) };
}
_warpPhase(phase, amt, mode) {
switch (mode) {
case WarpMode.NONE: {
return phase;
}
case WarpMode.ASYM: {
const a = 0.01 + 0.99 * amt;
return phase < a ? (0.5 * phase) / a : 0.5 + (0.5 * (phase - a)) / (1 - a);
}
case WarpMode.MIRROR: {
// Asym, then mirror
return this._mirror(this._warpPhase(phase, amt, WarpMode.ASYM));
}
case WarpMode.BENDP: {
return Math.pow(phase, 1 + 3 * amt);
}
case WarpMode.BENDM: {
return Math.pow(phase, 1 / (1 + 3 * amt));
}
case WarpMode.BENDMP: {
return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2);
}
case WarpMode.SYNC: {
const syncRatio = Math.pow(16, amt * amt);
return (phase * syncRatio) % 1;
}
case WarpMode.QUANT: {
const { n } = this._toBits(amt);
return ffloor(phase * n) / n;
}
case WarpMode.FOLD: {
const K = 7;
const k = 1 + Math.max(1, Math.round(K * amt));
return Math.abs(frac(k * phase) - 0.5) * 2;
}
case WarpMode.PWM: {
const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1);
if (phase < w) return (phase / w) * 0.5;
return 0.5 + ((phase - w) / (1 - w)) * 0.5;
}
case WarpMode.ORBIT: {
const depth = 0.5 * amt;
const n = 3;
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase));
}
case WarpMode.SPIN: {
const depth = 0.5 * amt;
const { n } = this._toBits(amt, 1, 6);
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase));
}
case WarpMode.CHAOS: {
const r = 3.7 + 0.3 * amt;
const logistic = r * phase * (1 - phase);
return clamp((1 - amt) * phase + amt * logistic, 0, 1);
}
case WarpMode.PRIMES: {
const isPrime = (n) => {
if (n < 2) return false;
if (n % 2 === 0) return n === 2;
for (let d = 3; d * d <= n; d += 2) if (n % d === 0) return false;
return true;
};
let { n } = this._toBits(amt, 3);
while (!isPrime(n)) n++;
return ffloor(phase * n) / n;
}
case WarpMode.BINARY: {
let { b } = this._toBits(amt, 3);
b = Math.round(b);
const n = 1 << b;
const idx = ffloor(phase * n);
const ridx = bitReverse(idx, b);
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: {
const disp = 0.25 * amt * brownian(64 * phase, 4);
return frac(phase + disp);
}
case WarpMode.RECIPROCAL: {
const g = 2 + 4 * amt;
const num = phase * g;
const den = phase + (1 - phase) * g;
const y = den > 1e-12 ? num / den : 0;
return clamp(y, 0, 1);
}
case WarpMode.WORMHOLE: {
const gap = clamp(0.8 * amt, 0, 1);
const a = 0.5 * (1 - gap);
const b = 0.5 * (1 + gap);
if (phase < a) return (phase / a) * 0.5;
if (phase > b) return 0.5 * (1 + (phase - b) / (1 - b));
return 0.5;
}
case WarpMode.LOGISTIC: {
let x = phase;
const r = 3.6 + 0.4 * amt;
const iters = 1 + Math.round(2 * amt);
for (let i = 0; i < iters; i++) x = r * x * (1 - x);
return clamp(x, 0, 1);
}
case WarpMode.SIGMOID: {
const k = 1 + 10 * amt;
const x = phase - 0.5;
const y = 1 / (1 + Math.exp(-k * x));
const y0 = 1 / (1 + Math.exp(0.5 * k));
const y1 = 1 / (1 + Math.exp(-0.5 * k));
return (y - y0) / (y1 - y0);
}
case WarpMode.FRACTAL: {
const d = 0.5 * Math.sin(2 * Math.PI * phase) * amt;
return frac(phase + d);
}
case WarpMode.FLIP: {
return phase;
}
default:
return phase;
}
}
_sampleFrame(frame, phase) {
const len = frame.length;
const pos = phase * len;
const i = pos | 0;
const frac = pos - i;
const a = frame[i];
const i1 = i + 1 < len ? i + 1 : 0; // fast wrap
const b = frame[i1];
return a + (b - a) * frac;
}
_chooseMip(dphi) {
const approxHarm = clamp(dphi, 1e-6, 64);
let level = 0;
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
level++;
}
return level;
}
process(_inputs, outputs, parameters) {
if (currentTime >= parameters.end[0]) {
return false;
}
if (currentTime <= parameters.begin[0]) {
return true;
}
const outL = outputs[0][0];
const outR = outputs[0][1] || outputs[0][0];
if (!this.tables) {
outL.fill(0);
if (outR !== outL) outR.set(outL);
return true;
}
for (let i = 0; i < outL.length; i++) {
const detune = pv(parameters.detune, i);
const tablePos = clamp(pv(parameters.position, i), 0, 1);
const idx = tablePos * (this.numFrames - 1);
const fIdx = idx | 0;
const frac = idx - fIdx;
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
const warpMode = pv(parameters.warpMode, i);
const voices = pv(parameters.voices, i);
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
const spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 0;
const gain1 = Math.sqrt(0.5 - 0.5 * spread);
const gain2 = Math.sqrt(0.5 + 0.5 * spread);
let f = pv(parameters.frequency, i);
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const normalizer = 1 / Math.sqrt(voices);
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;
}
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
const dPhase = fVoice * this.invSR;
const level = this._chooseMip(dPhase);
const table = this.tables[level];
// warp phase then sample
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
const s0 = this._sampleFrame(table[fIdx], ph);
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
let s = s0 + (s1 - s0) * frac;
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
s = -s;
}
outL[i] += s * gainL * normalizer;
outR[i] += s * gainR * normalizer;
this.phase[n] = wrapPhase(this.phase[n] + dPhase);
}
}
return true;
}
}
registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor);
+1
View File
@@ -0,0 +1 @@
pattern.wav
+3
View File
@@ -0,0 +1,3 @@
# supradough
platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.
+123
View File
@@ -0,0 +1,123 @@
// this is a poc of how a pattern can be rendered as a wav file using node
// run via: node dough-export.mjs
import fs from 'node:fs';
import WavEncoder from 'wav-encoder';
import { evalScope } from '@strudel/core';
import { miniAllStrings } from '@strudel/mini';
import { Dough } from './dough.mjs';
await evalScope(
import('@strudel/core'),
import('@strudel/mini'),
import('@strudel/tonal'),
// import('@strudel/tonal'),
);
miniAllStrings(); // allows using single quotes for mini notation / skip transpilation
let sampleRate = 48000,
cps = 0.4;
/* await doughsamples('github:eddyflux/crate');
await doughsamples('github:eddyflux/wax'); */
let pat = note('c,eb,g,<bb c4 d4 eb4>')
.s('sine')
.press()
.add(note(24))
.fmi(3)
.fmh(5.01)
.dec(0.4)
.delay('.6:<.12 .22>:.8')
.jux(press)
.rarely(add(note('12')))
.lpf(400)
.lpq(0.2)
.lpd(0.4)
.lpenv(3)
.fmdecay(0.4)
.fmenv(1)
.postgain(0.6)
.stack(s('<pink white>*8').dec(0.07).rarely(ply('2')).delay(0.5).hpf(sine.range(200, 2000).slow(4)).hpq(0.2))
.stack(
s('[- white@3]*2')
.dec(0.4)
.hpf('<2000!3 <4000 8000>>*4')
.hpq(0.6)
.ply('<1 2>*4')
.postgain(0.5)
.delay(0.5)
.jux(rev)
.lpf(5000),
)
.stack(
note('<c2 - [- f1] ->*2')
.s('square')
.lpf(sine.range(100, 300).slow(4))
.lpe(1)
.segment(8)
.lpd(0.3)
.lpq(0.2)
.dec(0.2)
.speed('<1 2>')
.ply('<1 2>')
.postgain(1),
)
.stack(
chord('<Cm Cm7 Cm9 Cm11 Fm Fm7 Fm9 Fm11>')
.voicing()
.s('<sine>')
.clip(1)
.rel(0.4)
.vib('4:.2')
.gain(0.7)
.hpf(1200)
.fm(0.5)
.att(1)
.lpa(0.5)
.lpf(200)
.lpenv(4)
.chorus(0.8),
)
.slow(1 / cps);
let cycles = 30;
let seconds = cycles + 1; // 1s release tail
const haps = pat.queryArc(0, cycles);
const dough = new Dough(sampleRate);
console.log('spawn voices...');
haps.forEach((hap) => {
hap.value._begin = Number(hap.whole.begin);
hap.value._duration = hap.duration /* / cps */;
dough.scheduleSpawn(hap.value);
});
console.log(`render ${seconds}s long buffer, each dot is 1 second:`);
const buffers = [new Float32Array(seconds * sampleRate), new Float32Array(seconds * sampleRate)];
let t = performance.now();
while (dough.t <= buffers[0].length) {
dough.update();
buffers[0][dough.t] = dough.out[0];
buffers[1][dough.t] = dough.out[1];
if (dough.t % sampleRate === 0) {
process.stdout.write('.');
}
}
const took = (performance.now() - t) / 1000;
const load = (took / seconds) * 100;
const speed = (seconds / took).toFixed(2);
console.log('');
console.log(`done!
rendered ${seconds}s in ${took.toFixed(2)}s
speed: ${speed}x
load: ${load.toFixed(2)}%`);
const patternAudio = {
sampleRate,
channelData: buffers,
};
WavEncoder.encode(patternAudio).then((buffer) => {
fs.writeFileSync('pattern.wav', new Float32Array(buffer));
});
+39
View File
@@ -0,0 +1,39 @@
import { Dough } from './dough.mjs';
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
class DoughProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.dough = new Dough(sampleRate, currentTime);
this.port.onmessage = (event) => {
if (event.data.spawn) {
this.dough.scheduleSpawn(event.data.spawn);
} else if (event.data.sample) {
this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate);
} else if (event.data.samples) {
event.data.samples.forEach(([name, channels, sampleRate]) => {
this.dough.loadSample(name, channels, sampleRate);
});
} else {
console.log('unrecognized event type', event.data);
}
};
}
process(inputs, outputs, params) {
if (this.disconnected) {
return false;
}
const output = outputs[0];
for (let i = 0; i < output[0].length; i++) {
this.dough.update();
for (let c = 0; c < output.length; c++) {
//prevent speaker blowout via clipping if threshold exceeds
output[c][i] = clamp(this.dough.out[c], -1, 1);
}
}
return true; // keep the audio processing going
}
}
registerProcessor('dough-processor', DoughProcessor);
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
// import _workletUrl from './dough-worklet.mjs?url'; // only for dev (breaks for production build)
import _workletUrl from './dough-worklet.mjs?audioworklet'; // only for prod (breaks in development?!)
export * from './dough.mjs';
export const workletUrl = _workletUrl;
+37
View File
@@ -0,0 +1,37 @@
{
"name": "supradough",
"version": "1.2.3",
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"devDependencies": {
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*",
"wav-encoder": "^1.3.0"
},
"dependencies": {}
}
+119 -69
View File
@@ -7,80 +7,130 @@ This program is free software: you can redistribute it and/or modify it under th
// import { strict as assert } from 'assert';
import '../tonal.mjs'; // need to import this to add prototypes
import { pure, n, seq, note } from '@strudel/core';
import { pure, n, seq, note, noteToMidi } from '@strudel/core';
import { describe, it, expect } from 'vitest';
import { mini } from '../../mini/mini.mjs';
describe('tonal', () => {
it('Should run tonal functions ', () => {
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
describe('scaleTranspose', () => {
it('transposes notes by scale degrees', () => {
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
});
});
it('scale with plain values', () => {
expect(
seq(0, 1, 2)
.scale('C major')
.note()
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
describe('scale', () => {
it('converts plain values', () => {
expect(
seq(0, 1, 2)
.scale('C major')
.note()
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values', () => {
expect(
n(seq(0, 1, 2))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values (mini notation)', () => {
expect(
n(seq(0, 1, 2))
.scale('C:major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values (no tonic)', () => {
expect(
n(seq(0, 1, 2))
.scale('major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values (explicit mini notation)', () => {
expect(
n(seq(0, 1, 2))
.scale(mini('C:major'))
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts decorated n values', () => {
expect(
n(seq('0b', '1#', '-2', '3##', '4bb'))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']);
});
it('produces silence for mixed sharps and flats', () => {
expect(
n(seq('0b#', '1#b', '2#b#'))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual([]);
});
it('snaps notes (upwards) to scale', () => {
const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb'];
const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3'];
expect(
note(seq(inputNotes))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(expectedNotes);
});
it('snaps notes to the correct octave', () => {
const inputNotes = ['Cb0', 'Eb4', 'G1', 'A#19', 'Bb8'];
const expectedNotes = ['B#-1', 'D#4', 'G#1', 'A#19', 'A#8'];
expect(
note(seq(inputNotes))
.scale('A# minor') // A#, B#, C#, D#, E#, F#, G#
.firstCycleValues.map((h) => h.note),
).toEqual(expectedNotes);
});
it('handles scale names provided with colons', () => {
const inputNotes = ['Cb', 'E', 'G', 'A#', 'Bb'];
const expectedNotes = ['A#2', 'D#3', 'G#3', 'A#3', 'A#3'];
expect(
note(seq(inputNotes))
.scale('F#:pentatonic') // F#, G#, A#, C#, and D#
.firstCycleValues.map((h) => h.note),
).toEqual(expectedNotes);
});
});
it('scale with n values', () => {
expect(
n(seq(0, 1, 2))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with colon', () => {
expect(
n(seq(0, 1, 2))
.scale('C:major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale without tonic', () => {
expect(
n(seq(0, 1, 2))
.scale('major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with mininotation colon', () => {
expect(
n(seq(0, 1, 2))
.scale(mini('C:major'))
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('transposes note numbers with interval numbers', () => {
expect(
note(seq(40, 40, 40))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual([40, 41, 42]);
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
});
it('transposes note numbers with interval strings', () => {
expect(
note(seq(40, 40, 40))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual([40, 42, 43]);
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
});
it('transposes note strings with interval numbers', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'Db', 'D']);
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
});
it('transposes note strings with interval strings', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'D', 'Eb']);
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
describe('transpose', () => {
it('transposes note numbers with interval numbers', () => {
expect(
note(seq(40, 40, 40))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual([40, 41, 42]);
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
});
it('transposes note numbers with interval strings', () => {
expect(
note(seq(40, 40, 40))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual([40, 42, 43]);
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
});
it('transposes note strings with interval numbers', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'Db', 'D']);
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
});
it('transposes note strings with interval strings', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'D', 'Eb']);
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
});
});
});
+101 -53
View File
@@ -6,19 +6,28 @@ This program is free software: you can redistribute it and/or modify it under th
import { Note, Interval, Scale } from '@tonaljs/tonal';
import { register, _mod, silence, logger, pure, isNote } from '@strudel/core';
import { stepInNamedScale } from './tonleiter.mjs';
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
import { noteToMidi } from '../core/util.mjs';
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
function scaleStep(step, scale) {
scale = scale.replaceAll(':', ' ');
step = Math.ceil(step);
let { intervals, tonic, empty } = Scale.get(scale);
if ((empty && isNote(scale)) || (empty && !tonic)) {
throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`);
function getScale(scaleName) {
scaleName = scaleName.replaceAll(':', ' ');
const scale = Scale.get(scaleName);
const { tonic, empty } = scale;
if ((empty && isNote(scaleName)) || (empty && !tonic)) {
throw new Error(
`Scale name ${scaleName} is incomplete. Make sure to use ":" instead of spaces, example: .scale("C:major")`,
);
} else if (empty) {
throw new Error(`invalid scale "${scale}"`);
throw new Error(`Invalid scale name "${scaleName}"`);
}
return scale;
}
function scaleStep(step, scale) {
step = Math.ceil(step);
let { intervals, tonic } = getScale(scale);
tonic = tonic || 'C';
const { pc, oct = 3 } = Note.get(tonic);
const octaveOffset = Math.floor(step / intervals.length);
@@ -30,8 +39,7 @@ function scaleStep(step, scale) {
// transpose note inside scale by offset steps
// function scaleOffset(scale: string, offset: number, note: string) {
function scaleOffset(scale, offset, note) {
let [tonic, scaleName] = Scale.tokenize(scale);
let { notes } = Scale.get(`${tonic} ${scaleName}`);
let { notes } = getScale(scale);
notes = notes.map((note) => Note.get(note).pc); // use only pc!
offset = Number(offset);
if (isNaN(offset)) {
@@ -120,10 +128,7 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones)
: String(intervalOrSemitones);
// TODO: move simplify to player to preserve enharmonics
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
// TODO: check if this is still relevant..
const targetNote = Note.simplify(Note.transpose(note, interval));
const targetNote = Note.transpose(note, interval);
if (typeof hap.value === 'object') {
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
}
@@ -171,8 +176,59 @@ export const { scaleTranspose, scaleTrans, strans } = register(
},
);
// Converts a step value, which is a number optionally decorated with sharps and flats,
// to a number and an `offset` number of semitones
function _convertStepToNumberAndOffset(step) {
let asNumber = Number(step);
let offset = 0;
if (isNaN(asNumber)) {
step = String(step);
// Check to see if the step matches the expected format:
// - A number (possibly negative)
// - Some number of sharps or flats (but not both)
const match = /^(-?\d+)(#+|b+)?$/.exec(step);
if (!match) {
throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`);
}
asNumber = Number(match[1]);
// These decorations will determine the semitone offset based on the number of
// sharps or flats
const decorations = match[2] || '';
offset = decorations[0] === '#' ? decorations.length : -decorations.length;
}
return [asNumber, offset];
}
let scaleToMidisAndNotes = {};
// Finds the nearest scale note to `note`
function _getNearestScaleNote(scaleName, note, preferHigher = true) {
let noteMidi = typeof note === 'string' ? noteToMidi(note) : note;
if (scaleToMidisAndNotes[scaleName] === undefined) {
const { intervals, tonic } = getScale(scaleName);
const { pc } = Note.get(tonic);
const expandedIntervals = intervals.concat('8P'); // add the octave for wrapping
const sNotes = expandedIntervals.map((interval) => Note.transpose(pc + '0', interval));
const sMidi = sNotes.map(noteToMidi);
// Cache
scaleToMidisAndNotes[scaleName] = [sMidi, sNotes];
}
const [scaleMidis, scaleNotes] = scaleToMidisAndNotes[scaleName];
const rootMidi = scaleMidis[0];
const octaveDiff = Math.floor((noteMidi - rootMidi) / 12);
const alignedMidis = scaleMidis.map((m) => m + 12 * octaveDiff);
const noteIdx = nearestNumberIndex(noteMidi, alignedMidis, preferHigher);
const noteMatch = scaleNotes[noteIdx];
return Note.transpose(noteMatch, Interval.fromSemitones(12 * octaveDiff));
}
/**
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
* Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale.
*
* When describing notes via numbers, note that negative numbers can be used to wrap backwards
* in the scale as well as sharps or flats (but not both) to produce notes outside of the scale.
*
* Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
*
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
*
@@ -191,6 +247,12 @@ export const { scaleTranspose, scaleTrans, strans } = register(
* n(rand.range(0,12).segment(8))
* .scale("C:ritusen")
* .s("piano")
* @example
* n("<[0,7b] [-4# -4] [-2,7##] 4 [0,7] [-4# -4b] [-2,7###] 4b>*4")
* .scale("C:<major minor>/2")
* .s("piano")
* @example
* note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3)
*/
export const scale = register(
@@ -204,49 +266,35 @@ export const scale = register(
pat
.fmap((value) => {
const isObject = typeof value === 'object';
let step = isObject ? value.n : value;
if (isObject) {
// The case where the note has been defined via `n` or `pure`
if (!isObject || (isObject && ('n' in value || 'value' in value))) {
const step = isObject ? (value.n ?? value.value) : value;
delete value.n; // remove n so it won't cause trouble
}
if (isNote(step)) {
// legacy..
return pure(step);
}
let asNumber = Number(step);
let semitones = 0;
if (isNaN(asNumber)) {
step = String(step);
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
logger(
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
'error',
);
if (isNote(step)) {
// legacy..
return pure(step);
}
try {
const [number, offset] = _convertStepToNumberAndOffset(step);
let note;
if (isObject && value.anchor) {
note = stepInNamedScale(number, scale, value.anchor);
} else {
note = scaleStep(number, scale);
}
if (offset != 0) note = Note.transpose(note, Interval.fromSemitones(offset));
value = pure(isObject ? { ...value, note } : note);
} catch (err) {
logger(`[tonal] ${err.message}`, 'error');
return silence;
}
const isharp = step.indexOf('#');
if (isharp >= 0) {
asNumber = Number(step.substring(0, isharp));
semitones = step.length - isharp;
} else {
const iflat = step.indexOf('b');
asNumber = Number(step.substring(0, iflat));
semitones = iflat - step.length;
}
return value;
}
try {
let note;
if (isObject && value.anchor) {
note = stepInNamedScale(asNumber, scale, value.anchor);
} else {
note = scaleStep(asNumber, scale);
}
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
value = pure(isObject ? { ...value, note } : note);
} catch (err) {
logger(`[tonal] ${err.message}`, 'error');
value = silence;
// The case where the note has been defined via `note`
else {
const note = _getNearestScaleNote(scale, value.note);
return pure(isObject ? { ...value, note } : note);
}
return value;
})
.outerJoin()
// legacy:
+3 -4
View File
@@ -101,11 +101,11 @@ export function nearestNumberIndex(target, numbers, preferHigher) {
let scaleSteps = {}; // [scaleName]: semitones[]
export function stepInNamedScale(step, scale, anchor, preferHigher) {
let [root, scaleName] = Scale.tokenize(scale);
const [root, scaleName] = Scale.tokenize(scale);
const rootMidi = x2midi(root);
const rootChroma = midi2chroma(rootMidi);
if (!scaleSteps[scaleName]) {
let { intervals } = Scale.get(`C ${scaleName}`);
const { intervals } = Scale.get(`C ${scaleName}`);
// cache result
scaleSteps[scaleName] = intervals.map(step2semitones);
}
@@ -222,6 +222,7 @@ export const Note = {
};
// TODO: support octave numbers
// Example: Note("Bb3").transpose("c3")
export function transpose(note, step) {
// example: E, 3
const stepNumber = Step.tokenize(step)[1]; // 3
@@ -235,5 +236,3 @@ export function transpose(note, step) {
const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E"
return [targetNote, offsetAccidentals].join('');
}
//Note("Bb3").transpose("c3")
+1
View File
@@ -7,4 +7,5 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './webaudio.mjs';
export * from './scope.mjs';
export * from './spectrum.mjs';
export * from './supradough.mjs';
export * from 'superdough';
+2 -1
View File
@@ -35,7 +35,8 @@
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"superdough": "workspace:*"
"superdough": "workspace:*",
"supradough": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
+130
View File
@@ -0,0 +1,130 @@
import { Pattern } from '@strudel/core';
import { connectToDestination, getAudioContext, getWorklet } from 'superdough';
let doughWorklet;
function initDoughWorklet() {
const ac = getAudioContext();
doughWorklet = getWorklet(
ac,
'dough-processor',
{},
{
outputChannelCount: [2],
},
);
connectToDestination(doughWorklet); // channels?
}
const soundMap = new Map();
const loadedSounds = new Map();
Pattern.prototype.supradough = function () {
return this.onTrigger((hap, __, cps, begin) => {
hap.value._begin = begin;
hap.value._duration = hap.duration / cps;
!doughWorklet && initDoughWorklet();
const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s;
const n = hap.value.n ?? 0;
const soundKey = `${s}:${n}`;
if (soundMap.has(s)) {
hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers)
}
if (soundMap.has(s) && !loadedSounds.has(soundKey)) {
const urls = soundMap.get(s);
const url = urls[n % urls.length];
console.log(`load ${soundKey} from ${url}`);
const loadSample = fetchSample(url);
loadedSounds.set(soundKey, loadSample);
loadSample.then(({ channels, sampleRate }) =>
doughWorklet.port.postMessage({
sample: soundKey,
channels,
sampleRate,
}),
);
}
doughWorklet.port.postMessage({ spawn: hap.value });
}, 1);
};
function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl');
}
let [_, path] = base.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
if (path.split('/').length === 2) {
// assume main as default branch if none set
path += '/main';
}
return `https://raw.githubusercontent.com/${path}/${subpath}`;
}
export async function fetchSampleMap(url) {
if (url.startsWith('github:')) {
url = githubPath(url, 'strudel.json');
}
if (url.startsWith('local:')) {
url = `http://localhost:5432`;
}
if (url.startsWith('shabda:')) {
let [_, path] = url.split('shabda:');
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
}
if (url.startsWith('shabda/speech')) {
let [_, path] = url.split('shabda/speech');
path = path.startsWith('/') ? path.substring(1) : path;
let [params, words] = path.split(':');
let gender = 'f';
let language = 'en-GB';
if (params) {
[language, gender] = params.split('/');
}
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
const base = url.split('/').slice(0, -1).join('/');
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
const json = await fetch(url)
.then((res) => res.json())
.catch((error) => {
console.error(error);
throw new Error(`error loading "${url}"`);
});
return [json, json._base || base];
}
// for some reason, only piano and flute work.. is it because mp3??
async function fetchSample(url) {
const buffer = await fetch(url)
.then((res) => res.arrayBuffer())
.then((buf) => getAudioContext().decodeAudioData(buf));
let channels = [];
for (let i = 0; i < buffer.numberOfChannels; i++) {
channels.push(buffer.getChannelData(i));
}
return { channels, sampleRate: buffer.sampleRate };
}
export async function doughsamples(sampleMap, baseUrl) {
if (typeof sampleMap === 'string') {
const [json, base] = await fetchSampleMap(sampleMap);
// console.log('json', json, 'base', base);
return doughsamples(json, base);
}
Object.entries(sampleMap).map(async ([key, urls]) => {
if (key !== '_base') {
urls = urls.map((url) => baseUrl + url);
// console.log('set', key, urls);
soundMap.set(key, urls);
}
});
}
+6 -1
View File
@@ -5,7 +5,12 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as strudel from '@strudel/core';
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough';
import './supradough.mjs';
import { workletUrl } from 'supradough';
registerWorklet(workletUrl);
const { Pattern, logger, repl } = strudel;
setLogger(logger);
+9 -4
View File
@@ -139,10 +139,10 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) {
/* Load a new scale */
Tune.prototype.loadScale = function(name){
Tune.prototype.loadScale = function(scale){
/* load the scale */
var freqs = TuningList[name].frequencies
var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies
this.scale = []
for (var i=0;i<freqs.length-1;i++) {
this.scale.push(freqs[i]/freqs[0])
@@ -207,8 +207,13 @@ Tune.prototype.search = function(letters) {
return possible
}
Tune.prototype.isValidScale = function(name) {
return !!TuningList[name];
function isArrayOfNumbers(arg) {
return Array.isArray(arg) && arg.length > 0 && arg.every(item => typeof item === 'number' && !isNaN(item));
}
/* allow an array of values too */
Tune.prototype.isValidScale = function(scale) {
return !!TuningList[scale] || isArrayOfNumbers(scale) ;
}
/* Return a collection of notes as an array */
+20
View File
@@ -517,6 +517,18 @@ importers:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
packages/supradough:
devDependencies:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
wav-encoder:
specifier: ^1.3.0
version: 1.3.0
packages/tidal:
dependencies:
'@strudel/core':
@@ -625,6 +637,9 @@ importers:
superdough:
specifier: workspace:*
version: link:../superdough
supradough:
specifier: workspace:*
version: link:../supradough
devDependencies:
vite:
specifier: ^6.0.11
@@ -7537,6 +7552,9 @@ packages:
walk-up-path@3.0.1:
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
wav-encoder@1.3.0:
resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==}
wav@1.0.2:
resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==}
@@ -15958,6 +15976,8 @@ snapshots:
walk-up-path@3.0.1: {}
wav-encoder@1.3.0: {}
wav@1.0.2:
dependencies:
buffer-alloc: 1.2.0
+650 -65
View File
@@ -1847,6 +1847,27 @@ exports[`runs examples > example "chop" example index 0 1`] = `
]
`;
exports[`runs examples > example "chorus" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]",
"[ 1/4 → 1/2 | note:d s:sawtooth chorus:0.5 ]",
"[ 1/2 → 3/4 | note:a# s:sawtooth chorus:0.5 ]",
"[ 3/4 → 1/1 | note:a s:sawtooth chorus:0.5 ]",
"[ 1/1 → 5/4 | note:d s:sawtooth chorus:0.5 ]",
"[ 5/4 → 3/2 | note:d s:sawtooth chorus:0.5 ]",
"[ 3/2 → 7/4 | note:a# s:sawtooth chorus:0.5 ]",
"[ 7/4 → 2/1 | note:a s:sawtooth chorus:0.5 ]",
"[ 2/1 → 9/4 | note:d s:sawtooth chorus:0.5 ]",
"[ 9/4 → 5/2 | note:d s:sawtooth chorus:0.5 ]",
"[ 5/2 → 11/4 | note:a# s:sawtooth chorus:0.5 ]",
"[ 11/4 → 3/1 | note:a s:sawtooth chorus:0.5 ]",
"[ 3/1 → 13/4 | note:d s:sawtooth chorus:0.5 ]",
"[ 13/4 → 7/2 | note:d s:sawtooth chorus:0.5 ]",
"[ 7/2 → 15/4 | note:a# s:sawtooth chorus:0.5 ]",
"[ 15/4 → 4/1 | note:a s:sawtooth chorus:0.5 ]",
]
`;
exports[`runs examples > example "chunk" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:A4 ]",
@@ -2585,6 +2606,52 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = `
]
`;
exports[`runs examples > example "delayfeedback" example index 0 2`] = `
[
"[ 0/1 → 1/1 | s:bd delay:0.25 delayfeedback:0.25 ]",
"[ 1/1 → 2/1 | s:bd delay:0.25 delayfeedback:0.5 ]",
"[ 2/1 → 3/1 | s:bd delay:0.25 delayfeedback:0.75 ]",
"[ 3/1 → 4/1 | s:bd delay:0.25 delayfeedback:1 ]",
]
`;
exports[`runs examples > example "delayspeed" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
]
`;
exports[`runs examples > example "delaysync" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd delay:0.25 delaysync:0.125 ]",
@@ -2598,19 +2665,6 @@ exports[`runs examples > example "delaysync" example index 0 1`] = `
]
`;
exports[`runs examples > example "delaytime" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd delay:0.25 delaytime:0.125 ]",
"[ 1/2 → 1/1 | s:bd delay:0.25 delaytime:0.125 ]",
"[ 1/1 → 3/2 | s:bd delay:0.25 delaytime:0.25 ]",
"[ 3/2 → 2/1 | s:bd delay:0.25 delaytime:0.25 ]",
"[ 2/1 → 5/2 | s:bd delay:0.25 delaytime:0.5 ]",
"[ 5/2 → 3/1 | s:bd delay:0.25 delaytime:0.5 ]",
"[ 3/1 → 7/2 | s:bd delay:0.25 delaytime:1 ]",
"[ 7/2 → 4/1 | s:bd delay:0.25 delaytime:1 ]",
]
`;
exports[`runs examples > example "density" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
@@ -2777,26 +2831,38 @@ exports[`runs examples > example "distort" example index 1 1`] = `
exports[`runs examples > example "djf" example index 0 1`] = `
[
"[ 0/1 → 1/4 | n:0 s:superzow octave:3 djf:0.5 ]",
"[ 1/4 → 1/2 | n:3 s:superzow octave:3 djf:0.5 ]",
"[ 1/2 → 3/4 | n:7 s:superzow octave:3 djf:0.5 ]",
"[ 3/4 → 1/1 | n:10 s:superzow octave:3 djf:0.5 ]",
"[ 3/4 → 1/1 | n:24 s:superzow octave:3 djf:0.5 ]",
"[ 1/15/4 | n:0 s:superzow octave:3 djf:0.25 ]",
"[ 5/4 → 3/2 | n:3 s:superzow octave:3 djf:0.25 ]",
"[ 3/2 → 7/4 | n:7 s:superzow octave:3 djf:0.25 ]",
"[ 7/4 → 2/1 | n:10 s:superzow octave:3 djf:0.25 ]",
"[ 7/4 → 2/1 | n:24 s:superzow octave:3 djf:0.25 ]",
"[ 2/1 → 9/4 | n:0 s:superzow octave:3 djf:0.5 ]",
"[ 9/45/2 | n:3 s:superzow octave:3 djf:0.5 ]",
"[ 5/2 → 11/4 | n:7 s:superzow octave:3 djf:0.5 ]",
"[ 11/4 → 3/1 | n:10 s:superzow octave:3 djf:0.5 ]",
"[ 11/4 → 3/1 | n:24 s:superzow octave:3 djf:0.5 ]",
"[ 3/1 → 13/4 | n:0 s:superzow octave:3 djf:0.75 ]",
"[ 13/4 → 7/2 | n:3 s:superzow octave:3 djf:0.75 ]",
"[ 7/215/4 | n:7 s:superzow octave:3 djf:0.75 ]",
"[ 15/4 → 4/1 | n:10 s:superzow octave:3 djf:0.75 ]",
"[ 15/4 → 4/1 | n:24 s:superzow octave:3 djf:0.75 ]",
"[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]",
"[ 1/8 → 1/4 | note:G4 s:supersaw djf:0.5 ]",
"[ 1/4 → 3/8 | note:Bb3 s:supersaw djf:0.5 ]",
"[ 3/8 → 1/2 | note:C4 s:supersaw djf:0.5 ]",
"[ 1/2 → 5/8 | note:A3 s:supersaw djf:0.5 ]",
"[ 5/83/4 | note:F3 s:supersaw djf:0.5 ]",
"[ 3/4 → 7/8 | note:G3 s:supersaw djf:0.5 ]",
"[ 7/8 → 1/1 | note:C4 s:supersaw djf:0.5 ]",
"[ 1/1 → 9/8 | note:Eb4 s:supersaw djf:0.3 ]",
"[ 9/8 → 5/4 | note:G4 s:supersaw djf:0.3 ]",
"[ 5/4 → 11/8 | note:A4 s:supersaw djf:0.3 ]",
"[ 11/83/2 | note:F3 s:supersaw djf:0.3 ]",
"[ 3/2 → 13/8 | note:F4 s:supersaw djf:0.3 ]",
"[ 13/8 → 7/4 | note:D4 s:supersaw djf:0.3 ]",
"[ 7/4 → 15/8 | note:G3 s:supersaw djf:0.3 ]",
"[ 15/8 → 2/1 | note:F4 s:supersaw djf:0.3 ]",
"[ 2/117/8 | note:Eb5 s:supersaw djf:0.2 ]",
"[ 17/89/4 | note:D5 s:supersaw djf:0.2 ]",
"[ 9/4 → 19/8 | note:Bb3 s:supersaw djf:0.2 ]",
"[ 19/8 → 5/2 | note:C5 s:supersaw djf:0.2 ]",
"[ 5/2 → 21/8 | note:D4 s:supersaw djf:0.2 ]",
"[ 21/8 → 11/4 | note:F3 s:supersaw djf:0.2 ]",
"[ 11/4 → 23/8 | note:G4 s:supersaw djf:0.2 ]",
"[ 23/8 → 3/1 | note:D3 s:supersaw djf:0.2 ]",
"[ 3/1 → 25/8 | note:G3 s:supersaw djf:0.75 ]",
"[ 25/8 → 13/4 | note:Bb3 s:supersaw djf:0.75 ]",
"[ 13/4 → 27/8 | note:Eb5 s:supersaw djf:0.75 ]",
"[ 27/8 → 7/2 | note:C4 s:supersaw djf:0.75 ]",
"[ 7/2 → 29/8 | note:C4 s:supersaw djf:0.75 ]",
"[ 29/8 → 15/4 | note:Eb5 s:supersaw djf:0.75 ]",
"[ 15/4 → 31/8 | note:Bb4 s:supersaw djf:0.75 ]",
"[ 31/8 → 4/1 | note:A4 s:supersaw djf:0.75 ]",
]
`;
@@ -3039,57 +3105,50 @@ exports[`runs examples > example "dry" example index 0 1`] = `
exports[`runs examples > example "duckattack" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 0/1 → 1/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/8 → 1/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/4 → 3/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/8 → 1/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/2 → 5/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/8 → 3/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 3/4 → 7/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 7/8 → 1/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 1/1 → 9/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 9/8 → 5/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 5/4 → 11/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 11/8 → 3/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 3/2 → 13/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 13/8 → 7/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 7/4 → 15/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 15/8 → 2/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 2/1 → 17/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 17/8 → 9/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 9/4 → 19/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 19/8 → 5/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 5/2 → 21/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 21/8 → 11/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 11/4 → 23/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 23/8 → 3/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 3/1 → 25/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 25/8 → 13/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 13/4 → 27/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 27/8 → 7/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 7/2 → 29/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 29/8 → 15/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 15/4 → 31/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 31/8 → 4/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
]
`;
exports[`runs examples > example "duckattack" example index 1 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
]
`;
@@ -3150,6 +3209,94 @@ exports[`runs examples > example "duckdepth" example index 0 1`] = `
]
`;
exports[`runs examples > example "duckdepth" example index 1 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
]
`;
exports[`runs examples > example "duckonset" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
]
`;
exports[`runs examples > example "duckonset" example index 1 1`] = `
[
"[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
]
`;
exports[`runs examples > example "duckonset" example index 2 1`] = `
[
"[ 0/1 → 1/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 1/4 → 1/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 1/2 → 3/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 3/4 → 1/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 1/1 → 5/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 5/4 → 3/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 3/2 → 7/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 7/4 → 2/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 2/1 → 9/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 9/4 → 5/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 5/2 → 11/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 11/4 → 3/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 3/1 → 13/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 13/4 → 7/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 7/2 → 15/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 15/4 → 4/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
]
`;
exports[`runs examples > example "duckorbit" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
@@ -3175,6 +3322,31 @@ exports[`runs examples > example "duckorbit" example index 0 1`] = `
]
`;
exports[`runs examples > example "duckorbit" example index 1 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
]
`;
exports[`runs examples > example "duration" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c s:piano duration:0.5 ]",
@@ -3590,6 +3762,8 @@ exports[`runs examples > example "fast" example index 0 1`] = `
exports[`runs examples > example "fastChunk" example index 0 1`] = `
[
"[ 0/1 → 1/4 | color:red note:0 ]",
"[ 1/4 → 1/2 | color:red note:1 ]",
"[ 1/2 → 3/4 | note:E2 ]",
"[ 3/4 → 1/1 | note:F2 ]",
"[ 1/1 → 5/4 | note:G2 ]",
@@ -3598,6 +3772,8 @@ exports[`runs examples > example "fastChunk" example index 0 1`] = `
"[ 7/4 → 2/1 | note:C3 ]",
"[ 2/1 → 9/4 | note:D3 ]",
"[ 9/4 → 5/2 | note:D2 ]",
"[ 5/2 → 11/4 | color:red note:2 ]",
"[ 11/4 → 3/1 | color:red note:3 ]",
"[ 3/1 → 13/4 | note:G2 ]",
"[ 13/4 → 7/2 | note:A2 ]",
"[ 7/2 → 15/4 | note:B2 ]",
@@ -6506,6 +6682,27 @@ exports[`runs examples > example "note" example index 2 1`] = `
]
`;
exports[`runs examples > example "note" example index 3 1`] = `
[
"[ 0/1 → 1/4 | note:fbb1 s:saw ]",
"[ 1/4 → 1/2 | note:a#0 s:saw ]",
"[ 1/2 → 3/4 | note:cbbb-1 s:saw ]",
"[ 3/4 → 1/1 | note:e##-2 s:saw ]",
"[ 1/1 → 5/4 | note:fbb1 s:saw ]",
"[ 5/4 → 3/2 | note:a#0 s:saw ]",
"[ 3/2 → 7/4 | note:cbbb-1 s:saw ]",
"[ 7/4 → 2/1 | note:e##-2 s:saw ]",
"[ 2/1 → 9/4 | note:fbb1 s:saw ]",
"[ 9/4 → 5/2 | note:a#0 s:saw ]",
"[ 5/2 → 11/4 | note:cbbb-1 s:saw ]",
"[ 11/4 → 3/1 | note:e##-2 s:saw ]",
"[ 3/1 → 13/4 | note:fbb1 s:saw ]",
"[ 13/4 → 7/2 | note:a#0 s:saw ]",
"[ 7/2 → 15/4 | note:cbbb-1 s:saw ]",
"[ 15/4 → 4/1 | note:e##-2 s:saw ]",
]
`;
exports[`runs examples > example "nrpnn" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
@@ -7311,6 +7508,64 @@ exports[`runs examples > example "ply" example index 0 1`] = `
]
`;
exports[`runs examples > example "plyForEach" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:C3 ]",
"[ 1/4 → 1/2 | note:Eb3 ]",
"[ 1/2 → 3/4 | note:G3 ]",
"[ 3/4 → 1/1 | note:Bb3 ]",
"[ 1/1 → 9/8 | note:Eb3 ]",
"[ 9/8 → 5/4 | note:G3 ]",
"[ 5/4 → 11/8 | note:Bb3 ]",
"[ 11/8 → 3/2 | note:D4 ]",
"[ 3/2 → 13/8 | note:G3 ]",
"[ 13/8 → 7/4 | note:Bb3 ]",
"[ 7/4 → 15/8 | note:D4 ]",
"[ 15/8 → 2/1 | note:F4 ]",
"[ 2/1 → 9/4 | note:C3 ]",
"[ 9/4 → 5/2 | note:Eb3 ]",
"[ 5/2 → 11/4 | note:G3 ]",
"[ 11/4 → 3/1 | note:Bb3 ]",
"[ 3/1 → 25/8 | note:Eb3 ]",
"[ 25/8 → 13/4 | note:G3 ]",
"[ 13/4 → 27/8 | note:Bb3 ]",
"[ 27/8 → 7/2 | note:D4 ]",
"[ 7/2 → 29/8 | note:G3 ]",
"[ 29/8 → 15/4 | note:Bb3 ]",
"[ 15/4 → 31/8 | note:D4 ]",
"[ 31/8 → 4/1 | note:F4 ]",
]
`;
exports[`runs examples > example "plyWith" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:C3 ]",
"[ 1/4 → 1/2 | note:Eb3 ]",
"[ 1/2 → 3/4 | note:G3 ]",
"[ 3/4 → 1/1 | note:Bb3 ]",
"[ 1/1 → 9/8 | note:Eb3 ]",
"[ 9/8 → 5/4 | note:G3 ]",
"[ 5/4 → 11/8 | note:Bb3 ]",
"[ 11/8 → 3/2 | note:D4 ]",
"[ 3/2 → 13/8 | note:G3 ]",
"[ 13/8 → 7/4 | note:Bb3 ]",
"[ 7/4 → 15/8 | note:D4 ]",
"[ 15/8 → 2/1 | note:F4 ]",
"[ 2/1 → 9/4 | note:C3 ]",
"[ 9/4 → 5/2 | note:Eb3 ]",
"[ 5/2 → 11/4 | note:G3 ]",
"[ 11/4 → 3/1 | note:Bb3 ]",
"[ 3/1 → 25/8 | note:Eb3 ]",
"[ 25/8 → 13/4 | note:G3 ]",
"[ 13/4 → 27/8 | note:Bb3 ]",
"[ 27/8 → 7/2 | note:D4 ]",
"[ 7/2 → 29/8 | note:G3 ]",
"[ 29/8 → 15/4 | note:Bb3 ]",
"[ 15/4 → 31/8 | note:D4 ]",
"[ 31/8 → 4/1 | note:F4 ]",
]
`;
exports[`runs examples > example "polymeter" example index 0 1`] = `
[
"[ 0/1 → 1/6 | note:c ]",
@@ -8736,6 +8991,108 @@ exports[`runs examples > example "scale" example index 2 1`] = `
]
`;
exports[`runs examples > example "scale" example index 3 1`] = `
[
"[ 0/1 → 1/4 | note:C3 s:piano ]",
"[ 0/1 → 1/4 | note:B3 s:piano ]",
"[ 1/4 → 3/8 | note:Gb2 s:piano ]",
"[ 3/8 → 1/2 | note:F2 s:piano ]",
"[ 1/2 → 3/4 | note:A2 s:piano ]",
"[ 1/2 → 3/4 | note:D4 s:piano ]",
"[ 3/4 → 1/1 | note:G3 s:piano ]",
"[ 1/1 → 5/4 | note:C3 s:piano ]",
"[ 1/1 → 5/4 | note:C4 s:piano ]",
"[ 5/4 → 11/8 | note:Gb2 s:piano ]",
"[ 11/8 → 3/2 | note:E2 s:piano ]",
"[ 3/2 → 7/4 | note:A2 s:piano ]",
"[ 3/2 → 7/4 | note:Eb4 s:piano ]",
"[ 7/4 → 2/1 | note:F#3 s:piano ]",
"[ 2/1 → 9/4 | note:C3 s:piano ]",
"[ 2/1 → 9/4 | note:B3 s:piano ]",
"[ 9/4 → 19/8 | note:Gb2 s:piano ]",
"[ 19/8 → 5/2 | note:F2 s:piano ]",
"[ 5/2 → 11/4 | note:Ab2 s:piano ]",
"[ 5/2 → 11/4 | note:D4 s:piano ]",
"[ 11/4 → 3/1 | note:G3 s:piano ]",
"[ 3/1 → 13/4 | note:C3 s:piano ]",
"[ 3/1 → 13/4 | note:C4 s:piano ]",
"[ 13/4 → 27/8 | note:Gb2 s:piano ]",
"[ 27/8 → 7/2 | note:E2 s:piano ]",
"[ 7/2 → 15/4 | note:Ab2 s:piano ]",
"[ 7/2 → 15/4 | note:Eb4 s:piano ]",
"[ 15/4 → 4/1 | note:F#3 s:piano ]",
]
`;
exports[`runs examples > example "scale" example index 4 1`] = `
[
"[ 0/1 → 1/16 | note:Gb1 ]",
"[ 1/16 → 1/8 | note:Gb1 ]",
"[ 1/8 → 3/16 | note:Gb1 ]",
"[ 3/16 → 1/4 | note:Gb1 ]",
"[ 1/4 → 5/16 | note:Gb1 ]",
"[ 5/16 → 3/8 | note:Gb1 ]",
"[ 3/8 → 7/16 | note:Gb1 ]",
"[ 7/16 → 1/2 | note:Gb1 ]",
"[ 1/2 → 9/16 | note:Gb1 ]",
"[ 9/16 → 5/8 | note:Gb1 ]",
"[ 5/8 → 11/16 | note:Gb1 ]",
"[ 11/16 → 3/4 | note:Gb1 ]",
"[ 3/4 → 13/16 | note:Gb1 ]",
"[ 13/16 → 7/8 | note:Gb1 ]",
"[ 7/8 → 15/16 | note:Gb1 ]",
"[ 15/16 → 1/1 | note:Gb1 ]",
"[ 1/1 → 17/16 | note:Cb3 ]",
"[ 17/16 → 9/8 | note:Cb3 ]",
"[ 9/8 → 19/16 | note:Cb3 ]",
"[ 19/16 → 5/4 | note:Cb3 ]",
"[ 5/4 → 21/16 | note:Cb3 ]",
"[ 21/16 → 11/8 | note:Cb3 ]",
"[ 11/8 → 23/16 | note:Cb3 ]",
"[ 23/16 → 3/2 | note:Cb3 ]",
"[ 3/2 → 25/16 | note:Cb3 ]",
"[ 25/16 → 13/8 | note:Cb3 ]",
"[ 13/8 → 27/16 | note:Cb3 ]",
"[ 27/16 → 7/4 | note:Cb3 ]",
"[ 7/4 → 29/16 | note:Cb3 ]",
"[ 29/16 → 15/8 | note:Cb3 ]",
"[ 15/8 → 31/16 | note:Cb3 ]",
"[ 31/16 → 2/1 | note:Cb3 ]",
"[ 2/1 → 33/16 | note:Eb4 ]",
"[ 33/16 → 17/8 | note:Eb4 ]",
"[ 17/8 → 35/16 | note:Eb4 ]",
"[ 35/16 → 9/4 | note:Eb4 ]",
"[ 9/4 → 37/16 | note:Eb4 ]",
"[ 37/16 → 19/8 | note:Eb4 ]",
"[ 19/8 → 39/16 | note:Eb4 ]",
"[ 39/16 → 5/2 | note:Eb4 ]",
"[ 5/2 → 41/16 | note:Eb4 ]",
"[ 41/16 → 21/8 | note:Eb4 ]",
"[ 21/8 → 43/16 | note:Eb4 ]",
"[ 43/16 → 11/4 | note:Eb4 ]",
"[ 11/4 → 45/16 | note:Eb4 ]",
"[ 45/16 → 23/8 | note:Eb4 ]",
"[ 23/8 → 47/16 | note:Eb4 ]",
"[ 47/16 → 3/1 | note:Eb4 ]",
"[ 3/1 → 49/16 | note:Db2 ]",
"[ 49/16 → 25/8 | note:Db2 ]",
"[ 25/8 → 51/16 | note:Db2 ]",
"[ 51/16 → 13/4 | note:Db2 ]",
"[ 13/4 → 53/16 | note:Db2 ]",
"[ 53/16 → 27/8 | note:Db2 ]",
"[ 27/8 → 55/16 | note:Db2 ]",
"[ 55/16 → 7/2 | note:Db2 ]",
"[ 7/2 → 57/16 | note:Db2 ]",
"[ 57/16 → 29/8 | note:Db2 ]",
"[ 29/8 → 59/16 | note:Db2 ]",
"[ 59/16 → 15/4 | note:Db2 ]",
"[ 15/4 → 61/16 | note:Db2 ]",
"[ 61/16 → 31/8 | note:Db2 ]",
"[ 31/8 → 63/16 | note:Db2 ]",
"[ 63/16 → 4/1 | note:Db2 ]",
]
`;
exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:C3 ]",
@@ -11432,6 +11789,112 @@ exports[`runs examples > example "vowel" example index 1 1`] = `
]
`;
exports[`runs examples > example "warp" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (1/8 → 1/5) ⇝ 1/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 1/8 ⇜ (1/5 → 1/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 1/4 → 3/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (3/8 → 2/5) ⇝ 1/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 3/8 ⇜ (2/5 → 1/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (1/2 → 3/5) ⇝ 5/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 1/2 ⇜ (3/5 → 5/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 5/8 → 3/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (3/4 → 4/5) ⇝ 7/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 3/4 ⇜ (4/5 → 7/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 7/8 → 1/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 1/1 → 9/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (9/8 → 6/5) ⇝ 5/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 9/8 ⇜ (6/5 → 5/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 5/4 → 11/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (11/8 → 7/5) ⇝ 3/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 11/8 ⇜ (7/5 → 3/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (3/2 → 8/5) ⇝ 13/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 3/2 ⇜ (8/5 → 13/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 13/8 → 7/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (7/4 → 9/5) ⇝ 15/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 7/4 ⇜ (9/5 → 15/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 15/8 → 2/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 2/1 → 17/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (17/8 → 11/5) ⇝ 9/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 17/8 ⇜ (11/5 → 9/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 9/4 → 19/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (19/8 → 12/5) ⇝ 5/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 19/8 ⇜ (12/5 → 5/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (5/2 → 13/5) ⇝ 21/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 5/2 ⇜ (13/5 → 21/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 21/8 → 11/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (11/4 → 14/5) ⇝ 23/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 11/4 ⇜ (14/5 → 23/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 23/8 → 3/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 3/1 → 25/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (25/8 → 16/5) ⇝ 13/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 25/8 ⇜ (16/5 → 13/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 13/4 → 27/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (27/8 → 17/5) ⇝ 7/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 27/8 ⇜ (17/5 → 7/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (7/2 → 18/5) ⇝ 29/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 7/2 ⇜ (18/5 → 29/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 29/8 → 15/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (15/4 → 19/5) ⇝ 31/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 15/4 ⇜ (19/5 → 31/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 31/8 → 4/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
]
`;
exports[`runs examples > example "warpmode" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:asym ]",
"[ (1/8 → 1/5) ⇝ 1/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:asym ]",
"[ 1/8 ⇜ (1/5 → 1/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]",
"[ 1/4 → 3/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]",
"[ (3/8 → 2/5) ⇝ 1/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]",
"[ 3/8 ⇜ (2/5 → 1/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:asym ]",
"[ (1/2 → 3/5) ⇝ 5/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:bendp ]",
"[ 1/2 ⇜ (3/5 → 5/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]",
"[ 5/8 → 3/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]",
"[ (3/4 → 4/5) ⇝ 7/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]",
"[ 3/4 ⇜ (4/5 → 7/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:bendp ]",
"[ 7/8 → 1/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:bendp ]",
"[ 1/1 → 9/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (9/8 → 6/5) ⇝ 5/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 9/8 ⇜ (6/5 → 5/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 5/4 → 11/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (11/8 → 7/5) ⇝ 3/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 11/8 ⇜ (7/5 → 3/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (3/2 → 8/5) ⇝ 13/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:logistic ]",
"[ 3/2 ⇜ (8/5 → 13/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]",
"[ 13/8 → 7/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]",
"[ (7/4 → 9/5) ⇝ 15/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]",
"[ 7/4 ⇜ (9/5 → 15/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:logistic ]",
"[ 15/8 → 2/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:logistic ]",
"[ 2/1 → 17/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:sync ]",
"[ (17/8 → 11/5) ⇝ 9/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:sync ]",
"[ 17/8 ⇜ (11/5 → 9/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]",
"[ 9/4 → 19/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]",
"[ (19/8 → 12/5) ⇝ 5/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]",
"[ 19/8 ⇜ (12/5 → 5/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:sync ]",
"[ (5/2 → 13/5) ⇝ 21/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:wormhole ]",
"[ 5/2 ⇜ (13/5 → 21/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]",
"[ 21/8 → 11/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]",
"[ (11/4 → 14/5) ⇝ 23/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]",
"[ 11/4 ⇜ (14/5 → 23/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:wormhole ]",
"[ 23/8 → 3/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:wormhole ]",
"[ 3/1 → 25/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:brownian ]",
"[ (25/8 → 16/5) ⇝ 13/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:brownian ]",
"[ 25/8 ⇜ (16/5 → 13/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]",
"[ 13/4 → 27/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]",
"[ (27/8 → 17/5) ⇝ 7/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]",
"[ 27/8 ⇜ (17/5 → 7/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:brownian ]",
"[ (7/2 → 18/5) ⇝ 29/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:asym ]",
"[ 7/2 ⇜ (18/5 → 29/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]",
"[ 29/8 → 15/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]",
"[ (15/4 → 19/5) ⇝ 31/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]",
"[ 15/4 ⇜ (19/5 → 31/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:asym ]",
"[ 31/8 → 4/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:asym ]",
]
`;
exports[`runs examples > example "wchoose" example index 0 1`] = `
[
"[ 0/1 → 1/5 | note:c2 s:sine ]",
@@ -11636,6 +12099,128 @@ exports[`runs examples > example "withValue" example index 0 1`] = `
]
`;
exports[`runs examples > example "wt" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (1/8 → 1/5) ⇝ 1/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 1/8 ⇜ (1/5 → 1/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 1/4 → 3/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (3/8 → 2/5) ⇝ 1/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 3/8 ⇜ (2/5 → 1/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (1/2 → 3/5) ⇝ 5/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 1/2 ⇜ (3/5 → 5/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 5/8 → 3/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (3/4 → 4/5) ⇝ 7/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 3/4 ⇜ (4/5 → 7/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 7/8 → 1/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 1/1 → 9/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (9/8 → 6/5) ⇝ 5/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 9/8 ⇜ (6/5 → 5/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 5/4 → 11/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (11/8 → 7/5) ⇝ 3/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 11/8 ⇜ (7/5 → 3/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (3/2 → 8/5) ⇝ 13/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 3/2 ⇜ (8/5 → 13/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 13/8 → 7/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (7/4 → 9/5) ⇝ 15/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 7/4 ⇜ (9/5 → 15/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 15/8 → 2/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 2/1 → 17/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (17/8 → 11/5) ⇝ 9/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 17/8 ⇜ (11/5 → 9/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 9/4 → 19/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (19/8 → 12/5) ⇝ 5/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 19/8 ⇜ (12/5 → 5/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (5/2 → 13/5) ⇝ 21/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 5/2 ⇜ (13/5 → 21/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 21/8 → 11/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (11/4 → 14/5) ⇝ 23/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 11/4 ⇜ (14/5 → 23/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 23/8 → 3/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 3/1 → 25/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (25/8 → 16/5) ⇝ 13/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 25/8 ⇜ (16/5 → 13/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 13/4 → 27/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (27/8 → 17/5) ⇝ 7/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 27/8 ⇜ (17/5 → 7/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (7/2 → 18/5) ⇝ 29/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 7/2 ⇜ (18/5 → 29/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 29/8 → 15/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (15/4 → 19/5) ⇝ 31/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 15/4 ⇜ (19/5 → 31/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 31/8 → 4/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
]
`;
exports[`runs examples > example "wtphaserand" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/16 → 1/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/8 → 3/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 3/16 → 1/4 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/4 → 5/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 5/16 → 3/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 3/8 → 7/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 7/16 → 1/2 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/2 → 9/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 9/16 → 5/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 5/8 → 11/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 11/16 → 3/4 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 3/4 → 13/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 13/16 → 7/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 7/8 → 15/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 15/16 → 1/1 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/1 → 17/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 17/16 → 9/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 9/8 → 19/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 19/16 → 5/4 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 5/4 → 21/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 21/16 → 11/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 11/8 → 23/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 23/16 → 3/2 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 3/2 → 25/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 25/16 → 13/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 13/8 → 27/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 27/16 → 7/4 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 7/4 → 29/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 29/16 → 15/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 15/8 → 31/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 31/16 → 2/1 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 2/1 → 33/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 33/16 → 17/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 17/8 → 35/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 35/16 → 9/4 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 9/4 → 37/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 37/16 → 19/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 19/8 → 39/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 39/16 → 5/2 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 5/2 → 41/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 41/16 → 21/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 21/8 → 43/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 43/16 → 11/4 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 11/4 → 45/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 45/16 → 23/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 23/8 → 47/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 47/16 → 3/1 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 3/1 → 49/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 49/16 → 25/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 25/8 → 51/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 51/16 → 13/4 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 13/4 → 53/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 53/16 → 27/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 27/8 → 55/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 55/16 → 7/2 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 7/2 → 57/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 57/16 → 29/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 29/8 → 59/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 59/16 → 15/4 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 15/4 → 61/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 61/16 → 31/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 31/8 → 63/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 63/16 → 4/1 | s:basique bank:wt_digital wtphaserand:1 ]",
]
`;
exports[`runs examples > example "xfade" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh gain:0 ]",
+5
View File
@@ -0,0 +1,5 @@
The FontStruction “TIC-80 wide font”
(https://fontstruct.com/fontstructions/show/1388526) by “nesbox” is licensed
under a Creative Commons CC0 Public Domain Dedication license
(http://creativecommons.org/publicdomain/zero/1.0/).
[ancestry]
+16
View File
@@ -0,0 +1,16 @@
The font file in this archive was created using Fontstruct the free, online
font-building tool.
This font was created by “nesbox”.
This font has a homepage where this archive and other versions may be found:
https://fontstruct.com/fontstructions/show/1388526
[ancestry]
Try Fontstruct at https://fontstruct.com
Its easy and its fun.
Fontstruct is copyright ©2017-2025 Rob Meek
LEGAL NOTICE:
In using this font you must comply with the licensing terms described in the
file “license.txt” included with this archive.
If you redistribute the font file in this archive, it must be accompanied by all
the other files from this archive, including this one.
Binary file not shown.
+28
View File
@@ -0,0 +1,28 @@
{
"_base": "https://raw.githubusercontent.com/tidalcycles/uzu-wavetables/main/",
"wt_digital": [
"wt_digital/wt_bad_day.wav",
"wt_digital/wt_basique.wav",
"wt_digital/wt_crickets.wav",
"wt_digital/wt_curses.wav",
"wt_digital/wt_echoes.wav"
],
"wt_digital_bad_day": ["wt_digital/wt_bad_day.wav"],
"wt_digital_basique": ["wt_digital/wt_basique.wav"],
"wt_digital_crickets": ["wt_digital/wt_crickets.wav"],
"wt_digital_curses": ["wt_digital/wt_curses.wav"],
"wt_digital_echoes": ["wt_digital/wt_echoes.wav"],
"wt_vgame": [
"wt_vgame/wt_vgame10.wav",
"wt_vgame/wt_vgame11.wav",
"wt_vgame/wt_vgame12.wav",
"wt_vgame/wt_vgame13.wav",
"wt_vgame/wt_vgame14.wav",
"wt_vgame/wt_vgame15.wav",
"wt_vgame/wt_vgame16.wav",
"wt_vgame/wt_vgame17.wav",
"wt_vgame/wt_vgame18.wav",
"wt_vgame/wt_vgame19.wav",
"wt_vgame/wt_vgame20.wav"
]
}
+1
View File
@@ -103,6 +103,7 @@ export const SIDEBAR: Sidebar = {
Understand: [
{ text: 'Coding syntax', link: 'learn/code' },
{ text: 'Pitch', link: 'understand/pitch' },
{ text: 'Xen Harmonic Functions', link: 'learn/xen' },
{ text: 'Cycles', link: 'understand/cycles' },
{ text: 'Voicings', link: 'understand/voicings' },
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
+1 -1
View File
@@ -44,7 +44,7 @@ xxx("foo").yyy("bar")
Generally, `xxx` and `yyy` are called [_functions_](<https://en.wikipedia.org/wiki/Function_(computer_programming)>), while `foo` and `bar` are called function [_arguments_ or _parameters_](<https://en.wikipedia.org/wiki/Parameter_(computer_programming)>).
So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data.
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is appended with a dot (`.`).
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is preceded with a dot (`.`).
Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code.
You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects.
+8
View File
@@ -59,6 +59,14 @@ Furthermore, strudel also loads instrument samples from [VCSL](https://github.co
To see which sample names are available, open the `sounds` tab in the [REPL](https://strudel.cc/).
You can also create custom aliases for existing sounds using the `soundAlias` function:
<MiniRepl
client:idle
tune={`soundAlias("RolandTR808_bd", "kick")
s("kick")`}
/>
Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples themselves are not loaded until they are actually played.
This behaviour of loading things only when they are needed is also called `lazy loading`.
While it saves resources, it can also lead to sounds not being audible the first time they are triggered, because the sound is still loading.
+95
View File
@@ -0,0 +1,95 @@
---
title: Xen Harmonic Functions
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# Xen Harmonic Functions
These functions allow the use of scales other than your typical chromatic 12 based ones.
### tune(scale)
<JsDoc client:idle name="tune" h={0} />
Here's an example of how to configure a basic hexany scale:
<MiniRepl client:idle tune={`"0 1 2 3 4 5".tune("hexany15").mul("220").freq()`} />
Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3`
For a full list of available scales from tunejs, see http://abbernie.github.io/tune/scales.html
You can set your root to be a particular note with `getFreq`
<MiniRepl
client:idle
tune={`"4 8 9 10 - - 5 7 9 11 - -".tune("tranh3")
.mul(getFreq('c3'))
.freq().clip(.5).room(1)`}
/>
Some tunings become more pronounced with a longer reverb decay:
<MiniRepl
client:idle
tune={`"<[5 6 8 10] - [5 7 9 12] -> -".tune("gumbeng")
.mul(getFreq('c3'))
.freq().clip(.8).room("3:10").rdim(10000).rfade(5)`}
/>
Additionally, you can combo this with `fmap` so that the base note changes:
<MiniRepl
client:idle
tune={`"9 11 12 10 - - -".tune("gunkali")
.mul("<c3 c3 a3 d#3>".fmap(getFreq))
.freq().legato("2 .7").room("1:15").rdim(8500).rlp(14000).rfade(8)`}
/>
Combining this with various polyrhythm tricks can become very evocative:
<MiniRepl
client:idle
tune={`"<[0 3 1 -] [-1 4 2 8]> ~ ~,<-4 -5>"
.transpose(4)
.tune("iraq")
.mul("<c3 d3 c#3>".fmap(getFreq))
.freq().clip(.5).room(1).rfade(9)`}
/>
Another helpful trick when exploring new tunings is to strum them.
Many have a much more enchanting sound that was chosen over many generations of musicians for being strummed.
Take the `sanza` tuning:
<MiniRepl
client:idle
tune={`"4 5 6 7 8 9".tune("sanza")
.mul(getFreq('c3'))
.freq()`}
/>
Notes 7 and 9 will clash quite a bit if you arp them normally. Many tunings will have this sort of sound, and it can feel distracting on its own.
See how close they are on the pitch wheel?
<MiniRepl client:idle tune={`"[7 9]!3".tune("sanza").mul(getFreq('c3')).freq()._pitchwheel()`} />
This quality is often due to how the tunings were formed with instruments that were played differently than a piano.
As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical:
<MiniRepl
client:idle
tune={`"[0 1 2 3 4 5 6]@0.3 -"
.transpose("<2 5 8 1>")
.tune("sanza")
.mul(getFreq('c3')).freq()
.legato("3").room(1).rfade(5)`}
/>
Note the legato and reverb effects make sure the sound of the strumming gets to wash together. Alternating the direction of the strum can make the
tones sound even more alive, too.
The `tranh3` tuning has a similar set of notes, with two clashing. You might trying plugging that in above and see if you find a favorite strumming pattern.
@@ -7,6 +7,21 @@ layout: ../../layouts/MainLayout.astro
This Guide shows you the different ways to get started with using Strudel in your own project.
## Respect the license
First, please take a moment to understand Strudel's free/open source license,
[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.en.html).
Here is a lay summary, but check the license for legal definitions and responsibilities.
- You can distribute modified versions if you keep track of the changes and the date you made them.
- You must license derivative work under the same license.
- Source code must be distributed along with web publication.
Among other things, it means that when you share your work, the whole application must be shared under the same free/open source license, or one compatible with it. This is because we want Strudel to stay free/open source. In other words, you are not permitted to distribute integrations of Strudel with libraries or other code that does not have a compatible free/open source license.
This also applies to clones informed by reading Strudel's source code, as legally speaking, that counts as a 'derivative work'. Again, please [read the licence](https://www.gnu.org/licenses/agpl-3.0.en.html) for details.
## Embedding the Strudel REPL
There are 3 quick ways to embed strudel in your website:
+5 -5
View File
@@ -262,14 +262,14 @@ It is quite common that there are many ways to express the same idea.
**selecting sample numbers separately**
Instead of using ":", we can also use the `n` function to select sample numbers:
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
This is shorter and more readable than:
Instead of selecting sample numbers one by one:
<MiniRepl client:visible tune={`sound("jazz:0 jazz:1 [jazz:4 jazz:2] jazz:3*2")`} punchcard />
We can also use the `n` function to make it shorter and more readable:
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
## Recap
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
+10 -1
View File
@@ -80,6 +80,8 @@
min-width: 300px !important;
max-height: 400px !important;
background-color: var(--lineHighlight) !important;
overflow: auto;
background: var(--background) !important;
}
/* Main tooltip container */
@@ -91,7 +93,7 @@
font-size: var(--font-size, 13px);
line-height: 1.4;
max-width: 600px;
max-height: 400px;
height: 100%;
min-width: 400px;
white-space: normal !important;
overflow-y: auto !important;
@@ -112,6 +114,13 @@
margin: 0 0 8px 0;
}
.autocomplete-info-function-synonyms {
margin: 0 0 12px 0;
color: var(--foreground);
line-height: 1.5;
opacity: 0.8;
}
.autocomplete-info-function-description {
margin: 0 0 12px 0;
color: var(--foreground);
@@ -2,9 +2,32 @@ import { useMemo, useState } from 'react';
import jsdocJson from '../../../../../doc.json';
import { Textbox } from '../textbox/Textbox';
const availableFunctions = jsdocJson.docs
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
const availableFunctions = (() => {
const seen = new Set(); // avoid repetition
const functions = [];
for (const doc of jsdocJson.docs) {
if (!isValid(doc)) continue;
functions.push(doc);
const synonyms = doc.synonyms || [];
for (const s of synonyms) {
if (!s || seen.has(s)) continue;
seen.add(s);
// Swap `doc.name` in for `s` in the list of synonyms
const synonymsWithDoc = [doc.name, ...synonyms].filter((x) => x && x !== s);
functions.push({
...doc,
name: s, // update names for the synonym
longname: s,
synonyms: synonymsWithDoc,
synonyms_text: synonymsWithDoc.join(', '),
});
}
}
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
})();
const getInnerText = (html) => {
var div = document.createElement('div');
@@ -21,10 +44,10 @@ export function Reference() {
return true;
}
const lowCaseSearch = search.toLowerCase();
const lowerCaseSearch = search.toLowerCase();
return (
entry.name.toLowerCase().includes(lowCaseSearch) ||
(entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false)
entry.name.toLowerCase().includes(lowerCaseSearch) ||
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
);
});
}, [search]);
@@ -74,6 +74,7 @@ const fontFamilyOptions = {
FiraCode: 'FiraCode',
'FiraCode-SemiBold': 'FiraCode SemiBold',
teletext: 'teletext',
tic80: 'tic80',
mode7: 'mode7',
BigBlueTerminal: 'BigBlueTerminal',
x3270: 'x3270',
@@ -44,6 +44,9 @@ export function SoundsTab() {
if (soundsFilter === soundFilterType.SYNTHS) {
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
}
if (soundsFilter === soundFilterType.WAVETABLES) {
return filtered.filter(([_, { data }]) => data.type === 'wavetable');
}
//TODO: tidy this up, it does not need to be saved in settings
if (soundsFilter === 'importSounds') {
return [];
@@ -54,6 +57,9 @@ export function SoundsTab() {
// holds mutable ref to current triggered sound
const trigRef = useRef();
// Used to cycle through sound previews on banks with multiple sounds
let soundPreviewIdx = 0;
// stop current sound on mouseup
useEvent('mouseup', () => {
const t = trigRef.current;
@@ -74,6 +80,7 @@ export function SoundsTab() {
samples: 'samples',
drums: 'drum-machines',
synths: 'Synths',
wavetables: 'Wavetables',
user: 'User',
importSounds: 'import-sounds',
}}
@@ -110,11 +117,13 @@ export function SoundsTab() {
const params = {
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
s: name,
n: soundPreviewIdx,
clip: 1,
release: 0.5,
sustain: 1,
duration: 0.5,
};
soundPreviewIdx++;
const time = ctx.currentTime + 0.05;
const onended = () => trigRef.current?.node?.disconnect();
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
@@ -126,6 +135,7 @@ export function SoundsTab() {
{' '}
{name}
{data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
{data?.type === 'wavetable' ? `(${getSamples(data.tables)})` : ''}
{data?.type === 'soundfont' ? `(${data.fonts.length})` : ''}
</span>
);
+2 -8
View File
@@ -1,4 +1,4 @@
import { registerSound, onTriggerSample } from '@strudel/webaudio';
import { registerSampleSource } from '@strudel/webaudio';
import { isAudioFile } from './files.mjs';
import { logger } from '@strudel/core';
@@ -76,13 +76,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
})
.map((title) => titlePathMap.get(title));
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: value,
baseUrl: undefined,
prebake: false,
tag: undefined,
});
registerSampleSource(key, value, { prebake: false });
});
logger('imported sounds registered!', 'success');
+3
View File
@@ -32,6 +32,9 @@ export async function prebake() {
prebake: true,
tag: 'drum-machines',
}),
samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, {
prebake: true,
}),
samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }),
samples(
{
+1
View File
@@ -13,6 +13,7 @@ export const soundFilterType = {
DRUMS: 'drums',
SAMPLES: 'samples',
SYNTHS: 'synths',
WAVETABLES: 'wavetables',
ALL: 'all',
};
+5
View File
@@ -50,6 +50,11 @@
src: url('/fonts/teletext/EuropeanTeletext.ttf');
size-adjust: 90%;
}
@font-face {
font-family: 'tic80';
src: url('/fonts/tic80/tic-80-wide-font.otf');
size-adjust: 60%;
}
@font-face {
font-family: 'mode7';
src: url('/fonts/mode7/MODE7GX3.TTF');