diff --git a/.forgejo/workflows/deploy-warm-beta.yml b/.forgejo/workflows/deploy-warm-beta.yml index 2de6670c8..e6636a729 100644 --- a/.forgejo/workflows/deploy-warm-beta.yml +++ b/.forgejo/workflows/deploy-warm-beta.yml @@ -1,6 +1,9 @@ name: Build and Deploy to beta (warm.strudel.cc) -on: [workflow_dispatch] +on: + push: + branches: + - main # Allow one concurrent deployment concurrency: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..a13e379d9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,88 @@ +# changelog + +NOTE: you can generate this with `node warm.js`. it might still not be perfectly sorted... + +## november 2025 + +- 2025-11-27T22:03:53+01:00 [perf] in `noise`, let noiseMix do the disconnect when it exists by jeromew in: [#1783](https://codeberg.org/uzu/strudel/pulls/1783) +- 2025-11-27T22:01:01+01:00 Bug Fix: Retries for sounds tab by glossing in: [#1754](https://codeberg.org/uzu/strudel/pulls/1754) +- 2025-11-27T20:36:46+01:00 [hydra] return the hydra object when await initHydra(..) is called by jeromew in: [#1784](https://codeberg.org/uzu/strudel/pulls/1784) +- 2025-11-27T20:36:14+01:00 Use errorLogger for query and tonal errors by glossing in: [#1782](https://codeberg.org/uzu/strudel/pulls/1782) +- 2025-11-27T20:27:10+01:00 [perf] level 5 `connect-leak` on `vowel` by jeromew in: [#1779](https://codeberg.org/uzu/strudel/pulls/1779) +- 2025-11-26T08:23:38+01:00 feat: add prebake script import button for loading .strudel files by daslyfe in: [#1774](https://codeberg.org/uzu/strudel/pulls/1774) +- 2025-11-26T07:39:28+01:00 Fix Sampler port trampling by Dayglo in: [#1717](https://codeberg.org/uzu/strudel/pulls/1717) +- 2025-11-25T20:59:09+01:00 Feat: Hook up octave and fix example by glossing in: [#1773](https://codeberg.org/uzu/strudel/pulls/1773) +- 2025-11-25T20:48:09+01:00 [perf] fix `connect-leak` in `tremolo` param by jeromew in: [#1780](https://codeberg.org/uzu/strudel/pulls/1780) +- 2025-11-24T17:51:20+01:00 filter modulation improvements! by daslyfe in: [#1769](https://codeberg.org/uzu/strudel/pulls/1769) +- 2025-11-24T01:20:53+01:00 Add o as a synonym for orbit by daslyfe in: [#1766](https://codeberg.org/uzu/strudel/pulls/1766) +- 2025-11-23T22:23:26+01:00 [perf] fix `connect-leak` in `fm` modulation by jeromew in: [#1758](https://codeberg.org/uzu/strudel/pulls/1758) +- 2025-11-23T09:47:24+01:00 Fix interoperability issue between `all` and `await initHydra()` by jeromew in: [#1663](https://codeberg.org/uzu/strudel/pulls/1663) +- 2025-11-23T04:23:06+01:00 Bug fix: Swap l/r gains with temp variable by glossing in: [#1768](https://codeberg.org/uzu/strudel/pulls/1768) +- 2025-11-23T00:52:28+01:00 FIX: prevent LFO filter modulation from popping from negative values by daslyfe in: [#1767](https://codeberg.org/uzu/strudel/pulls/1767) +- 2025-11-21T01:56:11+01:00 Bug Fix: Use frac due to negative frequencies from FM by glossing in: [#1759](https://codeberg.org/uzu/strudel/pulls/1759) +- 2025-11-20T19:48:16+01:00 [perf] fix `connect-leak` added by #1742 when noise() is not used by jeromew in: [#1757](https://codeberg.org/uzu/strudel/pulls/1757) +- 2025-11-20T14:51:00+01:00 [perf] fix `connect-leak` in `delay` effect by jeromew in: [#1755](https://codeberg.org/uzu/strudel/pulls/1755) +- 2025-11-20T14:49:41+01:00 [perf] fix `connect leak` when .noise() is in the mix by jeromew in: [#1742](https://codeberg.org/uzu/strudel/pulls/1742) +- 2025-11-18T23:52:45+01:00 wchooseCycles has now notes in an example by scrappy_fiddler in: [#1748](https://codeberg.org/uzu/strudel/pulls/1748) +- 2025-11-17T05:31:54+01:00 Feature: Partials by glossing in: [#1659](https://codeberg.org/uzu/strudel/pulls/1659) +- 2025-11-16T22:06:14+01:00 README: update superdough documentation by TristanMlct in: [#1741](https://codeberg.org/uzu/strudel/pulls/1741) +- 2025-11-16T21:08:54+01:00 Worklet optimizations by glossing in: [#1730](https://codeberg.org/uzu/strudel/pulls/1730) +- 2025-11-15T14:59:30+01:00 [perf] disconnect Offline AudioNode connections in generateReverb by jeromew in: [#1740](https://codeberg.org/uzu/strudel/pulls/1740) +- 2025-11-15T14:57:47+01:00 [perf] disconnect `send` effect AudioNode when `room` is used by jeromew in: [#1736](https://codeberg.org/uzu/strudel/pulls/1736) +- 2025-11-12T21:22:34+01:00 [supradough] fix: unstable filter by froos in: [#1593](https://codeberg.org/uzu/strudel/pulls/1593) +- 2025-11-12T21:12:07+01:00 Fix link syntax in `project-start` by Kissaki in: [#1724](https://codeberg.org/uzu/strudel/pulls/1724) +- 2025-11-12T21:06:13+01:00 Fix web README sample code by Kissaki in: [#1725](https://codeberg.org/uzu/strudel/pulls/1725) +- 2025-11-12T20:59:39+01:00 Fix typo: 'studel' -> 'strudel' by drhayes in: [#1726](https://codeberg.org/uzu/strudel/pulls/1726) +- 2025-11-12T15:23:41+01:00 fix for node 24 support in tests - #1718 by ausav in: [#1719](https://codeberg.org/uzu/strudel/pulls/1719) +- 2025-11-11T09:00:49+01:00 soundAlias example fix by PepsiiMan in: [#1720](https://codeberg.org/uzu/strudel/pulls/1720) +- 2025-11-10T08:18:36+01:00 fix: Make Supradough package build by munshkr in: [#1711](https://codeberg.org/uzu/strudel/pulls/1711) +- 2025-11-10T08:18:00+01:00 added docs for spaces in scale names by ondras in: [#1715](https://codeberg.org/uzu/strudel/pulls/1715) +- 2025-11-10T02:44:01+01:00 Feature: LFOs for Filters by glossing in: [#1636](https://codeberg.org/uzu/strudel/pulls/1636) +- 2025-11-05T17:07:00+01:00 improvement: sync midi messages to audio context clock by daslyfe in: [#1708](https://codeberg.org/uzu/strudel/pulls/1708) +- 2025-11-04T19:36:14+01:00 Add stick button mappings to gamepad implementation and improve docs by kaosuryoko in: [#1696](https://codeberg.org/uzu/strudel/pulls/1696) +- 2025-11-04T19:29:35+01:00 Refactor sound stopping and triggering logic in SoundsTab component by IJOL in: [#1688](https://codeberg.org/uzu/strudel/pulls/1688) +- 2025-11-04T19:13:21+01:00 Add setting to toggle pattern auto-start on pattern change by moumar in: [#1690](https://codeberg.org/uzu/strudel/pulls/1690) +- 2025-11-04T18:58:50+01:00 Voicings JSDoc by sharkeys_lunchbox in: [#1686](https://codeberg.org/uzu/strudel/pulls/1686) +- 2025-11-01T07:50:40+01:00 FIX: Loading local samples uses too much memory by daslyfe in: [#1706](https://codeberg.org/uzu/strudel/pulls/1706) + + +## october 2025 + +- 2025-10-28T22:58:24+01:00 Bug Fix: Handle scale-for-notes when n also supplied by glossing in: [#1625](https://codeberg.org/uzu/strudel/pulls/1625) +- 2025-10-28T22:51:16+01:00 Repurpose vim shortcuts for usability by dtricks in: [#1624](https://codeberg.org/uzu/strudel/pulls/1624) +- 2025-10-28T22:35:21+01:00 Add support for euclidian in mondo with `bd&3:8` by TristanCacqueray in: [#1630](https://codeberg.org/uzu/strudel/pulls/1630) +- 2025-10-27T10:42:07+01:00 Use bunny cdn for all samples and json files by yaxu in: [#1701](https://codeberg.org/uzu/strudel/pulls/1701) +- 2025-10-26T22:52:54+01:00 degithub - switch some samples to bunnycdn by yaxu in: [#1697](https://codeberg.org/uzu/strudel/pulls/1697) +- 2025-10-26T17:09:22+01:00 Fix ZZFX example by moumar in: [#1685](https://codeberg.org/uzu/strudel/pulls/1685) +- 2025-10-23T15:56:04+02:00 Make osc port and host configurable. Changes dependencies. by yaxu in: [#1682](https://codeberg.org/uzu/strudel/pulls/1682) +- 2025-10-23T03:47:31+02:00 Adds back shape to superdough by glossing in: [#1672](https://codeberg.org/uzu/strudel/pulls/1672) +- 2025-10-22T22:15:19+02:00 Fix sampler.mjs githubPath by jeromew in: [#1684](https://codeberg.org/uzu/strudel/pulls/1684) +- 2025-10-22T16:20:50+02:00 Fix onPaint for widgets by yaxu in: [#1658](https://codeberg.org/uzu/strudel/pulls/1658) +- 2025-10-22T15:19:12+02:00 Fix a bug introduced by #4e17cfbdd6 by milliganf in: [#1679](https://codeberg.org/uzu/strudel/pulls/1679) +- 2025-10-18T04:29:20+02:00 Bug Fix: Wavetable: phase wrapping at 1 and detune by glossing in: [#1620](https://codeberg.org/uzu/strudel/pulls/1620) +- 2025-10-16T20:26:28+02:00 github samples: default to "samples" if repository is not specified by prezmop in: [#1644](https://codeberg.org/uzu/strudel/pulls/1644) +- 2025-10-16T17:33:39+02:00 Docs: add example of custom chained function by dariusk in: [#1642](https://codeberg.org/uzu/strudel/pulls/1642) +- 2025-10-14T12:39:48+02:00 textbox by yaxu in: [#1650](https://codeberg.org/uzu/strudel/pulls/1650) +- 2025-10-13T07:05:57+02:00 Feature: Wavetable FM by glossing in: [#1623](https://codeberg.org/uzu/strudel/pulls/1623) +- 2025-10-12T20:43:42+02:00 fix: repair REPL sample sources and URL concat (#1640) by erikfox in: [#1646](https://codeberg.org/uzu/strudel/pulls/1646) +- 2025-10-10T11:25:02+02:00 Add control-metadata by yaxu in: [#1634](https://codeberg.org/uzu/strudel/pulls/1634) +- 2025-10-07T22:59:39+02:00 Fix MQTT: change the trigger handler to match new hap by jurrchen in: [#1629](https://codeberg.org/uzu/strudel/pulls/1629) +- 2025-10-05T21:42:19+02:00 Fix case sensitivity for synonym search by vvolhejn in: [#1618](https://codeberg.org/uzu/strudel/pulls/1618) +- 2025-10-01T08:56:35+02:00 Optimize wavetable synth by glossing in: [#1613](https://codeberg.org/uzu/strudel/pulls/1613) +- 2025-10-01T02:02:33+02:00 fix: pattern switching by daslyfe in: [#1616](https://codeberg.org/uzu/strudel/pulls/1616) + +## September 2025 + +- 2025-09-29T02:10:54+02:00 fix wavetable JSON by daslyfe in: [#1611](https://codeberg.org/uzu/strudel/pulls/1611) +- 2025-09-28T21:28:16+02:00 rename wavetable controls to match existing naming conventions by daslyfe in: [#1610](https://codeberg.org/uzu/strudel/pulls/1610) +- 2025-09-27T23:28:13+02:00 Feature: Wavetable synth improvements by glossing in: [#1607](https://codeberg.org/uzu/strudel/pulls/1607) +- 2025-09-27T21:43:10+02:00 wavetable improvements by daslyfe in: [#1608](https://codeberg.org/uzu/strudel/pulls/1608) +- 2025-09-26T08:48:46+02:00 create orbit based DJ filter by daslyfe in: [#1603](https://codeberg.org/uzu/strudel/pulls/1603) +- 2025-09-23T13:28:20+02:00 fix: time signal by daslyfe in: [#1583](https://codeberg.org/uzu/strudel/pulls/1583) +- 2025-09-21T16:16:06+02:00 mondo: fix all + setcpm + setcps by froos in: [#1600](https://codeberg.org/uzu/strudel/pulls/1600) +- 2025-09-19T21:59:43+02:00 fix: osc error message by froos in: [#1597](https://codeberg.org/uzu/strudel/pulls/1597) +- 2025-09-18T21:09:56+02:00 osc: --debug flag to see incoming messages by froos in: [#1595](https://codeberg.org/uzu/strudel/pulls/1595) +- 2025-09-17T14:12:10+02:00 simplify osc usage by froos in: [#1588](https://codeberg.org/uzu/strudel/pulls/1588) +- 2025-09-16T16:59:58+02:00 dev: logger errors to console in dev mode by daslyfe in: [#1585](https://codeberg.org/uzu/strudel/pulls/1585) +- 2025-09-15T22:52:14+02:00 add tic80 font by froos in: [#1579](https://codeberg.org/uzu/strudel/pulls/1579) +- 2025-09-15T00:00:19+02:00 supradough: fix delay + add some jsdoc types by froos in: [#1578](https://codeberg.org/uzu/strudel/pulls/1578) \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7f65b5bf..b8a941fc1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,20 +4,13 @@ Thanks for wanting to contribute!!! There are many ways you can add value to thi ## Move to codeberg -We are currently in the process of moving from github to codeberg -- not everything is working, please bear with us. - -To update your local clone, you can run this command: - -``` -git remote set-url origin git@codeberg.org:uzu/strudel.git -``` - +Along with many other live coding projects, we have moved from Microsoft's Github platform to Codeberg for ethical reasons. Please don't fork the project back to github. ## Communication Channels -To get in touch with the contributors, either +To get in touch with the community, either -- [join the Tidal Discord Channel](https://discord.com/invite/HGEdXmRkzT) and go to the #strudel channel +- [join the Uzulang Discord Server](https://discord.com/invite/HGEdXmRkzT) and go to the strudel channels (Uzulangs are a family of live coding languages inspired by each other, including TidalCycles as well as Strudel) - Find related discussions on the [tidal club forum](https://club.tidalcycles.org/) ## Ask a Question @@ -36,22 +29,35 @@ Use one of the Communication Channels listed above and drop us a line or two! ## Share Music If you made some music with strudel, you can give back some love and share what you've done! -Your creation could also be part of the random selection in the REPL if you want. Use one of the Communication Channels listed above. +(There used to be a random selection of contributed patterns in the REPL, but that is unfortunately disabled for now due to abuse) ## Improve the Docs -If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/), -you can edit each file directly on codeburg. (we are currently fixing the "Edit this page" links in the right sidebar) +If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/), you can edit each file directly on codeberg. There are "Edit this page" links in the right sidebar that take you to the right place. ## Propose a Feature -If you want a specific feature that is not part of strudel yet, feel free to use one of the communication channels above. -Maybe you even want to help with the implementation of that feature! +If you want a specific feature that is not part of strudel yet, feel free to use one of the communication channels above. Please bear in mind that this is a free/open source project, oriented around collaborative discussion. Maybe you even want to help with the implementation of that feature! + +## Contribute a feature + +Pull requests welcome! Consider starting with discussion or proof-of-concept first, rather than do loads of work on something and then find it doesn't fit the collective goals of the project, or something like that. + +At the time of writing we have a PR backlog, and generally prioritise bugfixes and contributions that have arisen through community discussion. + +### AI/LLM policy + +Strudel is a project handmade by humans, with thought and nuance. + +If you have used LLMs (so called 'AI'), please detail that in the pull request. We are still developing our response to the onslaught of LLM technology, but for practical and legal reasons are currently not accepting wholly LLM-generated code. We are also not accepting PRs that add LLM features to strudel itself. + +There are #llm-chat and #llm-share channels on our discord. Please do not discuss or share LLM-related things outside of those channels. ## Report a Bug If you've found a bug, or some behaviour that does not seem right, you are welcome to file an [issue](https://codeberg.org/uzu/strudel/issues). + Please check that it has not been reported before. ## Fix a Bug @@ -118,8 +124,7 @@ There are also eslint extensions / plugins for most editors. ## Running all CI Checks -When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests. -You can run the same check with `pnpm check` +When opening a PR, the CI runner will (once approved by a human) check the code style and eslint, as well as run all tests. You can run the same check with `pnpm check`. ## Package Workflow diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 791558339..f28d13f62 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -72,6 +72,27 @@ export function registerControl(names, ...aliases) { return bag; } +export function registerMultiControl(names, maxControls, ...aliases) { + names = Array.isArray(names) ? names : [names]; + let bag = {}; + for (let i = 1; i <= maxControls; i++) { + let theseAliases = [...aliases]; + let theseNames = [...names]; + if (i === 1) { + // adds e.g. fm1 as an alias for fm + const aliases1 = theseAliases.map((a) => `${a}1`); + const names1 = theseNames.map((n) => `${n}1`); + theseAliases = theseAliases.concat(aliases1).concat(names1); + } else { + theseAliases = theseAliases.map((a) => `${a}${i}`); + theseNames = theseNames.map((n) => `${n}${i}`); + } + const subBag = registerControl(theseNames, ...theseAliases); + bag = { ...bag, ...subBag }; + } + return bag; +} + /** * Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters * separated by ':'. @@ -380,12 +401,13 @@ export const { accelerate } = registerControl('accelerate'); * Sets the velocity from 0 to 1. Is multiplied together with gain. * * @name velocity + * @synonyms vel * @example * s("hh*8") * .gain(".4!2 1 .4!2 1 .4 1") * .velocity(".4 1") */ -export const { velocity } = registerControl('velocity'); +export const { velocity, vel } = registerControl('velocity', 'vel'); /** * Controls the gain by an exponential amount. * @@ -435,6 +457,9 @@ export const { attack, att } = registerControl('attack', 'att'); * Whole numbers and simple ratios sound more natural, * while decimal numbers and complex ratios sound metallic. * + * A number may be added afterwards to control the harmonicity of + * any of the 8 individual FMs (e.g. `fmh2`) + * * @name fmh * @param {number | Pattern} harmonicity * @example @@ -444,25 +469,40 @@ export const { attack, att } = registerControl('attack', 'att'); * ._scope() * */ -export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh'); +export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerMultiControl(['fmh', 'fmi'], 8, 'fmh'); + /** * Sets the Frequency Modulation of the synth. * Controls the modulation index, which defines the brightness of the sound. * - * @name fm + * A number may be added afterwards to control the modulation index of + * any of the 8 individual FMs (e.g. `fm3`). Also, FMs may be routed into + * each other with matrix commands like `fm13`, which would send `fm1` back into + * `fm3` + * + * @name fmi * @param {number | Pattern} brightness modulation index - * @synonyms fmi + * @synonyms fm * @example * note("c e g b g e") * .fm("<0 1 2 8 32>") * ._scope() + * @example + * s("sine").note("F1").seg(8) + * .fm(4).fm2(rand.mul(4)).fm3(saw.mul(8).slow(8)) + * .fmh(1.06).fmh2(10).fmh3(0.1) * */ -export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm'); +export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8 } = + registerMultiControl(['fmi', 'fmh'], 8, 'fm'); + // fm envelope /** * Ramp type of fm envelope. Exp might be a bit broken.. * + * A number may be added afterwards to control the envelope of + * any of the 8 individual FMs (e.g. `fmenv4`) + * * @name fmenv * @param {number | Pattern} type lin | exp * @example @@ -474,11 +514,19 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm'); * ._scope() * */ -export const { fmenv } = registerControl('fmenv'); +export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fmenv8 } = registerMultiControl( + 'fmenv', + 8, +); + /** * Attack time for the FM envelope: time it takes to reach maximum modulation * + * A number may be added afterwards to control the attack of the envelope of + * any of the 8 individual FMs (e.g. `fmatt5`) + * * @name fmattack + * @synonyms fmatt * @param {number | Pattern} time attack time * @example * note("c e g b g e") @@ -487,11 +535,33 @@ export const { fmenv } = registerControl('fmenv'); * ._scope() * */ -export const { fmattack } = registerControl('fmattack'); +export const { + fmattack, + fmattack1, + fmattack2, + fmattack3, + fmattack4, + fmattack5, + fmattack6, + fmattack7, + fmattack8, + fmatt, + fmatt1, + fmatt2, + fmatt3, + fmatt4, + fmatt5, + fmatt6, + fmatt7, + fmatt8, +} = registerMultiControl('fmattack', 8, 'fmatt'); /** * Waveform of the fm modulator * + * A number may be added afterwards to control the waveform + * any of the 8 individual FMs (e.g. `fmwave6`) + * * @name fmwave * @param {number | Pattern} wave waveform * @example @@ -500,12 +570,19 @@ export const { fmattack } = registerControl('fmattack'); * n("0 1 2 3".fast(4)).chord("").voicing().s("sawtooth").fmwave("brown").fm(.6) * */ -export const { fmwave } = registerControl('fmwave'); +export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmwave7, fmwave8 } = registerMultiControl( + 'fmwave', + 8, +); /** * Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase. * + * A number may be added afterwards to control the decay of the envelope of + * any of the 8 individual FMs (e.g. `fmdec6`) + * * @name fmdecay + * @synonyms fmdec * @param {number | Pattern} time decay time * @example * note("c e g b g e") @@ -515,11 +592,35 @@ export const { fmwave } = registerControl('fmwave'); * ._scope() * */ -export const { fmdecay } = registerControl('fmdecay'); +export const { + fmdecay, + fmdecay1, + fmdecay2, + fmdecay3, + fmdecay4, + fmdecay5, + fmdecay6, + fmdecay7, + fmdecay8, + fmdec, + fmdec1, + fmdec2, + fmdec3, + fmdec4, + fmdec5, + fmdec6, + fmdec7, + fmdec8, +} = registerMultiControl('fmdecay', 8, 'fmdec'); + /** * Sustain level for the FM envelope: how much modulation is applied after the decay phase * + * A number may be added afterwards to control the sustain of the envelope of + * any of the 8 individual FMs (e.g. `fmsus7`) + * * @name fmsustain + * @synonyms fmsus * @param {number | Pattern} level sustain level * @example * note("c e g b g e") @@ -529,10 +630,69 @@ export const { fmdecay } = registerControl('fmdecay'); * ._scope() * */ -export const { fmsustain } = registerControl('fmsustain'); -// these are not really useful... skipping for now -export const { fmrelease } = registerControl('fmrelease'); -export const { fmvelocity } = registerControl('fmvelocity'); +export const { + fmsustain, + fmsustain1, + fmsustain2, + fmsustain3, + fmsustain4, + fmsustain5, + fmsustain6, + fmsustain7, + fmsustain8, + fmsus, + fmsus1, + fmsus2, + fmsus3, + fmsus4, + fmsus5, + fmsus6, + fmsus7, + fmsus8, +} = registerMultiControl('fmsustain', 8, 'fmsus'); + +/** + * Release time for the FM envelope: how much modulation is applied after the note is released + * + * A number may be added afterwards to control the release of the envelope of + * any of the 8 individual FMs (e.g. `fmrel8`) + * + * @name fmrelease + * @synonyms fmrel + * @param {number | Pattern} time release time + * + */ +export const { + fmrelease, + fmrelease1, + fmrelease2, + fmrelease3, + fmrelease4, + fmrelease5, + fmrelease6, + fmrelease7, + fmrelease8, + fmrel, + fmrel1, + fmrel2, + fmrel3, + fmrel4, + fmrel5, + fmrel6, + fmrel7, + fmrel8, +} = registerMultiControl('fmrelease', 8, 'fmrel'); + +// FM Matrix +// Note: we do not declare top-level exports here since it would add +// ~162 more explicit exports. This is likely fine as the most common use-case would be to at least +// declare one other FM prior to utilizing the matrix functionality, but if we ever decide we need it, +// TODO to add it explicitly / go with the globalThis approach +for (let i = 0; i <= 8; i++) { + for (let j = 0; j <= 8; j++) { + registerControl(`fmi${i}${j}`, `fm${i}${j}`); + } +} /** * Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`. @@ -1294,6 +1454,8 @@ export const { fanchor } = registerControl('fanchor'); * * @name lprate * @param {number | Pattern} rate rate in hertz + * @example + * note("*16").s("sawtooth").lpf(600).lprate("<4 8 2 1>") */ export const { lprate } = registerControl('lprate'); @@ -1302,6 +1464,8 @@ export const { lprate } = registerControl('lprate'); * * @name lpsync * @param {number | Pattern} rate rate in cycles + * @example + * note("*16").s("sawtooth").lpf(600).lpsync("<4 8 2 1>") */ export const { lpsync } = registerControl('lpsync'); @@ -1310,8 +1474,22 @@ export const { lpsync } = registerControl('lpsync'); * * @name lpdepth * @param {number | Pattern} depth depth of modulation + * @example + * note("*16").s("sawtooth").lpf(600).lpdepth("<1 .5 1.8 0>") */ + export const { lpdepth } = registerControl('lpdepth'); +/** + * Depth of the LFO for the lowpass filter, in HZ + * + * @name lpdepthfrequency + * @synonyms lpdepthfreq + * @param {number | Pattern} depth depth of modulation + * @example + * note("*16").s("sawtooth").lpf(600).lpdepthfrequency("<200 500 100 0>") + */ + +export const { lpdepthfrequency, lpdepthfreq } = registerControl('lpdepthfrequency', 'lpdepthfreq'); /** * Shape of the LFO for the lowpass filter @@ -1361,6 +1539,18 @@ export const { bpsync } = registerControl('bpsync'); */ export const { bpdepth } = registerControl('bpdepth'); +/** + * Depth of the LFO for the bandpass filter, in HZ + * + * @name bpdepthfrequency + * @synonyms bpdepthfreq + * @param {number | Pattern} depth depth of modulation + * @example + * note("*16").s("sawtooth").lpf(600).bpdepthfrequency("<200 500 100 0>") + */ + +export const { bpdepthfrequency, bpdepthfreq } = registerControl('bpdepthfrequency', 'bpdepthfreq'); + /** * Shape of the LFO for the bandpass filter * @@ -1409,6 +1599,18 @@ export const { hpsync } = registerControl('hpsync'); */ export const { hpdepth } = registerControl('hpdepth'); +/** + * Depth of the LFO for the hipass filter, in hz + * + * @name hpdepthfrequency + * @synonyms hpdepthfreq + * @param {number | Pattern} depth depth of modulation + * @example + * note("*16").s("sawtooth").lpf(600).hpdepthfrequency("<200 500 100 0>") + */ + +export const { hpdepthfrequency, hpdepthfreq } = registerControl('hpdepthfrequency', 'hpdepthfreq'); + /** * Shape of the LFO for the highpass filter * @@ -1820,12 +2022,12 @@ export const { nudge } = registerControl('nudge'); * Sets the default octave of a synth. * * @name octave + * @synonyms oct * @param {number | Pattern} octave octave number * @example - * n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc() - * @superDirtOnly + * n("0,4,7").scale("F:minor").s('supersaw').octave("<0 1 2 3>") */ -export const { octave } = registerControl('octave'); +export const { octave, oct } = registerControl('octave', 'oct'); // ['ophatdecay'], // TODO: example @@ -1833,6 +2035,7 @@ export const { octave } = registerControl('octave'); * An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects. * * @name orbit + * @synonyms o * @param {number | Pattern} number * @example * stack( @@ -1840,7 +2043,7 @@ export const { octave } = registerControl('octave'); * s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2) * ) */ -export const { orbit } = registerControl('orbit'); +export const { orbit } = registerControl('orbit', 'o'); // TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that export const { overgain } = registerControl('overgain'); // TODO: what is this? not found in tidal doc. Similar to above, but limited to 1 @@ -2264,7 +2467,6 @@ export const { tsdelay } = registerControl('tsdelay'); export const { real } = registerControl('real'); export const { imag } = registerControl('imag'); export const { enhance } = registerControl('enhance'); -export const { partials } = registerControl('partials'); export const { comb } = registerControl('comb'); export const { smear } = registerControl('smear'); export const { scram } = registerControl('scram'); @@ -2551,8 +2753,13 @@ export const as = register('as', (mapping, pat) => { mapping = Array.isArray(mapping) ? mapping : [mapping]; return pat.fmap((v) => { v = Array.isArray(v) ? v : [v]; - v = Object.fromEntries(mapping.map((prop, i) => [getControlName(prop), v[i]])); - return v; + const entries = []; + for (let i = 0; i < mapping.length; ++i) { + if (v[i] !== undefined) { + entries.push([getControlName(mapping[i]), v[i]]); + } + } + return Object.fromEntries(entries); }); }); @@ -2584,3 +2791,17 @@ export const scrub = register( }, false, ); + +/** + * Transient shaper. Gives independent control over the emphasis on transients + * and sustains + * + * @name transient + * @param {number | Pattern} attack Emphasis on transients; between -1 (deaccentuate) and 1 (accentuate) + * @param {number | Pattern} sustain Emphasis on the sustains; between -1 (deaccentuate) and 1 (accentuate) + * @example + * s("bd").transient("<-1 -0.5 0 0.5 1>") + * @example + * s("hh*16").bank("tr909").transient("<-1:1 1:-1>") + */ +export const { transient } = registerControl(['transient', 'transsustain']); diff --git a/packages/core/impure.mjs b/packages/core/impure.mjs new file mode 100644 index 000000000..304f801f5 --- /dev/null +++ b/packages/core/impure.mjs @@ -0,0 +1,90 @@ +/* +stateful.mjs - File of shame for stateful, impure and otherwise illegal pattern methods +Copyright (C) 2025 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { register, reify, Pattern } from './pattern.mjs'; + +let timelines = {}; + +export const reset_state = function () { + reset_timelines(); +}; + +export const reset_timelines = function () { + timelines = {}; +}; + +/*** + * Allows you to switch a pattern between different 'timelines'. This is particularly useful when + * live coding, for example when you want to cue a pattern up to play from its start. + * + * Timelines are specified by number, so that if you had a pattern like + * `n("<0 1 2 3>").s("num").timeline(1)` playing, then changed the '1' + * to '2', it would always align '0' to the nearest cycle. You will likely want to trigger + * an evaluation a little bit before the cycle starts, to avoid missing events. + * + * After the first use, a timeline will continue with the same 'offset'. That is, if you change + * a pattern without changing its timeline number, it will stay on that timeline without resetting. + * + * Rather than incrementing a timeline to reset it, it's easier to negate it, e.g. by switching between `-2` + * and `2`. This is because when you negate a timeline it will always reset. + * + * You can also pattern the timeline if you want, to create strange resetting patterns. + * @param {number | Pattern} timeline The timeline that the pattern should play on. + * @example + * n("<0 1 2 3>(3,8)") + * .sound("num") + * // resets the timeline every two cycles, by negating the timeline. + * // in a lot of cases this will be edited by a human live coder + * // rather than patterned! + * .timeline("<2 -2>".slow(2)) + */ + +export const timeline = register( + 'timeline', + function (tpat, pat) { + tpat = reify(tpat); + const f = function (state) { + // Is this called from the scheduler? (rather than from e.g. the visualiser) + const scheduler = !!state.controls.cyclist; + const timehaps = tpat.query(state); + const result = []; + for (const timehap of timehaps) { + const tlid = timehap.value; + let offset; + if (tlid === 0) { + offset = 0; + } else if (tlid in timelines) { + offset = timelines[tlid]; + } else { + const timearc = timehap.wholeOrPart(); + if (!scheduler || state.span.begin.lt(timearc.midpoint())) { + offset = timearc.begin; + } else { + // Sync to end of timearc if we first see it over halfway into its + // timespan. Allows 'cuing up' next timeline when live coding. + offset = timearc.end; + } + } + if (scheduler) { + // update state + timelines[tlid] = offset; + if (tlid !== 0) { + delete timelines[-tlid]; + } + } + + const pathaps = pat + .late(offset) + .query(state.setSpan(timehap.part)) + .map((h) => h.setContext(h.combineContext(timehap))); + result.push(...pathaps); + } + return result; + }; + return new Pattern(f, pat._steps); + }, + false, +); diff --git a/packages/core/index.mjs b/packages/core/index.mjs index e4daf445a..9260e3671 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -1,6 +1,6 @@ /* index.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2025 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ @@ -11,20 +11,21 @@ import createClock from './zyklus.mjs'; import { logger } from './logger.mjs'; export { Fraction, controls, createClock }; export * from './controls.mjs'; -export * from './hap.mjs'; -export * from './pattern.mjs'; -export * from './signal.mjs'; -export * from './pick.mjs'; -export * from './state.mjs'; -export * from './timespan.mjs'; -export * from './util.mjs'; -export * from './speak.mjs'; -export * from './evaluate.mjs'; -export * from './repl.mjs'; export * from './cyclist.mjs'; +export * from './evaluate.mjs'; +export * from './hap.mjs'; +export * from './impure.mjs'; export * from './logger.mjs'; +export * from './pattern.mjs'; +export * from './pick.mjs'; +export * from './repl.mjs'; +export * from './signal.mjs'; +export * from './speak.mjs'; +export * from './state.mjs'; export * from './time.mjs'; +export * from './timespan.mjs'; export * from './ui.mjs'; +export * from './util.mjs'; export { default as drawLine } from './drawLine.mjs'; // below won't work with runtime.mjs (json import fails) /* import * as p from './package.json'; diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index e29769900..6f214386f 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -25,7 +25,7 @@ import { stringifyValues, } from './util.mjs'; import drawLine from './drawLine.mjs'; -import { logger } from './logger.mjs'; +import { errorLogger, logger } from './logger.mjs'; let stringParser; @@ -414,7 +414,7 @@ export class Pattern { try { return this.query(new State(new TimeSpan(begin, end), controls)); } catch (err) { - logger(`[query]: ${err.message}`, 'error'); + errorLogger(err, 'query'); return []; } } @@ -574,7 +574,8 @@ export class Pattern { * Returns a new Pattern, which only returns haps that meet the given test. * @param {Function} hap_test - a function which returns false for haps to be removed from the pattern * @returns Pattern - * @noAutocomplete + * @example + * s("bd*8").velocity(rand).filterHaps((h) => (h.whole.begin % 1) < h.value.velocity) */ filterHaps(hap_test) { return new Pattern((state) => this.query(state).filter(hap_test)); @@ -585,7 +586,11 @@ export class Pattern { * inside haps. * @param {Function} value_test * @returns Pattern - * @noAutocomplete + * @example + * const drums = s("bd sd bd sd") + * kick: drums.filterValues((v) => v.s === 'bd').duck(2) + * snare: drums.filterValues((v) => v.s === 'sd') + * bass: s("saw!4").note("G#1").lpf(80).lpenv(4).orbit(2) */ filterValues(value_test) { return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))).setSteps(this._steps); @@ -1587,7 +1592,13 @@ export const func = curry((a, b) => reify(b).func(a)); * * @param {string | string[]} name name of the function, or an array of names to be used as synonyms * @param {function} func function with 1 or more params, where last is the current pattern - * @noAutocomplete + * @param {bool} patternify defaults to true; if set to false, you will have more control over the arguments to `func` as they will be + * in their raw form and it will be up to you to patternify them and/or query them for values + * @example + * const vlpf = register('vlpf', (freq, pat) => { + * return pat.fmap((v) => ({...v, cutoff: freq * (v.velocity ?? 1) })); + * }) + * s("saw").seg(8).velocity(rand).vlpf(800) * */ export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) { @@ -2234,7 +2245,7 @@ export const brak = register('brak', function (pat) { }); /** - * Reverse all haps in a pattern + * Reverse all cycles in a pattern. See also `revv` for reversing a whole pattern. * * @name rev * @memberof Pattern @@ -2266,6 +2277,23 @@ export const rev = register( true, ); +/** + * Reverse a whole pattern. See also `rev` for reversing each cycle. + * + * @name revv + * @memberof Pattern + * @returns Pattern + * @example + * // This is the same as `<[g e] [d c]>`. If `rev()` is used, you get + * // the same as `<[d c] [g e]>`, where each cycle reverses, but the order of + * // cycles stays the same. + * note("<[c d] [e g]>").revv() + */ +export const revv = register('revv', function (pat) { + const negateSpan = (span) => new TimeSpan(Fraction(0).sub(span.end), Fraction(0).sub(span.begin)); + return pat.withQuerySpan(negateSpan).withHapSpan(negateSpan); +}); + /** Like press, but allows you to specify the amount by which each * event is shifted. pressBy(0.5) is the same as press, while * pressBy(1/3) shifts each event by a third of its timespan. @@ -2659,8 +2687,13 @@ export const hsl = register('hsl', (h, s, l, pat) => { /** * Tags each Hap with an identifier. Good for filtering. The function populates Hap.context.tags (Array). * @name tag - * @noAutocomplete * @param {string} tag anything unique + * @example + * s("saw!16").note("F1") + * .lpf(tri.range(40, 80).slow(4)).lpenv(5).lpq(4).lpd(0.15) + * .when(rand.late(0.1).gte(0.5), x => x.transpose("12").tag('altered')) + * .when(rand.late(0.2).gte(0.5), x => x.s("square").tag('altered')) + * .when("<0 1>", x => x.filter((hap) => hap.hasTag('altered'))) */ Pattern.prototype.tag = function (tag) { return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) })); @@ -2671,15 +2704,16 @@ Pattern.prototype.tag = function (tag) { * @name filter * @param {Function} test function to test Hap * @example - * s("hh!7 oh").filter(hap => hap.value.s==='hh') + * s("hh!7 oh").filter(hap => hap.value.s === 'hh') */ export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test))); /** * Filters haps by their begin time * @name filterWhen - * @noAutocomplete * @param {Function} test function to test Hap.whole.begin + * @example + * oneCycle: s("bd*4").filterWhen((t) => t < 1) */ export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin))); @@ -3624,3 +3658,66 @@ for (const name of distAlgoNames) { return this.distort(argsPat); }; } + +/** + * Turns a list of patterns into a single pattern which outputs list-values + * + * @name parray + * @returns Pattern + */ +export const parray = (pats) => { + const pack = (...xs) => xs; + let acc = pure(curry(pack, null, pats.length)); + for (const p of pats) acc = acc.appBoth(reify(p)); + return acc; +}; + +const _ensureListPattern = (list) => { + if (Array.isArray(list)) { + return parray(list); + } + return reify(list); +}; + +/** + * Scale the magnitude of the harmonics of one of the core synths ('sine', 'tri', 'saw', ..) + * + * Can also be used to create a new synth via `s('user').partials(...)` + * + * @name partials + * @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped) + * @example + * s("user").seg(16).n(irand(8)).scale("A:major") + * .partials([1, 0, 1, 0, 0, 1]) + * @example + * s("saw").seg(8).n(irand(12)).scale("G#:minor") + * .partials(binaryL(irand(256).add("1"))) + */ +Pattern.prototype.partials = function (list) { + return this.withValue((v) => (l) => ({ ...v, partials: l })).appLeft(_ensureListPattern(list)); +}; + +// Also create a top-level function +export const partials = (list) => { + return _ensureListPattern(list).as('partials'); +}; + +/** + * Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases + * + * @name phases + * @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped) + * @example + * // Phase cancellation + * s("saw").seg(8).n(irand(12)).scale("G#1:minor") + * .partials(partials([1, 1, 1])) + * .superimpose(x => x.phases([0.5, 0.5, 0.5])) + */ +Pattern.prototype.phases = function (list) { + return this.withValue((v) => (l) => ({ ...v, phases: l })).appLeft(_ensureListPattern(list)); +}; + +// Also create a top-level function +export const phases = (list) => { + return _ensureListPattern(list).as('phases'); +}; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 171697eb9..470be47bc 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -5,6 +5,7 @@ import { errorLogger, logger } from './logger.mjs'; import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; +import { reset_state } from './impure.mjs'; export function repl({ defaultOutput, @@ -52,6 +53,9 @@ export function repl({ onToggle: (started) => { updateState({ started }); onToggle?.(started); + if (!started) { + reset_state(); + } }, setInterval, clearInterval, @@ -152,9 +156,9 @@ export function repl({ // allows muting a pattern x with x_ or _x return silence; } - if (id === '$') { + if (id.includes('$')) { // allows adding anonymous patterns with $: - id = `$${anonymousIndex}`; + id = `${id}${anonymousIndex}`; anonymousIndex++; } pPatterns[id] = this; @@ -215,8 +219,19 @@ export function repl({ let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { let patterns = []; + let soloActive = false; for (const [key, value] of Object.entries(pPatterns)) { - patterns.push(value.withState((state) => state.setControls({ id: key }))); + // handle soloed patterns ex: S$: s("bd!4") + const isSolod = key.length > 1 && key.startsWith('S'); + if (isSolod && soloActive === false) { + // first time we see a soloed pattern, clear existing patterns + patterns = []; + soloActive = true; + } + if (!soloActive || (soloActive && isSolod)) { + const valWithState = value.withState((state) => state.setControls({ id: key })); + patterns.push(valWithState); + } } if (eachTransform) { // Explicit lambda so only element (not index and array) are passed @@ -227,14 +242,13 @@ export function repl({ pattern = eachTransform(pattern); } if (allTransforms.length) { - for (let i in allTransforms) { - pattern = allTransforms[i](pattern); + for (const transform of allTransforms) { + pattern = transform(pattern); } } if (!isPattern(pattern)) { - const message = `got "${typeof evaluated}" instead of pattern`; - throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.')); + pattern = silence; } logger(`[eval] code updated`); pattern = await setPattern(pattern, autostart); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index e497fb1b0..a56009add 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -287,7 +287,7 @@ export const useRNG = (mode = 'legacy') => (RNG_MODE = mode); export const run = (n) => saw.range(0, n).round().segment(n); /** - * Creates a pattern from a binary number. + * Creates a binary pattern from a number. * * @name binary * @param {number} n - input number to convert to binary @@ -301,7 +301,7 @@ export const binary = (n) => { }; /** - * Creates a pattern from a binary number, padded to n bits long. + * Creates a binary pattern from a number, padded to n bits long. * * @name binaryN * @param {number} n - input number to convert to binary @@ -317,6 +317,51 @@ export const binaryN = (n, nBits = 16) => { return reify(n).segment(nBits).brshift(bitPos).band(pure(1)); }; +/** + * Creates a binary list pattern from a number. + * + * @name binaryL + * @param {number} n - input number to convert to binary + * s("saw").seg(8) + * .partials(binaryL(irand(4096).add(1))) + */ +export const binaryL = (n) => { + const nBits = reify(n).log2(0).floor().add(1); + return binaryNL(n, nBits); +}; + +/** + * Creates a binary list pattern from a number, padded to n bits long. + * + * @name binaryNL + * @param {number} n - input number to convert to binary + * @param {number} nBits - pattern length, defaults to 16 + */ +export const binaryNL = (n, nBits = 16) => { + return reify(n) + .withValue((v) => (bits) => { + const bList = []; + for (let i = bits - 1; i >= 0; i--) { + bList.push((v >> i) & 1); + } + return bList; + }) + .appLeft(reify(nBits)); +}; + +/** + * Creates a list of random numbers of the given length + * + * @name randL + * @param {number} n Number of random numbers to sample + * @example + * s("saw").seg(16).n(irand(12)).scale("F1:minor") + * .partials(randL(8)) + */ +export const randL = (n) => { + return signal((t) => (nVal) => timeToRands(t, nVal).map(Math.abs)).appLeft(reify(n)); +}; + export const randrun = (n) => { return signal((t, controls) => { // Without adding 0.5, the first cycle is always 0,1,2,3,... @@ -582,7 +627,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs); * @example * wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8) * @example - * wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s() + * wchooseCycles(["c c c",5], ["a a a",3], ["f f f",1]).fast(4).note() * @example * // The probability can itself be a pattern * wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s() @@ -936,3 +981,12 @@ export const whenKey = register('whenKey', function (input, func, pat) { export const keyDown = register('keyDown', function (pat) { return pat.fmap(_keyDown); }); + +/** + * A pattern that gives the duration of events that are combined with it. + * @example + * sound("bd sd [bd bd] sd*4 [- sd] [bd [bd bd]]").note(delta.withValue(x => 1/x + 20)) + */ +export const delta = new Pattern(function (state) { + return [new Hap(undefined, state.span, state.span.duration)]; +}); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 1df5c8776..408ff274b 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -586,6 +586,18 @@ describe('Pattern', () => { .map((a) => a.value), ).toStrictEqual(['c', 'b', 'a']); }); + it('Does not reverse the order of cycles', () => { + expect(fastcat('a', 'b', 'c', 'd').slow(2).rev().fast(2).sortHapsByPart().firstCycle()).toStrictEqual( + fastcat('b', 'a', 'd', 'c').firstCycle(), + ); + }); + }); + describe('revv()', () => { + it('Does reverse the order of cycles', () => { + expect(fastcat('a', 'b', 'c', 'd').slow(2).revv().fast(2).sortHapsByPart().firstCycle()).toStrictEqual( + fastcat('d', 'c', 'b', 'a').firstCycle(), + ); + }); }); describe('sequence()', () => { it('Can work like fastcat', () => { diff --git a/packages/hydra/hydra.mjs b/packages/hydra/hydra.mjs index 2cb265c2f..c13410121 100644 --- a/packages/hydra/hydra.mjs +++ b/packages/hydra/hydra.mjs @@ -35,6 +35,7 @@ export async function initHydra(options = {}) { hydra.synth.s0.init({ src: canvas }); } } + return hydra; } export function clearHydra() { diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 32f030125..cd38d3587 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -479,14 +479,24 @@ Pattern.prototype.midi = function (midiport, options = {}) { let listeners = {}; const refs = {}; +const refsByChan = {}; /** * MIDI input: Opens a MIDI input port to receive MIDI control change messages. + * + * The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel * @param {string | number} input MIDI device name or index defaulting to 0 - * @returns {Function} + * @returns {function(number, number=): Pattern} A function from (cc, channel?) to a pattern. + * When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1) + * that came through that cc number (and channel, if provided) * @example - * let cc = await midin('IAC Driver Bus 1') + * const cc = await midin('IAC Driver Bus 1') * note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth") + * @example + * const allCC = await midin('IAC Driver Bus 1') + * const cc = (ccNum) => allCC(ccNum, 2) // just channel 2 + * note("c a f e").s("saw") + * .when(cc(0).gt(0), x => x.postgain(0)) */ export async function midin(input) { if (isPattern(input)) { @@ -511,17 +521,24 @@ export async function midin(input) { }`, ); } - // ensure refs for this input are initialized - if (!refs[input]) { - refs[input] = {}; - } - const cc = (cc) => ref(() => refs[input][cc] || 0); + refs[input] ??= {}; + refsByChan[input] ??= {}; + const cc = (cc, chan) => { + if (chan !== undefined) { + return ref(() => refsByChan[input][cc]?.[chan] || 0); + } + return ref(() => refs[input][cc] || 0); + }; listeners[input] && device.removeListener('midimessage', listeners[input]); listeners[input] = (e) => { - const cc = e.dataBytes[0]; + const ccNum = e.dataBytes[0]; const v = e.dataBytes[1]; - refs[input] && (refs[input][cc] = v / 127); + const chan = e.message.channel; + const scaled = v / 127; + refsByChan[input][ccNum] ??= {}; + refsByChan[input][ccNum][chan] = scaled; + refs[input][ccNum] = scaled; }; device.addListener('midimessage', listeners[input]); return cc; diff --git a/packages/sampler/sample-server.mjs b/packages/sampler/sample-server.mjs index 31b83f5a3..1d5a8e7a6 100644 --- a/packages/sampler/sample-server.mjs +++ b/packages/sampler/sample-server.mjs @@ -9,6 +9,7 @@ import readline from 'readline'; import os from 'os'; const LOG = !!process.env.LOG || false; +const PORT = process.env.PORT || 5432; const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg']; const isAudioFile = (f) => { @@ -54,7 +55,6 @@ async function getBanks(directory, flat = false) { banks[bank].push(subDir); return subDir; }); - banks._base = `http://localhost:5432`; return { banks, files }; } @@ -134,8 +134,6 @@ const server = http.createServer(async (req, res) => { readStream.pipe(res); }); -// eslint-disable-next-line -const PORT = process.env.PORT || 5432; const IP_ADDRESS = '0.0.0.0'; let IP; const networkInterfaces = os.networkInterfaces(); diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 427f57e31..95e210890 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -6,6 +6,8 @@ import { getADSRValues, getPitchEnvelope, getVibratoOscillator, + onceEnded, + releaseAudioNode, } from '@strudel/webaudio'; import gm from './gm.mjs'; @@ -170,12 +172,11 @@ export function registerSoundfonts() { bufferSource.stop(envEnd); const stop = (releaseTime) => {}; - bufferSource.onended = () => { - bufferSource.disconnect(); - vibratoOscillator?.stop(); - node.disconnect(); + onceEnded(bufferSource, () => { + releaseAudioNode(bufferSource); + releaseAudioNode(vibratoOscillator); onended(); - }; + }); return { node, stop }; }, { type: 'soundfont', prebake: true, fonts }, diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 47161ed61..5abc2bad1 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,7 +1,7 @@ import { getAudioContext } from './audioContext.mjs'; -import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs'; -import { getNoiseBuffer } from './noise.mjs'; import { logger } from './logger.mjs'; +import { getNoiseBuffer } from './noise.mjs'; +import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs'; export const noises = ['pink', 'white', 'brown', 'crackle']; @@ -189,10 +189,9 @@ export function applyParameterModulators(audioContext, param, start, end, envelo getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve); } const lfo = getParamLfo(audioContext, param, start, end, lfoValues); - return { lfo, disconnect: () => lfo?.disconnect() }; + return lfo; } - -export function createFilter(context, start, end, params, cps) { +export function createFilter(context, start, end, params, cps, cycle) { let { frequency, anchor, @@ -202,12 +201,14 @@ export function createFilter(context, start, end, params, cps) { q = 1, drive = 0.69, depth, + depthfrequency, dcoffset = -0.5, skew, shape, rate, sync, } = params; + let frequencyParam, filter; if (model === 'ladder') { filter = getWorklet(context, 'ladder-processor', { frequency, q, drive }); @@ -238,9 +239,27 @@ export function createFilter(context, start, end, params, cps) { if (sync != null) { rate = cps * sync; } - const lfoValues = { depth, dcoffset, skew, shape, frequency: rate }; - getParamLfo(context, frequencyParam, start, end, lfoValues); - return filter; + const hasLFO = [depth, depthfrequency, skew, shape, rate].some((v) => v !== undefined); + let lfo; + if (hasLFO) { + depth = depth ?? 1; + const time = cycle / cps; + const modDepth = depthfrequency ?? (depth ?? 1) * frequency; + const lfoValues = { + depth: modDepth, + dcoffset, + skew, + shape, + frequency: rate ?? cps, + min: -frequency + 30, + max: 20000 - frequency, + time, + curve: 1, + }; + lfo = getParamLfo(context, frequencyParam, start, end, lfoValues); + } + + return { filter, lfo }; } // stays 1 until .5, then fades out @@ -262,7 +281,18 @@ export function drywet(dry, wet, wetAmount = 0) { let mix = ac.createGain(); dry_gain.connect(mix); wet_gain.connect(mix); - return mix; + return { + node: mix, + teardown: () => { + releaseAudioNode(dry_gain); + releaseAudioNode(wet_gain); + // it is not the responsability of drywet + // to call `releaseAudioNode` on + // the 2 external args dry and wet + dry.disconnect(dry_gain); + wet.disconnect(wet_gain); + }, + }; } let curves = ['linear', 'exponential']; @@ -297,6 +327,10 @@ export function getVibratoOscillator(param, value, t) { gain.gain.value = vibmod * 100; vibratoOscillator.connect(gain); gain.connect(param); + onceEnded(vibratoOscillator, () => { + releaseAudioNode(gain); + releaseAudioNode(vibratoOscillator); + }); vibratoOscillator.start(t); return vibratoOscillator; } @@ -318,25 +352,17 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { constantNode.connect(zeroGain); // Schedule the `onComplete` callback to occur at `stopTime` - constantNode.onended = () => { - // Ensure garbage collection - try { - zeroGain.disconnect(); - } catch { - // pass - } - try { - constantNode.disconnect(); - } catch { - // pass - } + onceEnded(constantNode, () => { + releaseAudioNode(zeroGain); + releaseAudioNode(constantNode); onComplete(); - }; + }); constantNode.start(startTime); constantNode.stop(stopTime); return constantNode; } -const mod = (freq, range = 1, type = 'sine') => { + +const mod = (freq, type = 'sine') => { const ctx = getAudioContext(); let osc; if (noises.includes(type)) { @@ -348,64 +374,89 @@ const mod = (freq, range = 1, type = 'sine') => { osc.type = type; osc.frequency.value = freq; } - osc.start(); - const g = new GainNode(ctx, { gain: range }); - osc.connect(g); // -range, range - return { node: g, stop: (t) => osc.stop(t) }; + return osc; }; -const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => { + +const fm = (frequencyparam, harmonicityRatio, wave = 'sine') => { const carrfreq = frequencyparam.value; const modfreq = carrfreq * harmonicityRatio; - const modgain = modfreq * modulationIndex; - return mod(modfreq, modgain, wave); + return { osc: mod(modfreq, wave), freq: modfreq }; }; + export function applyFM(param, value, begin) { - const { - fmh: fmHarmonicity = 1, - fmi: fmModulationIndex, - fmenv: fmEnvelopeType = 'exp', - fmattack: fmAttack, - fmdecay: fmDecay, - fmsustain: fmSustain, - fmrelease: fmRelease, - fmvelocity: fmVelocity, - fmwave: fmWaveform = 'sine', - duration, - } = value; - let modulator; - let stop = () => {}; - - if (fmModulationIndex) { - const ac = getAudioContext(); - const envGain = ac.createGain(); - const fmmod = fm(param, fmHarmonicity, fmModulationIndex, fmWaveform); - - modulator = fmmod.node; - stop = fmmod.stop; - if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) { - // no envelope by default - modulator.connect(param); - } else { - const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]); - const holdEnd = begin + duration; - getParamADSR( - envGain.gain, - attack, - decay, - sustain, - release, - 0, - 1, - begin, - holdEnd, - fmEnvelopeType === 'exp' ? 'exponential' : 'linear', - ); - modulator.connect(envGain); - envGain.connect(param); + const ac = getAudioContext(); + const toStop = []; // fm oscillators we will expose `stop` for + const fms = {}; + // Matrix + for (let i = 1; i <= 8; i++) { + for (let j = 0; j <= 8; j++) { + let control; + if (i === j + 1) { + // Standard fm3 -> fm2 -> fm1 -> param usage + const iS = i === 1 ? '' : i; + control = `fmi${iS}`; + } else { + control = `fmi${i}${j}`; + } + const amt = value[control]; + if (!amt) continue; + let io = []; + for (let [isMod, idx] of [ + [true, i], // source + [false, j], // target + ]) { + if (idx === 0) { + io.push(param); + continue; + } + if (!fms[idx]) { + const idxS = idx === 1 ? '' : idx; + const { osc, freq } = fm(param, value[`fmh${idxS}`] ?? 1, value[`fmwave${idxS}`] ?? 'sine'); + toStop.push(osc); + const toCleanup = [osc]; // nodes we want to cleanup after oscillator `stop` + const adsr = ['attack', 'decay', 'sustain', 'release'].map((s) => value[`fm${s}${idxS}`]); + let output = osc; + if (adsr.some((v) => v !== undefined)) { + const envGain = ac.createGain(); + const [attack, decay, sustain, release] = getADSRValues(adsr); + const holdEnd = begin + value.duration; + const fmEnvelopeType = value[`fmenv${idxS}`] ?? 'exp'; + getParamADSR( + envGain.gain, + attack, + decay, + sustain, + release, + 0, + 1, + begin, + holdEnd, + fmEnvelopeType === 'exp' ? 'exponential' : 'linear', + ); + toCleanup.push(envGain); + output = osc.connect(envGain); + } + fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup }; + } + const { input, output, freq, osc, toCleanup } = fms[idx]; + const g = gainNode(amt * freq); + io.push(isMod ? output.connect(g) : input); + cleanupOnEnd(osc, [...toCleanup, g]); + } + if (!io[1]) { + logger( + `[superdough] control ${control} failed to connect FM ${i} to target ${j} due to missing frequency parameter (likely because fm${j} is noise)`, + 'warning', + ); + continue; + } + io[0].connect(io[1]); } } - return { stop }; + return { + stop: (t) => toStop.forEach((m) => m?.stop(t)), + }; } // Saturation curves @@ -505,7 +556,7 @@ export const getDistortion = (distort, postgain, algorithm) => { }; export const getFrequencyFromValue = (value, defaultNote = 36) => { - let { note, freq } = value; + let { note, freq, octave = 0 } = value; note = note || defaultNote; if (typeof note === 'string') { note = noteToMidi(note); // e.g. c3 => 48 @@ -514,14 +565,64 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => { if (!freq && typeof note === 'number') { freq = midiToFreq(note); // + 48); } - + freq *= Math.pow(2, octave); return Number(freq); }; -export const destroyAudioWorkletNode = (node) => { - if (node == null) { - return; - } - node.disconnect(); - node.parameters.get('end')?.setValueAtTime(0, 0); +// This helper should be used instead of the `node.onended = callback` pattern +// It adds a mechanism to help minimize gc retention +export const onceEnded = (node, callback) => { + const onended = callback; + node.onended = function cleanup() { + onended && onended(); + this.onended = null; + }; +}; + +export const releaseAudioNode = (node) => { + if (node == null) return; + + // check we received an AudioNode + if (!(node instanceof AudioNode)) { + throw new Error('releaseAudioNode can only release an AudioNode'); + } + + // https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect + node.disconnect(); + + // make sure all AudioScheduledSourceNodes are in a stopped state + // https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode + if (node instanceof AudioScheduledSourceNode) { + if (node.onended && node.onended.name !== 'cleanup') { + logger( + `[superdough] Deprecation warning: it seems your code path is setting 'node.onended = callback' instead of using the onceEnded helper`, + ); + } + try { + node.stop(); + } catch (e) { + // At the stage, `start` was not called on the node + // but an `onended` callback releasing resources may exist + // and we want it to fire : + // - we force a start/stop cycle so that `onended` gets called + // - we `lock` the node so that no-one can start it + node.start(node.context.currentTime + 5); // will never happen + node.stop(); + } + } + + // https://www.w3.org/TR/webaudio-1.1/#AudioNode-actively-processing + // An AudioWorkletNode is actively processing when its AudioWorkletProcessor's [[callable process]] + // returns true and either its active source flag is true or + // any AudioNode connected to one of its inputs is actively processing. + if (node instanceof AudioWorkletNode) { + // while `end` is not native to the web audio API, it is common practice in superdough + // to use that param in the worklets to trigger returning false from the processor + node.parameters.get('end')?.setValueAtTime(0, 0); + } +}; + +// Once the `anchor` node has ended, release all nodes in `toCleanup` +export const cleanupOnEnd = (anchor, toCleanup) => { + onceEnded(anchor, () => toCleanup.forEach((n) => releaseAudioNode(n))); }; diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 816dd252b..52d9306a1 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -1,4 +1,4 @@ -import { drywet } from './helpers.mjs'; +import { drywet, onceEnded, releaseAudioNode } from './helpers.mjs'; import { getAudioContext } from './audioContext.mjs'; let noiseCache = {}; @@ -65,8 +65,12 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) { export function getNoiseMix(inputNode, wet, t) { const noiseOscillator = getNoiseOscillator('pink', t); const noiseMix = drywet(inputNode, noiseOscillator.node, wet); + onceEnded(noiseOscillator.node, () => { + releaseAudioNode(noiseOscillator.node); + }); return { - node: noiseMix, + node: noiseMix.node, stop: (time) => noiseOscillator?.stop(time), + teardown: noiseMix.teardown, }; } diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 84bac3582..2deeb4b74 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,7 +1,14 @@ -import { getCommonSampleInfo } from './util.mjs'; +import { getBaseURL, getCommonSampleInfo } from './util.mjs'; import { registerSound, registerWaveTable } from './index.mjs'; import { getAudioContext } from './audioContext.mjs'; -import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; +import { + getADSRValues, + getParamADSR, + getPitchEnvelope, + getVibratoOscillator, + onceEnded, + releaseAudioNode, +} from './helpers.mjs'; import { logger } from './logger.mjs'; const bufferCache = {}; // string: Promise @@ -211,7 +218,7 @@ export async function fetchSampleMap(url) { // not a browser return; } - const base = url.split('/').slice(0, -1).join('/'); + const base = getBaseURL(url); if (typeof fetch === 'undefined') { // skip fetch when in node / testing return; @@ -236,12 +243,6 @@ export async function fetchSampleMap(url) { * sd: '808sd/SD0010.WAV' * }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/'); * s("[bd ~]*2, [~ hh]*2, ~ sd") - * @example - * samples('shabda:noise,chimp:2') - * s("noise ") - * @example - * samples('shabda/speech/fr-FR/f:chocolat') - * s("chocolat*4") */ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => { @@ -286,17 +287,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl); - // asny stuff above took too long? - if (ac.currentTime > t) { - logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight'); - // console.warn('sample still loading:', s, n); - return; - } if (!bufferSource) { logger(`[sampler] could not load "${s}:${n}"`, 'error'); return; } + // async stuff above took too long? + if (ac.currentTime > t) { + logger(`[sampler] loading sound "${s}:${n}" took too long`, 'highlight'); + // AudioBufferSourceNode will never be used. discard it + releaseAudioNode(bufferSource); + return; + } + // vibrato let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); @@ -319,13 +322,13 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... node.connect(out); - bufferSource.onended = function () { - bufferSource.disconnect(); - vibratoOscillator?.stop(); - node.disconnect(); - out.disconnect(); + onceEnded(bufferSource, function () { + releaseAudioNode(bufferSource); + releaseAudioNode(vibratoOscillator); + releaseAudioNode(node); + releaseAudioNode(out); onended(); - }; + }); let envEnd = holdEnd + release + 0.01; bufferSource.stop(envEnd); const stop = (endTime) => { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 61457fa42..6765923e4 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -9,7 +9,16 @@ import './reverb.mjs'; import './vowel.mjs'; import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; -import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs'; +import { + createFilter, + gainNode, + getCompressor, + getDistortion, + getLfo, + getWorklet, + effectSend, + releaseAudioNode, +} from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; @@ -285,8 +294,8 @@ let audioReady; export async function initAudioOnFirstClick(options) { if (!audioReady) { audioReady = new Promise((resolve) => { - document.addEventListener('click', async function listener() { - document.removeEventListener('click', listener); + document.addEventListener('mousedown', async function listener() { + document.removeEventListener('mousedown', listener); await initAudio(options); resolve(); }); @@ -296,7 +305,7 @@ export async function initAudioOnFirstClick(options) { } let controller; -function getSuperdoughAudioController() { +export function getSuperdoughAudioController() { if (controller == null) { controller = new SuperdoughAudioController(getAudioContext()); } @@ -309,11 +318,11 @@ export function connectToDestination(input, channels) { function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) { const ac = getAudioContext(); - const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 }); + const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 }); //filters - const numStages = 2; //num of filters in series - let fOffset = 0; + const numStages = 1; //num of filters in series + let fOffset = 282; //for backward compat in #1800 const filterChain = []; for (let i = 0; i < numStages; i++) { const filter = ac.createBiquadFilter(); @@ -322,14 +331,11 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000 filter.frequency.value = centerFrequency + fOffset; filter.Q.value = 2 - Math.min(Math.max(depth * 2, 0), 1.9); - lfoGain.connect(filter.detune); + lfo.connect(filter.detune); fOffset += 282; - if (i > 0) { - filterChain[i - 1].connect(filter); - } filterChain.push(filter); } - return filterChain[filterChain.length - 1]; + return { filterChain, lfo }; } function getFilterType(ftype) { @@ -435,7 +441,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) release = 0, //phaser - phaserrate: phaser, + phaserrate, phaserdepth = getDefaultValue('phaserdepth'), phasersweep, phasercenter, @@ -473,6 +479,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) compressorKnee, compressorAttack, compressorRelease, + transient, + transsustain, } = value; delaytime = delaytime ?? cycleToSeconds(delaysync, cps); @@ -528,7 +536,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } else if (getSound(s)) { const { onTrigger } = getSound(s); const onEnded = () => { - audioNodes.forEach((n) => n?.disconnect()); + audioNodes.forEach((n) => releaseAudioNode(n)); activeSoundSources.delete(chainID); }; const soundHandle = await onTrigger(t, value, onEnded, cps); @@ -554,6 +562,23 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) chain.push(sourceNode); stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); + transient !== undefined && + chain.push( + getWorklet( + ac, + 'transient-processor', + {}, + { + processorOptions: { + attack: transient, + sustain: transsustain, + begin: t, + end: endWithRelease, + }, + }, + ), + ); + // gain stage chain.push(gainNode(gain)); @@ -575,16 +600,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) rate: 'lprate', sync: 'lpsync', depth: 'lpdepth', + depthfrequency: 'lpdepthfrequency', shape: 'lpshape', dcoffset: 'lpdc', skew: 'lpskew', }; const lpParams = pickAndRename(value, lpMap); lpParams.type = 'lowpass'; - let lp = () => createFilter(ac, t, end, lpParams, cps); - chain.push(lp()); + const lp = () => createFilter(ac, t, end, lpParams, cps, cycle); + const { filter: lpf1, lfo: lfo1 } = lp(); + chain.push(lpf1); + lfo1 && audioNodes.push(lfo1); if (ftype === '24db') { - chain.push(lp()); + const { filter: lpf2, lfo: lfo2 } = lp(); + chain.push(lpf2); + lfo2 && audioNodes.push(lfo2); } } @@ -603,16 +633,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) rate: 'hprate', sync: 'hpsync', depth: 'hpdepth', + depthfrequency: 'hpdepthfrequency', shape: 'hpshape', dcoffset: 'hpdc', skew: 'hpskew', }; const hpParams = pickAndRename(value, hpMap); hpParams.type = 'highpass'; - let hp = () => createFilter(ac, t, end, hpParams, cps); - chain.push(hp()); + const hp = () => createFilter(ac, t, end, hpParams, cps, cycle); + const { filter: hpf1, lfo: lfo1 } = hp(); + chain.push(hpf1); + lfo1 && audioNodes.push(lfo1); if (ftype === '24db') { - chain.push(hp()); + const { filter: hpf2, lfo: lfo2 } = hp(); + chain.push(hpf2); + lfo2 && audioNodes.push(lfo2); } } @@ -631,16 +666,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) rate: 'bprate', sync: 'bpsync', depth: 'bpdepth', + depthfrequency: 'bpdepthfrequency', shape: 'bpshape', dcoffset: 'bpdc', skew: 'bpskew', }; const bpParams = pickAndRename(value, bpMap); bpParams.type = 'bandpass'; - let bp = () => createFilter(ac, t, end, bpParams, cps); - chain.push(bp()); + const bp = () => createFilter(ac, t, end, bpParams, cps, cycle); + const { filter: bpf1, lfo: lfo1 } = bp(); + chain.push(bpf1); + lfo1 && audioNodes.push(lfo1); if (ftype === '24db') { - chain.push(bp()); + const { filter: bpf2, lfo: lfo2 } = bp(); + chain.push(bpf2); + lfo2 && audioNodes.push(lfo2); } } @@ -687,6 +727,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) curve: 1.5, }); lfo.connect(amGain.gain); + audioNodes.push(lfo); chain.push(amGain); } @@ -702,9 +743,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) chain.push(panner); } // phaser - if (phaser !== undefined && phaserdepth > 0) { - const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep); - chain.push(phaserFX); + if (phaserrate !== undefined && phaserdepth > 0) { + const { filterChain, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep); + audioNodes.push(lfo); + chain.push(...filterChain); } // last gain @@ -714,7 +756,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // delay if (delay > 0 && delaytime > 0 && delayfeedback > 0) { orbitBus.getDelay(delaytime, delayfeedback, t); - orbitBus.sendDelay(post, delay); + const send = orbitBus.sendDelay(post, delay); + audioNodes.push(send); } // reverb if (room > 0) { diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index a4235efd0..d8ead7d06 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -82,7 +82,7 @@ export class Orbit { } sendDelay(node, amount) { - effectSend(node, this.delayNode, amount); + return effectSend(node, this.delayNode, amount); } duck(t, onsettime = 0, attacktime = 0.1, depth = 1) { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index e35b98806..d9ee0d936 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -3,7 +3,6 @@ import { registerSound, soundMap } from './superdough.mjs'; import { getAudioContext } from './audioContext.mjs'; import { applyFM, - destroyAudioWorkletNode, gainNode, getADSRValues, getFrequencyFromValue, @@ -13,11 +12,14 @@ import { getVibratoOscillator, getWorklet, noises, + onceEnded, + releaseAudioNode, webAudioTimeout, } from './helpers.mjs'; +import { logger } from './logger.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; -const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; +const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user']; const waveformAliases = [ ['tri', 'triangle'], ['sqr', 'square'], @@ -47,19 +49,17 @@ export function registerSynthSounds() { [0.001, 0.05, 0.6, 0.01], ); - let sound = getOscillator(s, t, value); - let { node: o, stop, triggerRelease } = sound; - // turn down const g = gainNode(0.3); - const { duration } = value; - - o.onended = () => { - o.disconnect(); - g.disconnect(); + let sound = getOscillator(s, t, value, () => { + releaseAudioNode(g); onended(); - }; + }); + + let { node: o, stop, triggerRelease } = sound; + + const { duration } = value; const envGain = gainNode(1); let node = o.connect(g).connect(envGain); @@ -111,15 +111,15 @@ export function registerSynthSounds() { const mix = gainNode(mixGain); - o.onended = () => { - o.disconnect(); - g.disconnect(); - sat.disconnect(); - noise.node.disconnect(); - noiseGain.disconnect(); - mix.disconnect(); + onceEnded(o, () => { + releaseAudioNode(o); + releaseAudioNode(g); + releaseAudioNode(sat); + releaseAudioNode(noise.node); + releaseAudioNode(noiseGain); + releaseAudioNode(mix); onended(); - }; + }); const node = o.connect(sat).connect(g).connect(mix); noise.node.connect(noiseGain).connect(mix); @@ -193,8 +193,7 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(o); - envGain.disconnect(); + releaseAudioNode(o); onended(); fm?.stop(); vibratoOscillator?.stop(); @@ -271,8 +270,7 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(o); - envGain.disconnect(); + releaseAudioNode(o); onended(); }, begin, @@ -345,9 +343,8 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(o); - destroyAudioWorkletNode(lfo); - envGain.disconnect(); + releaseAudioNode(o); + releaseAudioNode(lfo); onended(); fm?.stop(); vibratoOscillator?.stop(); @@ -388,11 +385,11 @@ export function registerSynthSounds() { const { duration } = value; - o.onended = () => { - o.disconnect(); - g.disconnect(); + onceEnded(o, () => { + releaseAudioNode(o); + releaseAudioNode(g); onended(); - }; + }); const envGain = gainNode(1); let node = o.connect(g).connect(envGain); @@ -414,9 +411,13 @@ export function registerSynthSounds() { waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] })); } -export function waveformN(partials, type) { - const real = new Float32Array(partials + 1); - const imag = new Float32Array(partials + 1); +const PI2 = 2 * Math.PI; +export function waveformN(partials, phases, type) { + const isList = typeof partials === 'object'; + partials = isList ? partials : new Float32Array(partials).fill(1); + const len = partials.length; + const real = new Float32Array(len + 1); + const imag = new Float32Array(len + 1); const ac = getAudioContext(); const osc = ac.createOscillator(); @@ -424,20 +425,29 @@ export function waveformN(partials, type) { sawtooth: (n) => [0, -1 / n], square: (n) => [0, n % 2 === 0 ? 0 : 1 / n], triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0], + user: (_n) => [0, 1], }; if (!terms[type]) { throw new Error(`unknown wave type ${type}`); } - real[0] = 0; // dc offset - imag[0] = 0; - let n = 1; - while (n <= partials) { - const [r, i] = terms[type](n); - real[n] = r; - imag[n] = i; - n++; + for (let n = 0; n < len; n++) { + const mag = partials[n]; + const [r, i] = terms[type](n + 1); // we skip n === 0 as this is dc offset + const phase = phases?.[n] ?? 0; + // Scale by `partials` + let R = r * mag; + let I = i * mag; + // Apply rotation by the phase + if (phase !== 0) { + const c = Math.cos(PI2 * phase); + const s = Math.sin(PI2 * phase); + R = c * R - s * I; + I = s * R + c * I; + } + real[n + 1] = R; + imag[n + 1] = I; } const wave = ac.createPeriodicWave(real, imag); @@ -446,21 +456,28 @@ export function waveformN(partials, type) { } // expects one of waveforms as s -export function getOscillator(s, t, value) { - let { n: partials, duration, noise = 0 } = value; +export function getOscillator(s, t, value, onended) { + const { duration, noise = 0 } = value; + const partials = value.partials ?? value.n; let o; + if (s === 'user' && !partials) { + logger( + `[superdough] Synth 'user' was selected, but partials not specified. Defaulting to triangle. Use pat.partials to setup custom waveform`, + ); + s = 'triangle'; + } + s = s === 'user' && !partials ? 'triangle' : s; // If no partials are given, use stock waveforms - if (!partials || s === 'sine') { + if (!partials || partials?.length === 0 || s === 'sine') { o = getAudioContext().createOscillator(); o.type = s || 'triangle'; } // generate custom waveform if partials are given else { - o = waveformN(partials, s); + o = waveformN(partials, value.phases, s); } // set frequency o.frequency.value = getFrequencyFromValue(value); - o.start(t); let vibratoOscillator = getVibratoOscillator(o.detune, value, t); @@ -473,6 +490,14 @@ export function getOscillator(s, t, value) { noiseMix = getNoiseMix(o, noise, t); } + onceEnded(o, () => { + noiseMix?.teardown(); + releaseAudioNode(o); + releaseAudioNode(noiseMix?.node); + onended(); + }); + o.start(t); + return { node: noiseMix?.node || o, stop: (time) => { diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 44ec79f77..48830541a 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -114,3 +114,13 @@ export function getCommonSampleInfo(hapValue, bank) { export const pickAndRename = (source, map) => { return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]])); }; + +export const getBaseURL = (url) => { + try { + // For real URLs + return new URL('.', new URL(url)).href.replace(/\/$/, ''); // removes trailing slash + } catch { + // For pseudo URLS + return url.split('/').slice(0, -1).join('/'); + } +}; diff --git a/packages/superdough/vowel.mjs b/packages/superdough/vowel.mjs index 3f30aef1e..fda0c6479 100644 --- a/packages/superdough/vowel.mjs +++ b/packages/superdough/vowel.mjs @@ -45,7 +45,8 @@ if (typeof GainNode !== 'undefined') { throw new Error('vowel: unknown vowel ' + letter); } const { gains, qs, freqs } = vowelFormant[letter]; - const makeupGain = ac.createGain(); + this.makeupGain = ac.createGain(); + this.audioNodes = []; for (let i = 0; i < 5; i++) { const gain = ac.createGain(); gain.gain.value = gains[i]; @@ -53,14 +54,25 @@ if (typeof GainNode !== 'undefined') { filter.type = 'bandpass'; filter.Q.value = qs[i]; filter.frequency.value = freqs[i]; - this.connect(filter); + super.connect(filter); filter.connect(gain); - gain.connect(makeupGain); + this.audioNodes.push(filter); + gain.connect(this.makeupGain); + this.audioNodes.push(gain); } - makeupGain.gain.value = 8; // how much makeup gain to add? - this.connect = (target) => makeupGain.connect(target); + this.makeupGain.gain.value = 8; // how much makeup gain to add? return this; } + connect(target) { + this.makeupGain.connect(target); + } + disconnect() { + this.makeupGain.disconnect(); + this.audioNodes.forEach((n) => n.disconnect()); + super.disconnect(); + this.makeupGain = null; + this.audioNodes = null; + } } AudioContext.prototype.createVowelFilter = function (letter) { diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 01d4eb838..4311c5cab 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -1,15 +1,15 @@ import { getAudioContext, registerSound } from './index.mjs'; -import { getCommonSampleInfo } from './util.mjs'; +import { getBaseURL, getCommonSampleInfo } from './util.mjs'; import { applyFM, applyParameterModulators, - destroyAudioWorkletNode, getADSRValues, getFrequencyFromValue, getParamADSR, getPitchEnvelope, getVibratoOscillator, getWorklet, + releaseAudioNode, webAudioTimeout, } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -190,6 +190,7 @@ export const tables = async (url, frameLen, json, options = {}) => { if (url.startsWith('local:')) { url = `http://localhost:5432`; } + const base = getBaseURL(url); if (typeof fetch !== 'function') { // not a browser return; @@ -200,7 +201,7 @@ export const tables = async (url, frameLen, json, options = {}) => { } return fetch(url) .then((res) => res.json()) - .then((json) => _processTables(json, url, frameLen, options)) + .then((json) => _processTables(json, base, frameLen, options)) .catch((error) => { console.error(error); throw new Error(`error loading "${url}"`); @@ -318,12 +319,11 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(source); - vibratoOscillator?.stop(); + releaseAudioNode(source); + releaseAudioNode(vibratoOscillator); fm?.stop(); - node.disconnect(); - wtPosModulators?.disconnect(); - wtWarpModulators?.disconnect(); + releaseAudioNode(wtPosModulators); + releaseAudioNode(wtWarpModulators); onended(); }, t, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 359bc833a..a59aeb756 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -11,6 +11,9 @@ const PI = Math.PI; const TWO_PI = 2 * PI; const INVSR = 1 / sampleRate; +const timeToCoeff = (t) => 1 - Math.exp(-INVSR / t); +const dbToLin = (db) => Math.pow(10, db / 20); + const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const mod = (n, m) => ((n % m) + m) % m; const lerp = (a, b, n) => n * (b - a) + a; @@ -508,13 +511,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { ]; } process(_input, outputs, params) { - if (currentTime <= params.begin[0]) { - return true; - } if (currentTime >= params.end[0]) { - // this.port.postMessage({ type: 'onended' }); + // should terminate return false; } + if (currentTime <= params.begin[0]) { + // keep alive + return true; + } const output = outputs[0]; const voices = params.voices[0]; // k-rate for (let i = 0; i < output[0].length; i++) { @@ -532,7 +536,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n)); // We must wrap this here because it is passed into sawblep below which // has domain [0, 1] - const dt = ffrac(freqVoice * INVSR); + const dt = frac(freqVoice * INVSR); this.phase[n] = this.phase[n] ?? Math.random(); const v = waveshapes.sawblep(this.phase[n], dt); @@ -543,8 +547,9 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { if (pn >= 1.0) pn -= 1.0; this.phase[n] = pn; // invert right and left gain + const tmp = gainL; gainL = gainR; - gainR = gainL; + gainR = tmp; } } return true; @@ -1338,7 +1343,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const tablePos = clamp(pv(parameters.position, i), 0, 1); const idx = tablePos * (this.numFrames - 1); const fIdx = idx | 0; - const frac = idx - fIdx; + const interpT = idx - fIdx; const warpAmount = clamp(pv(parameters.warp, i), 0, 1); const warpMode = pv(parameters.warpMode, i); const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1); @@ -1368,13 +1373,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { 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; + let s = lerp(s0, s1, interpT); if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { s = -s; } outL[i] += s * gainL * normalizer; outR[i] += s * gainR * normalizer; - this.phase[n] = ffrac(this.phase[n] + dPhase); + this.phase[n] = frac(this.phase[n] + dPhase); } } return true; @@ -1382,3 +1387,82 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor); + +class TransientProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return []; + } + + constructor(options) { + super(); + this.gainCoeff = timeToCoeff(0.2); + this.avgGain = 1; + let { + attackTime = 0.003, + sustainTime = 0.08, + attack = 0, + sustain = 0, + sensitivity = 0.1, + mix = 1, + begin = 0, + end = 0, + } = options.processorOptions; + attackTime = clamp(attackTime, 0.0005, 0.05); + sustainTime = clamp(sustainTime, 0.01, 0.5); + this.attackCoeff = timeToCoeff(attackTime); + this.sustainCoeff = timeToCoeff(sustainTime); + this.attackAmt = clamp(attack, -1, 1); + this.sustainAmt = clamp(sustain, -1, 1); + this.scaling = 0.5 + 5 * clamp(sensitivity, 0, 1); + this.mix = clamp(mix, 0, 1); + this.begin = begin; + this.end = end; + this.attackEnv = new Float32Array(2); // assume stereo + this.sustainEnv = new Float32Array(2); + } + + process(inputs, outputs, _params) { + const input = inputs[0]; + const output = outputs[0]; + if (currentTime >= this.end) { + return false; + } + if (currentTime <= this.begin) { + return true; + } + const channels = input.length; + if (channels > this.attackEnv.length) { + this.attackEnv = new Float32Array(channels); + this.sustainEnv = new Float32Array(channels); + } + let avgGain = this.avgGain; + for (let ch = 0; ch < channels; ch++) { + let attEnv = this.attackEnv[ch]; + let susEnv = this.sustainEnv[ch]; + for (let n = 0; n < blockSize; n++) { + const sample = input[ch][n]; + const x = Math.abs(sample); + attEnv = lerp(attEnv, x, this.attackCoeff); + susEnv = lerp(susEnv, x, this.sustainCoeff); + const peakiness = clamp((this.scaling * (attEnv - susEnv)) / (susEnv + 1e-6), -1.5, 1.5); + const attScale = peakiness > 0 ? peakiness : 0; + const susScale = peakiness < 0 ? -peakiness : 0; + const attackGain = dbToLin(this.attackAmt * attScale * 18); + const sustainGain = dbToLin(this.sustainAmt * susScale * 36); + const gain = clamp(attackGain * sustainGain, 0, 8); + avgGain = lerp(avgGain, gain, this.gainCoeff); + const makeup = avgGain > 1e-3 ? 1 / avgGain : 1; + const wet = sample * gain * makeup; + let y = lerp(sample, wet, this.mix); + y /= 1 + Math.abs(y); // soft clip + output[ch][n] = y; + } + this.attackEnv[ch] = attEnv; + this.sustainEnv[ch] = susEnv; + } + this.avgGain = avgGain; + return true; + } +} + +registerProcessor('transient-processor', TransientProcessor); diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index 32db0395a..4a4c58c9c 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -3,6 +3,7 @@ import { midiToFreq, noteToMidi } from './util.mjs'; import { registerSound } from './superdough.mjs'; import { getAudioContext } from './audioContext.mjs'; import { buildSamples } from './zzfx_fork.mjs'; +import { onceEnded, releaseAudioNode } from './helpers.mjs'; export const getZZFX = (value, t) => { let { @@ -83,10 +84,10 @@ export function registerZZFXSounds() { wave, (t, value, onended) => { const { node: o } = getZZFX({ s: wave, ...value }, t); - o.onended = () => { - o.disconnect(); + onceEnded(o, () => { + releaseAudioNode(o); onended(); - }; + }); return { node: o, stop: () => {}, diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 427a66f4c..951f0f7e6 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -5,7 +5,16 @@ This program is free software: you can redistribute it and/or modify it under th */ import { Note, Interval, Scale } from '@tonaljs/tonal'; -import { register, _mod, logger, isNote, noteToMidi, removeUndefineds, getAccidentalsOffset } from '@strudel/core'; +import { + _mod, + errorLogger, + getAccidentalsOffset, + isNote, + logger, + noteToMidi, + register, + removeUndefineds, +} from '@strudel/core'; import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs'; const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P'; @@ -294,7 +303,7 @@ export const scale = register( } if (offset != 0) scaleNote = Note.transpose(scaleNote, Interval.fromSemitones(offset)); } catch (err) { - logger(`[tonal] ${err.message}`, 'error'); + errorLogger(err, 'tonal'); return; // will be removed } } diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 481315873..0b9774205 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1391,6 +1391,75 @@ exports[`runs examples > example "bpdecay" example index 0 1`] = ` ] `; +exports[`runs examples > example "bpdepthfrequency" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", +] +`; + exports[`runs examples > example "bpenv" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]", @@ -2715,6 +2784,59 @@ exports[`runs examples > example "delaysync" example index 0 1`] = ` ] `; +exports[`runs examples > example "delta" example index 0 1`] = ` +[ + "[ 0/1 → 1/6 | s:bd note:26 ]", + "[ 1/6 → 1/3 | s:sd note:26 ]", + "[ 1/3 → 5/12 | s:bd note:32 ]", + "[ 5/12 → 1/2 | s:bd note:32 ]", + "[ 1/2 → 13/24 | s:sd note:44 ]", + "[ 13/24 → 7/12 | s:sd note:44 ]", + "[ 7/12 → 5/8 | s:sd note:44 ]", + "[ 5/8 → 2/3 | s:sd note:44 ]", + "[ 3/4 → 5/6 | s:sd note:32 ]", + "[ 5/6 → 11/12 | s:bd note:32 ]", + "[ 11/12 → 23/24 | s:bd note:44 ]", + "[ 23/24 → 1/1 | s:bd note:44 ]", + "[ 1/1 → 7/6 | s:bd note:26 ]", + "[ 7/6 → 4/3 | s:sd note:26 ]", + "[ 4/3 → 17/12 | s:bd note:32 ]", + "[ 17/12 → 3/2 | s:bd note:32 ]", + "[ 3/2 → 37/24 | s:sd note:44 ]", + "[ 37/24 → 19/12 | s:sd note:44 ]", + "[ 19/12 → 13/8 | s:sd note:44 ]", + "[ 13/8 → 5/3 | s:sd note:44 ]", + "[ 7/4 → 11/6 | s:sd note:32 ]", + "[ 11/6 → 23/12 | s:bd note:32 ]", + "[ 23/12 → 47/24 | s:bd note:44 ]", + "[ 47/24 → 2/1 | s:bd note:44 ]", + "[ 2/1 → 13/6 | s:bd note:26 ]", + "[ 13/6 → 7/3 | s:sd note:26 ]", + "[ 7/3 → 29/12 | s:bd note:32 ]", + "[ 29/12 → 5/2 | s:bd note:32 ]", + "[ 5/2 → 61/24 | s:sd note:44 ]", + "[ 61/24 → 31/12 | s:sd note:44 ]", + "[ 31/12 → 21/8 | s:sd note:44 ]", + "[ 21/8 → 8/3 | s:sd note:44 ]", + "[ 11/4 → 17/6 | s:sd note:32 ]", + "[ 17/6 → 35/12 | s:bd note:32 ]", + "[ 35/12 → 71/24 | s:bd note:44 ]", + "[ 71/24 → 3/1 | s:bd note:44 ]", + "[ 3/1 → 19/6 | s:bd note:26 ]", + "[ 19/6 → 10/3 | s:sd note:26 ]", + "[ 10/3 → 41/12 | s:bd note:32 ]", + "[ 41/12 → 7/2 | s:bd note:32 ]", + "[ 7/2 → 85/24 | s:sd note:44 ]", + "[ 85/24 → 43/12 | s:sd note:44 ]", + "[ 43/12 → 29/8 | s:sd note:44 ]", + "[ 29/8 → 11/3 | s:sd note:44 ]", + "[ 15/4 → 23/6 | s:sd note:32 ]", + "[ 23/6 → 47/12 | s:bd note:32 ]", + "[ 47/12 → 95/24 | s:bd note:44 ]", + "[ 95/24 → 4/1 | s:bd note:44 ]", +] +`; + exports[`runs examples > example "density" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:crackle density:0.01 ]", @@ -3974,6 +4096,70 @@ exports[`runs examples > example "filter" example index 0 1`] = ` ] `; +exports[`runs examples > example "filterHaps" example index 0 1`] = ` +[ + "[ 1/8 → 1/4 | s:bd velocity:0.6852155700325966 ]", + "[ 1/4 → 3/8 | s:bd velocity:0.36975969187915325 ]", + "[ 3/8 → 1/2 | s:bd velocity:0.40139251574873924 ]", + "[ 1/1 → 9/8 | s:bd velocity:0.5195421651005745 ]", + "[ 9/8 → 5/4 | s:bd velocity:0.6724895145744085 ]", + "[ 5/4 → 11/8 | s:bd velocity:0.7287282031029463 ]", + "[ 3/2 → 13/8 | s:bd velocity:0.6084080748260021 ]", + "[ 2/1 → 17/8 | s:bd velocity:0.9595271199941635 ]", + "[ 17/8 → 9/4 | s:bd velocity:0.9033902939409018 ]", + "[ 9/4 → 19/8 | s:bd velocity:0.34548251144587994 ]", + "[ 19/8 → 5/2 | s:bd velocity:0.8365066405385733 ]", + "[ 3/1 → 25/8 | s:bd velocity:0.21728911064565182 ]", + "[ 25/8 → 13/4 | s:bd velocity:0.34324387833476067 ]", + "[ 13/4 → 27/8 | s:bd velocity:0.9923650715500116 ]", + "[ 27/8 → 7/2 | s:bd velocity:0.40869180113077164 ]", + "[ 29/8 → 15/4 | s:bd velocity:0.9391485787928104 ]", + "[ 15/4 → 31/8 | s:bd velocity:0.8121673222631216 ]", +] +`; + +exports[`runs examples > example "filterValues" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 1/4 → 1/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 1/2 → 3/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 3/4 → 1/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 1/1 → 5/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 5/4 → 3/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 3/2 → 7/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 7/4 → 2/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 2/1 → 9/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 9/4 → 5/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 5/2 → 11/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 11/4 → 3/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 3/1 → 13/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 13/4 → 7/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 7/2 → 15/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 15/4 → 4/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", +] +`; + +exports[`runs examples > example "filterWhen" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd ]", + "[ 1/4 → 1/2 | s:bd ]", + "[ 1/2 → 3/4 | s:bd ]", + "[ 3/4 → 1/1 | s:bd ]", + "[ 1/1 → 5/4 | s:bd ]", + "[ 5/4 → 3/2 | s:bd ]", + "[ 3/2 → 7/4 | s:bd ]", + "[ 7/4 → 2/1 | s:bd ]", + "[ 2/1 → 9/4 | s:bd ]", + "[ 9/4 → 5/2 | s:bd ]", + "[ 5/2 → 11/4 | s:bd ]", + "[ 11/4 → 3/1 | s:bd ]", + "[ 3/1 → 13/4 | s:bd ]", + "[ 13/4 → 7/2 | s:bd ]", + "[ 7/2 → 15/4 | s:bd ]", + "[ 15/4 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "firstOf" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:g3 ]", @@ -4023,35 +4209,6 @@ exports[`runs examples > example "floor" example index 0 1`] = ` ] `; -exports[`runs examples > example "fm" example index 0 1`] = ` -[ - "[ 0/1 → 1/6 | note:c fmi:0 ]", - "[ 1/6 → 1/3 | note:e fmi:0 ]", - "[ 1/3 → 1/2 | note:g fmi:0 ]", - "[ 1/2 → 2/3 | note:b fmi:0 ]", - "[ 2/3 → 5/6 | note:g fmi:0 ]", - "[ 5/6 → 1/1 | note:e fmi:0 ]", - "[ 1/1 → 7/6 | note:c fmi:1 ]", - "[ 7/6 → 4/3 | note:e fmi:1 ]", - "[ 4/3 → 3/2 | note:g fmi:1 ]", - "[ 3/2 → 5/3 | note:b fmi:1 ]", - "[ 5/3 → 11/6 | note:g fmi:1 ]", - "[ 11/6 → 2/1 | note:e fmi:1 ]", - "[ 2/1 → 13/6 | note:c fmi:2 ]", - "[ 13/6 → 7/3 | note:e fmi:2 ]", - "[ 7/3 → 5/2 | note:g fmi:2 ]", - "[ 5/2 → 8/3 | note:b fmi:2 ]", - "[ 8/3 → 17/6 | note:g fmi:2 ]", - "[ 17/6 → 3/1 | note:e fmi:2 ]", - "[ 3/1 → 19/6 | note:c fmi:8 ]", - "[ 19/6 → 10/3 | note:e fmi:8 ]", - "[ 10/3 → 7/2 | note:g fmi:8 ]", - "[ 7/2 → 11/3 | note:b fmi:8 ]", - "[ 11/3 → 23/6 | note:g fmi:8 ]", - "[ 23/6 → 4/1 | note:e fmi:8 ]", -] -`; - exports[`runs examples > example "fmattack" example index 0 1`] = ` [ "[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 ]", @@ -4168,6 +4325,72 @@ exports[`runs examples > example "fmh" example index 0 1`] = ` ] `; +exports[`runs examples > example "fmi" example index 0 1`] = ` +[ + "[ 0/1 → 1/6 | note:c fmi:0 ]", + "[ 1/6 → 1/3 | note:e fmi:0 ]", + "[ 1/3 → 1/2 | note:g fmi:0 ]", + "[ 1/2 → 2/3 | note:b fmi:0 ]", + "[ 2/3 → 5/6 | note:g fmi:0 ]", + "[ 5/6 → 1/1 | note:e fmi:0 ]", + "[ 1/1 → 7/6 | note:c fmi:1 ]", + "[ 7/6 → 4/3 | note:e fmi:1 ]", + "[ 4/3 → 3/2 | note:g fmi:1 ]", + "[ 3/2 → 5/3 | note:b fmi:1 ]", + "[ 5/3 → 11/6 | note:g fmi:1 ]", + "[ 11/6 → 2/1 | note:e fmi:1 ]", + "[ 2/1 → 13/6 | note:c fmi:2 ]", + "[ 13/6 → 7/3 | note:e fmi:2 ]", + "[ 7/3 → 5/2 | note:g fmi:2 ]", + "[ 5/2 → 8/3 | note:b fmi:2 ]", + "[ 8/3 → 17/6 | note:g fmi:2 ]", + "[ 17/6 → 3/1 | note:e fmi:2 ]", + "[ 3/1 → 19/6 | note:c fmi:8 ]", + "[ 19/6 → 10/3 | note:e fmi:8 ]", + "[ 10/3 → 7/2 | note:g fmi:8 ]", + "[ 7/2 → 11/3 | note:b fmi:8 ]", + "[ 11/3 → 23/6 | note:g fmi:8 ]", + "[ 23/6 → 4/1 | note:e fmi:8 ]", +] +`; + +exports[`runs examples > example "fmi" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | s:sine note:F1 fmi:4 fmi2:0 fmi3:0 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/8 → 1/4 | s:sine note:F1 fmi:4 fmi2:2.7408622801303864 fmi3:0.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/4 → 3/8 | s:sine note:F1 fmi:4 fmi2:1.479038767516613 fmi3:0.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/8 → 1/2 | s:sine note:F1 fmi:4 fmi2:1.605570062994957 fmi3:0.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/2 → 5/8 | s:sine note:F1 fmi:4 fmi2:1.041922464966774 fmi3:0.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 5/8 → 3/4 | s:sine note:F1 fmi:4 fmi2:0.5425434708595276 fmi3:0.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/4 → 7/8 | s:sine note:F1 fmi:4 fmi2:0.7833059206604958 fmi3:0.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 7/8 → 1/1 | s:sine note:F1 fmi:4 fmi2:1.590524323284626 fmi3:0.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/1 → 9/8 | s:sine note:F1 fmi:4 fmi2:2.078168660402298 fmi3:1 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 9/8 → 5/4 | s:sine note:F1 fmi:4 fmi2:2.689958058297634 fmi3:1.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 5/4 → 11/8 | s:sine note:F1 fmi:4 fmi2:2.914912812411785 fmi3:1.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 11/8 → 3/2 | s:sine note:F1 fmi:4 fmi2:0.7312150076031685 fmi3:1.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/2 → 13/8 | s:sine note:F1 fmi:4 fmi2:2.4336322993040085 fmi3:1.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 13/8 → 7/4 | s:sine note:F1 fmi:4 fmi2:1.987018845975399 fmi3:1.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 7/4 → 15/8 | s:sine note:F1 fmi:4 fmi2:0.8189515843987465 fmi3:1.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 15/8 → 2/1 | s:sine note:F1 fmi:4 fmi2:2.3782287165522575 fmi3:1.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 2/1 → 17/8 | s:sine note:F1 fmi:4 fmi2:3.838108479976654 fmi3:2 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 17/8 → 9/4 | s:sine note:F1 fmi:4 fmi2:3.613561175763607 fmi3:2.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 9/4 → 19/8 | s:sine note:F1 fmi:4 fmi2:1.3819300457835197 fmi3:2.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 19/8 → 5/2 | s:sine note:F1 fmi:4 fmi2:3.346026562154293 fmi3:2.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 5/2 → 21/8 | s:sine note:F1 fmi:4 fmi2:1.8344642966985703 fmi3:2.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 21/8 → 11/4 | s:sine note:F1 fmi:4 fmi2:0.6407739371061325 fmi3:2.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 11/4 → 23/8 | s:sine note:F1 fmi:4 fmi2:2.5329310819506645 fmi3:2.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 23/8 → 3/1 | s:sine note:F1 fmi:4 fmi2:0.06501668691635132 fmi3:2.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/1 → 25/8 | s:sine note:F1 fmi:4 fmi2:0.8691564425826073 fmi3:3 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 25/8 → 13/4 | s:sine note:F1 fmi:4 fmi2:1.3729755133390427 fmi3:3.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 13/4 → 27/8 | s:sine note:F1 fmi:4 fmi2:3.9694602862000465 fmi3:3.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 27/8 → 7/2 | s:sine note:F1 fmi:4 fmi2:1.6347672045230865 fmi3:3.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 7/2 → 29/8 | s:sine note:F1 fmi:4 fmi2:1.639795258641243 fmi3:3.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 29/8 → 15/4 | s:sine note:F1 fmi:4 fmi2:3.7565943151712418 fmi3:3.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 15/4 → 31/8 | s:sine note:F1 fmi:4 fmi2:3.2486692890524864 fmi3:3.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 31/8 → 4/1 | s:sine note:F1 fmi:4 fmi2:2.9446661546826363 fmi3:3.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", +] +`; + exports[`runs examples > example "fmsustain" example index 0 1`] = ` [ "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 ]", @@ -4786,6 +5009,75 @@ exports[`runs examples > example "hpdecay" example index 0 1`] = ` ] `; +exports[`runs examples > example "hpdepthfrequency" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", +] +`; + exports[`runs examples > example "hpenv" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]", @@ -5969,6 +6261,144 @@ exports[`runs examples > example "lpdecay" example index 0 1`] = ` ] `; +exports[`runs examples > example "lpdepth" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", +] +`; + +exports[`runs examples > example "lpdepthfrequency" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", +] +`; + exports[`runs examples > example "lpenv" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c2 s:sawtooth cutoff:300 lpattack:0.5 lpenv:4 ]", @@ -6157,6 +6587,75 @@ exports[`runs examples > example "lpq" example index 0 1`] = ` ] `; +exports[`runs examples > example "lprate" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 lprate:4 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 lprate:4 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 lprate:4 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 lprate:4 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 lprate:4 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 lprate:8 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 lprate:8 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 lprate:8 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 lprate:8 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 lprate:8 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 lprate:2 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 lprate:2 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 lprate:2 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 lprate:2 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 lprate:2 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 lprate:2 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 lprate:1 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 lprate:1 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 lprate:1 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 lprate:1 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 lprate:1 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 lprate:1 ]", +] +`; + exports[`runs examples > example "lprelease" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c2 s:sawtooth clip:0.5 cutoff:300 lpenv:4 lprelease:0.5 release:0.5 ]", @@ -6199,6 +6698,75 @@ exports[`runs examples > example "lpsustain" example index 0 1`] = ` ] `; +exports[`runs examples > example "lpsync" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 lpsync:4 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 lpsync:4 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 lpsync:4 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 lpsync:4 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 lpsync:4 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 lpsync:8 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 lpsync:8 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 lpsync:8 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 lpsync:8 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 lpsync:8 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 lpsync:2 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 lpsync:2 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 lpsync:2 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 lpsync:2 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 lpsync:2 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 lpsync:2 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 lpsync:1 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 lpsync:1 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 lpsync:1 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 lpsync:1 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 lpsync:1 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 lpsync:1 ]", +] +`; + exports[`runs examples > example "lrate" example index 0 1`] = ` [ "[ 0/1 → 1/1 | n:0 s:supersquare leslie:1 lrate:1 ]", @@ -6533,6 +7101,27 @@ exports[`runs examples > example "midin" example index 0 1`] = ` ] `; +exports[`runs examples > example "midin" example index 1 1`] = ` +[ + "[ 0/1 → 1/4 | note:c s:saw ]", + "[ 1/4 → 1/2 | note:a s:saw ]", + "[ 1/2 → 3/4 | note:f s:saw ]", + "[ 3/4 → 1/1 | note:e s:saw ]", + "[ 1/1 → 5/4 | note:c s:saw ]", + "[ 5/4 → 3/2 | note:a s:saw ]", + "[ 3/2 → 7/4 | note:f s:saw ]", + "[ 7/4 → 2/1 | note:e s:saw ]", + "[ 2/1 → 9/4 | note:c s:saw ]", + "[ 9/4 → 5/2 | note:a s:saw ]", + "[ 5/2 → 11/4 | note:f s:saw ]", + "[ 11/4 → 3/1 | note:e s:saw ]", + "[ 3/1 → 13/4 | note:c s:saw ]", + "[ 13/4 → 7/2 | note:a s:saw ]", + "[ 7/2 → 15/4 | note:f s:saw ]", + "[ 15/4 → 4/1 | note:e s:saw ]", +] +`; + exports[`runs examples > example "midiport" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c midiport:0 ]", @@ -6888,18 +7477,18 @@ exports[`runs examples > example "nrpv" example index 0 1`] = ` exports[`runs examples > example "octave" example index 0 1`] = ` [ - "[ 0/1 → 1/1 | n:0 s:supersquare octave:3 ]", - "[ 0/1 → 1/1 | n:4 s:supersquare octave:3 ]", - "[ 0/1 → 1/1 | n:7 s:supersquare octave:3 ]", - "[ 1/1 → 2/1 | n:0 s:supersquare octave:4 ]", - "[ 1/1 → 2/1 | n:4 s:supersquare octave:4 ]", - "[ 1/1 → 2/1 | n:7 s:supersquare octave:4 ]", - "[ 2/1 → 3/1 | n:0 s:supersquare octave:5 ]", - "[ 2/1 → 3/1 | n:4 s:supersquare octave:5 ]", - "[ 2/1 → 3/1 | n:7 s:supersquare octave:5 ]", - "[ 3/1 → 4/1 | n:0 s:supersquare octave:6 ]", - "[ 3/1 → 4/1 | n:4 s:supersquare octave:6 ]", - "[ 3/1 → 4/1 | n:7 s:supersquare octave:6 ]", + "[ 0/1 → 1/1 | note:F3 s:supersaw octave:0 ]", + "[ 0/1 → 1/1 | note:C4 s:supersaw octave:0 ]", + "[ 0/1 → 1/1 | note:F4 s:supersaw octave:0 ]", + "[ 1/1 → 2/1 | note:F3 s:supersaw octave:1 ]", + "[ 1/1 → 2/1 | note:C4 s:supersaw octave:1 ]", + "[ 1/1 → 2/1 | note:F4 s:supersaw octave:1 ]", + "[ 2/1 → 3/1 | note:F3 s:supersaw octave:2 ]", + "[ 2/1 → 3/1 | note:C4 s:supersaw octave:2 ]", + "[ 2/1 → 3/1 | note:F4 s:supersaw octave:2 ]", + "[ 3/1 → 4/1 | note:F3 s:supersaw octave:3 ]", + "[ 3/1 → 4/1 | note:C4 s:supersaw octave:3 ]", + "[ 3/1 → 4/1 | note:F4 s:supersaw octave:3 ]", ] `; @@ -7219,6 +7808,112 @@ exports[`runs examples > example "panchor" example index 0 1`] = ` ] `; +exports[`runs examples > example "partials" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 1/16 → 1/8 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 1/8 → 3/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 3/16 → 1/4 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 1/4 → 5/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 5/16 → 3/8 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 3/8 → 7/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 7/16 → 1/2 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 1/2 → 9/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 9/16 → 5/8 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 5/8 → 11/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 11/16 → 3/4 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 3/4 → 13/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 13/16 → 7/8 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 7/8 → 15/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 15/16 → 1/1 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 1/1 → 17/16 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 17/16 → 9/8 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 9/8 → 19/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 19/16 → 5/4 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 5/4 → 21/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 21/16 → 11/8 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 11/8 → 23/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 23/16 → 3/2 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 3/2 → 25/16 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 25/16 → 13/8 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 13/8 → 27/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 27/16 → 7/4 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 7/4 → 29/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 29/16 → 15/8 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 15/8 → 31/16 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 31/16 → 2/1 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 2/1 → 33/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 33/16 → 17/8 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 17/8 → 35/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 35/16 → 9/4 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 9/4 → 37/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 37/16 → 19/8 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 19/8 → 39/16 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 39/16 → 5/2 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 5/2 → 41/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 41/16 → 21/8 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 21/8 → 43/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 43/16 → 11/4 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 11/4 → 45/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 45/16 → 23/8 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 23/8 → 47/16 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 47/16 → 3/1 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 3/1 → 49/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 49/16 → 25/8 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 25/8 → 51/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 51/16 → 13/4 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 13/4 → 53/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 53/16 → 27/8 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 27/8 → 55/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 55/16 → 7/2 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 7/2 → 57/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 57/16 → 29/8 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 29/8 → 59/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 59/16 → 15/4 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 15/4 → 61/16 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 61/16 → 31/8 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 31/8 → 63/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 63/16 → 4/1 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", +] +`; + +exports[`runs examples > example "partials" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | s:saw note:G#3 partials:[1] ]", + "[ 1/8 → 1/4 | s:saw note:A#4 partials:[1 0 1 1 0 0 0 0] ]", + "[ 1/4 → 3/8 | s:saw note:D#4 partials:[1 0 1 1 1 1 1] ]", + "[ 3/8 → 1/2 | s:saw note:D#4 partials:[1 1 0 0 1 1 1] ]", + "[ 1/2 → 5/8 | s:saw note:C#4 partials:[1 0 0 0 0 1 1] ]", + "[ 5/8 → 3/4 | s:saw note:A#3 partials:[1 0 0 0 1 1] ]", + "[ 3/4 → 7/8 | s:saw note:B3 partials:[1 1 0 0 1 1] ]", + "[ 7/8 → 1/1 | s:saw note:D#4 partials:[1 1 0 0 1 1 0] ]", + "[ 1/1 → 9/8 | s:saw note:F#4 partials:[1 0 0 0 0 1 1 0] ]", + "[ 9/8 → 5/4 | s:saw note:A#4 partials:[1 0 1 0 1 1 0 1] ]", + "[ 5/4 → 11/8 | s:saw note:A#4 partials:[1 0 1 1 1 0 1 1] ]", + "[ 11/8 → 3/2 | s:saw note:B3 partials:[1 0 1 1 1 1] ]", + "[ 3/2 → 13/8 | s:saw note:G#4 partials:[1 0 0 1 1 1 0 0] ]", + "[ 13/8 → 7/4 | s:saw note:E4 partials:[1 0 0 0 0 0 0 0] ]", + "[ 7/4 → 15/8 | s:saw note:B3 partials:[1 1 0 1 0 1] ]", + "[ 15/8 → 2/1 | s:saw note:G#4 partials:[1 0 0 1 1 0 0 1] ]", + "[ 2/1 → 17/8 | s:saw note:D#5 partials:[1 1 1 1 0 1 1 0] ]", + "[ 17/8 → 9/4 | s:saw note:C#5 partials:[1 1 1 0 1 0 0 0] ]", + "[ 9/4 → 19/8 | s:saw note:D#4 partials:[1 0 1 1 0 0 1] ]", + "[ 19/8 → 5/2 | s:saw note:C#5 partials:[1 1 0 1 0 1 1 1] ]", + "[ 5/2 → 21/8 | s:saw note:E4 partials:[1 1 1 0 1 1 0] ]", + "[ 21/8 → 11/4 | s:saw note:A#3 partials:[1 0 1 0 1 0] ]", + "[ 11/4 → 23/8 | s:saw note:G#4 partials:[1 0 1 0 0 0 1 1] ]", + "[ 23/8 → 3/1 | s:saw note:G#3 partials:[1 0 1] ]", + "[ 3/1 → 25/8 | s:saw note:B3 partials:[1 1 1 0 0 0] ]", + "[ 25/8 → 13/4 | s:saw note:D#4 partials:[1 0 1 1 0 0 0] ]", + "[ 13/4 → 27/8 | s:saw note:D#5 partials:[1 1 1 1 1 1 1 1] ]", + "[ 27/8 → 7/2 | s:saw note:D#4 partials:[1 1 0 1 0 0 1] ]", + "[ 7/2 → 29/8 | s:saw note:D#4 partials:[1 1 0 1 0 0 1] ]", + "[ 29/8 → 15/4 | s:saw note:D#5 partials:[1 1 1 1 0 0 0 1] ]", + "[ 15/4 → 31/8 | s:saw note:B4 partials:[1 1 0 1 0 0 0 0] ]", + "[ 31/8 → 4/1 | s:saw note:A#4 partials:[1 0 1 1 1 1 0 1] ]", +] +`; + exports[`runs examples > example "pattack" example index 0 1`] = ` [ "[ 0/1 → 1/2 | note:c pattack:0 ]", @@ -7472,6 +8167,75 @@ exports[`runs examples > example "phasersweep" example index 0 1`] = ` ] `; +exports[`runs examples > example "phases" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:saw note:G#1 partials:{partials:1} ]", + "[ 0/1 → 1/8 | s:saw note:G#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 1/8 → 1/4 | s:saw note:A#2 partials:{partials:1} ]", + "[ 1/8 → 1/4 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 1/4 → 3/8 | s:saw note:D#2 partials:{partials:1} ]", + "[ 1/4 → 3/8 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 3/8 → 1/2 | s:saw note:D#2 partials:{partials:1} ]", + "[ 3/8 → 1/2 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 1/2 → 5/8 | s:saw note:C#2 partials:{partials:1} ]", + "[ 1/2 → 5/8 | s:saw note:C#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 5/8 → 3/4 | s:saw note:A#1 partials:{partials:1} ]", + "[ 5/8 → 3/4 | s:saw note:A#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 3/4 → 7/8 | s:saw note:B1 partials:{partials:1} ]", + "[ 3/4 → 7/8 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 7/8 → 1/1 | s:saw note:D#2 partials:{partials:1} ]", + "[ 7/8 → 1/1 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 1/1 → 9/8 | s:saw note:F#2 partials:{partials:1} ]", + "[ 1/1 → 9/8 | s:saw note:F#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 9/8 → 5/4 | s:saw note:A#2 partials:{partials:1} ]", + "[ 9/8 → 5/4 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 5/4 → 11/8 | s:saw note:A#2 partials:{partials:1} ]", + "[ 5/4 → 11/8 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 11/8 → 3/2 | s:saw note:B1 partials:{partials:1} ]", + "[ 11/8 → 3/2 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 3/2 → 13/8 | s:saw note:G#2 partials:{partials:1} ]", + "[ 3/2 → 13/8 | s:saw note:G#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 13/8 → 7/4 | s:saw note:E2 partials:{partials:1} ]", + "[ 13/8 → 7/4 | s:saw note:E2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 7/4 → 15/8 | s:saw note:B1 partials:{partials:1} ]", + "[ 7/4 → 15/8 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 15/8 → 2/1 | s:saw note:G#2 partials:{partials:1} ]", + "[ 15/8 → 2/1 | s:saw note:G#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 2/1 → 17/8 | s:saw note:D#3 partials:{partials:1} ]", + "[ 2/1 → 17/8 | s:saw note:D#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 17/8 → 9/4 | s:saw note:C#3 partials:{partials:1} ]", + "[ 17/8 → 9/4 | s:saw note:C#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 9/4 → 19/8 | s:saw note:D#2 partials:{partials:1} ]", + "[ 9/4 → 19/8 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 19/8 → 5/2 | s:saw note:C#3 partials:{partials:1} ]", + "[ 19/8 → 5/2 | s:saw note:C#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 5/2 → 21/8 | s:saw note:E2 partials:{partials:1} ]", + "[ 5/2 → 21/8 | s:saw note:E2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 21/8 → 11/4 | s:saw note:A#1 partials:{partials:1} ]", + "[ 21/8 → 11/4 | s:saw note:A#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 11/4 → 23/8 | s:saw note:G#2 partials:{partials:1} ]", + "[ 11/4 → 23/8 | s:saw note:G#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 23/8 → 3/1 | s:saw note:G#1 partials:{partials:1} ]", + "[ 23/8 → 3/1 | s:saw note:G#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 3/1 → 25/8 | s:saw note:B1 partials:{partials:1} ]", + "[ 3/1 → 25/8 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 25/8 → 13/4 | s:saw note:D#2 partials:{partials:1} ]", + "[ 25/8 → 13/4 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 13/4 → 27/8 | s:saw note:D#3 partials:{partials:1} ]", + "[ 13/4 → 27/8 | s:saw note:D#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 27/8 → 7/2 | s:saw note:D#2 partials:{partials:1} ]", + "[ 27/8 → 7/2 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 7/2 → 29/8 | s:saw note:D#2 partials:{partials:1} ]", + "[ 7/2 → 29/8 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 29/8 → 15/4 | s:saw note:D#3 partials:{partials:1} ]", + "[ 29/8 → 15/4 | s:saw note:D#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 15/4 → 31/8 | s:saw note:B2 partials:{partials:1} ]", + "[ 15/4 → 31/8 | s:saw note:B2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 31/8 → 4/1 | s:saw note:A#2 partials:{partials:1} ]", + "[ 31/8 → 4/1 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", +] +`; + exports[`runs examples > example "pianoroll" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", @@ -8193,6 +8957,75 @@ exports[`runs examples > example "rand" example index 0 1`] = ` ] `; +exports[`runs examples > example "randL" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:saw note:F1 partials:[0 0 0 0 0 0 0 0] ]", + "[ 1/16 → 1/8 | s:saw note:Bb2 partials:[0.8426077850162983 0.0886186733841896 0.9342893119901419 0.06848422810435295 0.6248505394905806 0.5372848063707352 0.8283301629126072 0.7969411127269268] ]", + "[ 1/8 → 3/16 | s:saw note:G2 partials:[0.6852155700325966 0.17723728902637959 0.8521428182721138 0.5022399611771107 0.9780306946486235 0.3186125475913286 0.1851645242422819 0.8495976086705923] ]", + "[ 3/16 → 1/4 | s:saw note:Ab1 partials:[0.20066574029624462 0.17195586115121841 0.2159541044384241 0.17005567252635956 0.6841662060469389 0.07906394638121128 0.004815371707081795 0.027107616886496544] ]", + "[ 1/4 → 5/16 | s:saw note:C2 partials:[0.36975969187915325 0.18273563869297504 0.012781793251633644 0.5423613861203194 0.12467948533594608 0.7188410349190235 0.856887087225914 0.8818203993141651] ]", + "[ 5/16 → 3/8 | s:saw note:Eb2 partials:[0.5675661638379097 0.15932588279247284 0.31376867927610874 0.32359412126243114 0.3281281068921089 0.011714376509189606 0.4433113746345043 0.68459103256464] ]", + "[ 3/8 → 7/16 | s:saw note:C2 partials:[0.40139251574873924 0.15799915604293346 0.9604704882949591 0.03873417526483536 0.4157217647880316 0.27756719291210175 0.8800551909953356 0.5692523363977671] ]", + "[ 7/16 → 1/2 | s:saw note:Bb1 partials:[0.3015887886285782 0.22311350144445896 0.47645803540945053 0.001545613631606102 0.53841289319098 0.330710094422102 0.2747743520885706 0.9471044968813658] ]", + "[ 1/2 → 9/16 | s:saw note:Bb1 partials:[0.2604806162416935 0.3654713351279497 0.9900747090578079 0.22572625242173672 0.9328999016433954 0.5273839123547077 0.7047769222408533 0.7311522178351879] ]", + "[ 9/16 → 5/8 | s:saw note:G1 partials:[0.16399178467690945 0.9559339117258787 0.21830322034657001 0.4903192054480314 0.2504696864634752 0.7302105724811554 0.6580934692174196 0.9855979979038239] ]", + "[ 5/8 → 11/16 | s:saw note:G1 partials:[0.1356358677148819 0.5403102282434702 0.07321739941835403 0.9646544624119997 0.7707359045743942 0.46888123638927937 0.9489036612212658 0.366748945787549] ]", + "[ 11/16 → 3/4 | s:saw note:C2 partials:[0.408811716362834 0.9613851886242628 0.7683626655489206 0.5862949229776859 0.9568102769553661 0.7154356613755226 0.6283384170383215 0.2193678840994835] ]", + "[ 3/4 → 13/16 | s:saw note:Ab1 partials:[0.19582648016512394 0.0024610888212919235 0.08272589556872845 0.09592138417065144 0.9678201265633106 0.3249557167291641 0.9463537875562906 0.20766610652208328] ]", + "[ 13/16 → 7/8 | s:saw note:Ab2 partials:[0.7516226731240749 0.15610851533710957 0.9039078876376152 0.8602268267422915 0.928601048886776 0.4479671400040388 0.5080656576901674 0.562442347407341] ]", + "[ 7/8 → 15/16 | s:saw note:C2 partials:[0.3976310808211565 0.13476407527923584 0.963376859202981 0.6579397786408663 0.5203975513577461 0.865439185872674 0.5583905670791864 0.2736878804862499] ]", + "[ 15/16 → 1/1 | s:saw note:G2 partials:[0.7029578909277916 0.6351367030292749 0.26143294386565685 0.6411824338138103 0.5676082745194435 0.4803342465311289 0.5140306428074837 0.9477859679609537] ]", + "[ 1/1 → 17/16 | s:saw note:Eb2 partials:[0.5195421651005745 0.9349692352116108 0.1718774326145649 0.9601866770535707 0.2802200373262167 0.8732441551983356 0.01810968853533268 0.42737805284559727] ]", + "[ 17/16 → 9/8 | s:saw note:Eb2 partials:[0.5486328881233931 0.7088070474565029 0.07877279818058014 0.9422610364854336 0.7696988433599472 0.8289324026554823 0.5925844628363848 0.3920764606446028] ]", + "[ 9/8 → 19/16 | s:saw note:G2 partials:[0.6724895145744085 0.21410975232720375 0.5229047331959009 0.20957534946501255 0.7402758821845055 0.2681974694132805 0.4059371296316385 0.9422509074211121] ]", + "[ 19/16 → 5/4 | s:saw note:F1 partials:[0.08174665085971355 0.2762963864952326 0.5428560189902782 0.29452037811279297 0.17946280725300312 0.366960596293211 0.26327736489474773 0.5002023074775934] ]", + "[ 5/4 → 21/16 | s:saw note:G2 partials:[0.7287282031029463 0.9341024849563837 0.5098182689398527 0.5608645845204592 0.6812942810356617 0.5015129633247852 0.4602120481431484 0.927369873970747] ]", + "[ 21/16 → 11/8 | s:saw note:F1 partials:[0.08104278706014156 0.5200720727443695 0.5363391060382128 0.9824271816760302 0.10652091167867184 0.23473932035267353 0.7656102329492569 0.4850195776671171] ]", + "[ 11/8 → 23/16 | s:saw note:Ab1 partials:[0.18280375190079212 0.40527783520519733 0.30504362285137177 0.7622128054499626 0.842598931863904 0.053796686232089996 0.5124214049428701 0.9178454764187336] ]", + "[ 23/16 → 3/2 | s:saw note:Eb2 partials:[0.5081270858645439 0.1421387754380703 0.8852288406342268 0.9066353775560856 0.7700740322470665 0.35437702015042305 0.20032598450779915 0.963155921548605] ]", + "[ 3/2 → 25/16 | s:saw note:F2 partials:[0.6084080748260021 0.4424846563488245 0.755185678601265 0.04328125901520252 0.04244415462017059 0.3994515985250473 0.787989066913724 0.3180440980941057] ]", + "[ 25/16 → 13/8 | s:saw note:Ab1 partials:[0.17092766426503658 0.7808954659849405 0.8234915658831596 0.8643151633441448 0.9781719036400318 0.5102015696465969 0.018259897828102112 0.25844234600663185] ]", + "[ 13/8 → 27/16 | s:saw note:Db2 partials:[0.49675471149384975 0.2965749856084585 0.09848833456635475 0.19172007404267788 0.22677889838814735 0.9841996673494577 0.11156083643436432 0.6079028844833374] ]", + "[ 27/16 → 7/4 | s:saw note:Bb1 partials:[0.29589061066508293 0.13011129200458527 0.8021425940096378 0.38040103763341904 0.33274981752038 0.29745868407189846 0.9898250997066498 0.743482418358326] ]", + "[ 7/4 → 29/16 | s:saw note:Ab1 partials:[0.20473789609968662 0.7480021081864834 0.30757393687963486 0.5827204789966345 0.22094514779746532 0.6370698790997267 0.16573002003133297 0.0638319905847311] ]", + "[ 29/16 → 15/8 | s:saw note:Db2 partials:[0.4695742893964052 0.6690364442765713 0.6216684486716986 0.41342394426465034 0.9508822970092297 0.8403679896146059 0.91759910620749 0.6591395419090986] ]", + "[ 15/8 → 31/16 | s:saw note:F2 partials:[0.5945571791380644 0.3962489552795887 0.563431927934289 0.4742323160171509 0.4185752831399441 0.9703940469771624 0.5282467231154442 0.6896266285330057] ]", + "[ 31/16 → 2/1 | s:saw note:Bb2 partials:[0.8672592639923096 0.3438429981470108 0.9795673936605453 0.46547594852745533 0.9770395755767822 0.36851615831255913 0.7725539114326239 0.876962523907423] ]", + "[ 2/1 → 33/16 | s:saw note:C3 partials:[0.9595271199941635 0.5427457969635725 0.45864437520504 0.8057199101895094 0.18388565629720688 0.17221237532794476 0.8838487900793552 0.4976818822324276] ]", + "[ 33/16 → 17/8 | s:saw note:Bb1 partials:[0.3324784208089113 0.6005446836352348 0.1564352661371231 0.765217661857605 0.12899602390825748 0.20880493707954884 0.9156261626631021 0.5550615340471268] ]", + "[ 17/8 → 35/16 | s:saw note:Bb2 partials:[0.9033902939409018 0.22919894196093082 0.8388008493930101 0.9108077362179756 0.32246968522667885 0.16122018359601498 0.31932324543595314 0.18524732999503613] ]", + "[ 35/16 → 9/4 | s:saw note:F1 partials:[0.037000780925154686 0.30360451713204384 0.7516817897558212 0.17065968923270702 0.24669718369841576 0.978821286931634 0.9620356522500515 0.12417634017765522] ]", + "[ 9/4 → 37/16 | s:saw note:C2 partials:[0.34548251144587994 0.30419893004000187 0.10205957666039467 0.9906709585338831 0.3156223688274622 0.3745057098567486 0.7668170686811209 0.9379972033202648] ]", + "[ 37/16 → 19/8 | s:saw note:F2 partials:[0.6649543270468712 0.41613837145268917 0.9078915324062109 0.2232209537178278 0.2918607946485281 0.4931973237544298 0.7035325299948454 0.29280414804816246] ]", + "[ 19/8 → 39/16 | s:saw note:Bb2 partials:[0.8365066405385733 0.5369812995195389 0.2263860274106264 0.21759207546710968 0.2172116208821535 0.09454222768545151 0.8075542915612459 0.9017798062413931] ]", + "[ 39/16 → 5/2 | s:saw note:G1 partials:[0.14231421053409576 0.6801821701228619 0.7958894111216068 0.9494249671697617 0.179213372990489 0.22531690262258053 0.7458387855440378 0.3595987129956484] ]", + "[ 5/2 → 41/16 | s:saw note:Db2 partials:[0.45861607417464256 0.6807645913213491 0.07009582966566086 0.1450389064848423 0.2579921595752239 0.612881500273943 0.5503941811621189 0.7027011960744858] ]", + "[ 41/16 → 21/8 | s:saw note:F2 partials:[0.6068673655390739 0.8489279896020889 0.027898555621504784 0.90520747192204 0.18605150654911995 0.30939464271068573 0.16843407973647118 0.5127317048609257] ]", + "[ 21/8 → 43/16 | s:saw note:G1 partials:[0.16019348427653313 0.47758268006145954 0.9749126061797142 0.8025561925023794 0.3600110989063978 0.324309878051281 0.5655391272157431 0.13364790193736553] ]", + "[ 43/16 → 11/4 | s:saw note:Eb2 partials:[0.538035349920392 0.4103843830525875 0.6443832442164421 0.7532152868807316 0.32128027081489563 0.29064710810780525 0.5972099266946316 0.8252892810851336] ]", + "[ 11/4 → 45/16 | s:saw note:F2 partials:[0.6332327704876661 0.7342763151973486 0.16935866326093674 0.4754706546664238 0.528122790157795 0.21934260986745358 0.7476693484932184 0.2995396424084902] ]", + "[ 45/16 → 23/8 | s:saw note:Bb2 partials:[0.8912940509617329 0.5256196465343237 0.25639201514422894 0.46860250271856785 0.08611978217959404 0.6175916157662868 0.990638293325901 0.11189854890108109] ]", + "[ 23/8 → 47/16 | s:saw note:F1 partials:[0.01625417172908783 0.28409438766539097 0.3075305689126253 0.40343056432902813 0.5556836389005184 0.6926821582019329 0.5635769125074148 0.017873913049697876] ]", + "[ 47/16 → 3/1 | s:saw note:Bb1 partials:[0.2830589488148689 0.3613663297146559 0.6216250211000443 0.6083405166864395 0.27202711440622807 0.771099541336298 0.3156145606189966 0.736228458583355] ]", + "[ 3/1 → 49/16 | s:saw note:Ab1 partials:[0.21728911064565182 0.7605195846408606 0.11240843310952187 0.4372697602957487 0.15630115941166878 0.1653979979455471 0.5303416550159454 0.07218753732740879] ]", + "[ 49/16 → 25/8 | s:saw note:F1 partials:[0.012518879026174545 0.6565517522394657 0.783835593611002 0.2481316588819027 0.27786846831440926 0.6153149046003819 0.39914070069789886 0.2175555843859911] ]", + "[ 25/8 → 51/16 | s:saw note:C2 partials:[0.34324387833476067 0.14098095521330833 0.012934491038322449 0.827436288818717 0.5283997394144535 0.8942463714629412 0.4924008697271347 0.9298017006367445] ]", + "[ 51/16 → 13/4 | s:saw note:Bb2 partials:[0.9057559575885534 0.5731389932334423 0.06561482697725296 0.02156665176153183 0.4685337021946907 0.000788738951086998 0.6995994579046965 0.9893404543399811] ]", + "[ 13/4 → 53/16 | s:saw note:C3 partials:[0.9923650715500116 0.7797339502722025 0.1992435697466135 0.9005183521658182 0.718992218375206 0.23745290748775005 0.28872333094477654 0.35448253713548183] ]", + "[ 53/16 → 27/8 | s:saw note:Bb1 partials:[0.3235324025154114 0.5165992900729179 0.6231792941689491 0.8034896682947874 0.6663950439542532 0.4207208137959242 0.9748435858637094 0.27676148526370525] ]", + "[ 27/8 → 55/16 | s:saw note:C2 partials:[0.40869180113077164 0.4018078800290823 0.34696186147630215 0.9572948440909386 0.400035472586751 0.3531211093068123 0.7240961641073227 0.8125612027943134] ]", + "[ 55/16 → 7/2 | s:saw note:Ab2 partials:[0.775677714496851 0.0502743236720562 0.3246541116386652 0.503994770348072 0.8252716399729252 0.89872563816607 0.060319963842630386 0.25497629307210445] ]", + "[ 7/2 → 57/16 | s:saw note:C2 partials:[0.40994881466031075 0.6455810181796551 0.35704658553004265 0.2732649203389883 0.31587288342416286 0.23243548162281513 0.22569947130978107 0.17241661623120308] ]", + "[ 57/16 → 29/8 | s:saw note:Ab2 partials:[0.8133465722203255 0.8327706772834063 0.32914630882442 0.23131784796714783 0.7526696771383286 0.1361286472529173 0.11441296897828579 0.36366880126297474] ]", + "[ 29/8 → 59/16 | s:saw note:C3 partials:[0.9391485787928104 0.3380728308111429 0.7723016366362572 0.998674126341939 0.7193406894803047 0.6963680125772953 0.9260678570717573 0.7829763628542423] ]", + "[ 59/16 → 15/4 | s:saw note:C3 partials:[0.9731374885886908 0.0494034755975008 0.990720022469759 0.9819309785962105 0.22877203114330769 0.7265191469341516 0.4922411348670721 0.7549834884703159] ]", + "[ 15/4 → 61/16 | s:saw note:Ab2 partials:[0.8121673222631216 0.25548945367336273 0.5759696904569864 0.6668333616107702 0.9233786761760712 0.11143362522125244 0.9734930321574211 0.369325939565897] ]", + "[ 61/16 → 31/8 | s:saw note:G1 partials:[0.1008925698697567 0.7558664139360189 0.1523460578173399 0.6523381881415844 0.9690635204315186 0.7343240566551685 0.9544064309448004 0.9150135722011328] ]", + "[ 31/8 → 63/16 | s:saw note:G2 partials:[0.7361665386706591 0.7657648548483849 0.21474426984786987 0.8228576295077801 0.5647660344839096 0.08148551359772682 0.9714624118059874 0.13992334716022015] ]", + "[ 63/16 → 4/1 | s:saw note:Bb1 partials:[0.3056422360241413 0.8881660811603069 0.7683485243469477 0.08641545102000237 0.5427133180201054 0.20803124643862247 0.5347504671663046 0.5977730695158243] ]", +] +`; + exports[`runs examples > example "range" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh cutoff:2250 ]", @@ -8406,6 +9239,43 @@ exports[`runs examples > example "ratio" example index 0 1`] = ` ] `; +exports[`runs examples > example "register" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:saw velocity:0 cutoff:0 ]", + "[ 1/8 → 1/4 | s:saw velocity:0.6852155700325966 cutoff:548.1724560260773 ]", + "[ 1/4 → 3/8 | s:saw velocity:0.36975969187915325 cutoff:295.8077535033226 ]", + "[ 3/8 → 1/2 | s:saw velocity:0.40139251574873924 cutoff:321.1140125989914 ]", + "[ 1/2 → 5/8 | s:saw velocity:0.2604806162416935 cutoff:208.3844929933548 ]", + "[ 5/8 → 3/4 | s:saw velocity:0.1356358677148819 cutoff:108.50869417190552 ]", + "[ 3/4 → 7/8 | s:saw velocity:0.19582648016512394 cutoff:156.66118413209915 ]", + "[ 7/8 → 1/1 | s:saw velocity:0.3976310808211565 cutoff:318.1048646569252 ]", + "[ 1/1 → 9/8 | s:saw velocity:0.5195421651005745 cutoff:415.6337320804596 ]", + "[ 9/8 → 5/4 | s:saw velocity:0.6724895145744085 cutoff:537.9916116595268 ]", + "[ 5/4 → 11/8 | s:saw velocity:0.7287282031029463 cutoff:582.982562482357 ]", + "[ 11/8 → 3/2 | s:saw velocity:0.18280375190079212 cutoff:146.2430015206337 ]", + "[ 3/2 → 13/8 | s:saw velocity:0.6084080748260021 cutoff:486.7264598608017 ]", + "[ 13/8 → 7/4 | s:saw velocity:0.49675471149384975 cutoff:397.4037691950798 ]", + "[ 7/4 → 15/8 | s:saw velocity:0.20473789609968662 cutoff:163.7903168797493 ]", + "[ 15/8 → 2/1 | s:saw velocity:0.5945571791380644 cutoff:475.6457433104515 ]", + "[ 2/1 → 17/8 | s:saw velocity:0.9595271199941635 cutoff:767.6216959953308 ]", + "[ 17/8 → 9/4 | s:saw velocity:0.9033902939409018 cutoff:722.7122351527214 ]", + "[ 9/4 → 19/8 | s:saw velocity:0.34548251144587994 cutoff:276.38600915670395 ]", + "[ 19/8 → 5/2 | s:saw velocity:0.8365066405385733 cutoff:669.2053124308586 ]", + "[ 5/2 → 21/8 | s:saw velocity:0.45861607417464256 cutoff:366.89285933971405 ]", + "[ 21/8 → 11/4 | s:saw velocity:0.16019348427653313 cutoff:128.1547874212265 ]", + "[ 11/4 → 23/8 | s:saw velocity:0.6332327704876661 cutoff:506.5862163901329 ]", + "[ 23/8 → 3/1 | s:saw velocity:0.01625417172908783 cutoff:13.003337383270264 ]", + "[ 3/1 → 25/8 | s:saw velocity:0.21728911064565182 cutoff:173.83128851652145 ]", + "[ 25/8 → 13/4 | s:saw velocity:0.34324387833476067 cutoff:274.59510266780853 ]", + "[ 13/4 → 27/8 | s:saw velocity:0.9923650715500116 cutoff:793.8920572400093 ]", + "[ 27/8 → 7/2 | s:saw velocity:0.40869180113077164 cutoff:326.9534409046173 ]", + "[ 7/2 → 29/8 | s:saw velocity:0.40994881466031075 cutoff:327.9590517282486 ]", + "[ 29/8 → 15/4 | s:saw velocity:0.9391485787928104 cutoff:751.3188630342484 ]", + "[ 15/4 → 31/8 | s:saw velocity:0.8121673222631216 cutoff:649.7338578104973 ]", + "[ 31/8 → 4/1 | s:saw velocity:0.7361665386706591 cutoff:588.9332309365273 ]", +] +`; + exports[`runs examples > example "release" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c3 release:0 ]", @@ -8598,6 +9468,19 @@ exports[`runs examples > example "rev" example index 0 1`] = ` ] `; +exports[`runs examples > example "revv" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | note:g ]", + "[ 1/2 → 1/1 | note:e ]", + "[ 1/1 → 3/2 | note:d ]", + "[ 3/2 → 2/1 | note:c ]", + "[ 2/1 → 5/2 | note:g ]", + "[ 5/2 → 3/1 | note:e ]", + "[ 3/1 → 7/2 | note:d ]", + "[ 7/2 → 4/1 | note:c ]", +] +`; + exports[`runs examples > example "ribbon" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:d ]", @@ -9014,42 +9897,6 @@ exports[`runs examples > example "samples" example index 1 1`] = ` ] `; -exports[`runs examples > example "samples" example index 2 1`] = ` -[ - "[ 0/1 → 1/2 | s:noise ]", - "[ 1/2 → 3/4 | s:chimp n:0 ]", - "[ 3/4 → 1/1 | s:chimp n:0 ]", - "[ 1/1 → 3/2 | s:noise ]", - "[ 3/2 → 2/1 | s:chimp n:1 ]", - "[ 2/1 → 5/2 | s:noise ]", - "[ 5/2 → 11/4 | s:chimp n:0 ]", - "[ 11/4 → 3/1 | s:chimp n:0 ]", - "[ 3/1 → 7/2 | s:noise ]", - "[ 7/2 → 4/1 | s:chimp n:1 ]", -] -`; - -exports[`runs examples > example "samples" example index 3 1`] = ` -[ - "[ 0/1 → 1/4 | s:chocolat ]", - "[ 1/4 → 1/2 | s:chocolat ]", - "[ 1/2 → 3/4 | s:chocolat ]", - "[ 3/4 → 1/1 | s:chocolat ]", - "[ 1/1 → 5/4 | s:chocolat ]", - "[ 5/4 → 3/2 | s:chocolat ]", - "[ 3/2 → 7/4 | s:chocolat ]", - "[ 7/4 → 2/1 | s:chocolat ]", - "[ 2/1 → 9/4 | s:chocolat ]", - "[ 9/4 → 5/2 | s:chocolat ]", - "[ 5/2 → 11/4 | s:chocolat ]", - "[ 11/4 → 3/1 | s:chocolat ]", - "[ 3/1 → 13/4 | s:chocolat ]", - "[ 13/4 → 7/2 | s:chocolat ]", - "[ 7/2 → 15/4 | s:chocolat ]", - "[ 15/4 → 4/1 | s:chocolat ]", -] -`; - exports[`runs examples > example "saw" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:c3 clip:0 ]", @@ -11124,6 +11971,75 @@ exports[`runs examples > example "sysexid" example index 0 1`] = ` ] `; +exports[`runs examples > example "tag" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:square note:F1 cutoff:40 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 1/16 → 1/8 | s:square note:F1 cutoff:41.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 1/8 → 3/16 | s:square note:F1 cutoff:42.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 3/16 → 1/4 | s:square note:F1 cutoff:43.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 1/4 → 5/16 | s:square note:F1 cutoff:45 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 5/16 → 3/8 | s:square note:F1 cutoff:46.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 3/8 → 7/16 | s:square note:F1 cutoff:47.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 7/16 → 1/2 | s:square note:F1 cutoff:48.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 1/2 → 9/16 | s:square note:F1 cutoff:50 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 9/16 → 5/8 | s:square note:F1 cutoff:51.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 5/8 → 11/16 | s:square note:F1 cutoff:52.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 11/16 → 3/4 | s:square note:F1 cutoff:53.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 3/4 → 13/16 | s:square note:F1 cutoff:55 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 13/16 → 7/8 | s:square note:F1 cutoff:56.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 7/8 → 15/16 | s:square note:F1 cutoff:57.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 15/16 → 1/1 | s:square note:F1 cutoff:58.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 1/1 → 17/16 | s:saw note:F2 cutoff:60 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 17/16 → 9/8 | s:saw note:F2 cutoff:61.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 9/8 → 19/16 | s:saw note:F2 cutoff:62.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 19/16 → 5/4 | s:saw note:F2 cutoff:63.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 5/4 → 21/16 | s:saw note:F2 cutoff:65 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 21/16 → 11/8 | s:saw note:F2 cutoff:66.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 11/8 → 23/16 | s:saw note:F2 cutoff:67.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 23/16 → 3/2 | s:saw note:F2 cutoff:68.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 3/2 → 25/16 | s:saw note:F2 cutoff:70 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 25/16 → 13/8 | s:saw note:F2 cutoff:71.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 13/8 → 27/16 | s:saw note:F2 cutoff:72.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 27/16 → 7/4 | s:saw note:F2 cutoff:73.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 7/4 → 29/16 | s:saw note:F2 cutoff:75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 29/16 → 15/8 | s:saw note:F2 cutoff:76.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 15/8 → 31/16 | s:saw note:F2 cutoff:77.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 31/16 → 2/1 | s:saw note:F2 cutoff:78.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 2/1 → 33/16 | s:saw note:F1 cutoff:80 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 33/16 → 17/8 | s:saw note:F1 cutoff:78.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 17/8 → 35/16 | s:saw note:F1 cutoff:77.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 35/16 → 9/4 | s:saw note:F1 cutoff:76.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 9/4 → 37/16 | s:saw note:F1 cutoff:75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 37/16 → 19/8 | s:saw note:F1 cutoff:73.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 19/8 → 39/16 | s:saw note:F1 cutoff:72.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 39/16 → 5/2 | s:saw note:F1 cutoff:71.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 5/2 → 41/16 | s:saw note:F1 cutoff:70 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 41/16 → 21/8 | s:saw note:F1 cutoff:68.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 21/8 → 43/16 | s:saw note:F1 cutoff:67.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 43/16 → 11/4 | s:saw note:F1 cutoff:66.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 11/4 → 45/16 | s:saw note:F1 cutoff:65 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 45/16 → 23/8 | s:saw note:F1 cutoff:63.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 23/8 → 47/16 | s:saw note:F1 cutoff:62.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 47/16 → 3/1 | s:saw note:F1 cutoff:61.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 3/1 → 49/16 | s:square note:F1 cutoff:60 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 49/16 → 25/8 | s:square note:F1 cutoff:58.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 25/8 → 51/16 | s:square note:F1 cutoff:57.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 51/16 → 13/4 | s:square note:F1 cutoff:56.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 13/4 → 53/16 | s:square note:F1 cutoff:55 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 53/16 → 27/8 | s:square note:F1 cutoff:53.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 27/8 → 55/16 | s:square note:F1 cutoff:52.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 55/16 → 7/2 | s:square note:F1 cutoff:51.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 7/2 → 57/16 | s:square note:F1 cutoff:50 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 57/16 → 29/8 | s:square note:F1 cutoff:48.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 29/8 → 59/16 | s:square note:F1 cutoff:47.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 59/16 → 15/4 | s:square note:F1 cutoff:46.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 15/4 → 61/16 | s:square note:F1 cutoff:45 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 61/16 → 31/8 | s:square note:F1 cutoff:43.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 31/8 → 63/16 | s:square note:F1 cutoff:42.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 63/16 → 4/1 | s:square note:F1 cutoff:41.25 lpenv:5 resonance:4 lpdecay:0.15 ]", +] +`; + exports[`runs examples > example "take" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:bd ]", @@ -11236,6 +12152,84 @@ exports[`runs examples > example "tour" example index 0 1`] = ` ] `; +exports[`runs examples > example "transient" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:bd transient:-1 ]", + "[ 1/1 → 2/1 | s:bd transient:-0.5 ]", + "[ 2/1 → 3/1 | s:bd transient:0 ]", + "[ 3/1 → 4/1 | s:bd transient:0.5 ]", +] +`; + +exports[`runs examples > example "transient" example index 1 1`] = ` +[ + "[ 0/1 → 1/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 1/16 → 1/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 1/8 → 3/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 3/16 → 1/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 1/4 → 5/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 5/16 → 3/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 3/8 → 7/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 7/16 → 1/2 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 1/2 → 9/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 9/16 → 5/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 5/8 → 11/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 11/16 → 3/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 3/4 → 13/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 13/16 → 7/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 7/8 → 15/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 15/16 → 1/1 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 1/1 → 17/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 17/16 → 9/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 9/8 → 19/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 19/16 → 5/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 5/4 → 21/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 21/16 → 11/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 11/8 → 23/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 23/16 → 3/2 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 3/2 → 25/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 25/16 → 13/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 13/8 → 27/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 27/16 → 7/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 7/4 → 29/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 29/16 → 15/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 15/8 → 31/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 31/16 → 2/1 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 2/1 → 33/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 33/16 → 17/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 17/8 → 35/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 35/16 → 9/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 9/4 → 37/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 37/16 → 19/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 19/8 → 39/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 39/16 → 5/2 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 5/2 → 41/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 41/16 → 21/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 21/8 → 43/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 43/16 → 11/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 11/4 → 45/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 45/16 → 23/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 23/8 → 47/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 47/16 → 3/1 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 3/1 → 49/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 49/16 → 25/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 25/8 → 51/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 51/16 → 13/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 13/4 → 53/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 53/16 → 27/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 27/8 → 55/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 55/16 → 7/2 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 7/2 → 57/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 57/16 → 29/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 29/8 → 59/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 59/16 → 15/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 15/4 → 61/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 61/16 → 31/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 31/8 → 63/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 63/16 → 4/1 | s:hh bank:tr909 transient:1 transsustain:-1 ]", +] +`; + exports[`runs examples > example "transpose" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C2 ]", @@ -12379,54 +13373,54 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = ` exports[`runs examples > example "wchooseCycles" example index 1 1`] = ` [ - "[ 0/1 → 1/12 | s:bd ]", - "[ 1/12 → 1/6 | s:bd ]", - "[ 1/6 → 1/4 | s:bd ]", - "[ 1/4 → 1/3 | s:bd ]", - "[ 1/3 → 5/12 | s:bd ]", - "[ 5/12 → 1/2 | s:bd ]", - "[ 1/2 → 7/12 | s:sd ]", - "[ 7/12 → 2/3 | s:sd ]", - "[ 2/3 → 3/4 | s:sd ]", - "[ 3/4 → 5/6 | s:bd ]", - "[ 5/6 → 11/12 | s:bd ]", - "[ 11/12 → 1/1 | s:bd ]", - "[ 1/1 → 13/12 | s:bd ]", - "[ 13/12 → 7/6 | s:bd ]", - "[ 7/6 → 5/4 | s:bd ]", - "[ 5/4 → 4/3 | s:bd ]", - "[ 4/3 → 17/12 | s:bd ]", - "[ 17/12 → 3/2 | s:bd ]", - "[ 3/2 → 19/12 | s:hh ]", - "[ 19/12 → 5/3 | s:hh ]", - "[ 5/3 → 7/4 | s:hh ]", - "[ 7/4 → 11/6 | s:bd ]", - "[ 11/6 → 23/12 | s:bd ]", - "[ 23/12 → 2/1 | s:bd ]", - "[ 2/1 → 25/12 | s:hh ]", - "[ 25/12 → 13/6 | s:hh ]", - "[ 13/6 → 9/4 | s:hh ]", - "[ 9/4 → 7/3 | s:hh ]", - "[ 7/3 → 29/12 | s:hh ]", - "[ 29/12 → 5/2 | s:hh ]", - "[ 5/2 → 31/12 | s:bd ]", - "[ 31/12 → 8/3 | s:bd ]", - "[ 8/3 → 11/4 | s:bd ]", - "[ 11/4 → 17/6 | s:bd ]", - "[ 17/6 → 35/12 | s:bd ]", - "[ 35/12 → 3/1 | s:bd ]", - "[ 3/1 → 37/12 | s:bd ]", - "[ 37/12 → 19/6 | s:bd ]", - "[ 19/6 → 13/4 | s:bd ]", - "[ 13/4 → 10/3 | s:sd ]", - "[ 10/3 → 41/12 | s:sd ]", - "[ 41/12 → 7/2 | s:sd ]", - "[ 7/2 → 43/12 | s:hh ]", - "[ 43/12 → 11/3 | s:hh ]", - "[ 11/3 → 15/4 | s:hh ]", - "[ 15/4 → 23/6 | s:bd ]", - "[ 23/6 → 47/12 | s:bd ]", - "[ 47/12 → 4/1 | s:bd ]", + "[ 0/1 → 1/12 | note:c ]", + "[ 1/12 → 1/6 | note:c ]", + "[ 1/6 → 1/4 | note:c ]", + "[ 1/4 → 1/3 | note:c ]", + "[ 1/3 → 5/12 | note:c ]", + "[ 5/12 → 1/2 | note:c ]", + "[ 1/2 → 7/12 | note:f ]", + "[ 7/12 → 2/3 | note:f ]", + "[ 2/3 → 3/4 | note:f ]", + "[ 3/4 → 5/6 | note:c ]", + "[ 5/6 → 11/12 | note:c ]", + "[ 11/12 → 1/1 | note:c ]", + "[ 1/1 → 13/12 | note:c ]", + "[ 13/12 → 7/6 | note:c ]", + "[ 7/6 → 5/4 | note:c ]", + "[ 5/4 → 4/3 | note:c ]", + "[ 4/3 → 17/12 | note:c ]", + "[ 17/12 → 3/2 | note:c ]", + "[ 3/2 → 19/12 | note:a ]", + "[ 19/12 → 5/3 | note:a ]", + "[ 5/3 → 7/4 | note:a ]", + "[ 7/4 → 11/6 | note:c ]", + "[ 11/6 → 23/12 | note:c ]", + "[ 23/12 → 2/1 | note:c ]", + "[ 2/1 → 25/12 | note:a ]", + "[ 25/12 → 13/6 | note:a ]", + "[ 13/6 → 9/4 | note:a ]", + "[ 9/4 → 7/3 | note:a ]", + "[ 7/3 → 29/12 | note:a ]", + "[ 29/12 → 5/2 | note:a ]", + "[ 5/2 → 31/12 | note:c ]", + "[ 31/12 → 8/3 | note:c ]", + "[ 8/3 → 11/4 | note:c ]", + "[ 11/4 → 17/6 | note:c ]", + "[ 17/6 → 35/12 | note:c ]", + "[ 35/12 → 3/1 | note:c ]", + "[ 3/1 → 37/12 | note:c ]", + "[ 37/12 → 19/6 | note:c ]", + "[ 19/6 → 13/4 | note:c ]", + "[ 13/4 → 10/3 | note:f ]", + "[ 10/3 → 41/12 | note:f ]", + "[ 41/12 → 7/2 | note:f ]", + "[ 7/2 → 43/12 | note:a ]", + "[ 43/12 → 11/3 | note:a ]", + "[ 11/3 → 15/4 | note:a ]", + "[ 15/4 → 23/6 | note:c ]", + "[ 23/6 → 47/12 | note:c ]", + "[ 47/12 → 4/1 | note:c ]", ] `; diff --git a/tools/dbpatch/package.json b/tools/dbpatch/package.json index 3ee042ad9..21d79e199 100644 --- a/tools/dbpatch/package.json +++ b/tools/dbpatch/package.json @@ -1,4 +1,5 @@ { + "name": "dbpatch", "dependencies": { "csv": "^6.3.11" }, diff --git a/warm.js b/warm.js new file mode 100644 index 000000000..0d148dee9 --- /dev/null +++ b/warm.js @@ -0,0 +1,14 @@ +fetch('https://codeberg.org/api/v1/repos/uzu/strudel/pulls?state=closed&page=1') + .then((res) => res.json()) + .then((pulls) => { + const r = pulls + .filter((pull) => pull.merged) + .sort((a, b) => new Date(b.closed_at) - new Date(a.closed_at)) + .map((pull) => `${pull.closed_at} ${pull.title} by ${pull.user.login || '?'} in: [#${pull.number}](${pull.url}) `) + .join('\n'); + console.log(r); + }); + +/* + + */ diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 0fcc41363..56b28c0e4 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -48,28 +48,94 @@ You can also use the `crackle` type to play some subtle noise crackles. You can ### Additive Synthesis -To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform: +Periodic waveforms are composed of several [harmonics](https://en.wikipedia.org/wiki/Harmonic) above a fundamental frequency, lying at integer multiples. These overtones combine to give a sound its unique timbral quality. + +For the basic waveforms, we offer you control over these harmonics with the `partials` and `phases` functions. + +#### Partials + +`partials` refers to the magnitude of each harmonic relative to the fundamental frequency. They can thus be used to spectrally filter these waveforms and tame some of their harshness: >".fast(2)) .sound("sawtooth") -.n("<32 16 8 4>") +.partials([1, 1, "<1 0>", "<1 0>", "<1 0>", "<1 0>", "<1 0>"]) ._scope()`} /> -When the `n` control is used on a basic waveform, it defines the number of harmonic partials the sound is getting. -You can also set `n` directly in mini notation with `sound`: +`partials` can also be used to construct _new_ waveforms not present in our basic set with the 'user' sound source: >".fast(2)) -.sound("sawtooth:<32 16 8 4>") +.sound("user") +.partials([1, 0, 0.3, 0, 0.1, 0, 0, 0.3]) ._scope()`} /> -Note for tidal users: `n` in tidal is synonymous to `note` for synths only. -In strudel, this is not the case, where `n` will always change timbre, be it though different samples or different waveforms. +We may algorithmically construct lists of magnitudes with Javascript code like: + +>".fast(2)) +.sound("saw") +.partials(new Array(numHarmonics).fill(1)) +._scope()`} +/> + +which acts as a spectral filter. Or: + +>").fast(2) +.sound("user") +.partials(new Array(50).fill(0) + .map((_, idx) => ((-1) ** (idx + 1)) / (idx + 1)) +) +._scope()`} +/> + +which recovers a familiar waveform. + +`partials` is also compatible with pattern functions designed to produce lists, like `randL` or `binaryL`: + +>").fast(2) +.sound("user") +.partials(randL(10)) +._scope()`} +/> + +and with lists _of_ patterns: + +>".fast(4)) +.sound("user") +.partials([1, 0, "0 1", "0 1 0.3", rand]) +._scope()`} +/> + +Note that the first value in the `partials` array controls the magnitude of the fundamental harmonic rather than the DC offset, which is fixed at 0. + +#### Phases + +Earlier, we mentioned that periodic waveforms can be broken into a set of harmonics above a fundamental frequency. Each harmonic has two defining properties: its magnitude (how loud it is) and its phase, which determines where in its cycle that sine wave starts when the waveform is built. + +These phases too can be declared in Strudel and can give your sounds interesting depth. + + ## Vibrato diff --git a/website/src/repl/components/button/action-button.jsx b/website/src/repl/components/button/action-button.jsx index d589b7bed..de674b696 100644 --- a/website/src/repl/components/button/action-button.jsx +++ b/website/src/repl/components/button/action-button.jsx @@ -8,3 +8,34 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but ); } + +export function SpecialActionButton(props) { + const { className, ...buttonProps } = props; + + return ( + + ); +} + +export function ActionInput({ label, className, ...props }) { + return ( + + ); +} + +export function SpecialActionInput({ className, ...props }) { + return ( + {props.label}} + /> + ); +} diff --git a/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx b/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx new file mode 100644 index 000000000..c997f6b6e --- /dev/null +++ b/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx @@ -0,0 +1,29 @@ +import { errorLogger } from '@strudel/core'; +import { useSettings, storePrebakeScript } from '../../../settings.mjs'; +import { SpecialActionInput } from '../button/action-button'; + +async function importScript(script) { + const reader = new FileReader(); + reader.readAsText(script); + + reader.onload = () => { + const text = reader.result; + storePrebakeScript(text); + }; + + reader.onerror = () => { + errorLogger(new Error('failed to import prebake script'), 'importScript'); + }; +} +export function ImportPrebakeScriptButton() { + const settings = useSettings(); + + return ( + importScript(e.target.files[0])} + /> + ); +} diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 85991f55c..f46fa661c 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -7,6 +7,8 @@ import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx'; import { confirmDialog } from '../../util.mjs'; import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio'; +import { ActionButton, SpecialActionButton } from '../button/action-button.jsx'; +import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx'; function Checkbox({ label, value, onChange, disabled = false }) { return ( @@ -113,6 +115,7 @@ export function SettingsTab({ started }) { isTabIndentationEnabled, isMultiCursorEnabled, patternAutoStart, + includePrebakeScriptInShare, } = useSettings(); const shouldAlwaysSync = isUdels(); const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; @@ -204,6 +207,15 @@ export function SettingsTab({ started }) { /> + + + settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)} + value={includePrebakeScriptInShare} + /> + + Try clicking the logo in the top left! - + ); diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index a9f10bf12..52fbf928c 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -14,6 +14,10 @@ import { prebake } from '@src/repl/prebake.mjs'; const getSamples = (samples) => Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1; +function wait(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + export function SoundsTab() { const sounds = useStore(soundMap); @@ -56,9 +60,7 @@ 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; + const numRef = useRef(0); // stop current sound on mouseup useEvent('mouseup', () => { @@ -66,6 +68,14 @@ export function SoundsTab() { trigRef.current = undefined; ref?.stop?.(getAudioContext().currentTime + 0.01); }); + useEvent('keydown', (e) => { + if (!isNaN(Number(e.key))) { + numRef.current = Number(e.key); + } + }); + useEvent('keyup', (e) => { + numRef.current = 0; + }); return (
setSearch(v)} /> @@ -115,25 +125,34 @@ export function SoundsTab() { const params = { note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined, s: name, - n: soundPreviewIdx, + n: numRef.current, clip: 1, release: 0.5, sustain: 1, duration: 0.5, }; - soundPreviewIdx++; const onended = () => trigRef.current?.node?.disconnect(); - try { - // Pre-load the sample by calling onTrigger with a future time - // This triggers the loading but schedules playback for later - const time = ctx.currentTime + 0.5; // Give 500ms for loading - const ref = await onTrigger(time, params, onended); - trigRef.current = ref; - if (ref?.node) { - connectToDestination(ref.node); + // Attempt to play the sample and retry every 200ms until 10 attempts have been reached + let errMsg; + for (let attempt = 0; attempt < 10; attempt++) { + try { + // Pre-load the sample by calling onTrigger with a future time + // This triggers the loading but schedules playback for later + const time = ctx.currentTime + 0.05; // Give 50ms for loading + const ref = await onTrigger(time, params, onended); + trigRef.current = ref; + if (ref?.node) { + connectToDestination(ref.node); + break; + } + } catch (err) { + errMsg = err; + } + if (attempt == 9) { + console.warn('Failed to trigger sound after 10 attempts' + (errMsg ? `: ${errMsg}` : '')); + } else { + await wait(200); } - } catch (err) { - console.warn('Failed to trigger sound:', err); } }} > diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 8a6ccc7e0..53a2a6003 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -3,6 +3,8 @@ import { aliasBank, registerSynthSounds, registerZZFXSounds, samples } from '@st import { registerSamplesFromDB } from './idbutils.mjs'; import './piano.mjs'; import './files.mjs'; +import { settingsMap } from '@src/settings.mjs'; +import { evaluate } from '@strudel/transpiler'; const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index ac30fe342..8dcb6b754 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core'; import { getDrawContext } from '@strudel/draw'; -import { transpiler } from '@strudel/transpiler'; +import { evaluate, transpiler } from '@strudel/transpiler'; import { getAudioContextCurrentTime, webaudioOutput, @@ -63,11 +63,10 @@ async function getModule(name) { const initialCode = `// LOADING`; export function useReplContext() { - const { isSyncEnabled, audioEngineTarget } = useSettings(); + const { isSyncEnabled, audioEngineTarget, prebakeScript, includePrebakeScriptInShare } = useSettings(); const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc; const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput; const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds; - const init = useCallback(() => { const drawTime = [-2, 2]; const drawContext = getDrawContext(); @@ -84,7 +83,12 @@ export function useReplContext() { pattern: silence, drawTime, drawContext, - prebake: async () => Promise.all([modulesLoading, presets]), + prebake: async () => + Promise.all([modulesLoading, presets]).then(() => { + if (prebakeScript?.length) { + return evaluate(prebakeScript ?? ''); + } + }), onUpdateState: (state) => { setReplState({ ...state }); }, @@ -214,7 +218,13 @@ export function useReplContext() { editorRef.current.repl.evaluate(code); }; - const handleShare = async () => shareCode(replState.code); + const handleShare = async () => { + let code = replState.code; + if (includePrebakeScriptInShare) { + code = prebakeScript + '\n' + code; + } + shareCode(code); + }; const context = { started, pending, diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index ac27158f4..4a7cb26a8 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -1,11 +1,10 @@ -import { evalScope, hash2code, logger } from '@strudel/core'; +import { code2hash, evalScope, hash2code, logger } from '@strudel/core'; import { settingPatterns } from '../settings.mjs'; import { setVersionDefaults } from '@strudel/webaudio'; import { getMetadata } from '../metadata_parser'; import { isTauri } from '../tauri.mjs'; import './Repl.css'; import { createClient } from '@supabase/supabase-js'; -import { nanoid } from 'nanoid'; import { writeText } from '@tauri-apps/plugin-clipboard-manager'; import { $featuredPatterns /* , loadDBPatterns */ } from '@src/user_pattern_utils.mjs'; @@ -109,9 +108,8 @@ export function confirmDialog(msg) { }); } -let lastShared; - //RIP due to SPAM +// let lastShared; // export async function shareCode(codeToShare) { // // const codeToShare = activeCode || code; // if (lastShared === codeToShare) { @@ -146,9 +144,10 @@ let lastShared; // }); // } -export async function shareCode() { +export async function shareCode(codeToShare) { try { - const shareUrl = window.location.href; + const hash = '#' + code2hash(codeToShare); + const shareUrl = window.location.origin + window.location.pathname + hash; if (isTauri()) { await writeText(shareUrl); } else { diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 7c62b0ded..8ec7d455a 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -45,11 +45,13 @@ export const defaultSettings = { isPanelOpen: true, togglePanelTrigger: 'click', //click | hover userPatterns: '{}', + prebakeScript: '', audioEngineTarget: audioEngineTargets.webaudio, isButtonRowHidden: false, isCSSAnimationDisabled: false, maxPolyphony: 128, multiChannelOrbits: false, + includePrebakeScriptInShare: true, }; let search = null; @@ -96,6 +98,7 @@ export function useSettings() { isPanelOpen: parseBoolean(state.isPanelOpen), userPatterns: userPatterns, multiChannelOrbits: parseBoolean(state.multiChannelOrbits), + includePrebakeScriptInShare: parseBoolean(state.includePrebakeScriptInShare), patternAutoStart: isUdels() ? false : state.patternAutoStart === undefined @@ -108,6 +111,8 @@ export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab); export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool); export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool); +export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script); + export const setIsZen = (active) => settingsMap.setKey('isZen', !!active); const patternSetting = (key) =>