Compare commits

..

1 Commits

Author SHA1 Message Date
Alex McLean a481a86c3b not quite.. 2025-07-15 17:38:50 +01:00
81 changed files with 361 additions and 3380 deletions
+2 -2
View File
@@ -3,6 +3,8 @@
Live coding patterns on the web
https://strudel.cc/
Development is moving to https://codeberg.org/uzu/strudel
- Try it here: <https://strudel.cc>
- Docs: <https://strudel.cc/learn>
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
@@ -45,5 +47,3 @@ There is a #strudel channel on the TidalCycles discord: <https://discord.com/inv
You can also ask questions and find related discussions on the tidal club forum: <https://club.tidalcycles.org/>
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
We also have a mastodon account: <a rel="me" href="https://social.toplap.org/@strudel">social.toplap.org/@strudel</a>
-11
View File
@@ -42,7 +42,6 @@ export default [
'**/hydra.mjs',
'**/jsdoc-synonyms.js',
'packages/hs2js/src/hs2js.mjs',
'packages/supradough/dough-export.mjs',
'**/samples',
],
},
@@ -84,14 +83,4 @@ export default [
],
},
},
{
// Properties provided by AudioWorkletGlobalScope
files: ['packages/superdough/worklets.mjs'],
languageOptions: {
globals: {
currentTime: 'readonly',
sampleRate: 'readonly',
},
},
},
];
+1 -1
View File
@@ -1,5 +1,5 @@
/*
jsdoc-synonyms.js - Add support for @synonyms tag
jsdoc-synonyms.js - Add support for @synonym tag
Copyright (C) 2023 Strudel contributors - see <https://codeberg.org/uzu/strudel/activity/contributors>
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/>.
*/
+52 -69
View File
@@ -1,91 +1,68 @@
import jsdoc from '../../doc.json';
// import { javascriptLanguage } from '@codemirror/lang-javascript';
import { autocompletion } from '@codemirror/autocomplete';
import { h } from './html';
const escapeHtml = (str) => {
function plaintext(str) {
const div = document.createElement('div');
div.innerText = str;
return div.innerHTML;
};
}
const stripHtml = (html) => {
const div = document.createElement('div');
const getDocLabel = (doc) => doc.name || doc.longname;
const getInnerText = (html) => {
var div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
};
const getDocLabel = (doc) => doc.name || doc.longname;
const buildParamsList = (params) =>
params?.length
? `
<div class="autocomplete-info-params-section">
<h4 class="autocomplete-info-section-title">Parameters</h4>
<ul class="autocomplete-info-params-list">
${params
.map(
({ name, type, description }) => `
<li class="autocomplete-info-param-item">
<span class="autocomplete-info-param-name">${name}</span>
<span class="autocomplete-info-param-type">${type.names?.join(' | ')}</span>
${description ? `<div class="autocomplete-info-param-desc">${stripHtml(description)}</div>` : ''}
</li>
`,
)
.join('')}
</ul>
</div>
`
: '';
const buildExamples = (examples) =>
examples?.length
? `
<div class="autocomplete-info-examples-section">
<h4 class="autocomplete-info-section-title">Examples</h4>
${examples
.map(
(example) => `
<pre class="autocomplete-info-example-code">${escapeHtml(example)}</pre>
`,
)
.join('')}
</div>
`
: '';
export const Autocomplete = ({ doc, label }) =>
h`
<div class="autocomplete-info-tooltip">
<h3 class="autocomplete-info-function-name">${label || getDocLabel(doc)}</h3>
${doc.description ? `<p class="autocomplete-info-function-description">${doc.description}</p>` : ''}
${buildParamsList(doc.params)}
${buildExamples(doc.examples)}
</div>
`[0];
const isValidDoc = (doc) => {
const label = getDocLabel(doc);
return label && !label.startsWith('_') && !['package'].includes(doc.kind);
};
const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
export function Autocomplete({ doc, label }) {
return h`<div class="prose dark:prose-invert max-h-[400px] overflow-auto p-2">
<h1 class="pt-0 mt-0">${label || getDocLabel(doc)}</h1>
${doc.description}
<ul>
${doc.params?.map(
({ name, type, description }) =>
`<li>${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}</li>`,
)}
</ul>
<div>
${doc.examples?.map((example) => `<div><pre>${plaintext(example)}</pre></div>`)}
</div>
</div>`[0];
/*
<pre
className="cursor-pointer"
onMouseDown={(e) => {
console.log('ola!');
navigator.clipboard.writeText(example);
e.stopPropagation();
}}
>
{example}
</pre>
*/
}
const jsdocCompletions = jsdoc.docs
.filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc))
.filter(
(doc) =>
getDocLabel(doc) &&
!getDocLabel(doc).startsWith('_') &&
!['package'].includes(doc.kind) &&
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
)
// https://codemirror.net/docs/ref/#autocomplete.Completion
.map((doc) => ({
.map((doc) /*: Completion */ => ({
label: getDocLabel(doc),
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
info: () => Autocomplete({ doc }),
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
}));
export const strudelAutocomplete = (context) => {
const word = context.matchBefore(/\w*/);
if (word.from === word.to && !context.explicit) return null;
export const strudelAutocomplete = (context /* : CompletionContext */) => {
let word = context.matchBefore(/\w*/);
if (word.from == word.to && !context.explicit) return null;
return {
from: word.from,
options: jsdocCompletions,
@@ -97,5 +74,11 @@ export const strudelAutocomplete = (context) => {
};
};
export const isAutoCompletionEnabled = (enabled) =>
enabled ? [autocompletion({ override: [strudelAutocomplete] })] : [];
export function isAutoCompletionEnabled(on) {
return on
? [
autocompletion({ override: [strudelAutocomplete] }),
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
]
: []; // autocompletion({ override: [] })
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "1.2.3",
"version": "1.2.2",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
+6 -6
View File
@@ -1,11 +1,11 @@
import { describe, bench } from 'vitest';
import { calculateSteps, sequence, stack } from '../index.mjs';
import { calculateTactus, sequence, stack } from '../index.mjs';
const pat64 = sequence(...Array(64).keys());
describe('steps', () => {
calculateSteps(true);
calculateTactus(true);
bench(
'+tactus',
() => {
@@ -14,7 +14,7 @@ describe('steps', () => {
{ time: 1000 },
);
calculateSteps(false);
calculateTactus(false);
bench(
'-tactus',
() => {
@@ -25,7 +25,7 @@ describe('steps', () => {
});
describe('stack', () => {
calculateSteps(true);
calculateTactus(true);
bench(
'+tactus',
() => {
@@ -34,7 +34,7 @@ describe('stack', () => {
{ time: 1000 },
);
calculateSteps(false);
calculateTactus(false);
bench(
'-tactus',
() => {
@@ -43,4 +43,4 @@ describe('stack', () => {
{ time: 1000 },
);
});
calculateSteps(true);
calculateTactus(true);
+16 -150
View File
@@ -252,20 +252,6 @@ export const { fmenv } = registerControl('fmenv');
*
*/
export const { fmattack } = registerControl('fmattack');
/**
* waveform of the fm modulator
*
* @name fmwave
* @param {number | Pattern} wave waveform
* @example
* n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("<sine square sawtooth crackle>").fm(4).fmh(2.01)
* @example
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6)
*
*/
export const { fmwave } = registerControl('fmwave');
/**
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
*
@@ -309,17 +295,6 @@ export const { fmvelocity } = registerControl('fmvelocity');
*/
export const { bank } = registerControl('bank');
/**
* mix control for the chorus effect
*
* @name chorus
* @param {string | Pattern} chorus mix amount between 0 and 1
* @example
* note("d d a# a").s("sawtooth").chorus(.5)
*
*/
export const { chorus } = registerControl('chorus');
// analyser node send amount 0 - 1 (used by scope)
export const { analyze } = registerControl('analyze');
// fftSize of analyser
@@ -331,7 +306,6 @@ export const { fft } = registerControl('fft');
*
* @name decay
* @param {number | Pattern} time decay time in seconds
* @synonyms dec
* @example
* note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0)
*
@@ -470,78 +444,6 @@ export const { crush } = registerControl('crush');
*/
export const { coarse } = registerControl('coarse');
/**
* modulate the amplitude of a sound with a continuous waveform
*
* @name tremolo
* @synonyms trem
* @param {number | Pattern} speed modulation speed in HZ
* @example
* note("d d d# d".fast(4)).s("supersaw").tremolo("<3 2 100> ").tremoloskew("<.5>")
*
*/
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
/**
* modulate the amplitude of a sound with a continuous waveform
*
* @name tremolosync
* @synonyms tremsync
* @param {number | Pattern} cycles modulation speed in cycles
* @example
* note("d d d# d".fast(4)).s("supersaw").tremolosync("4").tremoloskew("<1 .5 0>")
*
*/
export const { tremolosync } = registerControl(
['tremolosync', 'tremolodepth', 'tremoloskew', 'tremolophase'],
'tremsync',
);
/**
* depth of amplitude modulation
*
* @name tremolodepth
* @synonyms tremdepth
* @param {number | Pattern} depth
* @example
* note("a1 a1 a#1 a1".fast(4)).s("pulse").tremsync(4).tremolodepth("<1 2 .7>")
*
*/
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
/**
* alter the shape of the modulation waveform
*
* @name tremoloskew
* @synonyms tremskew
* @param {number | Pattern} amount between 0 & 1, the shape of the waveform
* @example
* note("{f a c e}%16").s("sawtooth").tremsync(4).tremoloskew("<.5 0 1>")
*
*/
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
/**
* alter the phase of the modulation waveform
*
* @name tremolophase
* @synonyms tremphase
* @param {number | Pattern} offset the offset in cycles of the modulation
* @example
* note("{f a c e}%16").s("sawtooth").tremsync(4).tremolophase("<0 .25 .66>")
*
*/
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
/**
* shape of amplitude modulation
*
* @name tremoloshape
* @param {number | Pattern} shape tri | square | sine | saw | ramp
* @example
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
*
*/
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
/**
* filter overdrive for supported filter types
*
@@ -551,42 +453,6 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
*
*/
/**
* modulate the amplitude of an orbit to create a "sidechain" like effect
*
* @name duckorbit
* @param {number | Pattern} orbit target orbit
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)
*
*/
export const { duck } = registerControl('duckorbit', 'duck');
/**
* the amount of ducking applied to target orbit
*
* @name duckdepth
* @param {number | Pattern} depth depth of modulation from 0 to 1
* @example
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>"))
*
*/
export const { duckdepth } = registerControl('duckdepth');
/**
* the attack time of the duck effect
*
* @name duckattack
* @param {number | Pattern} time
* @example
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1))
*
*/
export const { duckattack } = registerControl('duckattack', 'duckatt');
export const { drive } = registerControl('drive');
/**
@@ -1108,27 +974,14 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
*
*/
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
/**
* Sets the level of the signal that is fed back into the delay.
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>")
*
*/
export const { delayspeed } = registerControl('delayspeed');
/**
* Sets the time of the delay effect.
*
* @name delayspeed
* @param {number | Pattern} delayspeed controls the pitch of the delay feedback
* @name delaytime
* @param {number | Pattern} seconds between 0 and Infinity
* @synonyms delayt, dt
* @example
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
* s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")
*
*/
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
@@ -1708,6 +1561,18 @@ export const { density } = registerControl('density');
// ['modwheel'],
export const { expression } = registerControl('expression');
export const { sustainpedal } = registerControl('sustainpedal');
/* // TODO: doesn't seem to do anything
*
* Tremolo Audio DSP effect
*
* @name tremolodepth
* @param {number | Pattern} depth between 0 and 1
* @example
* n("0,4,7").tremolodepth("<0 .3 .6 .9>").osc()
*
*/
export const { tremolodepth, tremdp } = registerControl('tremolodepth', 'tremdp');
export const { tremolorate, tremr } = registerControl('tremolorate', 'tremr');
export const { fshift } = registerControl('fshift');
export const { fshiftnote } = registerControl('fshiftnote');
@@ -1784,6 +1649,7 @@ export const { zmod } = registerControl('zmod');
// like crush but scaled differently
export const { zcrush } = registerControl('zcrush');
export const { zdelay } = registerControl('zdelay');
export const { tremolo } = registerControl('tremolo');
export const { zzfx } = registerControl('zzfx');
/**
+2 -2
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import createClock from './zyklus.mjs';
import { errorLogger, logger } from './logger.mjs';
import { logger } from './logger.mjs';
export class Cyclist {
constructor({
@@ -76,7 +76,7 @@ export class Cyclist {
}
});
} catch (e) {
errorLogger(e);
logger(`[cyclist] error: ${e.message}`);
onError?.(e);
}
},
+1 -24
View File
@@ -10,7 +10,7 @@ https://rohandrape.net/?t=hmt
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 { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs';
import { timeCat, register, silence } from './pattern.mjs';
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
import Fraction, { lcm } from './fraction.mjs';
@@ -196,26 +196,3 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
return _euclidLegato(pulses, steps, rotation, pat);
});
/**
* A 'euclid' variant with an additional parameter that morphs the resulting
* rhythm from 0 (no morphing) to 1 (completely 'even'). For example
* `sound("bd").euclidish(3,8,0)` would be the same as
* `sound("bd").euclid(3,8)`, and `sound("bd").euclidish(3,8,1)` would be the
* same as `sound("bd bd bd")`. `sound("bd").euclidish(3,8,0.5)` would have a
* groove somewhere between.
* Inspired by the work of Malcom Braff.
* @name euclidish
* @synonyms eish
* @memberof Pattern
* @param {number} pulses the number of onsets
* @param {number} steps the number of steps to fill
* @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse)
* @example
* sound("hh").euclidish(7,12,sine.slow(8))
* .pan(sine.slow(8))
*/
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc);
return pat.struct(morphed).setSteps(steps);
});
+7 -2
View File
@@ -4,7 +4,6 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
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 Fraction from './fraction.mjs';
import { stringifyValues } from './util.mjs';
export class Hap {
/*
@@ -149,7 +148,13 @@ export class Hap {
}
showWhole(compact = false) {
return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`;
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
typeof this.value === 'object'
? compact
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
: JSON.stringify(this.value)
: this.value
}`;
}
combineContext(b) {
-6
View File
@@ -4,12 +4,6 @@ let debounce = 1000,
lastMessage,
lastTime;
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
logger(`[${origin}] error: ${e.message}`);
}
export function logger(message, type, data = {}) {
let t = performance.now();
if (lastMessage === message && t - lastTime < debounce) {
+3 -2
View File
@@ -8,9 +8,10 @@ import { logger } from './logger.mjs';
import { ClockCollator, cycleToSeconds } from './util.mjs';
export class NeoCyclist {
constructor({ onTrigger, onToggle, getTime }) {
constructor({ onTrigger, onToggle, getTime, latency = 0.1 }) {
this.started = false;
this.cps = 0.5;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.getTime = getTime; // get absolute time
this.time_at_last_tick_message = 0;
// the clock of the worker and the audio context clock can drift apart over time
@@ -19,7 +20,7 @@ export class NeoCyclist {
// in order to schedule events consistently.
this.collator = new ClockCollator({ getTargetClockTime: getTime });
this.onToggle = onToggle;
this.latency = 0.1; // fixed trigger time offset
this.latency = latency; // fixed trigger time offset
this.cycle = 0;
this.id = Math.round(Date.now() * Math.random());
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "1.2.3",
"version": "1.2.2",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
+8 -91
View File
@@ -21,8 +21,6 @@ import {
numeralArgs,
parseNumeral,
pairs,
zipWith,
stringifyValues,
} from './util.mjs';
import drawLine from './drawLine.mjs';
import { logger } from './logger.mjs';
@@ -854,29 +852,14 @@ export class Pattern {
);
}
/**
* Writes the content of the current event to the console (visible in the side menu).
* @name log
* @memberof Pattern
* @example
* s("bd sd").log()
*/
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) {
return this.onTrigger((...args) => {
logger(func(...args), undefined, getData(...args));
}, false);
}
/**
* A simplified version of `log` which writes all "values" (various configurable parameters)
* within the event to the console (visible in the side menu).
* @name logValues
* @memberof Pattern
* @example
* s("bd sd").gain("0.25 0.5 1").n("2 1 0").logValues()
*/
logValues(func = (value) => `[hap] ${stringifyValues(value, true)}`) {
return this.log((hap) => func(hap.value));
logValues(func = id) {
return this.log((_, hap) => func(hap.value));
}
//////////////////////////////////////////////////////////////////////
@@ -1305,7 +1288,7 @@ export function sequenceP(pats) {
* @synonyms polyrhythm, pr
* @example
* stack("g3", "b3", ["e4", "d4"]).note()
* // "g3,b3,[e4 d4]".note()
* // "g3,b3,[e4,d4]".note()
*
* @example
* // As a chained function:
@@ -1586,7 +1569,7 @@ export const func = curry((a, b) => reify(b).func(a));
/**
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
*
* @param {string | string[]} name name of the function, or an array of names to be used as synonyms
* @param {string} name name of the function
* @param {function} func function with 1 or more params, where last is the current pattern
* @noAutocomplete
*
@@ -2539,7 +2522,7 @@ export const { fastchunk, fastChunk } = register(
/**
* Like `chunk`, but the function is applied to a looped subcycle of the source pattern.
* @name chunkInto
* @synonyms chunkinto
* @synonym chunkinto
* @memberof Pattern
* @example
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
@@ -2552,7 +2535,7 @@ export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], fun
/**
* Like `chunkInto`, but moves backwards through the chunks.
* @name chunkBackInto
* @synonyms chunkbackinto
* @synonym chunkbackinto
* @memberof Pattern
* @example
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
@@ -2582,7 +2565,7 @@ export const bypass = register(
* Loops the pattern inside an `offset` for `cycles`.
* If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it.
* @name ribbon
* @synonyms rib
* @synonym rib
* @param {number} offset start point of loop in cycles
* @param {number} cycles loop length in cycles
* @example
@@ -3417,69 +3400,3 @@ export const { beat } = register(
['beat'],
__beat((x) => x.innerJoin()),
);
export const _morph = (from, to, by) => {
by = Fraction(by);
const dur = Fraction(1).div(from.length);
const positions = (list) => {
const result = [];
for (const [pos, value] of list.entries()) {
if (value) {
result.push([Fraction(pos).div(list.length), value]);
}
}
return result;
};
const arcs = zipWith(
([posa, valuea], [posb, valueb]) => {
const b = by.mul(posb - posa).add(posa);
const e = b.add(dur);
return new TimeSpan(b, e);
},
positions(from),
positions(to),
);
function query(state) {
const cycle = state.span.begin.sam();
const cycleArc = state.span.cycleArc();
const result = [];
for (const whole of arcs) {
const part = whole.intersection(cycleArc);
if (part !== undefined) {
result.push(
new Hap(
whole.withTime((x) => x.add(cycle)),
part.withTime((x) => x.add(cycle)),
true,
),
);
}
}
return result;
}
return new Pattern(query).splitQueries();
};
/**
* Takes two binary rhythms represented as lists of 1s and 0s, and a number
* between 0 and 1 that morphs between them. The two lists should contain the same
* number of true values.
* @example
* sound("hh").struct(morph([1,0,1,0,1,0,1,0], // straight rhythm
* [1,1,0,1,0,1,0], // wonky rhythm
* 0.25 // creates a slightly wonky rhythm
* )
* )
* @example
* sound("hh").struct(morph("1:0:1:0:1:0:1:0", // straight rhythm
* "1:1:0:1:0:1:0", // wonky rhythm
* sine.slow(8) // slowly morph between the rhythms
* )
* )
*/
export const morph = (frompat, topat, bypat) => {
frompat = reify(frompat);
topat = reify(topat);
bypat = reify(bypat);
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
};
+4 -2
View File
@@ -1,7 +1,7 @@
import { NeoCyclist } from './neocyclist.mjs';
import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs';
import { errorLogger, logger } from './logger.mjs';
import { logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
@@ -18,6 +18,7 @@ export function repl({
editPattern,
onUpdateState,
sync = false,
latency = 0.1,
setInterval,
clearInterval,
id,
@@ -56,6 +57,7 @@ export function repl({
setInterval,
clearInterval,
beforeStart,
latency,
};
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
@@ -256,6 +258,6 @@ export const getTrigger =
await hap.context.onTrigger(hap, getTime(), cps, t);
}
} catch (err) {
errorLogger(err, 'getTrigger');
logger(`[cyclist] error: ${err.message}`, 'error');
}
};
+1 -39
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import Fraction from 'fraction.js';
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect } from 'vitest';
import {
TimeSpan,
@@ -55,8 +55,6 @@ import {
expand,
} from '../index.mjs';
import { log, logValues } from '../pattern.mjs';
import { steady } from '../signal.mjs';
import { n, s } from '../controls.mjs';
@@ -1308,40 +1306,4 @@ describe('Pattern', () => {
);
});
});
describe('log', () => {
it('logs to console', () => {
const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
const pattern = pure('a').log();
const haps = pattern.queryArc(0, 1);
// Force a trigger
haps.forEach((hap) => {
hap.context?.onTrigger?.(hap);
});
expect(mockConsoleLog).toHaveBeenCalledWith(
'%c[hap] 0/1 → 1/1: a',
'background-color: black;color:white;border-radius:15px',
);
mockConsoleLog.mockRestore();
});
});
describe('logValues', () => {
it('logs values to console', () => {
const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
const pattern = pure('a').note('c#').logValues();
const haps = pattern.queryArc(0, 1);
// Force a trigger
haps.forEach((hap) => {
hap.context?.onTrigger?.(hap);
});
expect(mockConsoleLog).toHaveBeenCalledWith(
'%c[hap] value:a note:c#',
'background-color: black;color:white;border-radius:15px',
);
mockConsoleLog.mockRestore();
});
});
});
+1 -1
View File
@@ -72,7 +72,7 @@ export class TimeSpan {
}
intersection(other) {
// Intersection of two timespans, returns undefined if they don't intersect.
// Intersection of two timespans, returns None if they don't intersect.
const intersect_begin = this.begin.max(other.begin);
const intersect_end = this.end.min(other.end);
-10
View File
@@ -487,13 +487,3 @@ export function getCurrentKeyboardState() {
// }
// return lcm((x * y) / gcd(x, y), ...z);
// };
// Takes values -- typically derived from events, i.e. `hap`s -- and renders them
// into a readable format
export function stringifyValues(value, compact = false) {
return typeof value === 'object'
? compact
? JSON.stringify(value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
: JSON.stringify(value)
: value;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "1.2.4",
"version": "1.2.3",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
"version": "1.2.3",
"version": "1.2.2",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/embed",
"version": "1.1.1",
"version": "1.1.0",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/gamepad",
"version": "1.2.3",
"version": "1.2.2",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
"version": "1.2.3",
"version": "1.2.2",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
-3
View File
@@ -493,9 +493,6 @@ export async function midin(input) {
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`,
);
}
// ensure refs for this input are initialized
if (!refs[input]) {
refs[input] = {};
}
const cc = (cc) => ref(() => refs[input][cc] || 0);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "1.2.4",
"version": "1.2.3",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
+4 -4
View File
@@ -1,10 +1,10 @@
import { describe, bench } from 'vitest';
import { calculateSteps } from '../../core/index.mjs';
import { calculateTactus } from '../../core/index.mjs';
import { mini } from '../index.mjs';
describe('mini', () => {
calculateSteps(true);
calculateTactus(true);
bench(
'+tactus',
() => {
@@ -13,7 +13,7 @@ describe('mini', () => {
{ time: 1000 },
);
calculateSteps(false);
calculateTactus(false);
bench(
'-tactus',
() => {
@@ -21,5 +21,5 @@ describe('mini', () => {
},
{ time: 1000 },
);
calculateSteps(true);
calculateTactus(true);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
"version": "1.2.3",
"version": "1.2.2",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mondolang",
"version": "1.1.1",
"version": "1.1.0",
"description": "a language for functional composition that translates to js",
"main": "mondo.mjs",
"type": "module",
-1
View File
@@ -42,7 +42,6 @@ lib['%'] = pace;
lib['?'] = degradeBy; // todo: default 0.5 not working..
lib[':'] = tail;
lib['..'] = range;
lib['def'] = () => silence;
lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]"
//lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mondo",
"version": "1.1.1",
"version": "1.1.0",
"description": "mondo notation for strudel",
"main": "mondough.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/motion",
"version": "1.2.3",
"version": "1.2.2",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mqtt",
"version": "1.2.3",
"version": "1.2.2",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
"version": "1.2.3",
"version": "1.2.2",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/reference",
"version": "1.2.1",
"version": "1.2.0",
"description": "Headless reference of all strudel functions",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
"version": "1.2.4",
"version": "1.2.3",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -36,7 +36,7 @@ export async function prebake() {
samples(`${ds}/tidal-drum-machines.json`),
samples(`${ds}/piano.json`),
samples(`${ds}/Dirt-Samples.json`),
samples(`${ds}/uzu-drumkit.json`),
samples(`${ds}/EmuSP12.json`),
samples(`${ds}/vcsl.json`),
samples(`${ds}/mridangam.json`),
]);
-10
View File
@@ -20,13 +20,3 @@ samples('http://localhost:5432')
LOG=1 npx @strudel/sampler # adds logging
PORT=5555 npx @strudel/sampler # changes port
```
## static json
when running with `--json`, you will simply get the json logged back:
```sh
npx --yes @strudel/sampler --json > strudel.json
```
this is useful if you want to create a sample pack from the current folder.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/sampler",
"version": "0.2.2",
"version": "0.2.0",
"description": "",
"keywords": [
"tidalcycles",
+8 -18
View File
@@ -10,6 +10,14 @@ import os from 'os';
// eslint-disable-next-line
const LOG = !!process.env.LOG || false;
console.log(
cowsay.say({
text: 'welcome to @strudel/sampler',
e: 'oO',
T: 'U ',
}),
);
async function getFilesInDirectory(directory) {
let files = [];
const dirents = await readdir(directory, { withFileTypes: true });
@@ -52,26 +60,8 @@ async function getBanks(directory) {
return { banks, files };
}
const args = process.argv.slice(2);
// eslint-disable-next-line
const directory = process.cwd();
if (args.includes('--json')) {
const { banks, files } = await getBanks(directory);
const json = JSON.stringify(banks);
console.log(json);
process.exit(0);
}
console.log(
cowsay.say({
text: 'welcome to @strudel/sampler',
e: 'oO',
T: 'U ',
}),
);
const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const { banks, files } = await getBanks(directory);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
"version": "1.2.3",
"version": "1.2.2",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
"version": "1.2.4",
"version": "1.2.3",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
+8 -38
View File
@@ -1,8 +1,5 @@
import { getAudioContext } from './superdough.mjs';
import { clamp, nanFallback } from './util.mjs';
import { getNoiseBuffer } from './noise.mjs';
export const noises = ['pink', 'white', 'brown', 'crackle'];
export function gainNode(value) {
const node = getAudioContext().createGain();
@@ -209,46 +206,19 @@ export function getVibratoOscillator(param, value, t) {
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
// a bit of a hack, but it works very well :)
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
const constantNode = new ConstantSourceNode(audioContext);
// Certain browsers requires audio nodes to be connected in order for their onended events
// to fire, so we _mute it_ and then connect it to the destination
const zeroGain = gainNode(0);
zeroGain.connect(audioContext.destination);
constantNode.connect(zeroGain);
// Schedule the `onComplete` callback to occur at `stopTime`
constantNode.onended = () => {
// Ensure garbage collection
try {
zeroGain.disconnect();
} catch {
// pass
}
try {
constantNode.disconnect();
} catch {
// pass
}
onComplete();
};
const constantNode = audioContext.createConstantSource();
constantNode.start(startTime);
constantNode.stop(stopTime);
constantNode.onended = () => {
onComplete();
};
return constantNode;
}
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
let osc;
if (noises.includes(type)) {
osc = ctx.createBufferSource();
osc.buffer = getNoiseBuffer(type, 2);
osc.loop = true;
} else {
osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = freq;
}
const osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = freq;
osc.start();
const g = new GainNode(ctx, { gain: range });
osc.connect(g); // -range, range
@@ -283,7 +253,7 @@ export function applyFM(param, value, begin) {
modulator = fmmod.node;
stop = fmmod.stop;
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) {
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(param);
} else {
-6
View File
@@ -1,11 +1,5 @@
let log = (msg) => console.log(msg);
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
logger(`[${origin}] error: ${e.message}`);
}
export const logger = (...args) => log(...args);
export const setLogger = (fn) => {
+1 -1
View File
@@ -4,7 +4,7 @@ import { getAudioContext } from './superdough.mjs';
let noiseCache = {};
// lazy generates noise buffers and keeps them forever
export function getNoiseBuffer(type, density) {
function getNoiseBuffer(type, density) {
const ac = getAudioContext();
if (noiseCache[type]) {
return noiseCache[type];
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "1.2.4",
"version": "1.2.3",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
+43 -48
View File
@@ -196,52 +196,6 @@ function getSamplesPrefixHandler(url) {
return;
}
export async function fetchSampleMap(url) {
// check if custom prefix handler
const handler = getSamplesPrefixHandler(url);
if (handler) {
return handler(url);
}
url = resolveSpecialPaths(url);
if (url.startsWith('github:')) {
url = githubPath(url, 'strudel.json');
}
if (url.startsWith('local:')) {
url = `http://localhost:5432`;
}
if (url.startsWith('shabda:')) {
let [_, path] = url.split('shabda:');
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
}
if (url.startsWith('shabda/speech')) {
let [_, path] = url.split('shabda/speech');
path = path.startsWith('/') ? path.substring(1) : path;
let [params, words] = path.split(':');
let gender = 'f';
let language = 'en-GB';
if (params) {
[language, gender] = params.split('/');
}
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
const base = url.split('/').slice(0, -1).join('/');
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
const json = await fetch(url)
.then((res) => res.json())
.catch((error) => {
console.error(error);
throw new Error(`error loading "${url}"`);
});
return [json, json._base || base];
}
/**
* Loads a collection of samples to use with `s`
* @example
@@ -263,8 +217,49 @@ export async function fetchSampleMap(url) {
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
if (typeof sampleMap === 'string') {
const [json, base] = await fetchSampleMap(sampleMap);
return samples(json, baseUrl || base, options);
// check if custom prefix handler
const handler = getSamplesPrefixHandler(sampleMap);
if (handler) {
return handler(sampleMap);
}
sampleMap = resolveSpecialPaths(sampleMap);
if (sampleMap.startsWith('github:')) {
sampleMap = githubPath(sampleMap, 'strudel.json');
}
if (sampleMap.startsWith('local:')) {
sampleMap = `http://localhost:5432`;
}
if (sampleMap.startsWith('shabda:')) {
let [_, path] = sampleMap.split('shabda:');
sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`;
}
if (sampleMap.startsWith('shabda/speech')) {
let [_, path] = sampleMap.split('shabda/speech');
path = path.startsWith('/') ? path.substring(1) : path;
let [params, words] = path.split(':');
let gender = 'f';
let language = 'en-GB';
if (params) {
[language, gender] = params.split('/');
}
sampleMap = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
const base = sampleMap.split('/').slice(0, -1).join('/');
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
return fetch(sampleMap)
.then((res) => res.json())
.then((json) => samples(json, baseUrl || json._base || base, options))
.catch((error) => {
console.error(error);
throw new Error(`error loading "${sampleMap}"`);
});
}
const { prebake, tag } = options;
processSampleMap(
+53 -160
View File
@@ -7,11 +7,11 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs';
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
import { map } from 'nanostores';
import { logger, errorLogger } from './logger.mjs';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
export const DEFAULT_MAX_POLYPHONY = 128;
@@ -28,13 +28,6 @@ export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
function getModulationShapeInput(val) {
if (typeof val === 'number') {
return val % 5;
}
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
}
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
@@ -196,7 +189,7 @@ export const resetLoadedSounds = () => soundMap.set({});
let audioContext;
export const setDefaultAudioContext = () => {
audioContext = new AudioContext({ latencyHint: 'playback' });
audioContext = new AudioContext();
return audioContext;
};
@@ -212,17 +205,11 @@ export function getAudioContextCurrentTime() {
return getAudioContext().currentTime;
}
let externalWorklets = [];
export function registerWorklet(url) {
externalWorklets.push(url);
}
let workletsLoading;
function loadWorklets() {
if (!workletsLoading) {
const audioCtx = getAudioContext();
const allWorkletURLs = externalWorklets.concat([workletsUrl]);
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL)));
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
}
return workletsLoading;
@@ -337,40 +324,30 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = clamp(delayfeedback, 0, 0.98);
if (!orbits[orbit].delayNode) {
if (!delays[orbit]) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
connectToOrbit(dly, orbit);
orbits[orbit].delayNode = dly;
connectToDestination(dly, channels);
delays[orbit] = dly;
}
orbits[orbit].delayNode.delayTime.value !== delaytime &&
orbits[orbit].delayNode.delayTime.setValueAtTime(delaytime, t);
orbits[orbit].delayNode.feedback.value !== delayfeedback &&
orbits[orbit].delayNode.feedback.setValueAtTime(delayfeedback, t);
return orbits[orbit].delayNode;
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t);
return delays[orbit];
}
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
const lfoprops = {
export function getLfo(audioContext, time, end, properties = {}) {
return getWorklet(audioContext, 'lfo-processor', {
frequency: 1,
depth,
skew: 0.5,
depth: 1,
skew: 0,
phaseoffset: 0,
time: begin,
begin,
time,
end,
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
shape: 1,
dcoffset: -0.5,
...properties,
});
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
@@ -404,73 +381,31 @@ function getFilterType(ftype) {
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
}
//type orbit {
// gain: number,
// reverbNode: reverbNode
// delayNode: delayNode
//}
let orbits = {};
function connectToOrbit(node, orbit) {
if (orbits[orbit] == null) {
errorLogger(new Error('target orbit does not exist'), 'superdough');
}
node.connect(orbits[orbit].gain);
}
function setOrbit(audioContext, orbit, channels) {
if (orbits[orbit] == null) {
orbits[orbit] = {
gain: new GainNode(audioContext, { gain: 1 }),
};
connectToDestination(orbits[orbit].gain, channels);
}
}
function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1) {
const targetArr = [targetOrbit].flat();
targetArr.forEach((target) => {
if (orbits[target] == null) {
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
return;
}
webAudioTimeout(
audioContext,
() => {
orbits[target].gain.gain.cancelScheduledValues(t);
const currVal = orbits[target].gain.gain.value;
orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t);
orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime));
},
0,
t - 0.01,
);
});
}
let reverbs = {};
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir) {
function getReverb(orbit, duration, fade, lp, dim, ir, channels) {
// If no reverb has been created for a given orbit, create one
if (!orbits[orbit].reverbNode) {
if (!reverbs[orbit]) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
connectToOrbit(reverb, orbit);
orbits[orbit].reverbNode = reverb;
connectToDestination(reverb, channels);
reverbs[orbit] = reverb;
}
if (
hasChanged(duration, orbits[orbit].reverbNode.duration) ||
hasChanged(fade, orbits[orbit].reverbNode.fade) ||
hasChanged(lp, orbits[orbit].reverbNode.lp) ||
hasChanged(dim, orbits[orbit].reverbNode.dim) ||
orbits[orbit].reverbNode.ir !== ir
hasChanged(duration, reverbs[orbit].duration) ||
hasChanged(fade, reverbs[orbit].fade) ||
hasChanged(lp, reverbs[orbit].lp) ||
hasChanged(dim, reverbs[orbit].dim) ||
reverbs[orbit].ir !== ir
) {
// only regenerate when something has changed
// avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir);
reverbs[orbit].generate(duration, fade, lp, dim, ir);
}
return orbits[orbit].reverbNode;
return reverbs[orbit];
}
export let analysers = {},
@@ -513,7 +448,8 @@ function effectSend(input, effect, wet) {
}
export function resetGlobalEffects() {
orbits = {};
delays = {};
reverbs = {};
analysers = {};
analysersData = {};
}
@@ -525,10 +461,9 @@ function mapChannelNumbers(channels) {
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
}
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
export const superdough = async (value, t, hapDuration, cps = 0.5) => {
// new: t is always expected to be the absolute target onset time
const ac = getAudioContext();
let { stretch } = value;
if (stretch != null) {
//account for phase vocoder latency
@@ -554,25 +489,15 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
// destructure
let {
tremolo,
tremolosync,
tremolodepth = 1,
tremoloskew,
tremolophase = 0,
tremoloshape,
s = getDefaultValue('s'),
bank,
source,
gain = getDefaultValue('gain'),
postgain = getDefaultValue('postgain'),
density = getDefaultValue('density'),
duckorbit,
duckattack,
duckdepth,
// filters
fanchor = getDefaultValue('fanchor'),
drive = 0.69,
release = 0,
// low pass
cutoff,
lpenv,
@@ -605,9 +530,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
phasercenter,
//
coarse,
crush,
dry,
shape,
shapevol = getDefaultValue('shapevol'),
distort,
@@ -641,13 +564,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
const orbitChannels = mapChannelNumbers(
multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'),
);
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
setOrbit(ac, orbit, channels, t, cycle, cps);
if (duckorbit != null) {
duckOrbit(ac, duckorbit, t, duckattack, duckdepth);
}
gain = applyGainCurve(nanFallback(gain, 1));
postgain = applyGainCurve(postgain);
@@ -655,11 +572,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
distortvol = applyGainCurve(distortvol);
delay = applyGainCurve(delay);
velocity = applyGainCurve(velocity);
tremolodepth = applyGainCurve(tremolodepth);
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
const end = t + hapDuration;
const endWithRelease = end + release;
const chainID = Math.round(Math.random() * 1000000);
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
@@ -734,7 +648,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
lprelease,
lpenv,
t,
end,
t + hapDuration,
fanchor,
ftype,
drive,
@@ -758,7 +672,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
hprelease,
hpenv,
t,
end,
t + hapDuration,
fanchor,
);
chain.push(hp());
@@ -769,7 +683,20 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
if (bandf !== undefined) {
let bp = () =>
createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor);
createFilter(
ac,
'bandpass',
bandf,
bandq,
bpattack,
bpdecay,
bpsustain,
bprelease,
bpenv,
t,
t + hapDuration,
fanchor,
);
chain.push(bp());
if (ftype === '24db') {
chain.push(bp());
@@ -787,33 +714,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
if (tremolosync != null) {
tremolo = cps * tremolosync;
}
if (tremolo !== undefined) {
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
const gain = Math.max(1 - tremolodepth, 0);
const amGain = new GainNode(ac, { gain });
const time = cycle / cps;
const lfo = getLfo(ac, t, endWithRelease, {
skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1),
frequency: tremolo,
depth: tremolodepth,
time,
dcoffset: 0,
shape: tremoloshape,
phaseoffset: tremolophase,
min: 0,
max: 1,
curve: 1.5,
});
lfo.connect(amGain.gain);
chain.push(amGain);
}
compressorThreshold !== undefined &&
chain.push(
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease),
@@ -827,13 +727,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
// phaser
if (phaser !== undefined && phaserdepth > 0) {
const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep);
const phaserFX = getPhaser(t, t + hapDuration, phaser, phaserdepth, phasercenter, phasersweep);
chain.push(phaserFX);
}
// last gain
const post = new GainNode(ac, { gain: postgain });
chain.push(post);
connectToDestination(post, channels);
// delay
let delaySend;
@@ -868,14 +769,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
analyserSend = effectSend(post, analyserNode, 1);
audioNodes.push(analyserSend);
}
if (dry != null) {
dry = applyGainCurve(dry);
const dryGain = new GainNode(ac, { gain: dry });
chain.push(dryGain);
connectToOrbit(dryGain, orbit);
} else {
connectToOrbit(post, orbit);
}
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
+8 -85
View File
@@ -9,13 +9,12 @@ import {
getVibratoOscillator,
webAudioTimeout,
getWorklet,
noises,
} from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const getFrequencyFromValue = (value, defaultNote = 36) => {
const getFrequencyFromValue = (value) => {
let { note, freq } = value;
note = note || defaultNote;
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
@@ -41,17 +40,7 @@ const waveformAliases = [
['saw', 'sawtooth'],
['sin', 'sine'],
];
function makeSaturationCurve(amount, n_samples) {
const k = typeof amount === 'number' ? amount : 50;
const curve = new Float32Array(n_samples);
for (let i = 0; i < n_samples; i++) {
const x = (i * 2) / n_samples - 1;
curve[i] = Math.tanh(x * k);
}
return curve;
}
const noises = ['pink', 'white', 'brown', 'crackle'];
export function registerSynthSounds() {
[...waveforms].forEach((s) => {
@@ -95,75 +84,6 @@ export function registerSynthSounds() {
{ type: 'synth', prebake: true },
);
});
registerSound(
'sbd',
(t, value, onended) => {
const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value;
const ctx = getAudioContext();
const attackhold = 0.02;
const noiselvl = 1.2;
const noisedecay = 0.025;
const mixGain = 1;
const o = ctx.createOscillator();
o.type = 'triangle';
o.frequency.value = getFrequencyFromValue(value, 29);
o.detune.setValueAtTime(penv * 100, 0);
o.detune.setValueAtTime(penv * 100, t);
o.detune.exponentialRampToValueAtTime(0.001, t + pdecay);
const g = gainNode(1);
g.gain.setValueAtTime(1, t + attackhold);
g.gain.exponentialRampToValueAtTime(0.001, t + attackhold + decay);
o.start(t);
const noise = getNoiseOscillator('brown', t, 2);
const noiseGain = gainNode(1);
noiseGain.gain.setValueAtTime(noiselvl, t);
noiseGain.gain.exponentialRampToValueAtTime(0.001, t + noisedecay);
const sat = new WaveShaperNode(ctx);
// tri to sine diode shaper emulation
sat.curve = makeSaturationCurve(2, ctx.sampleRate);
const mix = gainNode(mixGain);
o.onended = () => {
o.disconnect();
g.disconnect();
sat.disconnect();
noise.node.disconnect();
noiseGain.disconnect();
mix.disconnect();
onended();
};
const node = o.connect(sat).connect(g).connect(mix);
noise.node.connect(noiseGain).connect(mix);
const holdEnd = t + decay;
let end = holdEnd + 0.01;
if (clip != null) {
end = Math.min(t + clip * duration, end);
}
// prevent clicking
mix.gain.setValueAtTime(mixGain, end - 0.01);
mix.gain.linearRampToValueAtTime(0, end);
o.stop(end);
noise.stop(end);
return {
node,
stop: (endTime) => {
o.stop(endTime);
},
};
},
{ type: 'synth', prebake: true },
);
registerSound(
'supersaw',
(begin, value, onended) => {
@@ -201,7 +121,10 @@ export function registerSynthSounds() {
const gainAdjustment = 1 / Math.sqrt(voices);
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
const fm = applyFM(o.parameters.get('frequency'), value, begin);
// const fm = applyFM(o.parameters.get('frequency'), value, begin);
// https://codeberg.org/uzu/strudel/issues/1428
// if you think about re-enabling this, please test with fm > 1 first
// it's like 10x gain, so it's really dangerous
let envGain = gainNode(1);
envGain = o.connect(envGain);
@@ -213,7 +136,7 @@ export function registerSynthSounds() {
destroyAudioWorkletNode(o);
envGain.disconnect();
onended();
fm?.stop();
// fm?.stop();
vibratoOscillator?.stop();
},
begin,
-4
View File
@@ -72,7 +72,3 @@ export const getSoundIndex = (n, numSounds) => {
export function cycleToSeconds(cycle, cps) {
return cycle / cps;
}
export function secondsToCycle(t, cps) {
return t * cps;
}
+26 -39
View File
@@ -8,28 +8,18 @@ import FFT from './fft.js';
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const _mod = (n, m) => ((n % m) + m) % m;
// Restrict phase to the range [0, maxPhase) via wrapping
function wrapPhase(phase, maxPhase = 1) {
if (phase >= maxPhase) {
phase -= maxPhase;
} else if (phase < 0) {
phase += maxPhase;
}
return phase;
}
const blockSize = 128;
// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
// adjust waveshape to remove frequencies above nyquist to prevent aliasing
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
function polyBlep(phase, dt) {
dt = Math.min(dt, 1 - dt);
// Start of cycle
// 0 <= phase < 1
if (phase < dt) {
phase /= dt;
// 2 * (phase - phase^2/2 - 0.5)
return phase + phase - phase * phase - 1;
}
// End of cycle
// -1 < phase < 0
else if (phase > 1 - dt) {
phase = (phase - 1) / dt;
// 2 * (phase^2/2 + phase + 0.5)
@@ -41,7 +31,7 @@ function polyBlep(phase, dt) {
return 0;
}
}
// The order is important for dough integration
const waveshapes = {
tri(phase, skew = 0.5) {
const x = 1 - skew;
@@ -91,12 +81,10 @@ function getParamValue(block, param) {
}
return param[0];
}
const waveShapeNames = Object.keys(waveshapes);
class LFOProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{ name: 'begin', defaultValue: 0 },
{ name: 'time', defaultValue: 0 },
{ name: 'end', defaultValue: 0 },
{ name: 'frequency', defaultValue: 0.5 },
@@ -104,10 +92,7 @@ class LFOProcessor extends AudioWorkletProcessor {
{ name: 'depth', defaultValue: 1 },
{ name: 'phaseoffset', defaultValue: 0 },
{ name: 'shape', defaultValue: 0 },
{ name: 'curve', defaultValue: 1 },
{ name: 'dcoffset', defaultValue: 0 },
{ name: 'min', defaultValue: 0 },
{ name: 'max', defaultValue: 1 },
];
}
@@ -124,13 +109,10 @@ class LFOProcessor extends AudioWorkletProcessor {
}
process(inputs, outputs, parameters) {
const begin = parameters['begin'][0];
// eslint-disable-next-line no-undef
if (currentTime >= parameters.end[0]) {
return false;
}
if (currentTime <= begin) {
return true;
}
const output = outputs[0];
const frequency = parameters['frequency'][0];
@@ -140,11 +122,7 @@ class LFOProcessor extends AudioWorkletProcessor {
const skew = parameters['skew'][0];
const phaseoffset = parameters['phaseoffset'][0];
const curve = parameters['curve'][0];
const dcoffset = parameters['dcoffset'][0];
const min = parameters['min'][0];
const max = parameters['max'][0];
const shape = waveShapeNames[parameters['shape'][0]];
const blockSize = output[0].length ?? 0;
@@ -152,12 +130,12 @@ class LFOProcessor extends AudioWorkletProcessor {
if (this.phase == null) {
this.phase = _mod(time * frequency + phaseoffset, 1);
}
// eslint-disable-next-line no-undef
const dt = frequency / sampleRate;
for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < output.length; i++) {
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
modval = Math.pow(modval, curve);
output[i][n] = clamp(modval, min, max);
const modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
output[i][n] = modval;
}
this.incrementPhase(dt);
}
@@ -313,6 +291,7 @@ class LadderProcessor extends AudioWorkletProcessor {
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
let cutoff = parameters.frequency[0];
// eslint-disable-next-line no-undef
cutoff = (cutoff * 2 * _PI) / sampleRate;
cutoff = cutoff > 1 ? 1 : cutoff;
@@ -445,13 +424,18 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
];
}
process(input, outputs, params) {
// eslint-disable-next-line no-undef
if (currentTime <= params.begin[0]) {
return true;
}
// eslint-disable-next-line no-undef
if (currentTime >= params.end[0]) {
// this.port.postMessage({ type: 'onended' });
return false;
}
let frequency = params.frequency[0];
//apply detune in cents
frequency = frequency * Math.pow(2, params.detune[0] / 1200);
const output = outputs[0];
const voices = params.voices[0];
@@ -462,6 +446,9 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1;
//applies unison "spread" detune in semitones
const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n));
let gainL = gain1;
let gainR = gain2;
// invert right and left gain
@@ -469,21 +456,21 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
gainL = gain2;
gainR = gain1;
}
// eslint-disable-next-line no-undef
const dt = freq / sampleRate;
for (let i = 0; i < output[0].length; i++) {
// Main detuning
let freq = applySemitoneDetuneToFrequency(params.frequency[i] ?? params.frequency[0], params.detune[0] / 100);
// Individual voice detuning
freq = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
// We must wrap this here because it is passed into sawblep below which
// has domain [0, 1]
const dt = _mod(freq / sampleRate, 1);
this.phase[n] = this.phase[n] ?? Math.random();
const v = waveshapes.sawblep(this.phase[n], dt);
output[0][i] = output[0][i] + v * gainL;
output[1][i] = output[1][i] + v * gainR;
this.phase[n] = wrapPhase(this.phase[n] + dt);
this.phase[n] += dt;
if (this.phase[n] > 1.0) {
this.phase[n] = this.phase[n] - 1;
}
}
}
return true;
@@ -492,7 +479,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
const BUFFERED_BLOCK_SIZE = 2048;
function genHannWindow(length) {
-1
View File
@@ -1 +0,0 @@
pattern.wav
-3
View File
@@ -1,3 +0,0 @@
# supradough
platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.
-123
View File
@@ -1,123 +0,0 @@
// this is a poc of how a pattern can be rendered as a wav file using node
// run via: node dough-export.mjs
import fs from 'node:fs';
import WavEncoder from 'wav-encoder';
import { evalScope } from '@strudel/core';
import { miniAllStrings } from '@strudel/mini';
import { Dough } from './dough.mjs';
await evalScope(
import('@strudel/core'),
import('@strudel/mini'),
import('@strudel/tonal'),
// import('@strudel/tonal'),
);
miniAllStrings(); // allows using single quotes for mini notation / skip transpilation
let sampleRate = 48000,
cps = 0.4;
/* await doughsamples('github:eddyflux/crate');
await doughsamples('github:eddyflux/wax'); */
let pat = note('c,eb,g,<bb c4 d4 eb4>')
.s('sine')
.press()
.add(note(24))
.fmi(3)
.fmh(5.01)
.dec(0.4)
.delay('.6:<.12 .22>:.8')
.jux(press)
.rarely(add(note('12')))
.lpf(400)
.lpq(0.2)
.lpd(0.4)
.lpenv(3)
.fmdecay(0.4)
.fmenv(1)
.postgain(0.6)
.stack(s('<pink white>*8').dec(0.07).rarely(ply('2')).delay(0.5).hpf(sine.range(200, 2000).slow(4)).hpq(0.2))
.stack(
s('[- white@3]*2')
.dec(0.4)
.hpf('<2000!3 <4000 8000>>*4')
.hpq(0.6)
.ply('<1 2>*4')
.postgain(0.5)
.delay(0.5)
.jux(rev)
.lpf(5000),
)
.stack(
note('<c2 - [- f1] ->*2')
.s('square')
.lpf(sine.range(100, 300).slow(4))
.lpe(1)
.segment(8)
.lpd(0.3)
.lpq(0.2)
.dec(0.2)
.speed('<1 2>')
.ply('<1 2>')
.postgain(1),
)
.stack(
chord('<Cm Cm7 Cm9 Cm11 Fm Fm7 Fm9 Fm11>')
.voicing()
.s('<sine>')
.clip(1)
.rel(0.4)
.vib('4:.2')
.gain(0.7)
.hpf(1200)
.fm(0.5)
.att(1)
.lpa(0.5)
.lpf(200)
.lpenv(4)
.chorus(0.8),
)
.slow(1 / cps);
let cycles = 30;
let seconds = cycles + 1; // 1s release tail
const haps = pat.queryArc(0, cycles);
const dough = new Dough(sampleRate);
console.log('spawn voices...');
haps.forEach((hap) => {
hap.value._begin = Number(hap.whole.begin);
hap.value._duration = hap.duration /* / cps */;
dough.scheduleSpawn(hap.value);
});
console.log(`render ${seconds}s long buffer, each dot is 1 second:`);
const buffers = [new Float32Array(seconds * sampleRate), new Float32Array(seconds * sampleRate)];
let t = performance.now();
while (dough.t <= buffers[0].length) {
dough.update();
buffers[0][dough.t] = dough.out[0];
buffers[1][dough.t] = dough.out[1];
if (dough.t % sampleRate === 0) {
process.stdout.write('.');
}
}
const took = (performance.now() - t) / 1000;
const load = (took / seconds) * 100;
const speed = (seconds / took).toFixed(2);
console.log('');
console.log(`done!
rendered ${seconds}s in ${took.toFixed(2)}s
speed: ${speed}x
load: ${load.toFixed(2)}%`);
const patternAudio = {
sampleRate,
channelData: buffers,
};
WavEncoder.encode(patternAudio).then((buffer) => {
fs.writeFileSync('pattern.wav', new Float32Array(buffer));
});
-39
View File
@@ -1,39 +0,0 @@
import { Dough } from './dough.mjs';
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
class DoughProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.dough = new Dough(sampleRate, currentTime);
this.port.onmessage = (event) => {
if (event.data.spawn) {
this.dough.scheduleSpawn(event.data.spawn);
} else if (event.data.sample) {
this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate);
} else if (event.data.samples) {
event.data.samples.forEach(([name, channels, sampleRate]) => {
this.dough.loadSample(name, channels, sampleRate);
});
} else {
console.log('unrecognized event type', event.data);
}
};
}
process(inputs, outputs, params) {
if (this.disconnected) {
return false;
}
const output = outputs[0];
for (let i = 0; i < output[0].length; i++) {
this.dough.update();
for (let c = 0; c < output.length; c++) {
//prevent speaker blowout via clipping if threshold exceeds
output[c][i] = clamp(this.dough.out[c], -1, 1);
}
}
return true; // keep the audio processing going
}
}
registerProcessor('dough-processor', DoughProcessor);
-976
View File
@@ -1,976 +0,0 @@
// this is dough, the superdough without dependencies
const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000;
const PI_DIV_SR = Math.PI / SAMPLE_RATE;
const ISR = 1 / SAMPLE_RATE;
let gainCurveFunc = (val) => Math.pow(val, 2);
function applyGainCurve(val) {
return gainCurveFunc(val);
}
/**
* Equal Power Crossfade function.
* Smoothly transitions between signals A and B, maintaining consistent perceived loudness.
*
* @param {number} a - Signal A (can be a single value or an array value in buffer processing).
* @param {number} b - Signal B (can be a single value or an array value in buffer processing).
* @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix).
* @returns {number} Crossfaded output value.
*/
function crossfade(a, b, m) {
const aGain = Math.sin((1 - m) * 0.5 * Math.PI);
const bGain = Math.sin(m * 0.5 * Math.PI);
return a * aGain + b * bGain;
}
// function setGainCurve(newGainCurveFunc) {
// gainCurveFunc = newGainCurveFunc;
// }
// https://garten.salat.dev/audio-DSP/oscillators.html
export class SineOsc {
phase = 0;
update(freq) {
const value = Math.sin(this.phase * 2 * Math.PI);
this.phase = (this.phase + freq / SAMPLE_RATE) % 1;
return value;
}
}
export class ZawOsc {
phase = 0;
update(freq) {
this.phase += ISR * freq;
return (this.phase % 1) * 2 - 1;
}
}
function polyBlep(t, dt) {
// 0 <= t < 1
if (t < dt) {
t /= dt;
// 2 * (t - t^2/2 - 0.5)
return t + t - t * t - 1;
}
// -1 < t < 0
if (t > 1 - dt) {
t = (t - 1) / dt;
// 2 * (t^2/2 + t + 0.5)
return t * t + t + t + 1;
}
// 0 otherwise
return 0;
}
export class SawOsc {
constructor(props = {}) {
this.phase = props.phase ?? 0;
}
update(freq) {
const dt = freq / SAMPLE_RATE;
let p = polyBlep(this.phase, dt);
let s = 2 * this.phase - 1 - p;
this.phase += dt;
if (this.phase > 1) {
this.phase -= 1;
}
return s;
}
}
function getUnisonDetune(unison, detune, voiceIndex) {
if (unison < 2) {
return 0;
}
const lerp = (a, b, n) => {
return n * (b - a) + a;
};
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
}
function applySemitoneDetuneToFrequency(frequency, detune) {
return frequency * Math.pow(2, detune / 12);
}
export class SupersawOsc {
constructor(props = {}) {
//TODO: figure out a good way to pass in these params
this.voices = props.voices ?? 5;
this.freqspread = props.freqspread ?? 0.2;
this.panspread = props.panspread ?? 0.4;
this.phase = new Float32Array(this.voices).map(() => Math.random());
}
update(freq) {
const gain1 = Math.sqrt(1 - this.panspread);
const gain2 = Math.sqrt(this.panspread);
let sl = 0;
let sr = 0;
for (let n = 0; n < this.voices; n++) {
const freqAdjusted = applySemitoneDetuneToFrequency(freq, getUnisonDetune(this.voices, this.freqspread, n));
const dt = freqAdjusted / SAMPLE_RATE;
const isOdd = (n & 1) == 1;
let gainL = gain1;
let gainR = gain2;
// invert right and left gain
if (isOdd) {
gainL = gain2;
gainR = gain1;
}
let p = polyBlep(this.phase[n], dt);
let s = 2 * this.phase[n] - 1 - p;
sl = sl + s * gainL;
sr = sr + s * gainL;
this.phase[n] += dt;
if (this.phase[n] > 1) {
this.phase[n] -= 1;
}
}
return sl + sr;
//TODO: make stereo
// return [sl, sr];
}
}
export class TriOsc {
phase = 0;
update(freq) {
this.phase += ISR * freq;
let phase = this.phase % 1;
let value = phase < 0.5 ? 2 * phase : 1 - 2 * (phase - 0.5);
return value * 2 - 1;
}
}
export class TwoPoleFilter {
s0 = 0;
s1 = 0;
update(s, cutoff, resonance = 0) {
// Out of bound values can produce NaNs
resonance = Math.max(resonance, 0);
cutoff = Math.min(cutoff, 20000);
const c = 2 * Math.sin(cutoff * PI_DIV_SR);
const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
const mrc = 1 - r * c;
this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
this.s1 = mrc * this.s1 + c * this.s0; // lpf
return this.s1; // return lpf by default
}
}
class PulseOsc {
constructor(phase = 0) {
this.phase = phase;
}
saw(offset, dt) {
let phase = (this.phase + offset) % 1;
let p = polyBlep(phase, dt);
return 2 * phase - 1 - p;
}
update(freq, pw = 0.5) {
const dt = freq / SAMPLE_RATE;
let pulse = this.saw(0, dt) - this.saw(pw, dt);
this.phase = (this.phase + dt) % 1;
return pulse + pw * 2 - 1;
}
}
// non bandlimited (has aliasing)
export class PulzeOsc {
phase = 0;
update(freq, duty = 0.5) {
this.phase += ISR * freq;
let cyclePos = this.phase % 1;
return cyclePos < duty ? 1 : -1;
}
}
export class Dust {
update = (density) => (Math.random() < density * ISR ? Math.random() : 0);
}
export class WhiteNoise {
update() {
return Math.random() * 2 - 1;
}
}
export class BrownNoise {
constructor() {
this.out = 0;
}
update() {
let white = Math.random() * 2 - 1;
this.out = (this.out + 0.02 * white) / 1.02;
return this.out;
}
}
export class PinkNoise {
constructor() {
this.b0 = 0;
this.b1 = 0;
this.b2 = 0;
this.b3 = 0;
this.b4 = 0;
this.b5 = 0;
this.b6 = 0;
}
update() {
const white = Math.random() * 2 - 1;
this.b0 = 0.99886 * this.b0 + white * 0.0555179;
this.b1 = 0.99332 * this.b1 + white * 0.0750759;
this.b2 = 0.969 * this.b2 + white * 0.153852;
this.b3 = 0.8665 * this.b3 + white * 0.3104856;
this.b4 = 0.55 * this.b4 + white * 0.5329522;
this.b5 = -0.7616 * this.b5 - white * 0.016898;
const pink = this.b0 + this.b1 + this.b2 + this.b3 + this.b4 + this.b5 + this.b6 + white * 0.5362;
this.b6 = white * 0.115926;
return pink * 0.11;
}
}
export class Impulse {
phase = 1;
update(freq) {
this.phase += ISR * freq;
let v = this.phase >= 1 ? 1 : 0;
this.phase = this.phase % 1;
return v;
}
}
export class ClockDiv {
inSgn = true;
outSgn = true;
clockCnt = 0;
update(clock, factor) {
let curSgn = clock > 0;
if (this.inSgn != curSgn) {
this.clockCnt++;
if (this.clockCnt >= factor) {
this.clockCnt = 0;
this.outSgn = !this.outSgn;
}
}
this.inSgn = curSgn;
return this.outSgn ? 1 : -1;
}
}
export class Hold {
value = 0;
trigSgn = false;
update(input, trig) {
if (!this.trigSgn && trig > 0) this.value = input;
this.trigSgn = trig > 0;
return this.value;
}
}
function lerp(x, y0, y1, exponent = 1) {
if (x <= 0) return y0;
if (x >= 1) return y1;
let curvedX;
if (exponent === 0) {
curvedX = x; // linear
} else if (exponent > 0) {
curvedX = Math.pow(x, exponent); // ease-in
} else {
curvedX = 1 - Math.pow(1 - x, -exponent); // ease-out
}
return y0 + (y1 - y0) * curvedX;
}
export class ADSR {
constructor(props = {}) {
this.state = 'off';
this.startTime = 0;
this.startVal = 0;
this.decayCurve = props.decayCurve ?? 1;
}
update(curTime, gate, attack, decay, susVal, release) {
switch (this.state) {
case 'off': {
if (gate > 0) {
this.state = 'attack';
this.startTime = curTime;
this.startVal = 0;
}
return 0;
}
case 'attack': {
let time = curTime - this.startTime;
if (time > attack) {
this.state = 'decay';
this.startTime = curTime;
return 1;
}
return lerp(time / attack, this.startVal, 1, 1);
}
case 'decay': {
let time = curTime - this.startTime;
let curVal = lerp(time / decay, 1, susVal, -this.decayCurve);
if (gate <= 0) {
this.state = 'release';
this.startTime = curTime;
this.startVal = curVal;
return curVal;
}
if (time > decay) {
this.state = 'sustain';
this.startTime = curTime;
return susVal;
}
return curVal;
}
case 'sustain': {
if (gate <= 0) {
this.state = 'release';
this.startTime = curTime;
this.startVal = susVal;
}
return susVal;
}
case 'release': {
let time = curTime - this.startTime;
if (time > release) {
this.state = 'off';
return 0;
}
let curVal = lerp(time / release, this.startVal, 0, -this.decayCurve);
if (gate > 0) {
this.state = 'attack';
this.startTime = curTime;
this.startVal = curVal;
}
return curVal;
}
}
throw 'invalid envelope state';
}
}
/*
impulse(1).ad(.1).mul(sine(200))
.add(x=>x.delay(.1).mul(.8))
.out()*/
const MAX_DELAY_TIME = 10;
export class PitchDelay {
lpf = new TwoPoleFilter();
constructor(_props = {}) {
this.buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE);
this.writeIdx = 0;
this.readIdx = 0;
this.numSamples = 0;
}
write(s, delayTime) {
// Calculate how far in the past to read
this.numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1);
this.writeIdx = (this.writeIdx + 1) % this.numSamples;
this.buffer[this.writeIdx] = s;
this.readIdx = this.writeIdx - this.numSamples + 1;
// If past the start of the buffer, wrap around (Q: is this possible?)
if (this.readIdx < 0) this.readIdx += this.numSamples;
}
update(input, delayTime, speed = 1) {
this.write(input, delayTime);
let index = this.readIdx;
if (speed < 0) {
index = this.numSamples - Math.floor(Math.abs(this.readIdx * speed) % this.numSamples);
} else {
index = Math.floor(this.readIdx * speed) % this.numSamples;
}
const s = this.lpf.update(this.buffer[index], 0.9, 0);
return s;
}
}
export class Delay {
writeIdx = 0;
readIdx = 0;
buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); //.fill(0)
write(s, delayTime) {
this.writeIdx = (this.writeIdx + 1) % this.buffer.length;
this.buffer[this.writeIdx] = s;
// Calculate how far in the past to read
let numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1);
this.readIdx = this.writeIdx - numSamples;
// If past the start of the buffer, wrap around
if (this.readIdx < 0) this.readIdx += this.buffer.length;
}
update(input, delayTime) {
this.write(input, delayTime);
return this.buffer[this.readIdx];
}
}
//TODO: Figure out why clicking at the start off the buffer
export class Chorus {
delay = new Delay();
modulator = new TriOsc();
update(input, mix, delayTime, modulationFreq, modulationDepth) {
const m = this.modulator.update(modulationFreq) * modulationDepth;
const c = this.delay.update(input, delayTime * (1 + m));
return crossfade(input, c, mix);
}
}
export class Fold {
update(input = 0, rate = 0) {
if (rate < 0) rate = 0;
rate = rate + 1;
input = input * rate;
return 4 * (Math.abs(0.25 * input + 0.25 - Math.round(0.25 * input + 0.25)) - 0.25);
}
}
export class Lag {
lagUnit = 4410;
s = 0;
update(input, rate) {
// Remap so the useful range is around [0, 1]
rate = rate * this.lagUnit;
if (rate < 1) rate = 1;
this.s += (1 / rate) * (input - this.s);
return this.s;
}
}
export class Slew {
last = 0;
update(input, up, dn) {
const upStep = up * ISR;
const downStep = dn * ISR;
let delta = input - this.last;
if (delta > upStep) {
delta = upStep;
} else if (delta < -downStep) {
delta = -downStep;
}
this.last += delta;
return this.last;
}
}
// overdrive style distortion (adapted from noisecraft) currently unused
export function applyDistortion(x, amount) {
amount = Math.min(Math.max(amount, 0), 1);
amount -= 0.01;
var k = (2 * amount) / (1 - amount);
var y = ((1 + k) * x) / (1 + k * Math.abs(x));
return y;
}
export class Sequence {
clockSgn = true;
step = 0;
first = true;
update(clock, ...ins) {
if (!this.clockSgn && clock > 0) {
this.step = (this.step + 1) % ins.length;
this.clockSgn = clock > 0;
return 0; // set first sample to zero to retrigger gates on step change...
}
this.clockSgn = clock > 0;
return ins[this.step];
}
}
// sample rate bit crusher
export class Coarse {
hold = 0;
t = 0;
update(input, coarse) {
if (this.t++ % coarse === 0) {
this.t = 0;
this.hold = input;
}
return this.hold;
}
}
// amplitude bit crusher
export class Crush {
update(input, crush) {
crush = Math.max(1, crush);
const x = Math.pow(2, crush - 1);
return Math.round(input * x) / x;
}
}
// this is the distort from superdough
export class Distort {
update(input, distort = 0, postgain = 1) {
postgain = Math.max(0.001, Math.min(1, postgain));
const shape = Math.expm1(distort);
return (((1 + shape) * input) / (1 + shape * Math.abs(input))) * postgain;
}
}
// distortion could be expressed as a function, because it's stateless
export class BufferPlayer {
static samples = new Map(); // string -> { channels, sampleRate }
buffer; // Float32Array
sampleRate;
pos = 0;
sampleFreq = note2freq();
constructor(buffer, sampleRate, normalize) {
this.buffer = buffer;
this.sampleRate = sampleRate;
this.duration = this.buffer.length / this.sampleRate;
this.speed = SAMPLE_RATE / this.sampleRate;
if (normalize) {
// this will make the buffer last 1s if freq = sampleFreq
// it's useful to loop samples (e.g. fit function)
this.speed *= this.duration;
}
}
update(freq) {
if (this.pos >= this.buffer.length) {
return 0;
}
const speed = (freq / this.sampleFreq) * this.speed;
let s = this.buffer[Math.floor(this.pos)];
this.pos = this.pos + speed;
return s;
}
}
export function _rangex(sig, min, max) {
let logmin = Math.log(min);
let range = Math.log(max) - logmin;
const unipolar = (sig + 1) / 2;
return Math.exp(unipolar * range + logmin);
}
// duplicate
export const getADSR = (params, curve = 'linear', defaultValues) => {
const envmin = curve === 'exponential' ? 0.001 : 0.001;
const releaseMin = 0.01;
const envmax = 1;
const [a, d, s, r] = params;
if (a == null && d == null && s == null && r == null) {
return defaultValues ?? [envmin, envmin, envmax, releaseMin];
}
const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin;
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
};
let shapes = {
sine: SineOsc,
saw: SawOsc,
zaw: ZawOsc,
sawtooth: SawOsc,
zawtooth: ZawOsc,
supersaw: SupersawOsc,
tri: TriOsc,
triangle: TriOsc,
pulse: PulseOsc,
square: PulseOsc,
pulze: PulzeOsc,
dust: Dust,
crackle: Dust,
impulse: Impulse,
white: WhiteNoise,
brown: BrownNoise,
pink: PinkNoise,
};
const defaultDefaultValues = {
chorus: 0,
note: 48,
s: 'triangle',
bank: '',
gain: 1,
postgain: 1,
velocity: 1,
density: '.03',
ftype: '12db',
fanchor: 0,
//resonance: 1, // superdough resonance is scaled differently
resonance: 0,
//hresonance: 1, // superdough resonance is scaled differently
hresonance: 0,
// bandq: 1, // superdough resonance is scaled differently
bandq: 0,
channels: [1, 2],
phaserdepth: 0.75,
shapevol: 1,
distortvol: 1,
delay: 0,
byteBeatExpression: '0',
delayfeedback: 0.5,
delayspeed: 1,
delaytime: 0.25,
orbit: 1,
i: 1,
fft: 8,
z: 'triangle',
pan: 0.5,
fmh: 1,
fmenv: 0, // differs from superdough
speed: 1,
pw: 0.5,
};
let getDefaultValue = (key) => defaultDefaultValues[key];
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
const accs = { '#': 1, b: -1, s: 1, f: -1 };
const note2midi = (note, defaultOctave = 3) => {
let [pc, acc = '', oct = ''] =
String(note)
.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)
?.slice(1) || [];
if (!pc) {
throw new Error('not a note: "' + note + '"');
}
const chroma = chromas[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
oct = Number(oct || defaultOctave);
return (oct + 1) * 12 + chroma + offset;
};
const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440;
const note2freq = (note) => {
note = note || getDefaultValue('note');
if (typeof note === 'string') {
note = note2midi(note, 3); // e.g. c3 => 48
}
return midi2freq(note);
};
export class DoughVoice {
out = [0, 0];
constructor(value) {
value.freq ??= note2freq(value.note);
let $ = this;
Object.assign($, value);
$.s = $.s ?? getDefaultValue('s');
$.gain = applyGainCurve($.gain ?? getDefaultValue('gain'));
$.velocity = applyGainCurve($.velocity ?? getDefaultValue('velocity'));
$.postgain = applyGainCurve($.postgain ?? getDefaultValue('postgain'));
$.density = $.density ?? getDefaultValue('density');
$.fanchor = $.fanchor ?? getDefaultValue('fanchor');
$.drive = $.drive ?? 0.69;
$.phaserdepth = $.phaserdepth ?? getDefaultValue('phaserdepth');
$.shapevol = applyGainCurve($.shapevol ?? getDefaultValue('shapevol'));
$.distortvol = applyGainCurve($.distortvol ?? getDefaultValue('distortvol'));
$.i = $.i ?? getDefaultValue('i');
$.chorus = $.chorus ?? getDefaultValue('chorus');
$.fft = $.fft ?? getDefaultValue('fft');
$.pan = $.pan ?? getDefaultValue('pan');
$.orbit = $.orbit ?? getDefaultValue('orbit');
$.fmenv = $.fmenv ?? getDefaultValue('fmenv');
$.resonance = $.resonance ?? getDefaultValue('resonance');
$.hresonance = $.hresonance ?? getDefaultValue('hresonance');
$.bandq = $.bandq ?? getDefaultValue('bandq');
$.speed = $.speed ?? getDefaultValue('speed');
$.pw = $.pw ?? getDefaultValue('pw');
[$.attack, $.decay, $.sustain, $.release] = getADSR([$.attack, $.decay, $.sustain, $.release]);
$._holdEnd = $._begin + $._duration; // needed for gate
$._end = $._holdEnd + $.release + 0.01; // needed for despawn
if ($.fmi && ($.s === 'saw' || $.s === 'sawtooth')) {
$.s = 'zaw'; // polyblepped saw when fm is applied
}
if (shapes[$.s]) {
const SourceClass = shapes[$.s];
$._sound = new SourceClass();
$._channels = 1;
} else if (BufferPlayer.samples.has($.s)) {
const sample = BufferPlayer.samples.get($.s);
$._buffers = [];
$._channels = sample.channels.length;
for (let i = 0; i < $._channels; i++) {
$._buffers.push(new BufferPlayer(sample.channels[i], sample.sampleRate, $.unit === 'c')); // tbd unit === 'c'
}
} else {
console.warn('sound not loaded', $.s);
}
if ($.penv) {
$._penv = new ADSR({ decayCurve: 4 });
[$.pattack, $.pdecay, $.psustain, $.prelease] = getADSR([$.pattack, $.pdecay, $.psustain, $.prelease]);
}
if ($.vib) {
$._vib = new SineOsc();
$.vibmod = $.vibmod ?? getDefaultValue('vibmod');
}
if ($.fmi) {
$._fm = new SineOsc();
$.fmh = $.fmh ?? getDefaultValue('fmh');
if ($.fmenv) {
$._fmenv = new ADSR({ decayCurve: 2 });
[$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease] = getADSR([$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease]);
}
}
// gain envelope
$._adsr = new ADSR({ decayCurve: 2 });
// delay
$.delay = applyGainCurve($.delay ?? getDefaultValue('delay'));
$.delayfeedback = $.delayfeedback ?? getDefaultValue('delayfeedback');
$.delayspeed = $.delayspeed ?? getDefaultValue('delayspeed');
$.delaytime = $.delaytime ?? getDefaultValue('delaytime');
// filter setup
if ($.lpenv) {
$._lpenv = new ADSR({ decayCurve: 4 });
[$.lpattack, $.lpdecay, $.lpsustain, $.lprelease] = getADSR([$.lpattack, $.lpdecay, $.lpsustain, $.lprelease]);
}
if ($.hpenv) {
$._hpenv = new ADSR({ decayCurve: 4 });
[$.hpattack, $.hpdecay, $.hpsustain, $.hprelease] = getADSR([$.hpattack, $.hpdecay, $.hpsustain, $.hprelease]);
}
if ($.bpenv) {
$._bpenv = new ADSR({ decayCurve: 4 });
[$.bpattack, $.bpdecay, $.bpsustain, $.bprelease] = getADSR([$.bpattack, $.bpdecay, $.bpsustain, $.bprelease]);
}
// channelwise effects setup
$._chorus = $.chorus ? [] : null;
$._lpf = $.cutoff ? [] : null;
$._hpf = $.hcutoff ? [] : null;
$._bpf = $.bandf ? [] : null;
$._coarse = $.coarse ? [] : null;
$._crush = $.crush ? [] : null;
$._distort = $.distort ? [] : null;
for (let i = 0; i < this._channels; i++) {
$._lpf?.push(new TwoPoleFilter());
$._hpf?.push(new TwoPoleFilter());
$._bpf?.push(new TwoPoleFilter());
$._chorus?.push(new Chorus());
$._coarse?.push(new Coarse());
$._crush?.push(new Crush());
$._distort?.push(new Distort());
}
}
update(t) {
if (!this._sound && !this._buffers) {
return 0;
}
let gate = Number(t >= this._begin && t <= this._holdEnd);
let freq = this.freq * this.speed;
// frequency modulation
if (this._fm) {
let fmi = this.fmi;
if (this._fmenv) {
const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease);
fmi = this.fmenv * env * fmi;
}
const modfreq = freq * this.fmh;
const modgain = modfreq * fmi;
freq = freq + this._fm.update(modfreq) * modgain;
}
// vibrato
if (this._vib) {
freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12);
}
// pitch envelope
if (this._penv) {
const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease);
freq = freq + env * this.penv;
}
// filters
let lpf = this.cutoff;
if (this._lpf) {
if (this._lpenv) {
const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease);
lpf = this.lpenv * env * lpf + lpf;
}
}
let hpf = this.hcutoff;
if (this._hpf) {
if (this._hpenv) {
const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease);
hpf = 2 ** this.hpenv * env * hpf + hpf;
}
}
let bpf = this.bandf;
if (this._bpf) {
if (this._bpenv) {
const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease);
bpf = 2 ** this.bpenv * env * bpf + bpf;
}
}
// gain envelope
const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release);
// channelwise dsp
for (let i = 0; i < this._channels; i++) {
// sound source
if (this._sound && this.s === 'pulse') {
this.out[i] = this._sound.update(freq, this.pw);
} else if (this._sound) {
this.out[i] = this._sound.update(freq);
} else if (this._buffers) {
this.out[i] = this._buffers[i].update(freq);
}
this.out[i] = this.out[i] * this.gain * this.velocity;
if (this._chorus) {
const c = this._chorus[i].update(this.out[i], this.chorus, 0.03 + 0.05 * i, 1, 0.11);
this.out[i] = c + this.out[i];
}
if (this._lpf) {
this._lpf[i].update(this.out[i], lpf, this.resonance);
this.out[i] = this._lpf[i].s1;
}
if (this._hpf) {
this._hpf[i].update(this.out[i], hpf, this.hresonance);
this.out[i] = this.out[i] - this._hpf[i].s1;
}
if (this._bpf) {
this._bpf[i].update(this.out[i], bpf, this.bandq);
this.out[i] = this._bpf[i].s0;
}
if (this._coarse) {
this.out[i] = this._coarse[i].update(this.out[i], this.coarse);
}
if (this._crush) {
this.out[i] = this._crush[i].update(this.out[i], this.crush);
}
if (this._distort) {
this.out[i] = this._distort[i].update(this.out[i], this.distort, this.distortvol);
}
this.out[i] = this.out[i] * env;
this.out[i] = this.out[i] * this.postgain;
if (!this._buffers) {
this.out[i] = this.out[i] * 0.2; // turn down waveform
}
}
if (this._channels === 1) {
this.out[1] = this.out[0];
}
if (this.pan !== 0.5) {
const panpos = (this.pan * Math.PI) / 2;
this.out[0] = this.out[0] * Math.cos(panpos);
this.out[1] = this.out[1] * Math.sin(panpos);
}
}
}
// this class is the interface to the "outer world"
// it handles spawning and despawning of DoughVoice's
export class Dough {
voices = []; // DoughVoice[]
vid = 0;
q = [];
out = [0, 0];
delaysend = [0, 0];
delaytime = getDefaultValue('delaytime');
delayfeedback = getDefaultValue('delayfeedback');
delayspeed = getDefaultValue('delayspeed');
t = 0;
// sampleRate: number, currentTime: number (seconds)
constructor(sampleRate = 48000, currentTime = 0) {
this.sampleRate = sampleRate;
this.t = Math.floor(currentTime * sampleRate); // samples
// console.log('init dough', this.sampleRate, this.t);
this._delayL = new PitchDelay();
this._delayR = new PitchDelay();
}
loadSample(name, channels, sampleRate) {
BufferPlayer.samples.set(name, { channels, sampleRate });
}
scheduleSpawn(value) {
if (value._begin === undefined) {
throw new Error('[dough]: scheduleSpawn expected _begin to be set');
}
if (value._duration === undefined) {
throw new Error('[dough]: scheduleSpawn expected _duration to be set');
}
value.sampleRate = this.sampleRate;
// convert seconds to samples
const time = Math.floor(value._begin * this.sampleRate); // set from supradough.mjs
this.schedule({ time, type: 'spawn', arg: value });
}
spawn(value) {
value.id = this.vid++;
const voice = new DoughVoice(value);
this.voices.push(voice);
// console.log('spawn', voice.id, 'voices:', this.voices.length);
// schedule removal
const endTime = Math.ceil(voice._end * this.sampleRate);
this.schedule({ time: endTime /* + 48000 */, type: 'despawn', arg: voice.id });
}
despawn(vid) {
this.voices = this.voices.filter((v) => v.id !== vid);
// console.log('despawn', vid, 'voices:', this.voices.length);
}
// schedules a function call with a single argument
// msg = {time:number,type:string, arg: any}
// the Dough method "type" will be called with "arg" at "time"
schedule(msg) {
if (!this.q.length) {
// if empty, just push
this.q.push(msg);
return;
}
// not empty
// find index where msg.time fits in
let i = 0;
while (i < this.q.length && this.q[i].time < msg.time) {
i++;
}
// this ensures q stays sorted by time, so we only need to check q[0]
this.q.splice(i, 0, msg);
}
// maybe update should be called once per block instead for perf reasons?
update() {
// go over q
while (this.q.length > 0 && this.q[0].time <= this.t) {
// console.log('schedule', this.q[0]);
// trigger due messages. q is sorted, so we only need to check q[0]
this[this.q[0].type](this.q[0].arg); // type is expected to be a Dough method
this.q.shift();
}
// add active voices
this.out[0] = 0;
this.out[1] = 0;
for (let v = 0; v < this.voices.length; v++) {
this.voices[v].update(this.t / this.sampleRate);
this.out[0] += this.voices[v].out[0];
this.out[1] += this.voices[v].out[1];
if (this.voices[v].delay) {
this.delaysend[0] += this.voices[v].out[0] * this.voices[v].delay;
this.delaysend[1] += this.voices[v].out[1] * this.voices[v].delay;
this.delaytime = this.voices[v].delaytime; // we trust that these are initialized in the voice
this.delayspeed = this.voices[v].delayspeed; // we trust that these are initialized in the voice
this.delayfeedback = this.voices[v].delayfeedback;
}
}
// todo: how to change delaytime / delayfeedback from a voice?
const delayL = this._delayL.update(this.delaysend[0], this.delaytime, this.delayspeed);
const delayR = this._delayR.update(this.delaysend[1], this.delaytime, this.delayspeed);
this.delaysend[0] = delayL * this.delayfeedback;
this.delaysend[1] = delayR * this.delayfeedback;
this.out[0] += delayL;
this.out[1] += delayR;
this.t++;
}
}
-4
View File
@@ -1,4 +0,0 @@
import _workletUrl from './dough-worklet.mjs?url'; // todo: change ?url to ?audioworklet before build (?audioworklet doesn't hot reload)
export * from './dough.mjs';
export const workletUrl = _workletUrl;
-37
View File
@@ -1,37 +0,0 @@
{
"name": "supradough",
"version": "1.2.3",
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"devDependencies": {
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*",
"wav-encoder": "^1.3.0"
},
"dependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
"version": "1.2.3",
"version": "1.2.2",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
+17 -22
View File
@@ -88,14 +88,13 @@ function scaleOffset(scale, offset, note) {
* @returns Pattern
* @memberof Pattern
* @name transpose
* @synonyms trans
* @example
* "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note()
* @example
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()
*/
export const { transpose, trans } = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) {
export const transpose = register('transpose', function (intervalOrSemitones, pat) {
return pat.withHap((hap) => {
const note = hap.value.note ?? hap.value;
if (typeof note === 'number') {
@@ -143,7 +142,6 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
* @name scaleTranspose
* @param {offset} offset number of steps inside the scale
* @returns Pattern
* @synonyms scaleTrans, strans
* @example
* "-8 [2,4,6]"
* .scale('C4 bebop major')
@@ -151,25 +149,22 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
* .note()
*/
export const { scaleTranspose, scaleTrans, strans } = register(
['scaleTranspose', 'scaleTrans', 'strans'],
function (offset /* : number | string */, pat) {
return pat.withHap((hap) => {
if (!hap.context.scale) {
throw new Error('can only use scaleTranspose after .scale');
}
if (typeof hap.value === 'object')
return hap.withValue(() => ({
...hap.value,
note: scaleOffset(hap.context.scale, Number(offset), hap.value.note),
}));
if (typeof hap.value !== 'string') {
throw new Error('can only use scaleTranspose with notes');
}
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
});
},
);
export const scaleTranspose = register('scaleTranspose', function (offset /* : number | string */, pat) {
return pat.withHap((hap) => {
if (!hap.context.scale) {
throw new Error('can only use scaleTranspose after .scale');
}
if (typeof hap.value === 'object')
return hap.withValue(() => ({
...hap.value,
note: scaleOffset(hap.context.scale, Number(offset), hap.value.note),
}));
if (typeof hap.value !== 'string') {
throw new Error('can only use scaleTranspose with notes');
}
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
});
});
/**
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/transpiler",
"version": "1.2.3",
"version": "1.2.2",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
"version": "1.2.4",
"version": "1.2.3",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs",
"publishConfig": {
-1
View File
@@ -7,5 +7,4 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './webaudio.mjs';
export * from './scope.mjs';
export * from './spectrum.mjs';
export * from './supradough.mjs';
export * from 'superdough';
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
"version": "1.2.4",
"version": "1.2.3",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
@@ -35,8 +35,7 @@
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"superdough": "workspace:*",
"supradough": "workspace:*"
"superdough": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
-130
View File
@@ -1,130 +0,0 @@
import { Pattern } from '@strudel/core';
import { connectToDestination, getAudioContext, getWorklet } from 'superdough';
let doughWorklet;
function initDoughWorklet() {
const ac = getAudioContext();
doughWorklet = getWorklet(
ac,
'dough-processor',
{},
{
outputChannelCount: [2],
},
);
connectToDestination(doughWorklet); // channels?
}
const soundMap = new Map();
const loadedSounds = new Map();
Pattern.prototype.supradough = function () {
return this.onTrigger((hap, __, cps, begin) => {
hap.value._begin = begin;
hap.value._duration = hap.duration / cps;
!doughWorklet && initDoughWorklet();
const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s;
const n = hap.value.n ?? 0;
const soundKey = `${s}:${n}`;
if (soundMap.has(s)) {
hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers)
}
if (soundMap.has(s) && !loadedSounds.has(soundKey)) {
const urls = soundMap.get(s);
const url = urls[n % urls.length];
console.log(`load ${soundKey} from ${url}`);
const loadSample = fetchSample(url);
loadedSounds.set(soundKey, loadSample);
loadSample.then(({ channels, sampleRate }) =>
doughWorklet.port.postMessage({
sample: soundKey,
channels,
sampleRate,
}),
);
}
doughWorklet.port.postMessage({ spawn: hap.value });
}, 1);
};
function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl');
}
let [_, path] = base.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
if (path.split('/').length === 2) {
// assume main as default branch if none set
path += '/main';
}
return `https://raw.githubusercontent.com/${path}/${subpath}`;
}
export async function fetchSampleMap(url) {
if (url.startsWith('github:')) {
url = githubPath(url, 'strudel.json');
}
if (url.startsWith('local:')) {
url = `http://localhost:5432`;
}
if (url.startsWith('shabda:')) {
let [_, path] = url.split('shabda:');
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
}
if (url.startsWith('shabda/speech')) {
let [_, path] = url.split('shabda/speech');
path = path.startsWith('/') ? path.substring(1) : path;
let [params, words] = path.split(':');
let gender = 'f';
let language = 'en-GB';
if (params) {
[language, gender] = params.split('/');
}
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
const base = url.split('/').slice(0, -1).join('/');
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
const json = await fetch(url)
.then((res) => res.json())
.catch((error) => {
console.error(error);
throw new Error(`error loading "${url}"`);
});
return [json, json._base || base];
}
// for some reason, only piano and flute work.. is it because mp3??
async function fetchSample(url) {
const buffer = await fetch(url)
.then((res) => res.arrayBuffer())
.then((buf) => getAudioContext().decodeAudioData(buf));
let channels = [];
for (let i = 0; i < buffer.numberOfChannels; i++) {
channels.push(buffer.getChannelData(i));
}
return { channels, sampleRate: buffer.sampleRate };
}
export async function doughsamples(sampleMap, baseUrl) {
if (typeof sampleMap === 'string') {
const [json, base] = await fetchSampleMap(sampleMap);
// console.log('json', json, 'base', base);
return doughsamples(json, base);
}
Object.entries(sampleMap).map(async ([key, urls]) => {
if (key !== '_base') {
urls = urls.map((url) => baseUrl + url);
// console.log('set', key, urls);
soundMap.set(key, urls);
}
});
}
+3 -8
View File
@@ -5,12 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as strudel from '@strudel/core';
import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough';
import './supradough.mjs';
import { workletUrl } from 'supradough';
registerWorklet(workletUrl);
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
const { Pattern, logger, repl } = strudel;
setLogger(logger);
@@ -22,8 +17,8 @@ const hap2value = (hap) => {
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
// TODO: refactor output callbacks to eliminate deadline
export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => {
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => {
return superdough(hap2value(hap), t, hapDuration, cps);
};
export function webaudioRepl(options = {}) {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/xen",
"version": "1.2.3",
"version": "1.2.2",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"type": "module",
-20
View File
@@ -517,18 +517,6 @@ importers:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
packages/supradough:
devDependencies:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
wav-encoder:
specifier: ^1.3.0
version: 1.3.0
packages/tidal:
dependencies:
'@strudel/core':
@@ -637,9 +625,6 @@ importers:
superdough:
specifier: workspace:*
version: link:../superdough
supradough:
specifier: workspace:*
version: link:../supradough
devDependencies:
vite:
specifier: ^6.0.11
@@ -7550,9 +7535,6 @@ packages:
walk-up-path@3.0.1:
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
wav-encoder@1.3.0:
resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==}
wav@1.0.2:
resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==}
@@ -15973,8 +15955,6 @@ snapshots:
walk-up-path@3.0.1: {}
wav-encoder@1.3.0: {}
wav@1.0.2:
dependencies:
buffer-alloc: 1.2.0
+13 -867
View File
@@ -1847,27 +1847,6 @@ exports[`runs examples > example "chop" example index 0 1`] = `
]
`;
exports[`runs examples > example "chorus" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]",
"[ 1/4 → 1/2 | note:d s:sawtooth chorus:0.5 ]",
"[ 1/2 → 3/4 | note:a# s:sawtooth chorus:0.5 ]",
"[ 3/4 → 1/1 | note:a s:sawtooth chorus:0.5 ]",
"[ 1/1 → 5/4 | note:d s:sawtooth chorus:0.5 ]",
"[ 5/4 → 3/2 | note:d s:sawtooth chorus:0.5 ]",
"[ 3/2 → 7/4 | note:a# s:sawtooth chorus:0.5 ]",
"[ 7/4 → 2/1 | note:a s:sawtooth chorus:0.5 ]",
"[ 2/1 → 9/4 | note:d s:sawtooth chorus:0.5 ]",
"[ 9/4 → 5/2 | note:d s:sawtooth chorus:0.5 ]",
"[ 5/2 → 11/4 | note:a# s:sawtooth chorus:0.5 ]",
"[ 11/4 → 3/1 | note:a s:sawtooth chorus:0.5 ]",
"[ 3/1 → 13/4 | note:d s:sawtooth chorus:0.5 ]",
"[ 13/4 → 7/2 | note:d s:sawtooth chorus:0.5 ]",
"[ 7/2 → 15/4 | note:a# s:sawtooth chorus:0.5 ]",
"[ 15/4 → 4/1 | note:a s:sawtooth chorus:0.5 ]",
]
`;
exports[`runs examples > example "chunk" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:A4 ]",
@@ -2606,52 +2585,6 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = `
]
`;
exports[`runs examples > example "delayfeedback" example index 0 2`] = `
[
"[ 0/1 → 1/1 | s:bd delay:0.25 delayfeedback:0.25 ]",
"[ 1/1 → 2/1 | s:bd delay:0.25 delayfeedback:0.5 ]",
"[ 2/1 → 3/1 | s:bd delay:0.25 delayfeedback:0.75 ]",
"[ 3/1 → 4/1 | s:bd delay:0.25 delayfeedback:1 ]",
]
`;
exports[`runs examples > example "delayspeed" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
]
`;
exports[`runs examples > example "delaysync" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd delay:0.25 delaysync:0.125 ]",
@@ -2665,6 +2598,19 @@ exports[`runs examples > example "delaysync" example index 0 1`] = `
]
`;
exports[`runs examples > example "delaytime" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd delay:0.25 delaytime:0.125 ]",
"[ 1/2 → 1/1 | s:bd delay:0.25 delaytime:0.125 ]",
"[ 1/1 → 3/2 | s:bd delay:0.25 delaytime:0.25 ]",
"[ 3/2 → 2/1 | s:bd delay:0.25 delaytime:0.25 ]",
"[ 2/1 → 5/2 | s:bd delay:0.25 delaytime:0.5 ]",
"[ 5/2 → 3/1 | s:bd delay:0.25 delaytime:0.5 ]",
"[ 3/1 → 7/2 | s:bd delay:0.25 delaytime:1 ]",
"[ 7/2 → 4/1 | s:bd delay:0.25 delaytime:1 ]",
]
`;
exports[`runs examples > example "density" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
@@ -3090,145 +3036,6 @@ exports[`runs examples > example "dry" example index 0 1`] = `
]
`;
exports[`runs examples > example "duckattack" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 0/1 → 1/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/8 → 1/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/4 → 3/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/8 → 1/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/2 → 5/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/8 → 3/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 3/4 → 7/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 7/8 → 1/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 1/1 → 9/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 9/8 → 5/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 5/4 → 11/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 11/8 → 3/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 3/2 → 13/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 13/8 → 7/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 7/4 → 15/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 15/8 → 2/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 2/1 → 17/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 17/8 → 9/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 9/4 → 19/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 19/8 → 5/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 5/2 → 21/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 21/8 → 11/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 11/4 → 23/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 23/8 → 3/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 3/1 → 25/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 25/8 → 13/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 13/4 → 27/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 27/8 → 7/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 7/2 → 29/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 29/8 → 15/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 15/4 → 31/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 31/8 → 4/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
]
`;
exports[`runs examples > example "duckdepth" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 0/1 → 1/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/8 → 1/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/4 → 3/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/8 → 1/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/2 → 5/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/8 → 3/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 3/4 → 7/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 7/8 → 1/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]",
"[ 1/1 → 9/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 9/8 → 5/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]",
"[ 5/4 → 11/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 11/8 → 3/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]",
"[ 3/2 → 13/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 13/8 → 7/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]",
"[ 7/4 → 15/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]",
"[ 15/8 → 2/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]",
"[ 2/1 → 17/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 17/8 → 9/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]",
"[ 9/4 → 19/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 19/8 → 5/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]",
"[ 5/2 → 21/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 21/8 → 11/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]",
"[ 11/4 → 23/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]",
"[ 23/8 → 3/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]",
"[ 3/1 → 25/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 25/8 → 13/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]",
"[ 13/4 → 27/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 27/8 → 7/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]",
"[ 7/2 → 29/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 29/8 → 15/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]",
"[ 15/4 → 31/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]",
"[ 31/8 → 4/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
]
`;
exports[`runs examples > example "duckorbit" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
]
`;
exports[`runs examples > example "duration" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c s:piano duration:0.5 ]",
@@ -3464,39 +3271,6 @@ exports[`runs examples > example "euclidRot" example index 0 1`] = `
]
`;
exports[`runs examples > example "euclidish" example index 0 1`] = `
[
"[ 0/1 → 1/12 | s:hh pan:0.5 ]",
"[ 13/84 → 5/21 | s:hh pan:0.5606253170575308 ]",
"[ 15/56 → 59/168 | s:hh pan:0.604413082836085 ]",
"[ 71/168 → 85/168 | s:hh pan:0.6629314122869361 ]",
"[ 97/168 → 37/56 | s:hh pan:0.7190455010067492 ]",
"[ 29/42 → 65/84 | s:hh pan:0.7580531369037533 ]",
"[ 71/84 → 13/14 | s:hh pan:0.8080762739548087 ]",
"[ 1/1 → 13/12 | s:hh pan:0.8535533905932737 ]",
"[ 451099417/393511398 → 322594689/262340932 | s:hh pan:0.891768001805729 ]",
"[ 335923379/262340932 → 536677685/393511398 | s:hh pan:0.9222657853371297 ]",
"[ 1122946175/787022796 → 99044284/65585233 | s:hh pan:0.9501869591788796 ]",
"[ 1238122213/787022796 → 651853723/393511398 | s:hh pan:0.9721673436944069 ]",
"[ 335923379/196755699 → 469759583/262340932 | s:hh pan:0.9868472639237561 ]",
"[ 729434777/393511398 → 1524454787/787022796 | s:hh pan:0.9967009321321423 ]",
"[ 2/1 → 25/12 | s:hh pan:1 ]",
"[ 15/7 → 187/84 | s:hh pan:0.9968561049466214 ]",
"[ 16/7 → 199/84 | s:hh pan:0.9874639560909118 ]",
"[ 17/7 → 211/84 | s:hh pan:0.9719416651541839 ]",
"[ 18/7 → 223/84 | s:hh pan:0.9504844339512095 ]",
"[ 19/7 → 235/84 | s:hh pan:0.9233620996141421 ]",
"[ 20/7 → 247/84 | s:hh pan:0.890915741234015 ]",
"[ 3/1 → 37/12 | s:hh pan:0.8535533905932737 ]",
"[ 1238122213/393511398 → 847276553/262340932 | s:hh pan:0.8106731928589048 ]",
"[ 860605243/262340932 → 1323700481/393511398 | s:hh pan:0.7677528833339 ]",
"[ 2696991767/787022796 → 230214750/65585233 | s:hh pan:0.7175585019834292 ]",
"[ 2812167805/787022796 → 1438876519/393511398 | s:hh pan:0.6644931595798675 ]",
"[ 729434777/196755699 → 994441447/262340932 | s:hh pan:0.6139286689554151 ]",
"[ 1516457573/393511398 → 3098500379/787022796 | s:hh pan:0.557342689325327 ]",
]
`;
exports[`runs examples > example "every" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:g3 ]",
@@ -3932,144 +3706,6 @@ exports[`runs examples > example "fmsustain" example index 0 1`] = `
]
`;
exports[`runs examples > example "fmwave" example index 0 1`] = `
[
"[ 0/1 → 1/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 1/16 → 1/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 1/8 → 3/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 3/16 → 1/4 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 1/4 → 5/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 5/16 → 3/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 3/8 → 7/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 7/16 → 1/2 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 1/2 → 9/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 9/16 → 5/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 5/8 → 11/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 11/16 → 3/4 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 3/4 → 13/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 13/16 → 7/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 7/8 → 15/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 15/16 → 1/1 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]",
"[ 1/1 → 17/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 17/16 → 9/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 9/8 → 19/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 19/16 → 5/4 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 5/4 → 21/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 21/16 → 11/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 11/8 → 23/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 23/16 → 3/2 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 3/2 → 25/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 25/16 → 13/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 13/8 → 27/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 27/16 → 7/4 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 7/4 → 29/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 29/16 → 15/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 15/8 → 31/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 31/16 → 2/1 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]",
"[ 2/1 → 33/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 33/16 → 17/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 17/8 → 35/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 35/16 → 9/4 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 9/4 → 37/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 37/16 → 19/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 19/8 → 39/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 39/16 → 5/2 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 5/2 → 41/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 41/16 → 21/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 21/8 → 43/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 43/16 → 11/4 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 11/4 → 45/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 45/16 → 23/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 23/8 → 47/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 47/16 → 3/1 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]",
"[ 3/1 → 49/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 49/16 → 25/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 25/8 → 51/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 51/16 → 13/4 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 13/4 → 53/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 53/16 → 27/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 27/8 → 55/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 55/16 → 7/2 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 7/2 → 57/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 57/16 → 29/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 29/8 → 59/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 59/16 → 15/4 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 15/4 → 61/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 61/16 → 31/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 31/8 → 63/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
"[ 63/16 → 4/1 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]",
]
`;
exports[`runs examples > example "fmwave" example index 1 1`] = `
[
"[ 0/1 → 1/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 1/16 → 1/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 1/8 → 3/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 3/16 → 1/4 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 1/4 → 5/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 5/16 → 3/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 3/8 → 7/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 7/16 → 1/2 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 1/2 → 9/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 9/16 → 5/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 5/8 → 11/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 11/16 → 3/4 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 3/4 → 13/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 13/16 → 7/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 7/8 → 15/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 15/16 → 1/1 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 1/1 → 17/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 17/16 → 9/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 9/8 → 19/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 19/16 → 5/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 5/4 → 21/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 21/16 → 11/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 11/8 → 23/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 23/16 → 3/2 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 3/2 → 25/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 25/16 → 13/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 13/8 → 27/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 27/16 → 7/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 7/4 → 29/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 29/16 → 15/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 15/8 → 31/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 31/16 → 2/1 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 2/1 → 33/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 33/16 → 17/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 17/8 → 35/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 35/16 → 9/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 9/4 → 37/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 37/16 → 19/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 19/8 → 39/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 39/16 → 5/2 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 5/2 → 41/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 41/16 → 21/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 21/8 → 43/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 43/16 → 11/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 11/4 → 45/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 45/16 → 23/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 23/8 → 47/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 47/16 → 3/1 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 3/1 → 49/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 49/16 → 25/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 25/8 → 51/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 51/16 → 13/4 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 13/4 → 53/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 53/16 → 27/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 27/8 → 55/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 55/16 → 7/2 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 7/2 → 57/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 57/16 → 29/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 29/8 → 59/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 59/16 → 15/4 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 15/4 → 61/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 61/16 → 31/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 31/8 → 63/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]",
"[ 63/16 → 4/1 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]",
]
`;
exports[`runs examples > example "focus" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:sd ]",
@@ -5511,40 +5147,6 @@ exports[`runs examples > example "lock" example index 0 1`] = `
]
`;
exports[`runs examples > example "log" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd ]",
"[ 1/2 → 1/1 | s:sd ]",
"[ 1/1 → 3/2 | s:bd ]",
"[ 3/2 → 2/1 | s:sd ]",
"[ 2/1 → 5/2 | s:bd ]",
"[ 5/2 → 3/1 | s:sd ]",
"[ 3/1 → 7/2 | s:bd ]",
"[ 7/2 → 4/1 | s:sd ]",
]
`;
exports[`runs examples > example "logValues" example index 0 1`] = `
[
"[ (0/1 → 1/3) ⇝ 1/2 | s:bd gain:0.25 n:2 ]",
"[ 0/1 ⇜ (1/3 → 1/2) | s:bd gain:0.5 n:1 ]",
"[ (1/2 → 2/3) ⇝ 1/1 | s:sd gain:0.5 n:1 ]",
"[ 1/2 ⇜ (2/3 → 1/1) | s:sd gain:1 n:0 ]",
"[ (1/1 → 4/3) ⇝ 3/2 | s:bd gain:0.25 n:2 ]",
"[ 1/1 ⇜ (4/3 → 3/2) | s:bd gain:0.5 n:1 ]",
"[ (3/2 → 5/3) ⇝ 2/1 | s:sd gain:0.5 n:1 ]",
"[ 3/2 ⇜ (5/3 → 2/1) | s:sd gain:1 n:0 ]",
"[ (2/1 → 7/3) ⇝ 5/2 | s:bd gain:0.25 n:2 ]",
"[ 2/1 ⇜ (7/3 → 5/2) | s:bd gain:0.5 n:1 ]",
"[ (5/2 → 8/3) ⇝ 3/1 | s:sd gain:0.5 n:1 ]",
"[ 5/2 ⇜ (8/3 → 3/1) | s:sd gain:1 n:0 ]",
"[ (3/1 → 10/3) ⇝ 7/2 | s:bd gain:0.25 n:2 ]",
"[ 3/1 ⇜ (10/3 → 7/2) | s:bd gain:0.5 n:1 ]",
"[ (7/2 → 11/3) ⇝ 4/1 | s:sd gain:0.5 n:1 ]",
"[ 7/2 ⇜ (11/3 → 4/1) | s:sd gain:1 n:0 ]",
]
`;
exports[`runs examples > example "loop" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:casio loop:1 ]",
@@ -6224,48 +5826,6 @@ exports[`runs examples > example "miditouch" example index 0 1`] = `
]
`;
exports[`runs examples > example "morph" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh ]",
"[ 25/112 → 39/112 | s:hh ]",
"[ 27/56 → 17/28 | s:hh ]",
"[ 83/112 → 97/112 | s:hh ]",
"[ 1/1 → 9/8 | s:hh ]",
"[ 137/112 → 151/112 | s:hh ]",
"[ 83/56 → 45/28 | s:hh ]",
"[ 195/112 → 209/112 | s:hh ]",
"[ 2/1 → 17/8 | s:hh ]",
"[ 249/112 → 263/112 | s:hh ]",
"[ 139/56 → 73/28 | s:hh ]",
"[ 307/112 → 321/112 | s:hh ]",
"[ 3/1 → 25/8 | s:hh ]",
"[ 361/112 → 375/112 | s:hh ]",
"[ 195/56 → 101/28 | s:hh ]",
"[ 419/112 → 433/112 | s:hh ]",
]
`;
exports[`runs examples > example "morph" example index 1 1`] = `
[
"[ 0/1 → 1/8 | s:hh ]",
"[ 11/56 → 9/28 | s:hh ]",
"[ 13/28 → 33/56 | s:hh ]",
"[ 41/56 → 6/7 | s:hh ]",
"[ 1/1 → 9/8 | s:hh ]",
"[ 303934523/262340932 → 673454279/524681864 | s:hh ]",
"[ 188758485/131170466 → 820619173/524681864 | s:hh ]",
"[ 451099417/262340932 → 967784067/524681864 | s:hh ]",
"[ 2/1 → 17/8 | s:hh ]",
"[ 15/7 → 127/56 | s:hh ]",
"[ 17/7 → 143/56 | s:hh ]",
"[ 19/7 → 159/56 | s:hh ]",
"[ 3/1 → 25/8 | s:hh ]",
"[ 828616387/262340932 → 1722818007/524681864 | s:hh ]",
"[ 451099417/131170466 → 1869982901/524681864 | s:hh ]",
"[ 975781281/262340932 → 2017147795/524681864 | s:hh ]",
]
`;
exports[`runs examples > example "mousex" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:C3 ]",
@@ -10550,420 +10110,6 @@ exports[`runs examples > example "transpose" example index 1 1`] = `
]
`;
exports[`runs examples > example "tremolo" example index 0 1`] = `
[
"[ 0/1 → 1/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 1/16 → 1/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 1/8 → 3/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 3/16 → 1/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 1/4 → 5/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 5/16 → 3/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 3/8 → 7/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 7/16 → 1/2 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 1/2 → 9/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 9/16 → 5/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 5/8 → 11/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 11/16 → 3/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 3/4 → 13/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 13/16 → 7/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 7/8 → 15/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 15/16 → 1/1 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 1/1 → 17/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 17/16 → 9/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 9/8 → 19/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 19/16 → 5/4 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 5/4 → 21/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 21/16 → 11/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 11/8 → 23/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 23/16 → 3/2 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 3/2 → 25/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 25/16 → 13/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 13/8 → 27/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 27/16 → 7/4 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 7/4 → 29/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 29/16 → 15/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 15/8 → 31/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 31/16 → 2/1 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]",
"[ 2/1 → 33/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 33/16 → 17/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 17/8 → 35/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 35/16 → 9/4 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 9/4 → 37/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 37/16 → 19/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 19/8 → 39/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 39/16 → 5/2 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 5/2 → 41/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 41/16 → 21/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 21/8 → 43/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 43/16 → 11/4 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 11/4 → 45/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 45/16 → 23/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 23/8 → 47/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 47/16 → 3/1 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]",
"[ 3/1 → 49/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 49/16 → 25/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 25/8 → 51/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 51/16 → 13/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 13/4 → 53/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 53/16 → 27/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 27/8 → 55/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 55/16 → 7/2 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 7/2 → 57/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 57/16 → 29/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 29/8 → 59/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 59/16 → 15/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 15/4 → 61/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 61/16 → 31/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 31/8 → 63/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]",
"[ 63/16 → 4/1 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]",
]
`;
exports[`runs examples > example "tremolodepth" example index 0 1`] = `
[
"[ 0/1 → 1/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 1/16 → 1/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 1/8 → 3/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 3/16 → 1/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 1/4 → 5/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 5/16 → 3/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 3/8 → 7/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 7/16 → 1/2 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 1/2 → 9/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 9/16 → 5/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 5/8 → 11/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 11/16 → 3/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 3/4 → 13/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 13/16 → 7/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 7/8 → 15/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 15/16 → 1/1 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 1/1 → 17/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 17/16 → 9/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 9/8 → 19/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 19/16 → 5/4 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 5/4 → 21/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 21/16 → 11/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 11/8 → 23/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 23/16 → 3/2 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 3/2 → 25/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 25/16 → 13/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 13/8 → 27/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 27/16 → 7/4 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 7/4 → 29/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 29/16 → 15/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 15/8 → 31/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 31/16 → 2/1 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]",
"[ 2/1 → 33/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 33/16 → 17/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 17/8 → 35/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 35/16 → 9/4 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 9/4 → 37/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 37/16 → 19/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 19/8 → 39/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 39/16 → 5/2 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 5/2 → 41/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 41/16 → 21/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 21/8 → 43/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 43/16 → 11/4 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 11/4 → 45/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 45/16 → 23/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 23/8 → 47/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 47/16 → 3/1 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]",
"[ 3/1 → 49/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 49/16 → 25/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 25/8 → 51/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 51/16 → 13/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 13/4 → 53/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 53/16 → 27/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 27/8 → 55/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 55/16 → 7/2 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 7/2 → 57/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 57/16 → 29/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 29/8 → 59/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 59/16 → 15/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 15/4 → 61/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 61/16 → 31/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 31/8 → 63/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]",
"[ 63/16 → 4/1 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]",
]
`;
exports[`runs examples > example "tremolophase" example index 0 1`] = `
[
"[ 0/1 → 1/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 1/16 → 1/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 1/8 → 3/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 3/16 → 1/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 1/4 → 5/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 5/16 → 3/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 3/8 → 7/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 7/16 → 1/2 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 1/2 → 9/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 9/16 → 5/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 5/8 → 11/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 11/16 → 3/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 3/4 → 13/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 13/16 → 7/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 7/8 → 15/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 15/16 → 1/1 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 1/1 → 17/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 17/16 → 9/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 9/8 → 19/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 19/16 → 5/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 5/4 → 21/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 21/16 → 11/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 11/8 → 23/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 23/16 → 3/2 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 3/2 → 25/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 25/16 → 13/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 13/8 → 27/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 27/16 → 7/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 7/4 → 29/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 29/16 → 15/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 15/8 → 31/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 31/16 → 2/1 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]",
"[ 2/1 → 33/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 33/16 → 17/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 17/8 → 35/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 35/16 → 9/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 9/4 → 37/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 37/16 → 19/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 19/8 → 39/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 39/16 → 5/2 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 5/2 → 41/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 41/16 → 21/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 21/8 → 43/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 43/16 → 11/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 11/4 → 45/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 45/16 → 23/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 23/8 → 47/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 47/16 → 3/1 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]",
"[ 3/1 → 49/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 49/16 → 25/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 25/8 → 51/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 51/16 → 13/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 13/4 → 53/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 53/16 → 27/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 27/8 → 55/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 55/16 → 7/2 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 7/2 → 57/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 57/16 → 29/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 29/8 → 59/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 59/16 → 15/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 15/4 → 61/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 61/16 → 31/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 31/8 → 63/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]",
"[ 63/16 → 4/1 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]",
]
`;
exports[`runs examples > example "tremoloshape" example index 0 1`] = `
[
"[ 0/1 → 1/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 1/16 → 1/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 1/8 → 3/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 3/16 → 1/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 1/4 → 5/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 5/16 → 3/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 3/8 → 7/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 7/16 → 1/2 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 1/2 → 9/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 9/16 → 5/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 5/8 → 11/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 11/16 → 3/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 3/4 → 13/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 13/16 → 7/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 7/8 → 15/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 15/16 → 1/1 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 1/1 → 17/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 17/16 → 9/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 9/8 → 19/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 19/16 → 5/4 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 5/4 → 21/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 21/16 → 11/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 11/8 → 23/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 23/16 → 3/2 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 3/2 → 25/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 25/16 → 13/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 13/8 → 27/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 27/16 → 7/4 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 7/4 → 29/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 29/16 → 15/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 15/8 → 31/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 31/16 → 2/1 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]",
"[ 2/1 → 33/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 33/16 → 17/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 17/8 → 35/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 35/16 → 9/4 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 9/4 → 37/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 37/16 → 19/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 19/8 → 39/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 39/16 → 5/2 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 5/2 → 41/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 41/16 → 21/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 21/8 → 43/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 43/16 → 11/4 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 11/4 → 45/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 45/16 → 23/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 23/8 → 47/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 47/16 → 3/1 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]",
"[ 3/1 → 49/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 49/16 → 25/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 25/8 → 51/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 51/16 → 13/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 13/4 → 53/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 53/16 → 27/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 27/8 → 55/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 55/16 → 7/2 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 7/2 → 57/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 57/16 → 29/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 29/8 → 59/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 59/16 → 15/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 15/4 → 61/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 61/16 → 31/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 31/8 → 63/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]",
"[ 63/16 → 4/1 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]",
]
`;
exports[`runs examples > example "tremoloskew" example index 0 1`] = `
[
"[ 0/1 → 1/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 1/16 → 1/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 1/8 → 3/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 3/16 → 1/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 1/4 → 5/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 5/16 → 3/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 3/8 → 7/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 7/16 → 1/2 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 1/2 → 9/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 9/16 → 5/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 5/8 → 11/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 11/16 → 3/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 3/4 → 13/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 13/16 → 7/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 7/8 → 15/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 15/16 → 1/1 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 1/1 → 17/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 17/16 → 9/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 9/8 → 19/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 19/16 → 5/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 5/4 → 21/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 21/16 → 11/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 11/8 → 23/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 23/16 → 3/2 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 3/2 → 25/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 25/16 → 13/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 13/8 → 27/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 27/16 → 7/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 7/4 → 29/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 29/16 → 15/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 15/8 → 31/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 31/16 → 2/1 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]",
"[ 2/1 → 33/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 33/16 → 17/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 17/8 → 35/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 35/16 → 9/4 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 9/4 → 37/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 37/16 → 19/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 19/8 → 39/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 39/16 → 5/2 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 5/2 → 41/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 41/16 → 21/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 21/8 → 43/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 43/16 → 11/4 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 11/4 → 45/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 45/16 → 23/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 23/8 → 47/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 47/16 → 3/1 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]",
"[ 3/1 → 49/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 49/16 → 25/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 25/8 → 51/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 51/16 → 13/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 13/4 → 53/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 53/16 → 27/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 27/8 → 55/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 55/16 → 7/2 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 7/2 → 57/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 57/16 → 29/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 29/8 → 59/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 59/16 → 15/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 15/4 → 61/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 61/16 → 31/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 31/8 → 63/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
"[ 63/16 → 4/1 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]",
]
`;
exports[`runs examples > example "tremolosync" example index 0 1`] = `
[
"[ 0/1 → 1/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 1/16 → 1/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 1/8 → 3/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 3/16 → 1/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 1/4 → 5/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 5/16 → 3/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 3/8 → 7/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 7/16 → 1/2 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 1/2 → 9/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 9/16 → 5/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 5/8 → 11/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 11/16 → 3/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 3/4 → 13/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 13/16 → 7/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 7/8 → 15/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 15/16 → 1/1 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 1/1 → 17/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 17/16 → 9/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 9/8 → 19/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 19/16 → 5/4 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 5/4 → 21/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 21/16 → 11/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 11/8 → 23/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 23/16 → 3/2 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 3/2 → 25/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 25/16 → 13/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 13/8 → 27/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 27/16 → 7/4 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 7/4 → 29/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 29/16 → 15/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 15/8 → 31/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 31/16 → 2/1 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]",
"[ 2/1 → 33/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 33/16 → 17/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 17/8 → 35/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 35/16 → 9/4 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 9/4 → 37/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 37/16 → 19/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 19/8 → 39/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 39/16 → 5/2 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 5/2 → 41/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 41/16 → 21/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 21/8 → 43/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 43/16 → 11/4 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 11/4 → 45/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 45/16 → 23/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 23/8 → 47/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 47/16 → 3/1 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]",
"[ 3/1 → 49/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 49/16 → 25/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 25/8 → 51/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 51/16 → 13/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 13/4 → 53/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 53/16 → 27/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 27/8 → 55/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 55/16 → 7/2 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 7/2 → 57/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 57/16 → 29/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 29/8 → 59/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 59/16 → 15/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 15/4 → 61/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 61/16 → 31/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 31/8 → 63/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]",
"[ 63/16 → 4/1 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]",
]
`;
exports[`runs examples > example "tri" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:C3 ]",
+2 -1
View File
@@ -8,7 +8,8 @@
"start": "astro dev",
"build": "astro build",
"preview": "astro preview --port 3009 --host 0.0.0.0",
"astro": "astro"
"astro": "astro",
"postinstall": "cp node_modules/hs2js/dist/tree-sitter.wasm public && cp node_modules/hs2js/dist/tree-sitter-haskell.wasm public"
},
"dependencies": {
"@algolia/client-search": "^5.20.0",
+17
View File
@@ -0,0 +1,17 @@
{
"_base": "https://raw.githubusercontent.com/ritchse/tidal-drum-machines/main/machines/EmuSP12/",
"bd": ["emusp12-bd/Bassdrum-01.wav","emusp12-bd/Bassdrum-02.wav","emusp12-bd/Bassdrum-03.wav","emusp12-bd/Bassdrum-04.wav","emusp12-bd/Bassdrum-05.wav","emusp12-bd/Bassdrum-06.wav","emusp12-bd/Bassdrum-07.wav","emusp12-bd/Bassdrum-08.wav","emusp12-bd/Bassdrum-09.wav","emusp12-bd/Bassdrum-10.wav","emusp12-bd/Bassdrum-11.wav","emusp12-bd/Bassdrum-12.wav","emusp12-bd/Bassdrum-13.wav","emusp12-bd/Bassdrum-14.wav"],
"cb": ["emusp12-cb/Cowbell.wav"],
"cp": ["emusp12-cp/Clap.wav"],
"cr": ["emusp12-cr/Crash.wav"],
"hh": ["emusp12-hh/Hat Closed-01.wav","emusp12-hh/Hat Closed-02.wav"],
"ht": ["emusp12-ht/Tom H-01.wav","emusp12-ht/Tom H-02.wav","emusp12-ht/Tom H-03.wav","emusp12-ht/Tom H-04.wav","emusp12-ht/Tom H-05.wav","emusp12-ht/Tom H-06.wav"],
"lt": ["emusp12-lt/Tom L-01.wav","emusp12-lt/Tom L-02.wav","emusp12-lt/Tom L-03.wav","emusp12-lt/Tom L-04.wav","emusp12-lt/Tom L-05.wav","emusp12-lt/Tom L-06.wav"],
"misc": ["emusp12-misc/Metal-01.wav","emusp12-misc/Metal-02.wav","emusp12-misc/Metal-03.wav","emusp12-misc/Scratch.wav","emusp12-misc/Shot-01.wav","emusp12-misc/Shot-02.wav","emusp12-misc/Shot-03.wav"],
"mt": ["emusp12-mt/Tom M-01.wav","emusp12-mt/Tom M-02.wav","emusp12-mt/Tom M-03.wav","emusp12-mt/Tom M-05.wav"],
"oh": ["emusp12-oh/Hhopen1.wav"],
"perc": ["emusp12-perc/Blow1.wav"],
"rd": ["emusp12-rd/Ride.wav"],
"rim": ["emusp12-rim/zRim Shot-01.wav","emusp12-rim/zRim Shot-02.wav"],
"sd": ["emusp12-sd/Snaredrum-01.wav","emusp12-sd/Snaredrum-02.wav","emusp12-sd/Snaredrum-03.wav","emusp12-sd/Snaredrum-04.wav","emusp12-sd/Snaredrum-05.wav","emusp12-sd/Snaredrum-06.wav","emusp12-sd/Snaredrum-07.wav","emusp12-sd/Snaredrum-08.wav","emusp12-sd/Snaredrum-09.wav","emusp12-sd/Snaredrum-10.wav","emusp12-sd/Snaredrum-11.wav","emusp12-sd/Snaredrum-12.wav","emusp12-sd/Snaredrum-13.wav","emusp12-sd/Snaredrum-14.wav","emusp12-sd/Snaredrum-15.wav","emusp12-sd/Snaredrum-16.wav","emusp12-sd/Snaredrum-17.wav","emusp12-sd/Snaredrum-18.wav","emusp12-sd/Snaredrum-19.wav","emusp12-sd/Snaredrum-20.wav","emusp12-sd/Snaredrum-21.wav"]
}
-76
View File
@@ -1,76 +0,0 @@
{
"_base": "https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/",
"bd": [
"bd/10_bd_switchangel.wav",
"bd/11_bd_mot4i.wav",
"bd/12_bd_mot4i.wav",
"bd/13_bd_mot4i.wav",
"bd/14_bd_switchangel.wav",
"bd/15_bd_switchangel.wav",
"bd/16_bd_switchangel.wav",
"bd/17_bd_switchangel.wav"
],
"brk": [
"brk/10_break_amen_pprocessed.wav"
],
"cb": [
"cb/10_perc_switchangel.wav"
],
"cp": [
"cp/10_cp_switchangel.wav",
"cp/11_cp_mot4i.wav"
],
"cr": [
"cr/10_cr_switchangel.wav",
"cr/11_cr_mot4i.wav"
],
"hh": [
"hh/10_hh_switchangel.wav",
"hh/11_hh_mot4i.wav",
"hh/12_hh_switchangel.wav",
"hh/13_hh_switchangel.wav",
"hh/14_hh_mot4i.wav"
],
"ht": [
"ht/10_ht_mot4i.wav"
],
"lt": [
"lt/10_lt_mot4i.wav"
],
"misc": [
"misc/10_misc_switchangel_ludens.wav",
"misc/11_misc_switchangel_ludens.wav",
"misc/12_misc_switchangel_ludens.wav",
"misc/13_misc_switchangel_ludens.wav",
"misc/14_misc_switchangel_ludens.wav"
],
"mt": [
"mt/10_mt_mot4i.wav"
],
"oh": [
"oh/10_oh_switchangel.wav",
"oh/11_oh_switchangel.wav",
"oh/12_oh_switchangel.wav",
"oh/13_oh_switchangel.wav"
],
"rd": [
"rd/10_rd_switchangel.wav"
],
"rim": [
"rim/10_rim_switchangel.wav",
"rim/11_rim_switch_angel.wav"
],
"sd": [
"sd/10_sd_switchangel-bounce-2.wav",
"sd/11_sd_switchangel_3.wav",
"sd/12_sd_switchangel_2.wav",
"sd/13_sd_switchangel_2.wav",
"sd/14_sd.wav"
],
"sh": [
"sh/10_sh_switchangel.wav"
],
"tb": [
"tb/10_tb.wav"
]
}
@@ -34,11 +34,11 @@ import { JsDoc } from '../../docs/JsDoc';
## arp
<JsDoc client:idle name="arp" h={0} />
<JsDoc client:idle name="Pattern#arp" h={0} />
## arpWith 🧪
<JsDoc client:idle name="arpWith" h={0} />
<JsDoc client:idle name="Pattern#arpWith" h={0} />
## struct
@@ -58,7 +58,7 @@ import { JsDoc } from '../../docs/JsDoc';
## hush
<JsDoc client:idle name="Pattern#hush" h={0} />
<JsDoc client:idle name="hush" h={0} />
## invert
-42
View File
@@ -57,34 +57,6 @@ Each filter has 2 parameters:
<JsDoc client:idle name="vowel" h={0} />
# Amplitude Modulation
Amplitude modulation changes the amplitude (gain) periodically over time.
## am
<JsDoc client:idle name="am" h={0} />
## tremolosync
<JsDoc client:idle name="tremolosync" h={0} />
## tremolodepth
<JsDoc client:idle name="tremolodepth" h={0} />
## tremoloskew
<JsDoc client:idle name="tremoloskew" h={0} />
## tremolophase
<JsDoc client:idle name="tremolophase" h={0} />
## tremoloshape
<JsDoc client:idle name="tremoloshape" h={0} />
# Amplitude Envelope
The amplitude [envelope](<https://en.wikipedia.org/wiki/Envelope_(music)>) controls the dynamic contour of a sound.
@@ -339,18 +311,4 @@ global effects use the same chain for all events of the same orbit:
<JsDoc client:idle name="phasersweep" h={0} />
## Duck
### duckorbit
<JsDoc client:idle name="duckorbit" h={0} />
### duckattack
<JsDoc client:idle name="duckattack" h={0} />
### duckdepth
<JsDoc client:idle name="duckdepth" h={0} />
Next, we'll look at input / output via [MIDI, OSC and other methods](/learn/input-output).
@@ -176,16 +176,3 @@ $ chord <Dm9!3 Db7> # voicing
/>
The `$` sign is an alias for `,` so it will create a stack behind the scenes.
## variables
using the `def` keyword, you can define variables:
<MiniRepl
client:idle
mondo
tune={`
$ def melody [0 1 2 3]
$ n melody # scale C:minor
`}
/>
-14
View File
@@ -178,16 +178,6 @@ the version number).
It is also possible, of course, to just remove it from cache (deleting cache in browser Privacy settings,
or from the dev console if you're technically minded, or by using a cache deleting extension).
## Generating strudel.json
You can use [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler) to generate a strudel.json file for you, by running:
```sh
npx --yes @strudel/sampler --json > strudel.json
```
See other uses of strudel/sampler further below, under "From Disk via @strudel/sampler".
## Github Shortcut
Because loading samples from github is common, there is a shortcut:
@@ -371,10 +361,6 @@ Sampler effects are functions that can be used to change the behaviour of sample
<JsDoc client:idle name="splice" h={0} />
### scrub
<JsDoc client:idle name="Pattern.scrub" h={0} />{' '}
### speed
<JsDoc client:idle name="speed" h={0} />
-130
View File
@@ -69,133 +69,3 @@
text-decoration: underline 0.18rem;
text-underline-offset: 0.22rem;
}
/* Override default styles from the codemirror inline css for autocomplete info tooltip*/
.cm-tooltip.cm-completionInfo {
padding: 12px !important;
padding-bottom: 12px !important;
border: 1px solid var(--foreground) !important;
border-radius: 4px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
max-width: 500px !important;
min-width: 300px !important;
max-height: 400px !important;
white-space: normal !important;
overflow: auto !important;
background-color: var(--lineHighlight) !important;
}
/* Main tooltip container */
.autocomplete-info-tooltip {
border-radius: 4px !important;
color: var(--foreground);
font-family: var(--font-family, 'SF Mono', 'Monaco', monospace);
font-size: var(--font-size, 13px);
line-height: 1.4;
max-width: 600px;
max-height: 400px;
min-width: 400px;
}
.autocomplete-info-function-name {
font-size: 15px;
font-weight: 600;
color: var(--foreground);
margin: 0 0 8px 0;
}
.autocomplete-info-function-description {
margin: 0 0 12px 0;
color: var(--foreground);
line-height: 1.5;
opacity: 0.8;
}
.autocomplete-info-section-title {
font-size: 12px;
font-weight: 600;
color: var(--foreground);
margin: 16px 0 6px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.autocomplete-info-section-title:first-child {
margin-top: 0;
}
.autocomplete-info-params-section {
margin-top: 12px;
}
.autocomplete-info-params-list {
list-style: none;
margin: 0;
padding: 0;
}
.autocomplete-info-param-item {
margin-bottom: 8px;
padding: 8px;
background-color: var(--lineBackground);
border-radius: 3px;
border-left: 2px solid var(--foreground, #555);
}
.autocomplete-info-param-item:last-child {
margin-bottom: 0;
}
.autocomplete-info-param-name {
font-weight: 600;
color: var(--variable, var(--foreground));
margin-right: 8px;
}
.autocomplete-info-param-type {
color: var(--comment);
font-size: 12px;
background-color: var(--gutterForeground);
padding: 1px 4px;
border-radius: 2px;
}
.autocomplete-info-param-desc {
color: var(--foreground);
font-size: 10px;
margin-top: 4px;
line-height: 1.4;
opacity: 0.7;
}
.autocomplete-info-examples-section {
margin-top: 12px;
}
.autocomplete-info-example-code {
background: var(--lineBackground);
color: var(--foreground);
padding: 8px;
border-radius: 3px;
font-family: var(--font-family, 'SF Mono', 'Monaco', monospace);
font-size: 12px;
line-height: 1.5;
margin: 4px 0;
overflow-x: auto;
white-space: pre;
border: 1px solid var(--foreground, #3a3a3a);
}
.autocomplete-info-tooltip::-webkit-scrollbar {
width: 4px;
}
.autocomplete-info-tooltip::-webkit-scrollbar-track {
}
.autocomplete-info-tooltip::-webkit-scrollbar-thumb {
border-radius: 2px;
}
.autocomplete-info-tooltip::-webkit-scrollbar-thumb:hover {
}
@@ -125,7 +125,7 @@ function UserPatterns({ context }) {
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,text/x-markdown,application/json"
accept="text/plain,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
import
@@ -108,6 +108,7 @@ export function SettingsTab({ started }) {
audioEngineTarget,
togglePanelTrigger,
maxPolyphony,
latency,
multiChannelOrbits,
isTabIndentationEnabled,
isMultiCursorEnabled,
@@ -165,6 +166,24 @@ export function SettingsTab({ started }) {
value={maxPolyphony ?? ''}
/>
</FormItem>
<FormItem label="Latency">
<Textbox
min={0}
max={Infinity}
onBlur={(e) => {
let v = parseFloat(e.target.value);
v = isNaN(v) ? 0.1 : v;
settingsMap.setKey('latency', v);
}}
onChange={(v) => {
v = parseFloat(v);
settingsMap.setKey('latency', isNaN(v) ? undefined : v);
}}
type="number"
placeholder=""
value={latency ?? 0.1}
/>
</FormItem>
<FormItem>
<Checkbox
label="Multi Channel Orbits"
+1 -4
View File
@@ -28,10 +28,7 @@ export async function prebake() {
prebake: true,
tag: 'drum-machines',
}),
samples(`${baseNoTrailing}/uzu-drumkit.json`, undefined, {
prebake: true,
tag: 'drum-machines',
}),
samples(`${baseNoTrailing}/EmuSP12.json`, undefined, { prebake: true, tag: 'drum-machines' }),
samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }),
samples(
{
+2 -1
View File
@@ -37,7 +37,7 @@ import './Repl.css';
import { setInterval, clearInterval } from 'worker-timers';
import { getMetadata } from '../metadata_parser';
const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get();
const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits, latency } = settingsMap.get();
let modulesLoading, presets, drawContext, clearCanvas, audioReady;
if (typeof window !== 'undefined') {
@@ -73,6 +73,7 @@ export function useReplContext() {
const drawContext = getDrawContext();
const editor = new StrudelMirror({
sync: isSyncEnabled,
latency,
defaultOutput,
getTime,
setInterval,
+1 -1
View File
@@ -197,7 +197,7 @@ export async function importPatterns(fileList) {
if (file.type === 'application/json') {
const userPatterns = userPattern.getAll();
setUserPatterns({ ...userPatterns, ...parseJSON(content) });
} else if (['text/x-markdown', 'text/plain'].includes(file.type)) {
} else if (file.type === 'text/plain') {
const id = file.name.replace(/\.[^/.]+$/, '');
userPattern.update(id, { code: content });
}