Compare commits

..

54 Commits

Author SHA1 Message Date
Switch Angel AKA Jade Rose ae202ac5bb Merge branch 'main' into glossing/highlighting 2025-12-08 06:21:27 +01:00
Aria d7bf5db8be Merge pull request 'Feat: Add channel support to midi in' (#1775) from glossing/midin-channels into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1775
2025-12-07 21:51:39 +01:00
Aria 48231ea12a Merge branch 'main' into glossing/midin-channels 2025-12-07 21:23:57 +01:00
Aria c8a442234e Merge pull request 'Feat: Transient shaper' (#1777) from glossing/transient-processor into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1777
2025-12-07 21:23:42 +01:00
Aria efda32eedb Merge branch 'main' into glossing/transient-processor 2025-12-07 20:29:50 +01:00
Aria 95d04d1149 Merge branch 'main' into glossing/midin-channels 2025-12-07 20:29:39 +01:00
Aria 358b1e3f56 Make sure processor turns off; fix array sizes for JIT 2025-12-07 13:28:57 -06:00
Aria fe7e8f7add Clearer docstring 2025-12-07 13:21:06 -06:00
Aria 45d329d3d1 Merge pull request 'Add vel as a synonym for velocity & update a few docstrings' (#1781) from glossing/controls-various into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1781
2025-12-07 19:15:42 +01:00
Aria 5e99393f5a Merge branch 'main' into glossing/controls-various 2025-12-07 18:54:18 +01:00
Aria 9650c8eb50 Merge pull request '[perf] fix phaser leak of unused biquads' (#1800) from jeromew/strudel:fix-perf10 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1800
Reviewed-by: Aria <glossing@noreply.codeberg.org>
2025-12-07 18:53:03 +01:00
Aria fab8d2193d Merge branch 'main' into glossing/midin-channels 2025-12-07 18:45:16 +01:00
Aria 150835aa4d Merge branch 'main' into glossing/transient-processor 2025-12-07 18:45:11 +01:00
Aria 76237016a3 Merge branch 'main' into glossing/controls-various 2025-12-07 18:45:02 +01:00
Aria 6f4d8ebacf Merge branch 'main' into fix-perf10 2025-12-07 18:44:47 +01:00
Aria 3cecf4a50e Merge pull request 'Bug fix: Remove failing tests due to shabda removal' (#1820) from glossing/shabda-test-fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1820
2025-12-07 18:42:49 +01:00
Aria 16b5faf333 Removing failing tests due to shabda removal 2025-12-07 11:23:55 -06:00
Aria c4826b7dfb Merge branch 'main' into glossing/midin-channels 2025-12-07 18:11:59 +01:00
Aria bd61c1dc7e Merge branch 'main' into glossing/transient-processor 2025-12-07 18:11:50 +01:00
Aria ceb3aa797d Merge branch 'main' into glossing/controls-various 2025-12-07 18:11:39 +01:00
Aria 9b3277da1c Merge branch 'main' into fix-perf10 2025-12-07 18:09:00 +01:00
jeromew 0b711e879c Improve naming & backward compat 2025-12-04 08:23:36 +00:00
Aria e01621d0cc Merge pull request 'Feature: Eight FMs' (#1628) from glossing/strudel:glossing/fmsss into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1628
2025-12-03 18:27:27 +01:00
Aria 196f70ab6b Formatting 2025-12-03 11:13:21 -06:00
Aria 1d4f2b81d8 Switch back to explicit exports and use new release mechanism 2025-12-03 11:12:30 -06:00
Aria d28c8dac8e Merge branch 'main' into glossing/fmsss 2025-12-03 10:42:35 -06:00
Aria eb18648a2a Merge pull request '[perf] release unused AudioBufferSourceNode + releaseAudioNode' (#1805) from jeromew/strudel:fix-perf11 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1805
Reviewed-by: Aria <glossing@noreply.codeberg.org>
2025-12-03 17:35:03 +01:00
jeromew 0ec9826de5 onceEnded mechanism to clean audioNode.onended callbacks 2025-12-02 09:29:50 +00:00
jeromew af7855402f Accept non-instanciated nodes for better developer experience 2025-12-01 20:43:55 +00:00
jeromew 68b1cb87ca [perf] release unused AudioBufferSourceNode + releaseAudioNode 2025-12-01 15:29:19 +00:00
jeromew b65cc2128f [perf] fix phaser leak of unused biquads 2025-11-29 14:47:03 +00:00
Aria a82ebbeef6 Remove unused import 2025-11-28 21:56:04 -06:00
Aria d4a1b571fd Throttle in draw 2025-11-28 21:55:00 -06:00
Aria d44f6d3ded Throttle fps 2025-11-28 20:39:22 -06:00
Aria 3b8f309a22 Don't activate draw when highlighting setting off 2025-11-28 20:12:20 -06:00
Aria 39e7f70079 Remove synonym 2025-11-24 18:44:29 -06:00
Aria 7e5a907ce5 Add a few more functions to autocomplete 2025-11-24 18:41:25 -06:00
Aria d7afb8a6c0 Add register example 2025-11-24 18:01:20 -06:00
Aria a278f21f1b Allow register in autocomplete, add vel as synonym for velocity, fix some controls 2025-11-24 17:52:00 -06:00
Aria 36587ae747 Switch back to original version 2025-11-23 22:55:36 -06:00
Aria 6c01c16a0c Fix typo 2025-11-23 22:39:21 -06:00
Aria be36898803 Formatting and examples 2025-11-23 22:29:33 -06:00
Aria df047fd48d Optimizations 2025-11-23 22:28:10 -06:00
Aria 68840de188 Working version 2025-11-23 20:58:28 -06:00
Aria 020a6bc75f First pass at transient processor 2025-11-23 20:31:31 -06:00
Aria 7bc6fcbf2a Add channel support to midi in 2025-11-23 18:24:48 -06:00
Aria 654c1a86f0 Simplify declarations via globalThis; fix cleanup/stop 2025-11-18 16:02:33 -06:00
Aria ac3f477f30 Merge branch 'main' into glossing/fmsss 2025-11-16 22:32:38 -06:00
Aria 573cd1ae41 Merge branch 'main' into glossing/fmsss 2025-11-16 22:23:31 -06:00
Aria 4c49cd9297 More examples 2025-10-18 11:24:56 -05:00
Aria 70d2da81d8 Merge branch 'main' into glossing/fmsss 2025-10-18 11:01:23 -05:00
Aria bd73d96847 Cleanup 2025-10-03 18:49:49 -05:00
Aria 7cf9db4872 Big matrix refactor 2025-10-03 18:20:41 -05:00
Aria c9381a11f9 Working version 2025-10-03 17:21:05 -05:00
15 changed files with 887 additions and 251 deletions
+2 -26
View File
@@ -1,27 +1,16 @@
name: Strudel test + warm deploy name: Strudel tests
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
test-and-warm-deploy: build:
runs-on: docker runs-on: docker
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
strategy: strategy:
matrix: matrix:
node-version: [20] node-version: [20]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Debug Forgejo context
run: |
echo "hmm"
echo "EVENT_NAME = ${{ forgejo.event_name }}"
echo "REF = ${{ forgejo.ref }}"
echo "REF_NAME = ${{ forgejo.ref_name }}"
echo "EVENT_NAME = $FORGEJO_EVENT_NAME"
echo "REF = $FORGEJO_REF"
echo "REF_NAME = $FORGEJO_REF_NAME"
- name: apt install ztd - name: apt install ztd
run: apt update && apt install -y zstd run: apt update && apt install -y zstd
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
@@ -35,16 +24,3 @@ jobs:
- run: pnpm run format-check - run: pnpm run format-check
- run: pnpm run lint - run: pnpm run lint
- run: pnpm test - run: pnpm test
- name: Build
if: ${{ forgejo.event_name == 'push' && forgejo.ref == 'refs/heads/main' }}
run: pnpm build
- name: Warm deploy
# automatically deploy to warm on pushes to main
if: ${{ forgejo.event_name == 'push' && forgejo.ref == 'refs/heads/main' }}
run: |
eval $(ssh-agent -s)
echo "$SSH_PRIVATE_KEY" | ssh-add -
apt update && apt install -y rsync
mkdir ~/.ssh
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/warm.strudel.cc
+19 -2
View File
@@ -162,6 +162,7 @@ export class StrudelMirror {
this.onDraw = onDraw || this.draw; this.onDraw = onDraw || this.draw;
this.id = id || s4(); this.id = id || s4();
this.solo = solo; this.solo = solo;
this.hasPainters = false;
this.drawer = new Drawer((haps, time, _, painters) => { this.drawer = new Drawer((haps, time, _, painters) => {
const currentFrame = haps.filter((hap) => hap.isActive(time)); const currentFrame = haps.filter((hap) => hap.isActive(time));
@@ -177,7 +178,7 @@ export class StrudelMirror {
onToggle: (started) => { onToggle: (started) => {
replOptions?.onToggle?.(started); replOptions?.onToggle?.(started);
if (started) { if (started) {
this.drawer.start(this.repl.scheduler); this.shouldAnimate() && this.drawer.start(this.repl.scheduler);
if (this.solo) { if (this.solo) {
// stop other repls when this one is started // stop other repls when this one is started
document.dispatchEvent( document.dispatchEvent(
@@ -207,9 +208,10 @@ export class StrudelMirror {
updateWidgets(this.editor, widgets); updateWidgets(this.editor, widgets);
updateMiniLocations(this.editor, this.miniLocations); updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options); replOptions?.afterEval?.(options);
this.hasPainters = options.pattern.getPainters().length > 0;
// if no painters are set (.onPaint was not called), then we only need // if no painters are set (.onPaint was not called), then we only need
// the present moment (for highlighting) // the present moment (for highlighting)
const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0]; const drawTime = this.hasPainters ? this.drawTime : [0, 0];
this.drawer.setDrawTime(drawTime); this.drawer.setDrawTime(drawTime);
// invalidate drawer after we've set the appropriate drawTime // invalidate drawer after we've set the appropriate drawTime
this.drawer.invalidate(this.repl.scheduler); this.drawer.invalidate(this.repl.scheduler);
@@ -237,6 +239,7 @@ export class StrudelMirror {
cmEditor.style.backgroundColor = 'transparent'; cmEditor.style.backgroundColor = 'transparent';
} }
const settings = codemirrorSettings.get(); const settings = codemirrorSettings.get();
this.isPatternHighlightingEnabled = parseBooleans(settings.isPatternHighlightingEnabled);
this.setFontSize(settings.fontSize); this.setFontSize(settings.fontSize);
this.setFontFamily(settings.fontFamily); this.setFontFamily(settings.fontFamily);
@@ -321,8 +324,17 @@ export class StrudelMirror {
flash(this.editor, ms); flash(this.editor, ms);
} }
highlight(haps, time) { highlight(haps, time) {
if (!this.isPatternHighlightingEnabled) {
return;
}
highlightMiniLocations(this.editor, time, haps); highlightMiniLocations(this.editor, time, haps);
} }
shouldAnimate() {
return (
this.repl.scheduler.started &&
(this.isPatternHighlightingEnabled || this.hasPainters || this.onDraw !== this.draw)
);
}
setFontSize(size) { setFontSize(size) {
this.root.style.fontSize = size + 'px'; this.root.style.fontSize = size + 'px';
} }
@@ -343,6 +355,11 @@ export class StrudelMirror {
this.editor.dispatch({ this.editor.dispatch({
effects: compartments[key].reconfigure(newValue), effects: compartments[key].reconfigure(newValue),
}); });
if (key === 'isPatternHighlightingEnabled') {
this.isPatternHighlightingEnabled = value;
this.drawer.stop();
this.shouldAnimate() && this.drawer.start(this.repl.scheduler);
}
if (key === 'theme') { if (key === 'theme') {
activateTheme(value); activateTheme(value);
} }
+194 -23
View File
@@ -72,6 +72,27 @@ export function registerControl(names, ...aliases) {
return bag; return bag;
} }
export function registerMultiControl(names, maxControls, ...aliases) {
names = Array.isArray(names) ? names : [names];
let bag = {};
for (let i = 1; i <= maxControls; i++) {
let theseAliases = [...aliases];
let theseNames = [...names];
if (i === 1) {
// adds e.g. fm1 as an alias for fm
const aliases1 = theseAliases.map((a) => `${a}1`);
const names1 = theseNames.map((n) => `${n}1`);
theseAliases = theseAliases.concat(aliases1).concat(names1);
} else {
theseAliases = theseAliases.map((a) => `${a}${i}`);
theseNames = theseNames.map((n) => `${n}${i}`);
}
const subBag = registerControl(theseNames, ...theseAliases);
bag = { ...bag, ...subBag };
}
return bag;
}
/** /**
* Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters * Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters
* separated by ':'. * separated by ':'.
@@ -380,12 +401,13 @@ export const { accelerate } = registerControl('accelerate');
* Sets the velocity from 0 to 1. Is multiplied together with gain. * Sets the velocity from 0 to 1. Is multiplied together with gain.
* *
* @name velocity * @name velocity
* @synonyms vel
* @example * @example
* s("hh*8") * s("hh*8")
* .gain(".4!2 1 .4!2 1 .4 1") * .gain(".4!2 1 .4!2 1 .4 1")
* .velocity(".4 1") * .velocity(".4 1")
*/ */
export const { velocity } = registerControl('velocity'); export const { velocity, vel } = registerControl('velocity', 'vel');
/** /**
* Controls the gain by an exponential amount. * Controls the gain by an exponential amount.
* *
@@ -435,6 +457,9 @@ export const { attack, att } = registerControl('attack', 'att');
* Whole numbers and simple ratios sound more natural, * Whole numbers and simple ratios sound more natural,
* while decimal numbers and complex ratios sound metallic. * while decimal numbers and complex ratios sound metallic.
* *
* A number may be added afterwards to control the harmonicity of
* any of the 8 individual FMs (e.g. `fmh2`)
*
* @name fmh * @name fmh
* @param {number | Pattern} harmonicity * @param {number | Pattern} harmonicity
* @example * @example
@@ -444,25 +469,40 @@ export const { attack, att } = registerControl('attack', 'att');
* ._scope() * ._scope()
* *
*/ */
export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh'); export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerMultiControl(['fmh', 'fmi'], 8, 'fmh');
/** /**
* Sets the Frequency Modulation of the synth. * Sets the Frequency Modulation of the synth.
* Controls the modulation index, which defines the brightness of the sound. * Controls the modulation index, which defines the brightness of the sound.
* *
* @name fm * A number may be added afterwards to control the modulation index of
* any of the 8 individual FMs (e.g. `fm3`). Also, FMs may be routed into
* each other with matrix commands like `fm13`, which would send `fm1` back into
* `fm3`
*
* @name fmi
* @param {number | Pattern} brightness modulation index * @param {number | Pattern} brightness modulation index
* @synonyms fmi * @synonyms fm
* @example * @example
* note("c e g b g e") * note("c e g b g e")
* .fm("<0 1 2 8 32>") * .fm("<0 1 2 8 32>")
* ._scope() * ._scope()
* @example
* s("sine").note("F1").seg(8)
* .fm(4).fm2(rand.mul(4)).fm3(saw.mul(8).slow(8))
* .fmh(1.06).fmh2(10).fmh3(0.1)
* *
*/ */
export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm'); export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8 } =
registerMultiControl(['fmi', 'fmh'], 8, 'fm');
// fm envelope // fm envelope
/** /**
* Ramp type of fm envelope. Exp might be a bit broken.. * Ramp type of fm envelope. Exp might be a bit broken..
* *
* A number may be added afterwards to control the envelope of
* any of the 8 individual FMs (e.g. `fmenv4`)
*
* @name fmenv * @name fmenv
* @param {number | Pattern} type lin | exp * @param {number | Pattern} type lin | exp
* @example * @example
@@ -474,11 +514,19 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
* ._scope() * ._scope()
* *
*/ */
export const { fmenv } = registerControl('fmenv'); export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fmenv8 } = registerMultiControl(
'fmenv',
8,
);
/** /**
* Attack time for the FM envelope: time it takes to reach maximum modulation * Attack time for the FM envelope: time it takes to reach maximum modulation
* *
* A number may be added afterwards to control the attack of the envelope of
* any of the 8 individual FMs (e.g. `fmatt5`)
*
* @name fmattack * @name fmattack
* @synonyms fmatt
* @param {number | Pattern} time attack time * @param {number | Pattern} time attack time
* @example * @example
* note("c e g b g e") * note("c e g b g e")
@@ -487,11 +535,33 @@ export const { fmenv } = registerControl('fmenv');
* ._scope() * ._scope()
* *
*/ */
export const { fmattack } = registerControl('fmattack'); export const {
fmattack,
fmattack1,
fmattack2,
fmattack3,
fmattack4,
fmattack5,
fmattack6,
fmattack7,
fmattack8,
fmatt,
fmatt1,
fmatt2,
fmatt3,
fmatt4,
fmatt5,
fmatt6,
fmatt7,
fmatt8,
} = registerMultiControl('fmattack', 8, 'fmatt');
/** /**
* Waveform of the fm modulator * Waveform of the fm modulator
* *
* A number may be added afterwards to control the waveform
* any of the 8 individual FMs (e.g. `fmwave6`)
*
* @name fmwave * @name fmwave
* @param {number | Pattern} wave waveform * @param {number | Pattern} wave waveform
* @example * @example
@@ -500,12 +570,19 @@ export const { fmattack } = registerControl('fmattack');
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6) * n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6)
* *
*/ */
export const { fmwave } = registerControl('fmwave'); export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmwave7, fmwave8 } = registerMultiControl(
'fmwave',
8,
);
/** /**
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase. * Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
* *
* A number may be added afterwards to control the decay of the envelope of
* any of the 8 individual FMs (e.g. `fmdec6`)
*
* @name fmdecay * @name fmdecay
* @synonyms fmdec
* @param {number | Pattern} time decay time * @param {number | Pattern} time decay time
* @example * @example
* note("c e g b g e") * note("c e g b g e")
@@ -515,11 +592,35 @@ export const { fmwave } = registerControl('fmwave');
* ._scope() * ._scope()
* *
*/ */
export const { fmdecay } = registerControl('fmdecay'); export const {
fmdecay,
fmdecay1,
fmdecay2,
fmdecay3,
fmdecay4,
fmdecay5,
fmdecay6,
fmdecay7,
fmdecay8,
fmdec,
fmdec1,
fmdec2,
fmdec3,
fmdec4,
fmdec5,
fmdec6,
fmdec7,
fmdec8,
} = registerMultiControl('fmdecay', 8, 'fmdec');
/** /**
* Sustain level for the FM envelope: how much modulation is applied after the decay phase * Sustain level for the FM envelope: how much modulation is applied after the decay phase
* *
* A number may be added afterwards to control the sustain of the envelope of
* any of the 8 individual FMs (e.g. `fmsus7`)
*
* @name fmsustain * @name fmsustain
* @synonyms fmsus
* @param {number | Pattern} level sustain level * @param {number | Pattern} level sustain level
* @example * @example
* note("c e g b g e") * note("c e g b g e")
@@ -529,10 +630,69 @@ export const { fmdecay } = registerControl('fmdecay');
* ._scope() * ._scope()
* *
*/ */
export const { fmsustain } = registerControl('fmsustain'); export const {
// these are not really useful... skipping for now fmsustain,
export const { fmrelease } = registerControl('fmrelease'); fmsustain1,
export const { fmvelocity } = registerControl('fmvelocity'); fmsustain2,
fmsustain3,
fmsustain4,
fmsustain5,
fmsustain6,
fmsustain7,
fmsustain8,
fmsus,
fmsus1,
fmsus2,
fmsus3,
fmsus4,
fmsus5,
fmsus6,
fmsus7,
fmsus8,
} = registerMultiControl('fmsustain', 8, 'fmsus');
/**
* Release time for the FM envelope: how much modulation is applied after the note is released
*
* A number may be added afterwards to control the release of the envelope of
* any of the 8 individual FMs (e.g. `fmrel8`)
*
* @name fmrelease
* @synonyms fmrel
* @param {number | Pattern} time release time
*
*/
export const {
fmrelease,
fmrelease1,
fmrelease2,
fmrelease3,
fmrelease4,
fmrelease5,
fmrelease6,
fmrelease7,
fmrelease8,
fmrel,
fmrel1,
fmrel2,
fmrel3,
fmrel4,
fmrel5,
fmrel6,
fmrel7,
fmrel8,
} = registerMultiControl('fmrelease', 8, 'fmrel');
// FM Matrix
// Note: we do not declare top-level exports here since it would add
// ~162 more explicit exports. This is likely fine as the most common use-case would be to at least
// declare one other FM prior to utilizing the matrix functionality, but if we ever decide we need it,
// TODO to add it explicitly / go with the globalThis approach
for (let i = 0; i <= 8; i++) {
for (let j = 0; j <= 8; j++) {
registerControl(`fmi${i}${j}`, `fm${i}${j}`);
}
}
/** /**
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`. * Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
@@ -1323,14 +1483,13 @@ export const { lpdepth } = registerControl('lpdepth');
* Depth of the LFO for the lowpass filter, in HZ * Depth of the LFO for the lowpass filter, in HZ
* *
* @name lpdepthfrequency * @name lpdepthfrequency
* @synonyms * @synonyms lpdepthfreq
* lpdethfreq
* @param {number | Pattern} depth depth of modulation * @param {number | Pattern} depth depth of modulation
* @example * @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpdepthfrequency("<200 500 100 0>") * note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpdepthfrequency("<200 500 100 0>")
*/ */
export const { lpdepthfrequency } = registerControl('lpdepthfrequency', 'lpdepthfreq'); export const { lpdepthfrequency, lpdepthfreq } = registerControl('lpdepthfrequency', 'lpdepthfreq');
/** /**
* Shape of the LFO for the lowpass filter * Shape of the LFO for the lowpass filter
@@ -1384,14 +1543,13 @@ export const { bpdepth } = registerControl('bpdepth');
* Depth of the LFO for the bandpass filter, in HZ * Depth of the LFO for the bandpass filter, in HZ
* *
* @name bpdepthfrequency * @name bpdepthfrequency
* @synonyms * @synonyms bpdepthfreq
* bpdethfreq
* @param {number | Pattern} depth depth of modulation * @param {number | Pattern} depth depth of modulation
* @example * @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).bpdepthfrequency("<200 500 100 0>") * note("<c c c# c c c4>*16").s("sawtooth").lpf(600).bpdepthfrequency("<200 500 100 0>")
*/ */
export const { bpdepthfrequency } = registerControl('bpdepthfrequency', 'bpdepthfreq'); export const { bpdepthfrequency, bpdepthfreq } = registerControl('bpdepthfrequency', 'bpdepthfreq');
/** /**
* Shape of the LFO for the bandpass filter * Shape of the LFO for the bandpass filter
@@ -1439,20 +1597,19 @@ export const { hpsync } = registerControl('hpsync');
* @name hpdepth * @name hpdepth
* @param {number | Pattern} depth depth of modulation * @param {number | Pattern} depth depth of modulation
*/ */
export const { hpdepth, hpdepthfreq } = registerControl('hpdepth'); export const { hpdepth } = registerControl('hpdepth');
/** /**
* Depth of the LFO for the hipass filter, in hz * Depth of the LFO for the hipass filter, in hz
* *
* @name hpdepthfrequency * @name hpdepthfrequency
* @synonyms * @synonyms hpdepthfreq
* hpdethfreq
* @param {number | Pattern} depth depth of modulation * @param {number | Pattern} depth depth of modulation
* @example * @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).hpdepthfrequency("<200 500 100 0>") * 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 { hpdepthfrequency, hpdepthfreq } = registerControl('hpdepthfrequency', 'hpdepthfreq');
/** /**
* Shape of the LFO for the highpass filter * Shape of the LFO for the highpass filter
@@ -2629,3 +2786,17 @@ export const scrub = register(
}, },
false, false,
); );
/**
* Transient shaper. Gives independent control over the emphasis on transients
* and sustains
*
* @name transient
* @param {number | Pattern} attack Emphasis on transients; between -1 (deaccentuate) and 1 (accentuate)
* @param {number | Pattern} sustain Emphasis on the sustains; between -1 (deaccentuate) and 1 (accentuate)
* @example
* s("bd").transient("<-1 -0.5 0 0.5 1>")
* @example
* s("hh*16").bank("tr909").transient("<-1:1 1:-1>")
*/
export const { transient } = registerControl(['transient', 'transsustain']);
+23 -6
View File
@@ -574,7 +574,8 @@ export class Pattern {
* Returns a new Pattern, which only returns haps that meet the given test. * Returns a new Pattern, which only returns haps that meet the given test.
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern * @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
* @returns Pattern * @returns Pattern
* @noAutocomplete * @example
* s("bd*8").velocity(rand).filterHaps((h) => (h.whole.begin % 1) < h.value.velocity)
*/ */
filterHaps(hap_test) { filterHaps(hap_test) {
return new Pattern((state) => this.query(state).filter(hap_test)); return new Pattern((state) => this.query(state).filter(hap_test));
@@ -585,7 +586,11 @@ export class Pattern {
* inside haps. * inside haps.
* @param {Function} value_test * @param {Function} value_test
* @returns Pattern * @returns Pattern
* @noAutocomplete * @example
* const drums = s("bd sd bd sd")
* kick: drums.filterValues((v) => v.s === 'bd').duck(2)
* snare: drums.filterValues((v) => v.s === 'sd')
* bass: s("saw!4").note("G#1").lpf(80).lpenv(4).orbit(2)
*/ */
filterValues(value_test) { filterValues(value_test) {
return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))).setSteps(this._steps); return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))).setSteps(this._steps);
@@ -1587,7 +1592,13 @@ export const func = curry((a, b) => reify(b).func(a));
* *
* @param {string | string[]} name name of the function, or an array of names to be used as synonyms * @param {string | string[]} name name of the function, or an array of names to be used as synonyms
* @param {function} func function with 1 or more params, where last is the current pattern * @param {function} func function with 1 or more params, where last is the current pattern
* @noAutocomplete * @param {bool} patternify defaults to true; if set to false, you will have more control over the arguments to `func` as they will be
* in their raw form and it will be up to you to patternify them and/or query them for values
* @example
* const vlpf = register('vlpf', (freq, pat) => {
* return pat.fmap((v) => ({...v, cutoff: freq * (v.velocity ?? 1) }));
* })
* s("saw").seg(8).velocity(rand).vlpf(800)
* *
*/ */
export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) { export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) {
@@ -2676,8 +2687,13 @@ export const hsl = register('hsl', (h, s, l, pat) => {
/** /**
* Tags each Hap with an identifier. Good for filtering. The function populates Hap.context.tags (Array). * Tags each Hap with an identifier. Good for filtering. The function populates Hap.context.tags (Array).
* @name tag * @name tag
* @noAutocomplete
* @param {string} tag anything unique * @param {string} tag anything unique
* @example
* s("saw!16").note("F1")
* .lpf(tri.range(40, 80).slow(4)).lpenv(5).lpq(4).lpd(0.15)
* .when(rand.late(0.1).gte(0.5), x => x.transpose("12").tag('altered'))
* .when(rand.late(0.2).gte(0.5), x => x.s("square").tag('altered'))
* .when("<0 1>", x => x.filter((hap) => hap.hasTag('altered')))
*/ */
Pattern.prototype.tag = function (tag) { Pattern.prototype.tag = function (tag) {
return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) })); return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) }));
@@ -2688,15 +2704,16 @@ Pattern.prototype.tag = function (tag) {
* @name filter * @name filter
* @param {Function} test function to test Hap * @param {Function} test function to test Hap
* @example * @example
* s("hh!7 oh").filter(hap => hap.value.s==='hh') * s("hh!7 oh").filter(hap => hap.value.s === 'hh')
*/ */
export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test))); export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test)));
/** /**
* Filters haps by their begin time * Filters haps by their begin time
* @name filterWhen * @name filterWhen
* @noAutocomplete
* @param {Function} test function to test Hap.whole.begin * @param {Function} test function to test Hap.whole.begin
* @example
* oneCycle: s("bd*4").filterWhen((t) => t < 1)
*/ */
export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin))); export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin)));
+8
View File
@@ -6,6 +6,8 @@ This program is free software: you can redistribute it and/or modify it under th
import { Pattern, getTime, State, TimeSpan } from '@strudel/core'; import { Pattern, getTime, State, TimeSpan } from '@strudel/core';
const ANIMATE_INTERVAL = 1000 / 15; // 15 FPS
export const getDrawContext = (id = 'test-canvas', options) => { export const getDrawContext = (id = 'test-canvas', options) => {
let { contextType = '2d', pixelated = false, pixelRatio = window.devicePixelRatio } = options || {}; let { contextType = '2d', pixelated = false, pixelRatio = window.devicePixelRatio } = options || {};
let canvas = document.querySelector('#' + id); let canvas = document.querySelector('#' + id);
@@ -112,7 +114,13 @@ export class Framer {
} }
start() { start() {
const self = this; const self = this;
let lastTime = 0;
let frame = requestAnimationFrame(function updateHighlights(time) { let frame = requestAnimationFrame(function updateHighlights(time) {
if (time - lastTime < ANIMATE_INTERVAL) {
frame = requestAnimationFrame(updateHighlights);
return;
}
lastTime = time;
try { try {
self.onFrame(time); self.onFrame(time);
} catch (err) { } catch (err) {
+26 -9
View File
@@ -479,14 +479,24 @@ Pattern.prototype.midi = function (midiport, options = {}) {
let listeners = {}; let listeners = {};
const refs = {}; const refs = {};
const refsByChan = {};
/** /**
* MIDI input: Opens a MIDI input port to receive MIDI control change messages. * MIDI input: Opens a MIDI input port to receive MIDI control change messages.
*
* The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel
* @param {string | number} input MIDI device name or index defaulting to 0 * @param {string | number} input MIDI device name or index defaulting to 0
* @returns {Function} * @returns {function(number, number=): Pattern} A function from (cc, channel?) to a pattern.
* When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1)
* that came through that cc number (and channel, if provided)
* @example * @example
* let cc = await midin('IAC Driver Bus 1') * const cc = await midin('IAC Driver Bus 1')
* note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth") * note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth")
* @example
* const allCC = await midin('IAC Driver Bus 1')
* const cc = (ccNum) => allCC(ccNum, 2) // just channel 2
* note("c a f e").s("saw")
* .when(cc(0).gt(0), x => x.postgain(0))
*/ */
export async function midin(input) { export async function midin(input) {
if (isPattern(input)) { if (isPattern(input)) {
@@ -511,17 +521,24 @@ export async function midin(input) {
}`, }`,
); );
} }
// ensure refs for this input are initialized refs[input] ??= {};
if (!refs[input]) { refsByChan[input] ??= {};
refs[input] = {}; const cc = (cc, chan) => {
} if (chan !== undefined) {
const cc = (cc) => ref(() => refs[input][cc] || 0); return ref(() => refsByChan[input][cc]?.[chan] || 0);
}
return ref(() => refs[input][cc] || 0);
};
listeners[input] && device.removeListener('midimessage', listeners[input]); listeners[input] && device.removeListener('midimessage', listeners[input]);
listeners[input] = (e) => { listeners[input] = (e) => {
const cc = e.dataBytes[0]; const ccNum = e.dataBytes[0];
const v = e.dataBytes[1]; const v = e.dataBytes[1];
refs[input] && (refs[input][cc] = v / 127); const chan = e.message.channel;
const scaled = v / 127;
refsByChan[input][ccNum] ??= {};
refsByChan[input][ccNum][chan] = scaled;
refs[input][ccNum] = scaled;
}; };
device.addListener('midimessage', listeners[input]); device.addListener('midimessage', listeners[input]);
return cc; return cc;
+3 -2
View File
@@ -6,6 +6,7 @@ import {
getADSRValues, getADSRValues,
getPitchEnvelope, getPitchEnvelope,
getVibratoOscillator, getVibratoOscillator,
onceEnded,
} from '@strudel/webaudio'; } from '@strudel/webaudio';
import gm from './gm.mjs'; import gm from './gm.mjs';
@@ -170,12 +171,12 @@ export function registerSoundfonts() {
bufferSource.stop(envEnd); bufferSource.stop(envEnd);
const stop = (releaseTime) => {}; const stop = (releaseTime) => {};
bufferSource.onended = () => { onceEnded(bufferSource, () => {
bufferSource.disconnect(); bufferSource.disconnect();
vibratoOscillator?.stop(); vibratoOscillator?.stop();
node.disconnect(); node.disconnect();
onended(); onended();
}; });
return { node, stop }; return { node, stop };
}, },
{ type: 'soundfont', prebake: true, fonts }, { type: 'soundfont', prebake: true, fonts },
+137 -77
View File
@@ -1,7 +1,7 @@
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
import { getNoiseBuffer } from './noise.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
import { getNoiseBuffer } from './noise.mjs';
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
export const noises = ['pink', 'white', 'brown', 'crackle']; export const noises = ['pink', 'white', 'brown', 'crackle'];
@@ -349,25 +349,17 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
constantNode.connect(zeroGain); constantNode.connect(zeroGain);
// Schedule the `onComplete` callback to occur at `stopTime` // Schedule the `onComplete` callback to occur at `stopTime`
constantNode.onended = () => { onceEnded(constantNode, () => {
// Ensure garbage collection releaseAudioNode(zeroGain);
try { releaseAudioNode(constantNode);
zeroGain.disconnect();
} catch {
// pass
}
try {
constantNode.disconnect();
} catch {
// pass
}
onComplete(); onComplete();
}; });
constantNode.start(startTime); constantNode.start(startTime);
constantNode.stop(stopTime); constantNode.stop(stopTime);
return constantNode; return constantNode;
} }
const mod = (freq, range = 1, type = 'sine') => {
const mod = (freq, type = 'sine') => {
const ctx = getAudioContext(); const ctx = getAudioContext();
let osc; let osc;
if (noises.includes(type)) { if (noises.includes(type)) {
@@ -379,69 +371,89 @@ const mod = (freq, range = 1, type = 'sine') => {
osc.type = type; osc.type = type;
osc.frequency.value = freq; osc.frequency.value = freq;
} }
osc.start(); osc.start();
const g = gainNode(range); return osc;
osc.connect(g); // -range, range
return { node: g, stop: (t) => osc.stop(t), osc: osc };
}; };
const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => {
const fm = (frequencyparam, harmonicityRatio, wave = 'sine') => {
const carrfreq = frequencyparam.value; const carrfreq = frequencyparam.value;
const modfreq = carrfreq * harmonicityRatio; const modfreq = carrfreq * harmonicityRatio;
const modgain = modfreq * modulationIndex; return { osc: mod(modfreq, wave), freq: modfreq };
return mod(modfreq, modgain, wave);
}; };
export function applyFM(param, value, begin) { export function applyFM(param, value, begin) {
const { const ac = getAudioContext();
fmh: fmHarmonicity = 1, const toStop = []; // fm oscillators we will expose `stop` for
fmi: fmModulationIndex, const fms = {};
fmenv: fmEnvelopeType = 'exp', // Matrix
fmattack: fmAttack, for (let i = 1; i <= 8; i++) {
fmdecay: fmDecay, for (let j = 0; j <= 8; j++) {
fmsustain: fmSustain, let control;
fmrelease: fmRelease, if (i === j + 1) {
fmvelocity: fmVelocity, // Standard fm3 -> fm2 -> fm1 -> param usage
fmwave: fmWaveform = 'sine', const iS = i === 1 ? '' : i;
duration, control = `fmi${iS}`;
} = value; } else {
let modulator; control = `fmi${i}${j}`;
let stop = () => {}; }
const amt = value[control];
if (fmModulationIndex) { if (!amt) continue;
const ac = getAudioContext(); let io = [];
const envGain = ac.createGain(); for (let [isMod, idx] of [
const fmmod = fm(param, fmHarmonicity, fmModulationIndex, fmWaveform); [true, i], // source
[false, j], // target
modulator = fmmod.node; ]) {
stop = fmmod.stop; if (idx === 0) {
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) { io.push(param);
// no envelope by default continue;
modulator.connect(param); }
} else { if (!fms[idx]) {
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]); const idxS = idx === 1 ? '' : idx;
const holdEnd = begin + duration; const { osc, freq } = fm(param, value[`fmh${idxS}`] ?? 1, value[`fmwave${idxS}`] ?? 'sine');
getParamADSR( toStop.push(osc);
envGain.gain, const toCleanup = [osc]; // nodes we want to cleanup after oscillator `stop`
attack, const adsr = ['attack', 'decay', 'sustain', 'release'].map((s) => value[`fm${s}${idxS}`]);
decay, let output = osc;
sustain, if (adsr.some((v) => v !== undefined)) {
release, const envGain = ac.createGain();
0, const [attack, decay, sustain, release] = getADSRValues(adsr);
1, const holdEnd = begin + value.duration;
begin, const fmEnvelopeType = value[`fmenv${idxS}`] ?? 'exp';
holdEnd, getParamADSR(
fmEnvelopeType === 'exp' ? 'exponential' : 'linear', envGain.gain,
); attack,
modulator.connect(envGain); decay,
envGain.connect(param); sustain,
release,
0,
1,
begin,
holdEnd,
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
);
toCleanup.push(envGain);
output = osc.connect(envGain);
}
fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup };
}
const { input, output, freq, osc, toCleanup } = fms[idx];
const g = gainNode(amt * freq);
io.push(isMod ? output.connect(g) : input);
cleanupOnEnd(osc, [...toCleanup, g]);
}
if (!io[1]) {
logger(
`[superdough] control ${control} failed to connect FM ${i} to target ${j} due to missing frequency parameter (likely because fm${j} is noise)`,
'warning',
);
continue;
}
io[0].connect(io[1]);
} }
fmmod.osc.onended = () => {
envGain.disconnect();
modulator.disconnect();
fmmod.osc.disconnect();
};
} }
return { stop }; return {
stop: (t) => toStop.forEach((m) => m?.stop(t)),
};
} }
// Saturation curves // Saturation curves
@@ -554,10 +566,58 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => {
return Number(freq); return Number(freq);
}; };
export const destroyAudioWorkletNode = (node) => { // This helper should be used instead of the `node.onended = callback` pattern
if (node == null) { // It adds a mechanism to help minimize gc retention
return; export const onceEnded = (node, callback) => {
} const onended = callback;
node.disconnect(); node.onended = function cleanup() {
node.parameters.get('end')?.setValueAtTime(0, 0); onended && onended();
this.onended = null;
};
};
export const releaseAudioNode = (node) => {
if (node == null) return;
// check we received an AudioNode
if (!(node instanceof AudioNode)) {
throw new Error('releaseAudioNode can only release an AudioNode');
}
// https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect
node.disconnect();
// make sure all AudioScheduledSourceNodes are in a stopped state
// https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode
if (node instanceof AudioScheduledSourceNode) {
if (node.onended && node.onended.name !== 'cleanup') {
logger(
`[superdough] Deprecation warning: it seems your code path is setting 'node.onended = callback' instead of using the onceEnded helper`,
);
}
try {
node.stop();
} catch (e) {
// At the stage, `start` was not called on the node
// but an `onended` callback releasing resources may exist
// and we want it to fire :
// - we force a start/stop cycle so that `onended` gets called
// - we `lock` the node so that no-one can start it
node.start(node.context.currentTime + 5); // will never happen
node.stop();
}
}
// https://www.w3.org/TR/webaudio-1.1/#AudioNode-actively-processing
// An AudioWorkletNode is actively processing when its AudioWorkletProcessor's [[callable process]]
// returns true and either its active source flag is true or
// any AudioNode connected to one of its inputs is actively processing.
if (node instanceof AudioWorkletNode) {
node.parameters.get('end')?.setValueAtTime(0, 0);
}
};
// Once the `anchor` node has ended, release all nodes in `toCleanup`
export const cleanupOnEnd = (anchor, toCleanup) => {
onceEnded(anchor, () => toCleanup.forEach((n) => releaseAudioNode(n)));
}; };
+18 -15
View File
@@ -1,7 +1,14 @@
import { getBaseURL, getCommonSampleInfo } from './util.mjs'; import { getBaseURL, getCommonSampleInfo } from './util.mjs';
import { registerSound, registerWaveTable } from './index.mjs'; import { registerSound, registerWaveTable } from './index.mjs';
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import {
getADSRValues,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
onceEnded,
releaseAudioNode,
} from './helpers.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer> const bufferCache = {}; // string: Promise<ArrayBuffer>
@@ -236,12 +243,6 @@ export async function fetchSampleMap(url) {
* sd: '808sd/SD0010.WAV' * sd: '808sd/SD0010.WAV'
* }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/'); * }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
* s("[bd ~]*2, [~ hh]*2, ~ sd") * s("[bd ~]*2, [~ hh]*2, ~ sd")
* @example
* samples('shabda:noise,chimp:2')
* s("noise <chimp:0*2 chimp:1>")
* @example
* samples('shabda/speech/fr-FR/f:chocolat')
* s("chocolat*4")
*/ */
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => { export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
@@ -286,17 +287,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl); const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
// asny stuff above took too long?
if (ac.currentTime > t) {
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
// console.warn('sample still loading:', s, n);
return;
}
if (!bufferSource) { if (!bufferSource) {
logger(`[sampler] could not load "${s}:${n}"`, 'error'); logger(`[sampler] could not load "${s}:${n}"`, 'error');
return; return;
} }
// async stuff above took too long?
if (ac.currentTime > t) {
logger(`[sampler] loading sound "${s}:${n}" took too long`, 'highlight');
// AudioBufferSourceNode will never be used. discard it
releaseAudioNode(bufferSource);
return;
}
// vibrato // vibrato
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
@@ -319,13 +322,13 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
node.connect(out); node.connect(out);
bufferSource.onended = function () { onceEnded(bufferSource, function () {
bufferSource.disconnect(); bufferSource.disconnect();
vibratoOscillator?.stop(); vibratoOscillator?.stop();
node.disconnect(); node.disconnect();
out.disconnect(); out.disconnect();
onended(); onended();
}; });
let envEnd = holdEnd + release + 0.01; let envEnd = holdEnd + release + 0.01;
bufferSource.stop(envEnd); bufferSource.stop(envEnd);
const stop = (endTime) => { const stop = (endTime) => {
+24 -8
View File
@@ -290,8 +290,8 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000
const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 }); const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
//filters //filters
const numStages = 2; //num of filters in series const numStages = 1; //num of filters in series
let fOffset = 0; let fOffset = 282; //for backward compat in #1800
const filterChain = []; const filterChain = [];
for (let i = 0; i < numStages; i++) { for (let i = 0; i < numStages; i++) {
const filter = ac.createBiquadFilter(); const filter = ac.createBiquadFilter();
@@ -302,12 +302,9 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000
lfo.connect(filter.detune); lfo.connect(filter.detune);
fOffset += 282; fOffset += 282;
if (i > 0) {
filterChain[i - 1].connect(filter);
}
filterChain.push(filter); filterChain.push(filter);
} }
return { phaser: filterChain[filterChain.length - 1], lfo }; return { filterChain, lfo };
} }
function getFilterType(ftype) { function getFilterType(ftype) {
@@ -451,6 +448,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
compressorKnee, compressorKnee,
compressorAttack, compressorAttack,
compressorRelease, compressorRelease,
transient,
transsustain,
} = value; } = value;
delaytime = delaytime ?? cycleToSeconds(delaysync, cps); delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
@@ -532,6 +531,23 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
chain.push(sourceNode); chain.push(sourceNode);
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
transient !== undefined &&
chain.push(
getWorklet(
ac,
'transient-processor',
{},
{
processorOptions: {
attack: transient,
sustain: transsustain,
begin: t,
end: endWithRelease,
},
},
),
);
// gain stage // gain stage
chain.push(gainNode(gain)); chain.push(gainNode(gain));
@@ -697,9 +713,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
} }
// phaser // phaser
if (phaserrate !== undefined && phaserdepth > 0) { if (phaserrate !== undefined && phaserdepth > 0) {
const { phaser, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep); const { filterChain, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep);
audioNodes.push(lfo); audioNodes.push(lfo);
chain.push(phaser); chain.push(...filterChain);
} }
// last gain // last gain
+5 -8
View File
@@ -3,7 +3,6 @@ import { registerSound, soundMap } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { import {
applyFM, applyFM,
destroyAudioWorkletNode,
gainNode, gainNode,
getADSRValues, getADSRValues,
getFrequencyFromValue, getFrequencyFromValue,
@@ -13,6 +12,7 @@ import {
getVibratoOscillator, getVibratoOscillator,
getWorklet, getWorklet,
noises, noises,
releaseAudioNode,
webAudioTimeout, webAudioTimeout,
} from './helpers.mjs'; } from './helpers.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
@@ -192,8 +192,7 @@ export function registerSynthSounds() {
let timeoutNode = webAudioTimeout( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); releaseAudioNode(o);
envGain.disconnect();
onended(); onended();
fm?.stop(); fm?.stop();
vibratoOscillator?.stop(); vibratoOscillator?.stop();
@@ -270,8 +269,7 @@ export function registerSynthSounds() {
let timeoutNode = webAudioTimeout( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); releaseAudioNode(o);
envGain.disconnect();
onended(); onended();
}, },
begin, begin,
@@ -344,9 +342,8 @@ export function registerSynthSounds() {
let timeoutNode = webAudioTimeout( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); releaseAudioNode(o);
destroyAudioWorkletNode(lfo); releaseAudioNode(lfo);
envGain.disconnect();
onended(); onended();
fm?.stop(); fm?.stop();
vibratoOscillator?.stop(); vibratoOscillator?.stop();
+2 -3
View File
@@ -3,13 +3,13 @@ import { getBaseURL, getCommonSampleInfo } from './util.mjs';
import { import {
applyFM, applyFM,
applyParameterModulators, applyParameterModulators,
destroyAudioWorkletNode,
getADSRValues, getADSRValues,
getFrequencyFromValue, getFrequencyFromValue,
getParamADSR, getParamADSR,
getPitchEnvelope, getPitchEnvelope,
getVibratoOscillator, getVibratoOscillator,
getWorklet, getWorklet,
releaseAudioNode,
webAudioTimeout, webAudioTimeout,
} from './helpers.mjs'; } from './helpers.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
@@ -319,10 +319,9 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
const timeoutNode = webAudioTimeout( const timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(source); releaseAudioNode(source);
vibratoOscillator?.stop(); vibratoOscillator?.stop();
fm?.stop(); fm?.stop();
node.disconnect();
wtPosModulators?.disconnect(); wtPosModulators?.disconnect();
wtWarpModulators?.disconnect(); wtWarpModulators?.disconnect();
onended(); onended();
+87 -4
View File
@@ -11,6 +11,9 @@ const PI = Math.PI;
const TWO_PI = 2 * PI; const TWO_PI = 2 * PI;
const INVSR = 1 / sampleRate; const INVSR = 1 / sampleRate;
const timeToCoeff = (t) => 1 - Math.exp(-INVSR / t);
const dbToLin = (db) => Math.pow(10, db / 20);
const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const mod = (n, m) => ((n % m) + m) % m; const mod = (n, m) => ((n % m) + m) % m;
const lerp = (a, b, n) => n * (b - a) + a; const lerp = (a, b, n) => n * (b - a) + a;
@@ -508,13 +511,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
]; ];
} }
process(_input, outputs, params) { process(_input, outputs, params) {
if (currentTime <= params.begin[0]) {
return true;
}
if (currentTime >= params.end[0]) { if (currentTime >= params.end[0]) {
// this.port.postMessage({ type: 'onended' }); // should terminate
return false; return false;
} }
if (currentTime <= params.begin[0]) {
// keep alive
return true;
}
const output = outputs[0]; const output = outputs[0];
const voices = params.voices[0]; // k-rate const voices = params.voices[0]; // k-rate
for (let i = 0; i < output[0].length; i++) { for (let i = 0; i < output[0].length; i++) {
@@ -1383,3 +1387,82 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
} }
registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor); registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor);
class TransientProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [];
}
constructor(options) {
super();
this.gainCoeff = timeToCoeff(0.2);
this.avgGain = 1;
let {
attackTime = 0.003,
sustainTime = 0.08,
attack = 0,
sustain = 0,
sensitivity = 0.1,
mix = 1,
begin = 0,
end = 0,
} = options.processorOptions;
attackTime = clamp(attackTime, 0.0005, 0.05);
sustainTime = clamp(sustainTime, 0.01, 0.5);
this.attackCoeff = timeToCoeff(attackTime);
this.sustainCoeff = timeToCoeff(sustainTime);
this.attackAmt = clamp(attack, -1, 1);
this.sustainAmt = clamp(sustain, -1, 1);
this.scaling = 0.5 + 5 * clamp(sensitivity, 0, 1);
this.mix = clamp(mix, 0, 1);
this.begin = begin;
this.end = end;
this.attackEnv = new Float32Array(2); // assume stereo
this.sustainEnv = new Float32Array(2);
}
process(inputs, outputs, _params) {
const input = inputs[0];
const output = outputs[0];
if (currentTime >= this.end) {
return false;
}
if (currentTime <= this.begin) {
return true;
}
const channels = input.length;
if (channels > this.attackEnv.length) {
this.attackEnv = new Float32Array(channels);
this.sustainEnv = new Float32Array(channels);
}
let avgGain = this.avgGain;
for (let ch = 0; ch < channels; ch++) {
let attEnv = this.attackEnv[ch];
let susEnv = this.sustainEnv[ch];
for (let n = 0; n < blockSize; n++) {
const sample = input[ch][n];
const x = Math.abs(sample);
attEnv = lerp(attEnv, x, this.attackCoeff);
susEnv = lerp(susEnv, x, this.sustainCoeff);
const peakiness = clamp((this.scaling * (attEnv - susEnv)) / (susEnv + 1e-6), -1.5, 1.5);
const attScale = peakiness > 0 ? peakiness : 0;
const susScale = peakiness < 0 ? -peakiness : 0;
const attackGain = dbToLin(this.attackAmt * attScale * 18);
const sustainGain = dbToLin(this.sustainAmt * susScale * 36);
const gain = clamp(attackGain * sustainGain, 0, 8);
avgGain = lerp(avgGain, gain, this.gainCoeff);
const makeup = avgGain > 1e-3 ? 1 / avgGain : 1;
const wet = sample * gain * makeup;
let y = lerp(sample, wet, this.mix);
y /= 1 + Math.abs(y); // soft clip
output[ch][n] = y;
}
this.attackEnv[ch] = attEnv;
this.sustainEnv[ch] = susEnv;
}
this.avgGain = avgGain;
return true;
}
}
registerProcessor('transient-processor', TransientProcessor);
+4 -3
View File
@@ -3,6 +3,7 @@ import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound } from './superdough.mjs'; import { registerSound } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { buildSamples } from './zzfx_fork.mjs'; import { buildSamples } from './zzfx_fork.mjs';
import { onceEnded, releaseAudioNode } from './helpers.mjs';
export const getZZFX = (value, t) => { export const getZZFX = (value, t) => {
let { let {
@@ -83,10 +84,10 @@ export function registerZZFXSounds() {
wave, wave,
(t, value, onended) => { (t, value, onended) => {
const { node: o } = getZZFX({ s: wave, ...value }, t); const { node: o } = getZZFX({ s: wave, ...value }, t);
o.onended = () => { onceEnded(o, () => {
o.disconnect(); releaseAudioNode(o);
onended(); onended();
}; });
return { return {
node: o, node: o,
stop: () => {}, stop: () => {},
+335 -65
View File
@@ -4043,6 +4043,70 @@ exports[`runs examples > example "filter" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "filterHaps" example index 0 1`] = `
[
"[ 1/8 → 1/4 | s:bd velocity:0.6852155700325966 ]",
"[ 1/4 → 3/8 | s:bd velocity:0.36975969187915325 ]",
"[ 3/8 → 1/2 | s:bd velocity:0.40139251574873924 ]",
"[ 1/1 → 9/8 | s:bd velocity:0.5195421651005745 ]",
"[ 9/8 → 5/4 | s:bd velocity:0.6724895145744085 ]",
"[ 5/4 → 11/8 | s:bd velocity:0.7287282031029463 ]",
"[ 3/2 → 13/8 | s:bd velocity:0.6084080748260021 ]",
"[ 2/1 → 17/8 | s:bd velocity:0.9595271199941635 ]",
"[ 17/8 → 9/4 | s:bd velocity:0.9033902939409018 ]",
"[ 9/4 → 19/8 | s:bd velocity:0.34548251144587994 ]",
"[ 19/8 → 5/2 | s:bd velocity:0.8365066405385733 ]",
"[ 3/1 → 25/8 | s:bd velocity:0.21728911064565182 ]",
"[ 25/8 → 13/4 | s:bd velocity:0.34324387833476067 ]",
"[ 13/4 → 27/8 | s:bd velocity:0.9923650715500116 ]",
"[ 27/8 → 7/2 | s:bd velocity:0.40869180113077164 ]",
"[ 29/8 → 15/4 | s:bd velocity:0.9391485787928104 ]",
"[ 15/4 → 31/8 | s:bd velocity:0.8121673222631216 ]",
]
`;
exports[`runs examples > example "filterValues" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 1/4 → 1/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 1/2 → 3/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 3/4 → 1/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 1/1 → 5/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 5/4 → 3/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 3/2 → 7/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 7/4 → 2/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 2/1 → 9/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 9/4 → 5/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 5/2 → 11/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 11/4 → 3/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 3/1 → 13/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 13/4 → 7/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 7/2 → 15/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 15/4 → 4/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
]
`;
exports[`runs examples > example "filterWhen" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
"[ 1/4 → 1/2 | s:bd ]",
"[ 1/2 → 3/4 | s:bd ]",
"[ 3/4 → 1/1 | s:bd ]",
"[ 1/1 → 5/4 | s:bd ]",
"[ 5/4 → 3/2 | s:bd ]",
"[ 3/2 → 7/4 | s:bd ]",
"[ 7/4 → 2/1 | s:bd ]",
"[ 2/1 → 9/4 | s:bd ]",
"[ 9/4 → 5/2 | s:bd ]",
"[ 5/2 → 11/4 | s:bd ]",
"[ 11/4 → 3/1 | s:bd ]",
"[ 3/1 → 13/4 | s:bd ]",
"[ 13/4 → 7/2 | s:bd ]",
"[ 7/2 → 15/4 | s:bd ]",
"[ 15/4 → 4/1 | s:bd ]",
]
`;
exports[`runs examples > example "firstOf" example index 0 1`] = ` exports[`runs examples > example "firstOf" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:g3 ]", "[ 0/1 → 1/4 | note:g3 ]",
@@ -4092,35 +4156,6 @@ exports[`runs examples > example "floor" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "fm" example index 0 1`] = `
[
"[ 0/1 → 1/6 | note:c fmi:0 ]",
"[ 1/6 → 1/3 | note:e fmi:0 ]",
"[ 1/3 → 1/2 | note:g fmi:0 ]",
"[ 1/2 → 2/3 | note:b fmi:0 ]",
"[ 2/3 → 5/6 | note:g fmi:0 ]",
"[ 5/6 → 1/1 | note:e fmi:0 ]",
"[ 1/1 → 7/6 | note:c fmi:1 ]",
"[ 7/6 → 4/3 | note:e fmi:1 ]",
"[ 4/3 → 3/2 | note:g fmi:1 ]",
"[ 3/2 → 5/3 | note:b fmi:1 ]",
"[ 5/3 → 11/6 | note:g fmi:1 ]",
"[ 11/6 → 2/1 | note:e fmi:1 ]",
"[ 2/1 → 13/6 | note:c fmi:2 ]",
"[ 13/6 → 7/3 | note:e fmi:2 ]",
"[ 7/3 → 5/2 | note:g fmi:2 ]",
"[ 5/2 → 8/3 | note:b fmi:2 ]",
"[ 8/3 → 17/6 | note:g fmi:2 ]",
"[ 17/6 → 3/1 | note:e fmi:2 ]",
"[ 3/1 → 19/6 | note:c fmi:8 ]",
"[ 19/6 → 10/3 | note:e fmi:8 ]",
"[ 10/3 → 7/2 | note:g fmi:8 ]",
"[ 7/2 → 11/3 | note:b fmi:8 ]",
"[ 11/3 → 23/6 | note:g fmi:8 ]",
"[ 23/6 → 4/1 | note:e fmi:8 ]",
]
`;
exports[`runs examples > example "fmattack" example index 0 1`] = ` exports[`runs examples > example "fmattack" example index 0 1`] = `
[ [
"[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 ]", "[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 ]",
@@ -4237,6 +4272,72 @@ exports[`runs examples > example "fmh" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "fmi" example index 0 1`] = `
[
"[ 0/1 → 1/6 | note:c fmi:0 ]",
"[ 1/6 → 1/3 | note:e fmi:0 ]",
"[ 1/3 → 1/2 | note:g fmi:0 ]",
"[ 1/2 → 2/3 | note:b fmi:0 ]",
"[ 2/3 → 5/6 | note:g fmi:0 ]",
"[ 5/6 → 1/1 | note:e fmi:0 ]",
"[ 1/1 → 7/6 | note:c fmi:1 ]",
"[ 7/6 → 4/3 | note:e fmi:1 ]",
"[ 4/3 → 3/2 | note:g fmi:1 ]",
"[ 3/2 → 5/3 | note:b fmi:1 ]",
"[ 5/3 → 11/6 | note:g fmi:1 ]",
"[ 11/6 → 2/1 | note:e fmi:1 ]",
"[ 2/1 → 13/6 | note:c fmi:2 ]",
"[ 13/6 → 7/3 | note:e fmi:2 ]",
"[ 7/3 → 5/2 | note:g fmi:2 ]",
"[ 5/2 → 8/3 | note:b fmi:2 ]",
"[ 8/3 → 17/6 | note:g fmi:2 ]",
"[ 17/6 → 3/1 | note:e fmi:2 ]",
"[ 3/1 → 19/6 | note:c fmi:8 ]",
"[ 19/6 → 10/3 | note:e fmi:8 ]",
"[ 10/3 → 7/2 | note:g fmi:8 ]",
"[ 7/2 → 11/3 | note:b fmi:8 ]",
"[ 11/3 → 23/6 | note:g fmi:8 ]",
"[ 23/6 → 4/1 | note:e fmi:8 ]",
]
`;
exports[`runs examples > example "fmi" example index 1 1`] = `
[
"[ 0/1 → 1/8 | s:sine note:F1 fmi:4 fmi2:0 fmi3:0 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 1/8 → 1/4 | s:sine note:F1 fmi:4 fmi2:2.7408622801303864 fmi3:0.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 1/4 → 3/8 | s:sine note:F1 fmi:4 fmi2:1.479038767516613 fmi3:0.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 3/8 → 1/2 | s:sine note:F1 fmi:4 fmi2:1.605570062994957 fmi3:0.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 1/2 → 5/8 | s:sine note:F1 fmi:4 fmi2:1.041922464966774 fmi3:0.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 5/8 → 3/4 | s:sine note:F1 fmi:4 fmi2:0.5425434708595276 fmi3:0.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 3/4 → 7/8 | s:sine note:F1 fmi:4 fmi2:0.7833059206604958 fmi3:0.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 7/8 → 1/1 | s:sine note:F1 fmi:4 fmi2:1.590524323284626 fmi3:0.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 1/1 → 9/8 | s:sine note:F1 fmi:4 fmi2:2.078168660402298 fmi3:1 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 9/8 → 5/4 | s:sine note:F1 fmi:4 fmi2:2.689958058297634 fmi3:1.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 5/4 → 11/8 | s:sine note:F1 fmi:4 fmi2:2.914912812411785 fmi3:1.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 11/8 → 3/2 | s:sine note:F1 fmi:4 fmi2:0.7312150076031685 fmi3:1.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 3/2 → 13/8 | s:sine note:F1 fmi:4 fmi2:2.4336322993040085 fmi3:1.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 13/8 → 7/4 | s:sine note:F1 fmi:4 fmi2:1.987018845975399 fmi3:1.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 7/4 → 15/8 | s:sine note:F1 fmi:4 fmi2:0.8189515843987465 fmi3:1.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 15/8 → 2/1 | s:sine note:F1 fmi:4 fmi2:2.3782287165522575 fmi3:1.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 2/1 → 17/8 | s:sine note:F1 fmi:4 fmi2:3.838108479976654 fmi3:2 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 17/8 → 9/4 | s:sine note:F1 fmi:4 fmi2:3.613561175763607 fmi3:2.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 9/4 → 19/8 | s:sine note:F1 fmi:4 fmi2:1.3819300457835197 fmi3:2.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 19/8 → 5/2 | s:sine note:F1 fmi:4 fmi2:3.346026562154293 fmi3:2.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 5/2 → 21/8 | s:sine note:F1 fmi:4 fmi2:1.8344642966985703 fmi3:2.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 21/8 → 11/4 | s:sine note:F1 fmi:4 fmi2:0.6407739371061325 fmi3:2.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 11/4 → 23/8 | s:sine note:F1 fmi:4 fmi2:2.5329310819506645 fmi3:2.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 23/8 → 3/1 | s:sine note:F1 fmi:4 fmi2:0.06501668691635132 fmi3:2.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 3/1 → 25/8 | s:sine note:F1 fmi:4 fmi2:0.8691564425826073 fmi3:3 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 25/8 → 13/4 | s:sine note:F1 fmi:4 fmi2:1.3729755133390427 fmi3:3.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 13/4 → 27/8 | s:sine note:F1 fmi:4 fmi2:3.9694602862000465 fmi3:3.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 27/8 → 7/2 | s:sine note:F1 fmi:4 fmi2:1.6347672045230865 fmi3:3.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 7/2 → 29/8 | s:sine note:F1 fmi:4 fmi2:1.639795258641243 fmi3:3.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 29/8 → 15/4 | s:sine note:F1 fmi:4 fmi2:3.7565943151712418 fmi3:3.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 15/4 → 31/8 | s:sine note:F1 fmi:4 fmi2:3.2486692890524864 fmi3:3.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
"[ 31/8 → 4/1 | s:sine note:F1 fmi:4 fmi2:2.9446661546826363 fmi3:3.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
]
`;
exports[`runs examples > example "fmsustain" example index 0 1`] = ` exports[`runs examples > example "fmsustain" example index 0 1`] = `
[ [
"[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 ]", "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 ]",
@@ -6947,6 +7048,27 @@ exports[`runs examples > example "midin" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "midin" example index 1 1`] = `
[
"[ 0/1 → 1/4 | note:c s:saw ]",
"[ 1/4 → 1/2 | note:a s:saw ]",
"[ 1/2 → 3/4 | note:f s:saw ]",
"[ 3/4 → 1/1 | note:e s:saw ]",
"[ 1/1 → 5/4 | note:c s:saw ]",
"[ 5/4 → 3/2 | note:a s:saw ]",
"[ 3/2 → 7/4 | note:f s:saw ]",
"[ 7/4 → 2/1 | note:e s:saw ]",
"[ 2/1 → 9/4 | note:c s:saw ]",
"[ 9/4 → 5/2 | note:a s:saw ]",
"[ 5/2 → 11/4 | note:f s:saw ]",
"[ 11/4 → 3/1 | note:e s:saw ]",
"[ 3/1 → 13/4 | note:c s:saw ]",
"[ 13/4 → 7/2 | note:a s:saw ]",
"[ 7/2 → 15/4 | note:f s:saw ]",
"[ 15/4 → 4/1 | note:e s:saw ]",
]
`;
exports[`runs examples > example "midiport" example index 0 1`] = ` exports[`runs examples > example "midiport" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:c midiport:0 ]", "[ 0/1 → 1/4 | note:c midiport:0 ]",
@@ -9064,6 +9186,43 @@ exports[`runs examples > example "ratio" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "register" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:saw velocity:0 cutoff:0 ]",
"[ 1/8 → 1/4 | s:saw velocity:0.6852155700325966 cutoff:548.1724560260773 ]",
"[ 1/4 → 3/8 | s:saw velocity:0.36975969187915325 cutoff:295.8077535033226 ]",
"[ 3/8 → 1/2 | s:saw velocity:0.40139251574873924 cutoff:321.1140125989914 ]",
"[ 1/2 → 5/8 | s:saw velocity:0.2604806162416935 cutoff:208.3844929933548 ]",
"[ 5/8 → 3/4 | s:saw velocity:0.1356358677148819 cutoff:108.50869417190552 ]",
"[ 3/4 → 7/8 | s:saw velocity:0.19582648016512394 cutoff:156.66118413209915 ]",
"[ 7/8 → 1/1 | s:saw velocity:0.3976310808211565 cutoff:318.1048646569252 ]",
"[ 1/1 → 9/8 | s:saw velocity:0.5195421651005745 cutoff:415.6337320804596 ]",
"[ 9/8 → 5/4 | s:saw velocity:0.6724895145744085 cutoff:537.9916116595268 ]",
"[ 5/4 → 11/8 | s:saw velocity:0.7287282031029463 cutoff:582.982562482357 ]",
"[ 11/8 → 3/2 | s:saw velocity:0.18280375190079212 cutoff:146.2430015206337 ]",
"[ 3/2 → 13/8 | s:saw velocity:0.6084080748260021 cutoff:486.7264598608017 ]",
"[ 13/8 → 7/4 | s:saw velocity:0.49675471149384975 cutoff:397.4037691950798 ]",
"[ 7/4 → 15/8 | s:saw velocity:0.20473789609968662 cutoff:163.7903168797493 ]",
"[ 15/8 → 2/1 | s:saw velocity:0.5945571791380644 cutoff:475.6457433104515 ]",
"[ 2/1 → 17/8 | s:saw velocity:0.9595271199941635 cutoff:767.6216959953308 ]",
"[ 17/8 → 9/4 | s:saw velocity:0.9033902939409018 cutoff:722.7122351527214 ]",
"[ 9/4 → 19/8 | s:saw velocity:0.34548251144587994 cutoff:276.38600915670395 ]",
"[ 19/8 → 5/2 | s:saw velocity:0.8365066405385733 cutoff:669.2053124308586 ]",
"[ 5/2 → 21/8 | s:saw velocity:0.45861607417464256 cutoff:366.89285933971405 ]",
"[ 21/8 → 11/4 | s:saw velocity:0.16019348427653313 cutoff:128.1547874212265 ]",
"[ 11/4 → 23/8 | s:saw velocity:0.6332327704876661 cutoff:506.5862163901329 ]",
"[ 23/8 → 3/1 | s:saw velocity:0.01625417172908783 cutoff:13.003337383270264 ]",
"[ 3/1 → 25/8 | s:saw velocity:0.21728911064565182 cutoff:173.83128851652145 ]",
"[ 25/8 → 13/4 | s:saw velocity:0.34324387833476067 cutoff:274.59510266780853 ]",
"[ 13/4 → 27/8 | s:saw velocity:0.9923650715500116 cutoff:793.8920572400093 ]",
"[ 27/8 → 7/2 | s:saw velocity:0.40869180113077164 cutoff:326.9534409046173 ]",
"[ 7/2 → 29/8 | s:saw velocity:0.40994881466031075 cutoff:327.9590517282486 ]",
"[ 29/8 → 15/4 | s:saw velocity:0.9391485787928104 cutoff:751.3188630342484 ]",
"[ 15/4 → 31/8 | s:saw velocity:0.8121673222631216 cutoff:649.7338578104973 ]",
"[ 31/8 → 4/1 | s:saw velocity:0.7361665386706591 cutoff:588.9332309365273 ]",
]
`;
exports[`runs examples > example "release" example index 0 1`] = ` exports[`runs examples > example "release" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:c3 release:0 ]", "[ 0/1 → 1/4 | note:c3 release:0 ]",
@@ -9685,42 +9844,6 @@ exports[`runs examples > example "samples" example index 1 1`] = `
] ]
`; `;
exports[`runs examples > example "samples" example index 2 1`] = `
[
"[ 0/1 → 1/2 | s:noise ]",
"[ 1/2 → 3/4 | s:chimp n:0 ]",
"[ 3/4 → 1/1 | s:chimp n:0 ]",
"[ 1/1 → 3/2 | s:noise ]",
"[ 3/2 → 2/1 | s:chimp n:1 ]",
"[ 2/1 → 5/2 | s:noise ]",
"[ 5/2 → 11/4 | s:chimp n:0 ]",
"[ 11/4 → 3/1 | s:chimp n:0 ]",
"[ 3/1 → 7/2 | s:noise ]",
"[ 7/2 → 4/1 | s:chimp n:1 ]",
]
`;
exports[`runs examples > example "samples" example index 3 1`] = `
[
"[ 0/1 → 1/4 | s:chocolat ]",
"[ 1/4 → 1/2 | s:chocolat ]",
"[ 1/2 → 3/4 | s:chocolat ]",
"[ 3/4 → 1/1 | s:chocolat ]",
"[ 1/1 → 5/4 | s:chocolat ]",
"[ 5/4 → 3/2 | s:chocolat ]",
"[ 3/2 → 7/4 | s:chocolat ]",
"[ 7/4 → 2/1 | s:chocolat ]",
"[ 2/1 → 9/4 | s:chocolat ]",
"[ 9/4 → 5/2 | s:chocolat ]",
"[ 5/2 → 11/4 | s:chocolat ]",
"[ 11/4 → 3/1 | s:chocolat ]",
"[ 3/1 → 13/4 | s:chocolat ]",
"[ 13/4 → 7/2 | s:chocolat ]",
"[ 7/2 → 15/4 | s:chocolat ]",
"[ 15/4 → 4/1 | s:chocolat ]",
]
`;
exports[`runs examples > example "saw" example index 0 1`] = ` exports[`runs examples > example "saw" example index 0 1`] = `
[ [
"[ 0/1 → 1/8 | note:c3 clip:0 ]", "[ 0/1 → 1/8 | note:c3 clip:0 ]",
@@ -11725,6 +11848,75 @@ exports[`runs examples > example "sysexid" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "tag" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:square note:F1 cutoff:40 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 1/16 → 1/8 | s:square note:F1 cutoff:41.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 1/8 → 3/16 | s:square note:F1 cutoff:42.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 3/16 → 1/4 | s:square note:F1 cutoff:43.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 1/4 → 5/16 | s:square note:F1 cutoff:45 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 5/16 → 3/8 | s:square note:F1 cutoff:46.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 3/8 → 7/16 | s:square note:F1 cutoff:47.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 7/16 → 1/2 | s:square note:F1 cutoff:48.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 1/2 → 9/16 | s:square note:F1 cutoff:50 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 9/16 → 5/8 | s:square note:F1 cutoff:51.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 5/8 → 11/16 | s:square note:F1 cutoff:52.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 11/16 → 3/4 | s:square note:F1 cutoff:53.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 3/4 → 13/16 | s:square note:F1 cutoff:55 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 13/16 → 7/8 | s:square note:F1 cutoff:56.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 7/8 → 15/16 | s:square note:F1 cutoff:57.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 15/16 → 1/1 | s:square note:F1 cutoff:58.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 1/1 → 17/16 | s:saw note:F2 cutoff:60 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 17/16 → 9/8 | s:saw note:F2 cutoff:61.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 9/8 → 19/16 | s:saw note:F2 cutoff:62.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 19/16 → 5/4 | s:saw note:F2 cutoff:63.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 5/4 → 21/16 | s:saw note:F2 cutoff:65 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 21/16 → 11/8 | s:saw note:F2 cutoff:66.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 11/8 → 23/16 | s:saw note:F2 cutoff:67.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 23/16 → 3/2 | s:saw note:F2 cutoff:68.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 3/2 → 25/16 | s:saw note:F2 cutoff:70 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 25/16 → 13/8 | s:saw note:F2 cutoff:71.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 13/8 → 27/16 | s:saw note:F2 cutoff:72.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 27/16 → 7/4 | s:saw note:F2 cutoff:73.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 7/4 → 29/16 | s:saw note:F2 cutoff:75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 29/16 → 15/8 | s:saw note:F2 cutoff:76.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 15/8 → 31/16 | s:saw note:F2 cutoff:77.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 31/16 → 2/1 | s:saw note:F2 cutoff:78.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 2/1 → 33/16 | s:saw note:F1 cutoff:80 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 33/16 → 17/8 | s:saw note:F1 cutoff:78.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 17/8 → 35/16 | s:saw note:F1 cutoff:77.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 35/16 → 9/4 | s:saw note:F1 cutoff:76.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 9/4 → 37/16 | s:saw note:F1 cutoff:75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 37/16 → 19/8 | s:saw note:F1 cutoff:73.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 19/8 → 39/16 | s:saw note:F1 cutoff:72.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 39/16 → 5/2 | s:saw note:F1 cutoff:71.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 5/2 → 41/16 | s:saw note:F1 cutoff:70 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 41/16 → 21/8 | s:saw note:F1 cutoff:68.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 21/8 → 43/16 | s:saw note:F1 cutoff:67.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 43/16 → 11/4 | s:saw note:F1 cutoff:66.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 11/4 → 45/16 | s:saw note:F1 cutoff:65 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 45/16 → 23/8 | s:saw note:F1 cutoff:63.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 23/8 → 47/16 | s:saw note:F1 cutoff:62.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 47/16 → 3/1 | s:saw note:F1 cutoff:61.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 3/1 → 49/16 | s:square note:F1 cutoff:60 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 49/16 → 25/8 | s:square note:F1 cutoff:58.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 25/8 → 51/16 | s:square note:F1 cutoff:57.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 51/16 → 13/4 | s:square note:F1 cutoff:56.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 13/4 → 53/16 | s:square note:F1 cutoff:55 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 53/16 → 27/8 | s:square note:F1 cutoff:53.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 27/8 → 55/16 | s:square note:F1 cutoff:52.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 55/16 → 7/2 | s:square note:F1 cutoff:51.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 7/2 → 57/16 | s:square note:F1 cutoff:50 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 57/16 → 29/8 | s:square note:F1 cutoff:48.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 29/8 → 59/16 | s:square note:F1 cutoff:47.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 59/16 → 15/4 | s:square note:F1 cutoff:46.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 15/4 → 61/16 | s:square note:F1 cutoff:45 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 61/16 → 31/8 | s:square note:F1 cutoff:43.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 31/8 → 63/16 | s:square note:F1 cutoff:42.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 63/16 → 4/1 | s:square note:F1 cutoff:41.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
]
`;
exports[`runs examples > example "take" example index 0 1`] = ` exports[`runs examples > example "take" example index 0 1`] = `
[ [
"[ 0/1 → 1/2 | s:bd ]", "[ 0/1 → 1/2 | s:bd ]",
@@ -11837,6 +12029,84 @@ exports[`runs examples > example "tour" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "transient" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:bd transient:-1 ]",
"[ 1/1 → 2/1 | s:bd transient:-0.5 ]",
"[ 2/1 → 3/1 | s:bd transient:0 ]",
"[ 3/1 → 4/1 | s:bd transient:0.5 ]",
]
`;
exports[`runs examples > example "transient" example index 1 1`] = `
[
"[ 0/1 → 1/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 1/16 → 1/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 1/8 → 3/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 3/16 → 1/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 1/4 → 5/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 5/16 → 3/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 3/8 → 7/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 7/16 → 1/2 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 1/2 → 9/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 9/16 → 5/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 5/8 → 11/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 11/16 → 3/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 3/4 → 13/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 13/16 → 7/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 7/8 → 15/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 15/16 → 1/1 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 1/1 → 17/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 17/16 → 9/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 9/8 → 19/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 19/16 → 5/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 5/4 → 21/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 21/16 → 11/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 11/8 → 23/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 23/16 → 3/2 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 3/2 → 25/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 25/16 → 13/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 13/8 → 27/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 27/16 → 7/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 7/4 → 29/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 29/16 → 15/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 15/8 → 31/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 31/16 → 2/1 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 2/1 → 33/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 33/16 → 17/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 17/8 → 35/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 35/16 → 9/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 9/4 → 37/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 37/16 → 19/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 19/8 → 39/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 39/16 → 5/2 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 5/2 → 41/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 41/16 → 21/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 21/8 → 43/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 43/16 → 11/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 11/4 → 45/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 45/16 → 23/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 23/8 → 47/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 47/16 → 3/1 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 3/1 → 49/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 49/16 → 25/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 25/8 → 51/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 51/16 → 13/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 13/4 → 53/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 53/16 → 27/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 27/8 → 55/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 55/16 → 7/2 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 7/2 → 57/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 57/16 → 29/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 29/8 → 59/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 59/16 → 15/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 15/4 → 61/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 61/16 → 31/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 31/8 → 63/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 63/16 → 4/1 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
]
`;
exports[`runs examples > example "transpose" example index 0 1`] = ` exports[`runs examples > example "transpose" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:C2 ]", "[ 0/1 → 1/4 | note:C2 ]",