Compare commits

..

7 Commits

Author SHA1 Message Date
Felix Roos 301fb5e59a Merge remote-tracking branch 'origin/main' into example-fixes 2025-11-12 21:35:03 +01:00
Felix Roos 9ac691c19e fix: naming in draw package readme 2025-11-12 21:26:00 +01:00
Felix Roos 6e7512016c chore: package-lock 2025-11-12 21:25:47 +01:00
Felix Roos 3bbd808783 fix: use published web package for headless-repl 2025-11-12 21:25:40 +01:00
Felix Roos 7b245abd1a fix: headless-repl stop button 2025-11-12 21:25:18 +01:00
Felix Roos 9e5c9c7539 fix: update web example 2025-11-12 21:25:08 +01:00
Felix Roos d14194db6a fix: codemirror-repl example 2025-11-12 21:24:56 +01:00
46 changed files with 981 additions and 2441 deletions
-88
View File
@@ -1,88 +0,0 @@
# changelog
NOTE: you can generate this with `node warm.js`. it might still not be perfectly sorted...
## november 2025
- 2025-11-27T22:03:53+01:00 [perf] in `noise`, let noiseMix do the disconnect when it exists by jeromew in: [#1783](https://codeberg.org/uzu/strudel/pulls/1783)
- 2025-11-27T22:01:01+01:00 Bug Fix: Retries for sounds tab by glossing in: [#1754](https://codeberg.org/uzu/strudel/pulls/1754)
- 2025-11-27T20:36:46+01:00 [hydra] return the hydra object when await initHydra(..) is called by jeromew in: [#1784](https://codeberg.org/uzu/strudel/pulls/1784)
- 2025-11-27T20:36:14+01:00 Use errorLogger for query and tonal errors by glossing in: [#1782](https://codeberg.org/uzu/strudel/pulls/1782)
- 2025-11-27T20:27:10+01:00 [perf] level 5 `connect-leak` on `vowel` by jeromew in: [#1779](https://codeberg.org/uzu/strudel/pulls/1779)
- 2025-11-26T08:23:38+01:00 feat: add prebake script import button for loading .strudel files by daslyfe in: [#1774](https://codeberg.org/uzu/strudel/pulls/1774)
- 2025-11-26T07:39:28+01:00 Fix Sampler port trampling by Dayglo in: [#1717](https://codeberg.org/uzu/strudel/pulls/1717)
- 2025-11-25T20:59:09+01:00 Feat: Hook up octave and fix example by glossing in: [#1773](https://codeberg.org/uzu/strudel/pulls/1773)
- 2025-11-25T20:48:09+01:00 [perf] fix `connect-leak` in `tremolo` param by jeromew in: [#1780](https://codeberg.org/uzu/strudel/pulls/1780)
- 2025-11-24T17:51:20+01:00 filter modulation improvements! by daslyfe in: [#1769](https://codeberg.org/uzu/strudel/pulls/1769)
- 2025-11-24T01:20:53+01:00 Add o as a synonym for orbit by daslyfe in: [#1766](https://codeberg.org/uzu/strudel/pulls/1766)
- 2025-11-23T22:23:26+01:00 [perf] fix `connect-leak` in `fm` modulation by jeromew in: [#1758](https://codeberg.org/uzu/strudel/pulls/1758)
- 2025-11-23T09:47:24+01:00 Fix interoperability issue between `all` and `await initHydra()` by jeromew in: [#1663](https://codeberg.org/uzu/strudel/pulls/1663)
- 2025-11-23T04:23:06+01:00 Bug fix: Swap l/r gains with temp variable by glossing in: [#1768](https://codeberg.org/uzu/strudel/pulls/1768)
- 2025-11-23T00:52:28+01:00 FIX: prevent LFO filter modulation from popping from negative values by daslyfe in: [#1767](https://codeberg.org/uzu/strudel/pulls/1767)
- 2025-11-21T01:56:11+01:00 Bug Fix: Use frac due to negative frequencies from FM by glossing in: [#1759](https://codeberg.org/uzu/strudel/pulls/1759)
- 2025-11-20T19:48:16+01:00 [perf] fix `connect-leak` added by #1742 when noise() is not used by jeromew in: [#1757](https://codeberg.org/uzu/strudel/pulls/1757)
- 2025-11-20T14:51:00+01:00 [perf] fix `connect-leak` in `delay` effect by jeromew in: [#1755](https://codeberg.org/uzu/strudel/pulls/1755)
- 2025-11-20T14:49:41+01:00 [perf] fix `connect leak` when .noise() is in the mix by jeromew in: [#1742](https://codeberg.org/uzu/strudel/pulls/1742)
- 2025-11-18T23:52:45+01:00 wchooseCycles has now notes in an example by scrappy_fiddler in: [#1748](https://codeberg.org/uzu/strudel/pulls/1748)
- 2025-11-17T05:31:54+01:00 Feature: Partials by glossing in: [#1659](https://codeberg.org/uzu/strudel/pulls/1659)
- 2025-11-16T22:06:14+01:00 README: update superdough documentation by TristanMlct in: [#1741](https://codeberg.org/uzu/strudel/pulls/1741)
- 2025-11-16T21:08:54+01:00 Worklet optimizations by glossing in: [#1730](https://codeberg.org/uzu/strudel/pulls/1730)
- 2025-11-15T14:59:30+01:00 [perf] disconnect Offline AudioNode connections in generateReverb by jeromew in: [#1740](https://codeberg.org/uzu/strudel/pulls/1740)
- 2025-11-15T14:57:47+01:00 [perf] disconnect `send` effect AudioNode when `room` is used by jeromew in: [#1736](https://codeberg.org/uzu/strudel/pulls/1736)
- 2025-11-12T21:22:34+01:00 [supradough] fix: unstable filter by froos in: [#1593](https://codeberg.org/uzu/strudel/pulls/1593)
- 2025-11-12T21:12:07+01:00 Fix link syntax in `project-start` by Kissaki in: [#1724](https://codeberg.org/uzu/strudel/pulls/1724)
- 2025-11-12T21:06:13+01:00 Fix web README sample code by Kissaki in: [#1725](https://codeberg.org/uzu/strudel/pulls/1725)
- 2025-11-12T20:59:39+01:00 Fix typo: 'studel' -> 'strudel' by drhayes in: [#1726](https://codeberg.org/uzu/strudel/pulls/1726)
- 2025-11-12T15:23:41+01:00 fix for node 24 support in tests - #1718 by ausav in: [#1719](https://codeberg.org/uzu/strudel/pulls/1719)
- 2025-11-11T09:00:49+01:00 soundAlias example fix by PepsiiMan in: [#1720](https://codeberg.org/uzu/strudel/pulls/1720)
- 2025-11-10T08:18:36+01:00 fix: Make Supradough package build by munshkr in: [#1711](https://codeberg.org/uzu/strudel/pulls/1711)
- 2025-11-10T08:18:00+01:00 added docs for spaces in scale names by ondras in: [#1715](https://codeberg.org/uzu/strudel/pulls/1715)
- 2025-11-10T02:44:01+01:00 Feature: LFOs for Filters by glossing in: [#1636](https://codeberg.org/uzu/strudel/pulls/1636)
- 2025-11-05T17:07:00+01:00 improvement: sync midi messages to audio context clock by daslyfe in: [#1708](https://codeberg.org/uzu/strudel/pulls/1708)
- 2025-11-04T19:36:14+01:00 Add stick button mappings to gamepad implementation and improve docs by kaosuryoko in: [#1696](https://codeberg.org/uzu/strudel/pulls/1696)
- 2025-11-04T19:29:35+01:00 Refactor sound stopping and triggering logic in SoundsTab component by IJOL in: [#1688](https://codeberg.org/uzu/strudel/pulls/1688)
- 2025-11-04T19:13:21+01:00 Add setting to toggle pattern auto-start on pattern change by moumar in: [#1690](https://codeberg.org/uzu/strudel/pulls/1690)
- 2025-11-04T18:58:50+01:00 Voicings JSDoc by sharkeys_lunchbox in: [#1686](https://codeberg.org/uzu/strudel/pulls/1686)
- 2025-11-01T07:50:40+01:00 FIX: Loading local samples uses too much memory by daslyfe in: [#1706](https://codeberg.org/uzu/strudel/pulls/1706)
## october 2025
- 2025-10-28T22:58:24+01:00 Bug Fix: Handle scale-for-notes when n also supplied by glossing in: [#1625](https://codeberg.org/uzu/strudel/pulls/1625)
- 2025-10-28T22:51:16+01:00 Repurpose vim shortcuts for usability by dtricks in: [#1624](https://codeberg.org/uzu/strudel/pulls/1624)
- 2025-10-28T22:35:21+01:00 Add support for euclidian in mondo with `bd&3:8` by TristanCacqueray in: [#1630](https://codeberg.org/uzu/strudel/pulls/1630)
- 2025-10-27T10:42:07+01:00 Use bunny cdn for all samples and json files by yaxu in: [#1701](https://codeberg.org/uzu/strudel/pulls/1701)
- 2025-10-26T22:52:54+01:00 degithub - switch some samples to bunnycdn by yaxu in: [#1697](https://codeberg.org/uzu/strudel/pulls/1697)
- 2025-10-26T17:09:22+01:00 Fix ZZFX example by moumar in: [#1685](https://codeberg.org/uzu/strudel/pulls/1685)
- 2025-10-23T15:56:04+02:00 Make osc port and host configurable. Changes dependencies. by yaxu in: [#1682](https://codeberg.org/uzu/strudel/pulls/1682)
- 2025-10-23T03:47:31+02:00 Adds back shape to superdough by glossing in: [#1672](https://codeberg.org/uzu/strudel/pulls/1672)
- 2025-10-22T22:15:19+02:00 Fix sampler.mjs githubPath by jeromew in: [#1684](https://codeberg.org/uzu/strudel/pulls/1684)
- 2025-10-22T16:20:50+02:00 Fix onPaint for widgets by yaxu in: [#1658](https://codeberg.org/uzu/strudel/pulls/1658)
- 2025-10-22T15:19:12+02:00 Fix a bug introduced by #4e17cfbdd6 by milliganf in: [#1679](https://codeberg.org/uzu/strudel/pulls/1679)
- 2025-10-18T04:29:20+02:00 Bug Fix: Wavetable: phase wrapping at 1 and detune by glossing in: [#1620](https://codeberg.org/uzu/strudel/pulls/1620)
- 2025-10-16T20:26:28+02:00 github samples: default to "samples" if repository is not specified by prezmop in: [#1644](https://codeberg.org/uzu/strudel/pulls/1644)
- 2025-10-16T17:33:39+02:00 Docs: add example of custom chained function by dariusk in: [#1642](https://codeberg.org/uzu/strudel/pulls/1642)
- 2025-10-14T12:39:48+02:00 textbox by yaxu in: [#1650](https://codeberg.org/uzu/strudel/pulls/1650)
- 2025-10-13T07:05:57+02:00 Feature: Wavetable FM by glossing in: [#1623](https://codeberg.org/uzu/strudel/pulls/1623)
- 2025-10-12T20:43:42+02:00 fix: repair REPL sample sources and URL concat (#1640) by erikfox in: [#1646](https://codeberg.org/uzu/strudel/pulls/1646)
- 2025-10-10T11:25:02+02:00 Add control-metadata by yaxu in: [#1634](https://codeberg.org/uzu/strudel/pulls/1634)
- 2025-10-07T22:59:39+02:00 Fix MQTT: change the trigger handler to match new hap by jurrchen in: [#1629](https://codeberg.org/uzu/strudel/pulls/1629)
- 2025-10-05T21:42:19+02:00 Fix case sensitivity for synonym search by vvolhejn in: [#1618](https://codeberg.org/uzu/strudel/pulls/1618)
- 2025-10-01T08:56:35+02:00 Optimize wavetable synth by glossing in: [#1613](https://codeberg.org/uzu/strudel/pulls/1613)
- 2025-10-01T02:02:33+02:00 fix: pattern switching by daslyfe in: [#1616](https://codeberg.org/uzu/strudel/pulls/1616)
## September 2025
- 2025-09-29T02:10:54+02:00 fix wavetable JSON by daslyfe in: [#1611](https://codeberg.org/uzu/strudel/pulls/1611)
- 2025-09-28T21:28:16+02:00 rename wavetable controls to match existing naming conventions by daslyfe in: [#1610](https://codeberg.org/uzu/strudel/pulls/1610)
- 2025-09-27T23:28:13+02:00 Feature: Wavetable synth improvements by glossing in: [#1607](https://codeberg.org/uzu/strudel/pulls/1607)
- 2025-09-27T21:43:10+02:00 wavetable improvements by daslyfe in: [#1608](https://codeberg.org/uzu/strudel/pulls/1608)
- 2025-09-26T08:48:46+02:00 create orbit based DJ filter by daslyfe in: [#1603](https://codeberg.org/uzu/strudel/pulls/1603)
- 2025-09-23T13:28:20+02:00 fix: time signal by daslyfe in: [#1583](https://codeberg.org/uzu/strudel/pulls/1583)
- 2025-09-21T16:16:06+02:00 mondo: fix all + setcpm + setcps by froos in: [#1600](https://codeberg.org/uzu/strudel/pulls/1600)
- 2025-09-19T21:59:43+02:00 fix: osc error message by froos in: [#1597](https://codeberg.org/uzu/strudel/pulls/1597)
- 2025-09-18T21:09:56+02:00 osc: --debug flag to see incoming messages by froos in: [#1595](https://codeberg.org/uzu/strudel/pulls/1595)
- 2025-09-17T14:12:10+02:00 simplify osc usage by froos in: [#1588](https://codeberg.org/uzu/strudel/pulls/1588)
- 2025-09-16T16:59:58+02:00 dev: logger errors to console in dev mode by daslyfe in: [#1585](https://codeberg.org/uzu/strudel/pulls/1585)
- 2025-09-15T22:52:14+02:00 add tic80 font by froos in: [#1579](https://codeberg.org/uzu/strudel/pulls/1579)
- 2025-09-15T00:00:19+02:00 supradough: fix delay + add some jsdoc types by froos in: [#1578](https://codeberg.org/uzu/strudel/pulls/1578)
+1 -1
View File
@@ -1,4 +1,4 @@
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<script src="https://unpkg.com/@strudel/web@1.2.6"></script>
<button id="play">PLAY</button>
<script>
initStrudel({
+8 -8
View File
@@ -12,14 +12,14 @@
"vite": "^6.0.11"
},
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/soundfonts": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*"
"@strudel/codemirror": "1.2.6",
"@strudel/core": "1.2.5",
"@strudel/draw": "1.2.5",
"@strudel/mini": "1.2.5",
"@strudel/soundfonts": "1.2.6",
"@strudel/tonal": "1.2.5",
"@strudel/transpiler": "1.2.5",
"@strudel/webaudio": "1.2.6"
},
"engines": {
"node": ">=18.0.0"
+5 -8
View File
@@ -1,6 +1,6 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
samples('github:felixroos/samples')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
// make it 22 edo
@@ -28,13 +28,12 @@ samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-dru
.mask("<0@4 1@12>/4")
.reset("<x@15 [x(3,8) x*[4 8]]>")
// wait for it...
).fast(2/3)
).fast(4/3)
//.crush(6) // remove "//" if you dare`;
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
samples('github:felixroos/samples')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', {
m11: ['2M 3m 4P 7m'],
@@ -61,16 +60,14 @@ stack(
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
)
)
.late("[0 .05]*2").late(12)
.late("[0 .05]*2").late(12).fast(2)
`;
export const funk42 = `// froos - how to funk in 42 lines of code
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
samples('github:felixroos/samples')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5)
+1 -1
View File
@@ -13,7 +13,7 @@
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from '@strudel/web';
import { initStrudel, hush } from '@strudel/web';
initStrudel({
prebake: () => samples('github:tidalcycles/dirt-samples'),
});
+1 -1
View File
@@ -13,7 +13,7 @@
"vite": "^6.0.11"
},
"dependencies": {
"@strudel/web": "workspace:*"
"@strudel/web": "1.2.6"
},
"engines": {
"node": ">=18.0.0"
+18 -222
View File
@@ -72,27 +72,6 @@ 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 ':'.
@@ -456,9 +435,6 @@ 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
@@ -468,40 +444,25 @@ export const { attack, att } = registerControl('attack', 'att');
* ._scope()
*
*/
export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerMultiControl(['fmh', 'fmi'], 8, 'fmh');
export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh');
/**
* Sets the Frequency Modulation of the synth.
* Controls the modulation index, which defines the brightness of the sound.
*
* 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
* @name fm
* @param {number | Pattern} brightness modulation index
* @synonyms fm
* @synonyms fmi
* @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, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8 } =
registerMultiControl(['fmi', 'fmh'], 8, 'fm');
export const { fmi, fm } = registerControl(['fmi', 'fmh'], '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
@@ -513,19 +474,11 @@ export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2
* ._scope()
*
*/
export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fmenv8 } = registerMultiControl(
'fmenv',
8,
);
export const { fmenv } = registerControl('fmenv');
/**
* 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")
@@ -534,33 +487,11 @@ export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fm
* ._scope()
*
*/
export const {
fmattack,
fmattack1,
fmattack2,
fmattack3,
fmattack4,
fmattack5,
fmattack6,
fmattack7,
fmattack8,
fmatt,
fmatt1,
fmatt2,
fmatt3,
fmatt4,
fmatt5,
fmatt6,
fmatt7,
fmatt8,
} = registerMultiControl('fmattack', 8, 'fmatt');
export const { fmattack } = registerControl('fmattack');
/**
* 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
@@ -569,19 +500,12 @@ export const {
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6)
*
*/
export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmwave7, fmwave8 } = registerMultiControl(
'fmwave',
8,
);
export const { fmwave } = registerControl('fmwave');
/**
* 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")
@@ -591,35 +515,11 @@ export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmw
* ._scope()
*
*/
export const {
fmdecay,
fmdecay1,
fmdecay2,
fmdecay3,
fmdecay4,
fmdecay5,
fmdecay6,
fmdecay7,
fmdecay8,
fmdec,
fmdec1,
fmdec2,
fmdec3,
fmdec4,
fmdec5,
fmdec6,
fmdec7,
fmdec8,
} = registerMultiControl('fmdecay', 8, 'fmdec');
export const { fmdecay } = registerControl('fmdecay');
/**
* 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")
@@ -629,69 +529,10 @@ export const {
* ._scope()
*
*/
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}`);
}
}
export const { fmsustain } = registerControl('fmsustain');
// these are not really useful... skipping for now
export const { fmrelease } = registerControl('fmrelease');
export const { fmvelocity } = registerControl('fmvelocity');
/**
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
@@ -1453,8 +1294,6 @@ export const { fanchor } = registerControl('fanchor');
*
* @name lprate
* @param {number | Pattern} rate rate in hertz
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lprate("<4 8 2 1>")
*/
export const { lprate } = registerControl('lprate');
@@ -1463,8 +1302,6 @@ export const { lprate } = registerControl('lprate');
*
* @name lpsync
* @param {number | Pattern} rate rate in cycles
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpsync("<4 8 2 1>")
*/
export const { lpsync } = registerControl('lpsync');
@@ -1473,23 +1310,8 @@ export const { lpsync } = registerControl('lpsync');
*
* @name lpdepth
* @param {number | Pattern} depth depth of modulation
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpdepth("<1 .5 1.8 0>")
*/
export const { lpdepth } = registerControl('lpdepth');
/**
* Depth of the LFO for the lowpass filter, in HZ
*
* @name lpdepthfrequency
* @synonyms
* lpdethfreq
* @param {number | Pattern} depth depth of modulation
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpdepthfrequency("<200 500 100 0>")
*/
export const { lpdepthfrequency } = registerControl('lpdepthfrequency', 'lpdepthfreq');
/**
* Shape of the LFO for the lowpass filter
@@ -1539,19 +1361,6 @@ export const { bpsync } = registerControl('bpsync');
*/
export const { bpdepth } = registerControl('bpdepth');
/**
* Depth of the LFO for the bandpass filter, in HZ
*
* @name bpdepthfrequency
* @synonyms
* bpdethfreq
* @param {number | Pattern} depth depth of modulation
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).bpdepthfrequency("<200 500 100 0>")
*/
export const { bpdepthfrequency } = registerControl('bpdepthfrequency', 'bpdepthfreq');
/**
* Shape of the LFO for the bandpass filter
*
@@ -1598,20 +1407,7 @@ export const { hpsync } = registerControl('hpsync');
* @name hpdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { hpdepth, hpdepthfreq } = registerControl('hpdepth');
/**
* Depth of the LFO for the hipass filter, in hz
*
* @name hpdepthfrequency
* @synonyms
* hpdethfreq
* @param {number | Pattern} depth depth of modulation
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).hpdepthfrequency("<200 500 100 0>")
*/
export const { hpdepthfrequency } = registerControl('hpdepthfrequency', 'hpdepthfreq');
export const { hpdepth } = registerControl('hpdepth');
/**
* Shape of the LFO for the highpass filter
@@ -2024,12 +1820,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").scale("F:minor").s('supersaw').octave("<0 1 2 3>")
* n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc()
* @superDirtOnly
*/
export const { octave, oct } = registerControl('octave', 'oct');
export const { octave } = registerControl('octave');
// ['ophatdecay'],
// TODO: example
@@ -2037,7 +1833,6 @@ export const { octave, oct } = registerControl('octave', 'oct');
* 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(
@@ -2045,7 +1840,7 @@ export const { octave, oct } = registerControl('octave', 'oct');
* s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2)
* )
*/
export const { orbit } = registerControl('orbit', 'o');
export const { orbit } = registerControl('orbit');
// 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
@@ -2469,6 +2264,7 @@ 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');
+3 -83
View File
@@ -25,7 +25,7 @@ import {
stringifyValues,
} from './util.mjs';
import drawLine from './drawLine.mjs';
import { errorLogger, logger } from './logger.mjs';
import { 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) {
errorLogger(err, 'query');
logger(`[query]: ${err.message}`, 'error');
return [];
}
}
@@ -2234,7 +2234,7 @@ export const brak = register('brak', function (pat) {
});
/**
* Reverse all cycles in a pattern. See also `revv` for reversing a whole pattern.
* Reverse all haps in a pattern
*
* @name rev
* @memberof Pattern
@@ -2266,23 +2266,6 @@ 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.
@@ -3641,66 +3624,3 @@ 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');
};
+7 -17
View File
@@ -152,9 +152,9 @@ export function repl({
// allows muting a pattern x with x_ or _x
return silence;
}
if (id.includes('$')) {
if (id === '$') {
// allows adding anonymous patterns with $:
id = `${id}${anonymousIndex}`;
id = `$${anonymousIndex}`;
anonymousIndex++;
}
pPatterns[id] = this;
@@ -215,19 +215,8 @@ 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)) {
// 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);
}
patterns.push(value.withState((state) => state.setControls({ id: key })));
}
if (eachTransform) {
// Explicit lambda so only element (not index and array) are passed
@@ -238,13 +227,14 @@ export function repl({
pattern = eachTransform(pattern);
}
if (allTransforms.length) {
for (const transform of allTransforms) {
pattern = transform(pattern);
for (let i in allTransforms) {
pattern = allTransforms[i](pattern);
}
}
if (!isPattern(pattern)) {
pattern = silence;
const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
}
logger(`[eval] code updated`);
pattern = await setPattern(pattern, autostart);
+3 -48
View File
@@ -228,7 +228,7 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
export const run = (n) => saw.range(0, n).round().segment(n);
/**
* Creates a binary pattern from a number.
* Creates a pattern from a binary number.
*
* @name binary
* @param {number} n - input number to convert to binary
@@ -242,7 +242,7 @@ export const binary = (n) => {
};
/**
* Creates a binary pattern from a number, padded to n bits long.
* Creates a pattern from a binary number, padded to n bits long.
*
* @name binaryN
* @param {number} n - input number to convert to binary
@@ -258,51 +258,6 @@ export const binaryN = (n, nBits = 16) => {
return reify(n).segment(nBits).brshift(bitPos).band(pure(1));
};
/**
* Creates a binary list pattern from a number.
*
* @name binaryL
* @param {number} n - input number to convert to binary
* s("saw").seg(8)
* .partials(binaryL(irand(4096).add(1)))
*/
export const binaryL = (n) => {
const nBits = reify(n).log2(0).floor().add(1);
return binaryNL(n, nBits);
};
/**
* Creates a binary list pattern from a number, padded to n bits long.
*
* @name binaryNL
* @param {number} n - input number to convert to binary
* @param {number} nBits - pattern length, defaults to 16
*/
export const binaryNL = (n, nBits = 16) => {
return reify(n)
.withValue((v) => (bits) => {
const bList = [];
for (let i = bits - 1; i >= 0; i--) {
bList.push((v >> i) & 1);
}
return bList;
})
.appLeft(reify(nBits));
};
/**
* Creates a list of random numbers of the given length
*
* @name randL
* @param {number} n Number of random numbers to sample
* @example
* s("saw").seg(16).n(irand(12)).scale("F1:minor")
* .partials(randL(8))
*/
export const randL = (n) => {
return signal((t) => (nVal) => timeToRands(t, nVal).map(Math.abs)).appLeft(reify(n));
};
export const randrun = (n) => {
return signal((t) => {
// Without adding 0.5, the first cycle is always 0,1,2,3,...
@@ -524,7 +479,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
* @example
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
* @example
* wchooseCycles(["c c c",5], ["a a a",3], ["f f f",1]).fast(4).note()
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
* @example
* // The probability can itself be a pattern
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
-12
View File
@@ -586,18 +586,6 @@ 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', () => {
+2 -2
View File
@@ -1,9 +1,9 @@
# @strudel/canvas
# @strudel/draw
Helpers for drawing with the Canvas API and Strudel
## Install
```sh
npm i @strudel/canvas --save
npm i @strudel/draw --save
```
-1
View File
@@ -35,7 +35,6 @@ export async function initHydra(options = {}) {
hydra.synth.s0.init({ src: canvas });
}
}
return hydra;
}
export function clearHydra() {
+3 -1
View File
@@ -9,7 +9,6 @@ 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) => {
@@ -55,6 +54,7 @@ async function getBanks(directory, flat = false) {
banks[bank].push(subDir);
return subDir;
});
banks._base = `http://localhost:5432`;
return { banks, files };
}
@@ -134,6 +134,8 @@ 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();
+2 -3
View File
@@ -6,7 +6,6 @@ import {
getADSRValues,
getPitchEnvelope,
getVibratoOscillator,
onceEnded,
} from '@strudel/webaudio';
import gm from './gm.mjs';
@@ -171,12 +170,12 @@ export function registerSoundfonts() {
bufferSource.stop(envEnd);
const stop = (releaseTime) => {};
onceEnded(bufferSource, () => {
bufferSource.onended = () => {
bufferSource.disconnect();
vibratoOscillator?.stop();
node.disconnect();
onended();
});
};
return { node, stop };
},
{ type: 'soundfont', prebake: true, fonts },
+1 -1
View File
@@ -89,7 +89,7 @@ superdough({ s: 'bd', delay: 0.5 }, 0, 1);
- `decay`: seconds of decay phase
- `sustain`: gain of sustain phase
- `release`: seconds of release phase
- `deadline`: seconds from audio context initialization before playing the sound (getAudioContextCurrentTime() = immediate)
- `deadline`: seconds until the sound should play (0 = immediate)
- `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds
### registerSynthSounds()
-9
View File
@@ -1,12 +1,3 @@
/*
audioContext.mjs - Audio Context manager
Sets up a common and accessible audio context for all superdough operations
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/audiocontext.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
let audioContext;
export const setDefaultAudioContext = () => {
+87 -203
View File
@@ -1,8 +1,7 @@
import { getAudioContext } from './audioContext.mjs';
import { logger } from './logger.mjs';
import { getNoiseBuffer } from './noise.mjs';
import { getNodeFromPool, markWorkletAsDead } from './nodePools.mjs';
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
import { getNoiseBuffer } from './noise.mjs';
import { logger } from './logger.mjs';
export const noises = ['pink', 'white', 'brown', 'crackle'];
@@ -37,16 +36,6 @@ export function getWorklet(ac, processor, params, config) {
return node;
}
export function getPooledWorklet(ac, processor, params, constructorOptions, reInitOptions) {
const node = getNodeFromPool(processor, () => new AudioWorkletNode(ac, processor, constructorOptions), params);
node.port.postMessage({ type: 'initialize', payload: reInitOptions });
node.port.onmessage = (e) => {
if (e.data.type === 'died') markWorkletAsDead(node);
node.port.onmessage = null;
};
return node;
}
export const getParamADSR = (
param,
attack,
@@ -135,11 +124,10 @@ export function getLfo(audioContext, begin, end, properties = {}) {
...props,
};
return getPooledWorklet(audioContext, 'lfo-processor', lfoprops);
return getWorklet(audioContext, 'lfo-processor', lfoprops);
}
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
const node = getNodeFromPool('compressor', () => new DynamicsCompressorNode(ac, {}));
const options = {
threshold: threshold ?? -3,
ratio: ratio ?? 10,
@@ -147,11 +135,7 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) {
attack: attack ?? 0.005,
release: release ?? 0.05,
};
const now = ac.currentTime;
Object.entries(options).forEach(([key, value]) => {
node[key].setValueAtTime(value, now);
});
return node;
return new DynamicsCompressorNode(ac, options);
}
// changes the default values of the envelope based on what parameters the user has defined
@@ -207,7 +191,8 @@ export function applyParameterModulators(audioContext, param, start, end, envelo
const lfo = getParamLfo(audioContext, param, start, end, lfoValues);
return { lfo, disconnect: () => lfo?.disconnect() };
}
export function createFilter(context, start, end, params, cps, cycle) {
export function createFilter(context, start, end, params, cps) {
let {
frequency,
anchor,
@@ -217,26 +202,21 @@ export function createFilter(context, start, end, params, cps, cycle) {
q = 1,
drive = 0.69,
depth,
depthfrequency,
dcoffset = -0.5,
skew,
shape,
rate,
sync,
} = params;
let frequencyParam, filter;
if (model === 'ladder') {
filter = getPooledWorklet(context, 'ladder-processor', { frequency, q, drive });
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
frequencyParam = filter.parameters.get('frequency');
} else {
const factory = () => context.createBiquadFilter();
filter = getNodeFromPool('filter', factory);
filter = context.createBiquadFilter();
filter.type = type;
const now = context.currentTime;
Object.entries({ Q: q, frequency }).forEach(([key, value]) => {
filter[key].setValueAtTime(value, now);
});
filter.Q.value = q;
filter.frequency.value = frequency;
frequencyParam = filter.frequency;
}
const envelopeValues = [params.attack, params.decay, params.sustain, params.release];
@@ -258,27 +238,9 @@ export function createFilter(context, start, end, params, cps, cycle) {
if (sync != null) {
rate = cps * sync;
}
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 };
const lfoValues = { depth, dcoffset, skew, shape, frequency: rate };
getParamLfo(context, frequencyParam, start, end, lfoValues);
return filter;
}
// stays 1 until .5, then fades out
@@ -300,15 +262,7 @@ export function drywet(dry, wet, wetAmount = 0) {
let mix = ac.createGain();
dry_gain.connect(mix);
wet_gain.connect(mix);
return {
node: mix,
onended: () => {
dry_gain.disconnect(mix);
wet_gain.disconnect(mix);
dry.disconnect(dry_gain);
wet.disconnect(wet_gain);
},
};
return mix;
}
let curves = ['linear', 'exponential'];
@@ -343,10 +297,6 @@ export function getVibratoOscillator(param, value, t) {
gain.gain.value = vibmod * 100;
vibratoOscillator.connect(gain);
gain.connect(param);
vibratoOscillator.onended = () => {
gain.disconnect(param);
vibratoOscillator.disconnect(gain);
};
vibratoOscillator.start(t);
return vibratoOscillator;
}
@@ -368,17 +318,25 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
constantNode.connect(zeroGain);
// Schedule the `onComplete` callback to occur at `stopTime`
onceEnded(constantNode, () => {
releaseAudioNode(zeroGain);
releaseAudioNode(constantNode);
constantNode.onended = () => {
// Ensure garbage collection
try {
zeroGain.disconnect();
} catch {
// pass
}
try {
constantNode.disconnect();
} catch {
// pass
}
onComplete();
});
};
constantNode.start(startTime);
constantNode.stop(stopTime);
return constantNode;
}
const mod = (freq, type = 'sine') => {
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
let osc;
if (noises.includes(type)) {
@@ -390,89 +348,64 @@ const mod = (freq, type = 'sine') => {
osc.type = type;
osc.frequency.value = freq;
}
osc.start();
return osc;
};
const fm = (frequencyparam, harmonicityRatio, wave = 'sine') => {
osc.start();
const g = new GainNode(ctx, { gain: range });
osc.connect(g); // -range, range
return { node: g, stop: (t) => osc.stop(t) };
};
const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => {
const carrfreq = frequencyparam.value;
const modfreq = carrfreq * harmonicityRatio;
return { osc: mod(modfreq, wave), freq: modfreq };
const modgain = modfreq * modulationIndex;
return mod(modfreq, modgain, wave);
};
export function applyFM(param, value, begin) {
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]);
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);
}
}
return {
stop: (t) => toStop.forEach((m) => m?.stop(t)),
};
return { stop };
}
// Saturation curves
@@ -567,13 +500,12 @@ export const getDistortionAlgorithm = (algo) => {
return distortionAlgorithms[name];
};
export const getDistortion = (fullParams) => {
const { algorithm, ...params } = fullParams;
return getPooledWorklet(getAudioContext(), 'distort-processor', params, {}, { algorithm });
export const getDistortion = (distort, postgain, algorithm) => {
return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } });
};
export const getFrequencyFromValue = (value, defaultNote = 36) => {
let { note, freq, octave = 0 } = value;
let { note, freq } = value;
note = note || defaultNote;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
@@ -582,62 +514,14 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => {
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
freq *= Math.pow(2, octave);
return Number(freq);
};
// 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');
export const destroyAudioWorkletNode = (node) => {
if (node == null) {
return;
}
// https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect
node.disconnect();
// make sure all AudioScheduledSourceNodes are in a stopped state
// https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode
if (node instanceof AudioScheduledSourceNode) {
if (node.onended && node.onended.name !== 'cleanup') {
logger(
`[superdough] Deprecation warning: it seems your code path is setting 'node.onended = callback' instead of using the onceEnded helper`,
);
}
try {
node.stop();
} catch (e) {
// At the stage, `start` was not called on the node
// but an `onended` callback releasing resources may exist
// and we want it to fire :
// - we force a start/stop cycle so that `onended` gets called
// - we `lock` the node so that no-one can start it
node.start(node.context.currentTime + 5); // will never happen
node.stop();
}
}
// https://www.w3.org/TR/webaudio-1.1/#AudioNode-actively-processing
// An AudioWorkletNode is actively processing when its AudioWorkletProcessor's [[callable process]]
// returns true and either its active source flag is true or
// any AudioNode connected to one of its inputs is actively processing.
if (node instanceof AudioWorkletNode) {
node.parameters.get('end')?.setValueAtTime(0, 0);
}
};
// Once the `anchor` node has ended, release all nodes in `toCleanup`
export const cleanupOnEnd = (anchor, toCleanup) => {
onceEnded(anchor, () => toCleanup.forEach((n) => releaseAudioNode(n)));
node.parameters.get('end')?.setValueAtTime(0, 0);
};
-71
View File
@@ -1,71 +0,0 @@
/*
nodePools.mjs - Helper functions related to pooling and re-using audio nodes
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/nodePools.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const nodePools = new Map();
const POOL_KEY = Symbol('nodePoolKey');
const IS_WORKLET_DEAD = Symbol('nodePoolIsWorkletDead');
const MAX_POOL_SIZE = 64;
export const isPoolable = (node) => !!node[POOL_KEY];
export const releaseNodeToPool = (node) => {
node.disconnect();
if (node instanceof AudioScheduledSourceNode) {
// not reusable
return;
}
if (node[IS_WORKLET_DEAD]) {
// Worklet already terminated, don't pool it
return;
}
const key = node[POOL_KEY];
if (key == null) return;
const pool = nodePools.get(key) ?? [];
if (pool.length < MAX_POOL_SIZE) {
pool.push(new WeakRef(node));
nodePools.set(key, pool);
}
};
export const markWorkletAsDead = (worklet) => (worklet[IS_WORKLET_DEAD] = true);
// Attempt to get node from the pool. If this fails, fall back
// to building it with the factory
export const getNodeFromPool = (key, factory, params = {}) => {
const pool = nodePools.get(key) ?? [];
let node;
while (pool.length) {
const ref = pool.pop();
node = ref?.deref();
if (node != null && !node[IS_WORKLET_DEAD]) break;
}
if (node == null || node[IS_WORKLET_DEAD]) {
node = factory();
}
node[POOL_KEY] = key;
const paramMap = new Map();
if (node instanceof AudioWorkletNode) {
for (const [name, param] of node.parameters.entries()) {
paramMap.set(name, param);
}
} else {
for (const name of Object.getOwnPropertyNames(node)) {
const value = node[name];
if (value instanceof AudioParam) {
paramMap.set(name, value);
}
}
}
const now = node.context?.currentTime ?? 0;
paramMap.forEach((param, name) => {
param.cancelScheduledValues(now);
// Set values from `params` or restore defaults
const target = params[name] !== undefined ? params[name] : param.defaultValue;
param.setValueAtTime(target, now);
});
return node;
};
+1 -2
View File
@@ -65,9 +65,8 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) {
export function getNoiseMix(inputNode, wet, t) {
const noiseOscillator = getNoiseOscillator('pink', t);
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
noiseOscillator.node.onended = () => noiseMix.onended();
return {
node: noiseMix.node,
node: noiseMix,
stop: (time) => noiseOscillator?.stop(time),
};
}
+15 -7
View File
@@ -1,23 +1,22 @@
'use strict';
import { makeReusable } from './worklets-common.mjs';
// sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
const WEBAUDIO_BLOCK_SIZE = 128;
/** Overlap-Add Node */
class OLAProcessor extends makeReusable(AudioWorkletProcessor) {
class OLAProcessor extends AudioWorkletProcessor {
constructor(options) {
super(options);
this.started = false;
this.nbInputs = options.numberOfInputs;
this.nbOutputs = options.numberOfOutputs;
this.blockSize = options.processorOptions.blockSize;
// TODO for now, the only support hop size is the size of a web audio block
this.hopSize = WEBAUDIO_BLOCK_SIZE;
this.nbOverlaps = this.blockSize / this.hopSize;
}
initialize() {
this.nbOverlaps = this.blockSize / this.hopSize;
// pre-allocate input buffers (will be reallocated if needed)
this.inputBuffers = new Array(this.nbInputs);
this.inputBuffersHead = new Array(this.nbInputs);
@@ -55,6 +54,7 @@ class OLAProcessor extends makeReusable(AudioWorkletProcessor) {
allocateInputChannels(inputIndex, nbChannels) {
// allocate input buffers
this.inputBuffers[inputIndex] = new Array(nbChannels);
for (let i = 0; i < nbChannels; i++) {
this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE);
@@ -157,8 +157,15 @@ class OLAProcessor extends makeReusable(AudioWorkletProcessor) {
}
}
processActive(inputs, outputs, params) {
process(inputs, outputs, params) {
const input = inputs[0];
const hasInput = !(input[0] === undefined);
if (this.started && !hasInput) {
return false;
}
this.started = hasInput;
this.reallocateChannelsIfNeeded(inputs, outputs);
this.readInputs(inputs);
this.shiftInputBuffers();
this.prepareInputBuffersToSend();
@@ -166,6 +173,7 @@ class OLAProcessor extends makeReusable(AudioWorkletProcessor) {
this.handleOutputBuffersToRetrieve();
this.writeOutputs(outputs);
this.shiftOutputBuffers();
return true;
}
-2
View File
@@ -104,8 +104,6 @@ var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt,
player.start();
context.oncomplete = function (event) {
callback(event.renderedBuffer);
filter.disconnect();
player.disconnect();
};
context.startRendering();
+11 -20
View File
@@ -1,14 +1,7 @@
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
import { getCommonSampleInfo } from './util.mjs';
import { registerSound, registerWaveTable } from './index.mjs';
import { getAudioContext } from './audioContext.mjs';
import {
getADSRValues,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
onceEnded,
releaseAudioNode,
} from './helpers.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { logger } from './logger.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer>
@@ -218,7 +211,7 @@ export async function fetchSampleMap(url) {
// not a browser
return;
}
const base = getBaseURL(url);
const base = url.split('/').slice(0, -1).join('/');
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
@@ -293,16 +286,14 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
if (!bufferSource) {
logger(`[sampler] could not load "${s}:${n}"`, 'error');
// 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;
}
// 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);
if (!bufferSource) {
logger(`[sampler] could not load "${s}:${n}"`, 'error');
return;
}
@@ -328,13 +319,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);
onceEnded(bufferSource, function () {
bufferSource.onended = function () {
bufferSource.disconnect();
vibratoOscillator?.stop();
node.disconnect();
out.disconnect();
onended();
});
};
let envEnd = holdEnd + release + 0.01;
bufferSource.stop(envEnd);
const stop = (endTime) => {
+31 -65
View File
@@ -9,16 +9,7 @@ import './reverb.mjs';
import './vowel.mjs';
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import {
createFilter,
effectSend,
gainNode,
getCompressor,
getDistortion,
getLfo,
getPooledWorklet,
} from './helpers.mjs';
import { getNodeFromPool, isPoolable, releaseNodeToPool } from './nodePools.mjs';
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
import { map } from 'nanostores';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
@@ -272,8 +263,8 @@ let audioReady;
export async function initAudioOnFirstClick(options) {
if (!audioReady) {
audioReady = new Promise((resolve) => {
document.addEventListener('mousedown', async function listener() {
document.removeEventListener('mousedown', listener);
document.addEventListener('click', async function listener() {
document.removeEventListener('click', listener);
await initAudio(options);
resolve();
});
@@ -283,7 +274,7 @@ export async function initAudioOnFirstClick(options) {
}
let controller;
export function getSuperdoughAudioController() {
function getSuperdoughAudioController() {
if (controller == null) {
controller = new SuperdoughAudioController(getAudioContext());
}
@@ -296,27 +287,27 @@ export function connectToDestination(input, channels) {
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
const ac = getAudioContext();
const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
//filters
const numStages = 2; //num of filters in series
let fOffset = 0;
const filterChain = [];
for (let i = 0; i < numStages; i++) {
const filter = getNodeFromPool('filter', () => ac.createBiquadFilter());
const filter = ac.createBiquadFilter();
filter.type = 'notch';
filter.gain.value = 1;
filter.frequency.value = centerFrequency + fOffset;
filter.Q.value = 2 - Math.min(Math.max(depth * 2, 0), 1.9);
lfo.connect(filter.detune);
lfoGain.connect(filter.detune);
fOffset += 282;
if (i > 0) {
filterChain[i - 1].connect(filter);
}
filterChain.push(filter);
}
return { phaser: filterChain[filterChain.length - 1], lfo };
return filterChain[filterChain.length - 1];
}
function getFilterType(ftype) {
@@ -422,7 +413,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
release = 0,
//phaser
phaserrate,
phaserrate: phaser,
phaserdepth = getDefaultValue('phaserdepth'),
phasersweep,
phasercenter,
@@ -490,7 +481,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) {
const ch = activeSoundSources.entries().next();
const source = ch.value[1].deref();
const source = ch.value[1];
const chainID = ch.value[0];
const endTime = t + 0.25;
source?.node?.gain?.linearRampToValueAtTime(0, endTime);
@@ -515,14 +506,14 @@ 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) => (isPoolable(n) ? releaseNodeToPool(n) : n?.disconnect()));
audioNodes.forEach((n) => n?.disconnect());
activeSoundSources.delete(chainID);
};
const soundHandle = await onTrigger(t, value, onEnded, cps);
if (soundHandle) {
sourceNode = soundHandle.node;
activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC
activeSoundSources.set(chainID, soundHandle);
}
} else {
throw new Error(`sound ${s} not found! Is it loaded?`);
@@ -539,10 +530,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
const chain = []; // audio nodes that will be connected to each other sequentially
chain.push(sourceNode);
stretch !== undefined &&
chain.push(
getPooledWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch, begin: t, end: endWithRelease }),
);
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
// gain stage
chain.push(gainNode(gain));
@@ -565,21 +553,16 @@ 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';
const lp = () => createFilter(ac, t, end, lpParams, cps, cycle);
const { filter: lpf1, lfo: lfo1 } = lp();
chain.push(lpf1);
lfo1 && audioNodes.push(lfo1);
let lp = () => createFilter(ac, t, end, lpParams, cps);
chain.push(lp());
if (ftype === '24db') {
const { filter: lpf2, lfo: lfo2 } = lp();
chain.push(lpf2);
lfo2 && audioNodes.push(lfo2);
chain.push(lp());
}
}
@@ -598,21 +581,16 @@ 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';
const hp = () => createFilter(ac, t, end, hpParams, cps, cycle);
const { filter: hpf1, lfo: lfo1 } = hp();
chain.push(hpf1);
lfo1 && audioNodes.push(lfo1);
let hp = () => createFilter(ac, t, end, hpParams, cps);
chain.push(hp());
if (ftype === '24db') {
const { filter: hpf2, lfo: lfo2 } = hp();
chain.push(hpf2);
lfo2 && audioNodes.push(lfo2);
chain.push(hp());
}
}
@@ -631,21 +609,16 @@ 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';
const bp = () => createFilter(ac, t, end, bpParams, cps, cycle);
const { filter: bpf1, lfo: lfo1 } = bp();
chain.push(bpf1);
lfo1 && audioNodes.push(lfo1);
let bp = () => createFilter(ac, t, end, bpParams, cps);
chain.push(bp());
if (ftype === '24db') {
const { filter: bpf2, lfo: lfo2 } = bp();
chain.push(bpf2);
lfo2 && audioNodes.push(lfo2);
chain.push(bp());
}
}
@@ -655,13 +628,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
// effects
coarse !== undefined &&
chain.push(getPooledWorklet(ac, 'coarse-processor', { coarse, begin: t, end: endWithRelease }));
crush !== undefined && chain.push(getPooledWorklet(ac, 'crush-processor', { crush, begin: t, end: endWithRelease }));
shape !== undefined &&
chain.push(getPooledWorklet(ac, 'shape-processor', { shape, postgain: shapevol, begin: t, end: endWithRelease }));
distort !== undefined &&
chain.push(getDistortion({ distort, postgain: distortvol, algorithm: distorttype, begin: t, end: endWithRelease }));
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
if (tremolosync != null) {
tremolo = cps * tremolosync;
@@ -695,7 +665,6 @@ 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);
}
@@ -711,10 +680,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
chain.push(panner);
}
// phaser
if (phaserrate !== undefined && phaserdepth > 0) {
const { phaser, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep);
audioNodes.push(lfo);
chain.push(phaser);
if (phaser !== undefined && phaserdepth > 0) {
const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep);
chain.push(phaserFX);
}
// last gain
@@ -724,8 +692,7 @@ 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);
const send = orbitBus.sendDelay(post, delay);
audioNodes.push(send);
orbitBus.sendDelay(post, delay);
}
// reverb
if (room > 0) {
@@ -741,8 +708,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
roomIR = await loadBuffer(url, ac, ir, 0);
}
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
const send = orbitBus.sendReverb(post, room);
audioNodes.push(send);
orbitBus.sendReverb(post, room);
}
if (djf != null) {
+2 -11
View File
@@ -1,12 +1,3 @@
/*
superdoughoutput.mjs - Output controller for superdough
Handles setting up and mixing to the outputs as well as all global (orbit) effects
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdoughoutput.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
import { errorLogger } from './logger.mjs';
import { clamp } from './util.mjs';
@@ -87,11 +78,11 @@ export class Orbit {
return this.reverbNode;
}
sendReverb(node, amount) {
return effectSend(node, this.reverbNode, amount);
effectSend(node, this.reverbNode, amount);
}
sendDelay(node, amount) {
return effectSend(node, this.delayNode, amount);
effectSend(node, this.delayNode, amount);
}
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
+52 -68
View File
@@ -3,23 +3,21 @@ import { registerSound, soundMap } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs';
import {
applyFM,
destroyAudioWorkletNode,
gainNode,
getADSRValues,
getFrequencyFromValue,
getLfo,
getParamADSR,
getPitchEnvelope,
getPooledWorklet,
getVibratoOscillator,
getWorklet,
noises,
releaseAudioNode,
webAudioTimeout,
} from './helpers.mjs';
import { logger } from './logger.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
import { releaseNodeToPool } from './nodePools.mjs';
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const waveformAliases = [
['tri', 'triangle'],
['sqr', 'square'],
@@ -49,17 +47,19 @@ 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);
let sound = getOscillator(s, t, value, () => {
const { duration } = value;
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
});
let { node: o, stop, triggerRelease } = sound;
const { duration } = value;
};
const envGain = gainNode(1);
let node = o.connect(g).connect(envGain);
@@ -165,14 +165,22 @@ export function registerSynthSounds() {
const end = holdend + release + 0.01;
const voices = clamp(unison, 1, 100);
let panspread = voices > 1 ? clamp(spread, 0, 1) : 0;
const params = {
frequency,
begin,
end,
freqspread: detune,
panspread,
};
const o = getPooledWorklet(ac, 'supersaw-oscillator', params, { outputChannelCount: [2] }, { voices });
let o = getWorklet(
ac,
'supersaw-oscillator',
{
frequency,
begin,
end,
freqspread: detune,
voices,
panspread,
},
{
outputChannelCount: [2],
},
);
const gainAdjustment = 1 / Math.sqrt(voices);
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
@@ -185,7 +193,8 @@ export function registerSynthSounds() {
let timeoutNode = webAudioTimeout(
ac,
() => {
releaseNodeToPool(o);
destroyAudioWorkletNode(o);
envGain.disconnect();
onended();
fm?.stop();
vibratoOscillator?.stop();
@@ -239,7 +248,7 @@ export function registerSynthSounds() {
const holdend = begin + duration;
const end = holdend + release + 0.01;
let o = getPooledWorklet(
let o = getWorklet(
ac,
'byte-beat-processor',
{
@@ -262,7 +271,8 @@ export function registerSynthSounds() {
let timeoutNode = webAudioTimeout(
ac,
() => {
releaseAudioNode(o);
destroyAudioWorkletNode(o);
envGain.disconnect();
onended();
},
begin,
@@ -306,7 +316,7 @@ export function registerSynthSounds() {
);
const holdend = begin + duration;
const end = holdend + release + 0.01;
let o = getPooledWorklet(
let o = getWorklet(
ac,
'pulse-oscillator',
{
@@ -335,8 +345,9 @@ export function registerSynthSounds() {
let timeoutNode = webAudioTimeout(
ac,
() => {
releaseAudioNode(o);
releaseAudioNode(lfo);
destroyAudioWorkletNode(o);
destroyAudioWorkletNode(lfo);
envGain.disconnect();
onended();
fm?.stop();
vibratoOscillator?.stop();
@@ -403,13 +414,9 @@ export function registerSynthSounds() {
waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] }));
}
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);
export function waveformN(partials, type) {
const real = new Float32Array(partials + 1);
const imag = new Float32Array(partials + 1);
const ac = getAudioContext();
const osc = ac.createOscillator();
@@ -417,29 +424,20 @@ export function waveformN(partials, phases, 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}`);
}
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;
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++;
}
const wave = ac.createPeriodicWave(real, imag);
@@ -448,28 +446,21 @@ export function waveformN(partials, phases, type) {
}
// expects one of waveforms as s
export function getOscillator(s, t, value, onended) {
const { duration, noise = 0 } = value;
const partials = value.partials ?? value.n;
export function getOscillator(s, t, value) {
let { n: partials, duration, noise = 0 } = value;
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 || partials?.length === 0 || s === 'sine') {
if (!partials || s === 'sine') {
o = getAudioContext().createOscillator();
o.type = s || 'triangle';
}
// generate custom waveform if partials are given
else {
o = waveformN(partials, value.phases, s);
o = waveformN(partials, s);
}
// set frequency
o.frequency.value = getFrequencyFromValue(value);
o.start(t);
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
@@ -482,13 +473,6 @@ export function getOscillator(s, t, value, onended) {
noiseMix = getNoiseMix(o, noise, t);
}
o.onended = () => {
noiseMix || o.disconnect();
noiseMix?.node.disconnect();
onended();
};
o.start(t);
return {
node: noiseMix?.node || o,
stop: (time) => {
-10
View File
@@ -114,13 +114,3 @@ 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('/');
}
};
+5 -17
View File
@@ -45,8 +45,7 @@ if (typeof GainNode !== 'undefined') {
throw new Error('vowel: unknown vowel ' + letter);
}
const { gains, qs, freqs } = vowelFormant[letter];
this.makeupGain = ac.createGain();
this.audioNodes = [];
const makeupGain = ac.createGain();
for (let i = 0; i < 5; i++) {
const gain = ac.createGain();
gain.gain.value = gains[i];
@@ -54,25 +53,14 @@ if (typeof GainNode !== 'undefined') {
filter.type = 'bandpass';
filter.Q.value = qs[i];
filter.frequency.value = freqs[i];
super.connect(filter);
this.connect(filter);
filter.connect(gain);
this.audioNodes.push(filter);
gain.connect(this.makeupGain);
this.audioNodes.push(gain);
gain.connect(makeupGain);
}
this.makeupGain.gain.value = 8; // how much makeup gain to add?
makeupGain.gain.value = 8; // how much makeup gain to add?
this.connect = (target) => makeupGain.connect(target);
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) {
+24 -19
View File
@@ -1,17 +1,17 @@
import { getAudioContext, registerSound } from './index.mjs';
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
import { getCommonSampleInfo } from './util.mjs';
import {
applyFM,
applyParameterModulators,
destroyAudioWorkletNode,
getADSRValues,
getFrequencyFromValue,
getParamADSR,
getPitchEnvelope,
getPooledWorklet,
getVibratoOscillator,
getWorklet,
webAudioTimeout,
} from './helpers.mjs';
import { releaseNodeToPool } from './nodePools.mjs';
import { logger } from './logger.mjs';
export const Warpmode = Object.freeze({
@@ -190,7 +190,6 @@ 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;
@@ -201,7 +200,7 @@ export const tables = async (url, frameLen, json, options = {}) => {
}
return fetch(url)
.then((res) => res.json())
.then((json) => _processTables(json, base, frameLen, options))
.then((json) => _processTables(json, url, frameLen, options))
.catch((error) => {
console.error(error);
throw new Error(`error loading "${url}"`);
@@ -225,19 +224,24 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
}
const endWithRelease = holdEnd + release;
const envEnd = endWithRelease + 0.01;
payload.voices = Math.max(value.unison ?? 1, 1);
const params = {
begin: t,
end: envEnd,
frequency,
freqspread: value.detune,
position: value.wt,
warp: value.warp,
warpMode: warpmode,
panspread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
};
const source = getPooledWorklet(ac, 'wavetable-oscillator-processor', params, { outputChannelCount: [2] }, payload);
const source = getWorklet(
ac,
'wavetable-oscillator-processor',
{
begin: t,
end: envEnd,
frequency,
freqspread: value.detune,
position: value.wt,
warp: value.warp,
warpMode: warpmode,
voices: Math.max(value.unison ?? 1, 1),
panspread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
},
{ outputChannelCount: [2] },
);
source.port.postMessage({ type: 'table', payload });
if (ac.currentTime > t) {
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
return;
@@ -314,9 +318,10 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
const timeoutNode = webAudioTimeout(
ac,
() => {
releaseNodeToPool(source);
destroyAudioWorkletNode(source);
vibratoOscillator?.stop();
fm?.stop();
node.disconnect();
wtPosModulators?.disconnect();
wtWarpModulators?.disconnect();
onended();
-70
View File
@@ -1,70 +0,0 @@
/*
worklets-common.mjs - Common worklet code
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/worklets-common.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Mixin to make class "reusable" (i.e. have a begin and end and properly handle `initialize` messages)
export const makeReusable = (Base) =>
class extends Base {
static get parameterDescriptors() {
return [
...(super.parameterDescriptors ?? []),
{
name: 'begin',
defaultValue: 0,
min: 0,
max: Number.POSITIVE_INFINITY,
automationRate: 'k-rate',
},
{
name: 'end',
defaultValue: 0,
min: 0,
max: Number.POSITIVE_INFINITY,
automationRate: 'k-rate',
},
];
}
constructor(options) {
super(options);
this.isAlive = true;
this.graceSeconds = options?.processorOptions?.graceSeconds ?? 0.5;
this.port.onmessage = (e) => {
const { type, payload } = e.data || {};
if (type === 'initialize') {
this.initialize(payload);
} else if (this.handlePortMessage) {
this.handlePortMessage(type, payload, e);
}
};
}
initialize(_options) {
// defined on subclasses
}
processActive(_inputs, _outputs, _params) {
// processing subclasses should do when we're between begin/end
return true;
}
process(inputs, outputs, params) {
const begin = params.begin[0];
const end = params.end[0];
if (currentTime >= end + this.graceSeconds) {
if (this.isAlive) {
this.port.postMessage({ type: 'died' });
this.isAlive = false;
}
return false;
}
if (currentTime < begin || currentTime >= end) {
return true;
}
return this.processActive(inputs, outputs, params);
}
};
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -3,7 +3,6 @@ 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 {
@@ -84,10 +83,10 @@ export function registerZZFXSounds() {
wave,
(t, value, onended) => {
const { node: o } = getZZFX({ s: wave, ...value }, t);
onceEnded(o, () => {
releaseAudioNode(o);
o.onended = () => {
o.disconnect();
onended();
});
};
return {
node: o,
stop: () => {},
+2 -11
View File
@@ -5,16 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { Note, Interval, Scale } from '@tonaljs/tonal';
import {
_mod,
errorLogger,
getAccidentalsOffset,
isNote,
logger,
noteToMidi,
register,
removeUndefineds,
} from '@strudel/core';
import { register, _mod, logger, isNote, noteToMidi, removeUndefineds, getAccidentalsOffset } from '@strudel/core';
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
@@ -303,7 +294,7 @@ export const scale = register(
}
if (offset != 0) scaleNote = Note.transpose(scaleNote, Interval.fromSemitones(offset));
} catch (err) {
errorLogger(err, 'tonal');
logger(`[tonal] ${err.message}`, 'error');
return; // will be removed
}
}
+132 -20
View File
@@ -41,7 +41,7 @@ importers:
version: 2.2.7
'@vitest/coverage-v8':
specifier: 3.0.4
version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))
version: 3.0.4(vitest@3.0.4)
'@vitest/ui':
specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4)
@@ -85,29 +85,29 @@ importers:
examples/codemirror-repl:
dependencies:
'@strudel/codemirror':
specifier: workspace:*
version: link:../../packages/codemirror
specifier: 1.2.6
version: 1.2.6(@codemirror/lint@6.8.4)
'@strudel/core':
specifier: workspace:*
version: link:../../packages/core
specifier: 1.2.5
version: 1.2.5
'@strudel/draw':
specifier: workspace:*
version: link:../../packages/draw
specifier: 1.2.5
version: 1.2.5
'@strudel/mini':
specifier: workspace:*
version: link:../../packages/mini
specifier: 1.2.5
version: 1.2.5
'@strudel/soundfonts':
specifier: workspace:*
version: link:../../packages/soundfonts
specifier: 1.2.6
version: 1.2.6
'@strudel/tonal':
specifier: workspace:*
version: link:../../packages/tonal
specifier: 1.2.5
version: 1.2.5
'@strudel/transpiler':
specifier: workspace:*
version: link:../../packages/transpiler
specifier: 1.2.5
version: 1.2.5
'@strudel/webaudio':
specifier: workspace:*
version: link:../../packages/webaudio
specifier: 1.2.6
version: 1.2.6
devDependencies:
vite:
specifier: ^6.0.11
@@ -116,8 +116,8 @@ importers:
examples/headless-repl:
dependencies:
'@strudel/web':
specifier: workspace:*
version: link:../../packages/web
specifier: 1.2.6
version: 1.2.6
devDependencies:
vite:
specifier: ^6.0.11
@@ -2544,6 +2544,33 @@ packages:
'@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
'@strudel/codemirror@1.2.6':
resolution: {integrity: sha512-zjKbnbnOY40dlIAQu1poq28rcngYliLsuDwXXVnJsaXzCZSVuXEOMq91LqY4IGz/2QUOIDGm663ziz6Nb4IXeg==}
'@strudel/core@1.2.5':
resolution: {integrity: sha512-XO6ICbfuGfvaQ4sZmfIFsPDcjFez/+AVpNgJnmks5gtMMQNXpr0reuee61tvMiu2k5QYKzxWgzDl2FF9pJeIgA==}
'@strudel/draw@1.2.5':
resolution: {integrity: sha512-ZI4WPxUsVsxgDoeZMHo6VfFf+S1tsj8ubKBeSLcf6lH29JqPp7Nz6tPmbomOV1fzazZ7+xpkC7z772MdtgWy2g==}
'@strudel/mini@1.2.5':
resolution: {integrity: sha512-GmTWSBt49C3ogBy0hjM3bmf6K12PaJ1JJLsEvQu7j5J+lUd2n7GmLYR1UL6fntY4yVqT8ZwMhfUf6vIgUCTNbw==}
'@strudel/soundfonts@1.2.6':
resolution: {integrity: sha512-F7eBhohkCXCMo8WiufSAhD3JCfL5GTgCh9RlOJVT+1swiFeIss1vUAWJR3WjonB6moL8BiuNpcYOqO6i6cFy4A==}
'@strudel/tonal@1.2.5':
resolution: {integrity: sha512-mX3uW0nWH8Sa52HTLwnxlR5BrejFX77abeaiMbp+kQCMrX9I+XRdf8rEr7gdeInB6f/y2TzTVFpnsNsB5jLdoA==}
'@strudel/transpiler@1.2.5':
resolution: {integrity: sha512-mBZWfQBzknvnIYsjjzR8D67A+uIZ1iZ60U2WoXT21q5luXKz2c95RcTpqdx3oOGpLK9K00lDzcKjhQDJtj4OKw==}
'@strudel/web@1.2.6':
resolution: {integrity: sha512-e6VltuX90zZz8JVe1FtN23DCaXA9p8zGDakp5OuUb5kZ5hC6cySFkkOsHdGC6dJ0EAsZSCKXMihRxgk9GhBwKA==}
'@strudel/webaudio@1.2.6':
resolution: {integrity: sha512-4LnyNvyeAXYNmXQGGw2f0chIkDr9Rbwo+SK6Y0p8C3W8juDiOfKq2ihSVph95yaBBPRYFJ5Kttj1kLknRHYsog==}
'@supabase/auth-js@2.67.3':
resolution: {integrity: sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==}
@@ -7094,6 +7121,9 @@ packages:
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
superdough@1.2.6:
resolution: {integrity: sha512-zHEFfG+N4zhxJ9lUDf8p4Oj4sdGGQ5SrOPksdc0cKv5Yn0hzlNlMehEa5TkeIfDCiR27Y4H81ks7S8/CGSByhQ==}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -7102,6 +7132,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
supradough@1.2.4:
resolution: {integrity: sha512-/v3cL9fq05QCQVwbttLHNZcjyyFFz5wdKvg3j6V6moRXpDlQG89l8dQtUIpQvHsxYI5sXNod9rh5bf+Y4NMGeA==}
tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
@@ -10019,6 +10052,79 @@ snapshots:
'@sinclair/typebox@0.27.8': {}
'@strudel/codemirror@1.2.6(@codemirror/lint@6.8.4)':
dependencies:
'@codemirror/autocomplete': 6.18.4
'@codemirror/commands': 6.8.0
'@codemirror/lang-javascript': 6.2.2
'@codemirror/language': 6.10.8
'@codemirror/search': 6.5.8
'@codemirror/state': 6.5.1
'@codemirror/view': 6.36.2
'@lezer/highlight': 1.2.1
'@nanostores/persistent': 0.10.2(nanostores@0.11.3)
'@replit/codemirror-emacs': 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@replit/codemirror-vim': 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@replit/codemirror-vscode-keymap': 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@strudel/core': 1.2.5
'@strudel/draw': 1.2.5
'@strudel/tonal': 1.2.5
'@strudel/transpiler': 1.2.5
'@tonaljs/tonal': 4.10.0
nanostores: 0.11.3
superdough: 1.2.6
transitivePeerDependencies:
- '@codemirror/lint'
'@strudel/core@1.2.5':
dependencies:
fraction.js: 5.2.1
'@strudel/draw@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@strudel/mini@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@strudel/soundfonts@1.2.6':
dependencies:
'@strudel/core': 1.2.5
'@strudel/webaudio': 1.2.6
sfumato: 0.1.2
soundfont2: 0.5.0
'@strudel/tonal@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@tonaljs/tonal': 4.10.0
chord-voicings: 0.0.1
webmidi: 3.1.12
'@strudel/transpiler@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@strudel/mini': 1.2.5
acorn: 8.14.0
escodegen: 2.1.0
estree-walker: 3.0.3
'@strudel/web@1.2.6':
dependencies:
'@strudel/core': 1.2.5
'@strudel/mini': 1.2.5
'@strudel/tonal': 1.2.5
'@strudel/transpiler': 1.2.5
'@strudel/webaudio': 1.2.6
'@strudel/webaudio@1.2.6':
dependencies:
'@strudel/core': 1.2.5
'@strudel/draw': 1.2.5
superdough: 1.2.6
supradough: 1.2.4
'@supabase/auth-js@2.67.3':
dependencies:
'@supabase/node-fetch': 2.6.15
@@ -10554,7 +10660,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
'@vitest/coverage-v8@3.0.4(vitest@3.0.4)':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -15665,12 +15771,18 @@ snapshots:
pirates: 4.0.6
ts-interface-checker: 0.1.13
superdough@1.2.6:
dependencies:
nanostores: 0.11.3
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
supports-preserve-symlinks-flag@1.0.0: {}
supradough@1.2.4: {}
tabbable@6.2.0: {}
tailwindcss@3.4.17:
+89 -797
View File
@@ -1391,75 +1391,6 @@ 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 ]",
@@ -4092,6 +4023,35 @@ 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 ]",
@@ -4208,72 +4168,6 @@ 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 ]",
@@ -4892,75 +4786,6 @@ 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 ]",
@@ -6144,144 +5969,6 @@ 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 ]",
@@ -6470,75 +6157,6 @@ 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 ]",
@@ -6581,75 +6199,6 @@ 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 ]",
@@ -7339,18 +6888,18 @@ exports[`runs examples > example "nrpv" example index 0 1`] = `
exports[`runs examples > example "octave" example index 0 1`] = `
[
"[ 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 ]",
"[ 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 ]",
]
`;
@@ -7670,112 +7219,6 @@ 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 ]",
@@ -8029,75 +7472,6 @@ 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 ]",
@@ -8819,75 +8193,6 @@ 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 ]",
@@ -9293,19 +8598,6 @@ 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 ]",
@@ -12948,54 +12240,54 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = `
exports[`runs examples > example "wchooseCycles" example index 1 1`] = `
[
"[ 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 ]",
"[ 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 ]",
]
`;
-1
View File
@@ -1,5 +1,4 @@
{
"name": "dbpatch",
"dependencies": {
"csv": "^6.3.11"
},
-14
View File
@@ -1,14 +0,0 @@
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);
});
/*
*/
+7 -73
View File
@@ -48,94 +48,28 @@ You can also use the `crackle` type to play some subtle noise crackles. You can
### Additive Synthesis
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:
To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
.sound("sawtooth")
.partials([1, 1, "<1 0>", "<1 0>", "<1 0>", "<1 0>", "<1 0>"])
.n("<32 16 8 4>")
._scope()`}
/>
`partials` can also be used to construct _new_ waveforms not present in our basic set with the 'user' sound source:
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`:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
.sound("user")
.partials([1, 0, 0.3, 0, 0.1, 0, 0, 0.3])
.sound("sawtooth:<32 16 8 4>")
._scope()`}
/>
We may algorithmically construct lists of magnitudes with Javascript code like:
<MiniRepl
client:idle
tune={`const numHarmonics = 22;
note("c2 <eb2 <g2 g1>>".fast(2))
.sound("saw")
.partials(new Array(numHarmonics).fill(1))
._scope()`}
/>
which acts as a spectral filter. Or:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>").fast(2)
.sound("user")
.partials(new Array(50).fill(0)
.map((_, idx) => ((-1) ** (idx + 1)) / (idx + 1))
)
._scope()`}
/>
which recovers a familiar waveform.
`partials` is also compatible with pattern functions designed to produce lists, like `randL` or `binaryL`:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>").fast(2)
.sound("user")
.partials(randL(10))
._scope()`}
/>
and with lists _of_ patterns:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>".fast(4))
.sound("user")
.partials([1, 0, "0 1", "0 1 0.3", rand])
._scope()`}
/>
Note that the first value in the `partials` array controls the magnitude of the fundamental harmonic rather than the DC offset, which is fixed at 0.
#### Phases
Earlier, we mentioned that periodic waveforms can be broken into a set of harmonics above a fundamental frequency. Each harmonic has two defining properties: its magnitude (how loud it is) and its phase, which determines where in its cycle that sine wave starts when the waveform is built.
These phases too can be declared in Strudel and can give your sounds interesting depth.
<MiniRepl
client:idle
tune={`s("saw").seg(16).n(irand(12)).scale("F1:minor")
.penv(48).panchor(0).pdec(0.05)
.delay(0.25).room(0.25)
.compressor(-20).vib(0.3)
.partials(randL(200))
.phases(randL(200))`}
/>
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.
## Vibrato
@@ -8,34 +8,3 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but
</button>
);
}
export function SpecialActionButton(props) {
const { className, ...buttonProps } = props;
return (
<ActionButton
{...buttonProps}
className={cx('bg-background p-2 max-w-[300px] rounded-md hover:opacity-50', className)}
/>
);
}
export function ActionInput({ label, className, ...props }) {
return (
<label className={cx('inline-flex items-center cursor-pointer', className)}>
<input {...props} className="sr-only peer" />
<span className="inline-flex items-center peer-hover:opacity-50">{label}</span>
</label>
);
}
export function SpecialActionInput({ className, ...props }) {
return (
<ActionInput
{...props}
className={className}
label={<span className="bg-background p-2 max-w-[300px] rounded-md">{props.label}</span>}
/>
);
}
@@ -1,29 +0,0 @@
import { errorLogger } from '@strudel/core';
import { useSettings, storePrebakeScript } from '../../../settings.mjs';
import { SpecialActionInput } from '../button/action-button';
async function importScript(script) {
const reader = new FileReader();
reader.readAsText(script);
reader.onload = () => {
const text = reader.result;
storePrebakeScript(text);
};
reader.onerror = () => {
errorLogger(new Error('failed to import prebake script'), 'importScript');
};
}
export function ImportPrebakeScriptButton() {
const settings = useSettings();
return (
<SpecialActionInput
type="file"
label="import prebake script"
accept=".strudel"
onChange={(e) => importScript(e.target.files[0])}
/>
);
}
@@ -7,8 +7,6 @@ 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 (
@@ -115,7 +113,6 @@ export function SettingsTab({ started }) {
isTabIndentationEnabled,
isMultiCursorEnabled,
patternAutoStart,
includePrebakeScriptInShare,
} = useSettings();
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
@@ -207,15 +204,6 @@ export function SettingsTab({ started }) {
/>
</FormItem>
</div>
<FormItem label="Prebake">
<ImportPrebakeScriptButton />
<Checkbox
label="Include prebake script in share"
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
value={includePrebakeScriptInShare}
/>
</FormItem>
<FormItem label="Keybindings">
<ButtonGroup
value={keybindings}
@@ -325,7 +313,8 @@ export function SettingsTab({ started }) {
</FormItem>
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
<FormItem label="Reset Settings">
<SpecialActionButton
<button
className="bg-background p-2 max-w-[300px] rounded-md hover:opacity-50"
onClick={() => {
confirmDialog('Sure?').then((r) => {
if (r) {
@@ -336,7 +325,7 @@ export function SettingsTab({ started }) {
}}
>
restore default settings
</SpecialActionButton>
</button>
</FormItem>
</div>
);
+15 -34
View File
@@ -14,10 +14,6 @@ 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);
@@ -60,7 +56,9 @@ export function SoundsTab() {
// holds mutable ref to current triggered sound
const trigRef = useRef();
const numRef = useRef(0);
// Used to cycle through sound previews on banks with multiple sounds
let soundPreviewIdx = 0;
// stop current sound on mouseup
useEvent('mouseup', () => {
@@ -68,14 +66,6 @@ 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 (
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
@@ -125,34 +115,25 @@ export function SoundsTab() {
const params = {
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
s: name,
n: numRef.current,
n: soundPreviewIdx,
clip: 1,
release: 0.5,
sustain: 1,
duration: 0.5,
};
soundPreviewIdx++;
const onended = () => trigRef.current?.node?.disconnect();
// 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);
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);
}
} catch (err) {
console.warn('Failed to trigger sound:', err);
}
}}
>
-2
View File
@@ -3,8 +3,6 @@ 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;
+5 -15
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
import { getDrawContext } from '@strudel/draw';
import { evaluate, transpiler } from '@strudel/transpiler';
import { transpiler } from '@strudel/transpiler';
import {
getAudioContextCurrentTime,
webaudioOutput,
@@ -63,10 +63,11 @@ async function getModule(name) {
const initialCode = `// LOADING`;
export function useReplContext() {
const { isSyncEnabled, audioEngineTarget, prebakeScript, includePrebakeScriptInShare } = useSettings();
const { isSyncEnabled, audioEngineTarget } = 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();
@@ -83,12 +84,7 @@ export function useReplContext() {
pattern: silence,
drawTime,
drawContext,
prebake: async () =>
Promise.all([modulesLoading, presets]).then(() => {
if (prebakeScript?.length) {
return evaluate(prebakeScript ?? '');
}
}),
prebake: async () => Promise.all([modulesLoading, presets]),
onUpdateState: (state) => {
setReplState({ ...state });
},
@@ -218,13 +214,7 @@ export function useReplContext() {
editorRef.current.repl.evaluate(code);
};
const handleShare = async () => {
let code = replState.code;
if (includePrebakeScriptInShare) {
code = prebakeScript + '\n' + code;
}
shareCode(code);
};
const handleShare = async () => shareCode(replState.code);
const context = {
started,
pending,
+6 -5
View File
@@ -1,10 +1,11 @@
import { code2hash, evalScope, hash2code, logger } from '@strudel/core';
import { 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';
@@ -108,8 +109,9 @@ export function confirmDialog(msg) {
});
}
let lastShared;
//RIP due to SPAM
// let lastShared;
// export async function shareCode(codeToShare) {
// // const codeToShare = activeCode || code;
// if (lastShared === codeToShare) {
@@ -144,10 +146,9 @@ export function confirmDialog(msg) {
// });
// }
export async function shareCode(codeToShare) {
export async function shareCode() {
try {
const hash = '#' + code2hash(codeToShare);
const shareUrl = window.location.origin + window.location.pathname + hash;
const shareUrl = window.location.href;
if (isTauri()) {
await writeText(shareUrl);
} else {
-5
View File
@@ -45,13 +45,11 @@ 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;
@@ -98,7 +96,6 @@ export function useSettings() {
isPanelOpen: parseBoolean(state.isPanelOpen),
userPatterns: userPatterns,
multiChannelOrbits: parseBoolean(state.multiChannelOrbits),
includePrebakeScriptInShare: parseBoolean(state.includePrebakeScriptInShare),
patternAutoStart: isUdels()
? false
: state.patternAutoStart === undefined
@@ -111,8 +108,6 @@ 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) =>