diff --git a/eslint.config.mjs b/eslint.config.mjs
index 19d9bb39..e30b8e8a 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -83,4 +83,14 @@ export default [
],
},
},
+ {
+ // Properties provided by AudioWorkletGlobalScope
+ files: ['packages/superdough/worklets.mjs'],
+ languageOptions: {
+ globals: {
+ currentTime: 'readonly',
+ sampleRate: 'readonly',
+ },
+ },
+ },
];
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index b597bbd4..e6181d45 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -541,28 +541,40 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
*
*/
-// TODO: SUPRADOUGH implement post orbit "pump" sidechain effect
-// /**
-// * modulate the amplitude of an orbit to create a "sidechain" like effect
-// *
-// * @name pump
-// * @param {number | Pattern} speed modulation speed in cycles
-// * @example
-// * note("{f g c d}%16").s("sawtooth").pump(".25:.75")
-// *
-// */
-// export const { pump } = registerControl(['pump', 'pumpdepth']);
+/**
+ * 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');
-// /**
-// * modulate the amplitude of an orbit to create a "sidechain" like effect
-// *
-// * @name pumpdepth
-// * @param {number | Pattern} depth depth of modulation from 0 to 1
-// * @example
-// * note("{f g c d}%16").s("sawtooth").pump(".25").depth("<.25 .5 .75 1>")
-// *
-// */
-// export const { pumpdepth } = registerControl('pumpdepth');
+/**
+ * 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');
diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs
index 25e12b07..44ab07f1 100644
--- a/packages/core/euclid.mjs
+++ b/packages/core/euclid.mjs
@@ -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 .
*/
-import { timeCat, register, silence } from './pattern.mjs';
+import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs';
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
import Fraction, { lcm } from './fraction.mjs';
@@ -196,3 +196,26 @@ 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);
+});
diff --git a/packages/core/hap.mjs b/packages/core/hap.mjs
index a6e3c55a..5f820d64 100644
--- a/packages/core/hap.mjs
+++ b/packages/core/hap.mjs
@@ -4,6 +4,7 @@ Copyright (C) 2022 Strudel contributors - see .
*/
import Fraction from './fraction.mjs';
+import { stringifyValues } from './util.mjs';
export class Hap {
/*
@@ -148,13 +149,7 @@ export class Hap {
}
showWhole(compact = false) {
- 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
- }`;
+ return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`;
}
combineContext(b) {
diff --git a/packages/core/logger.mjs b/packages/core/logger.mjs
index 635505c5..4f200231 100644
--- a/packages/core/logger.mjs
+++ b/packages/core/logger.mjs
@@ -6,7 +6,7 @@ let debounce = 1000,
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
- // console.error(e)
+ // console.error(e);
logger(`[${origin}] error: ${e.message}`);
}
diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs
index 4168645b..f1ebbe3e 100644
--- a/packages/core/pattern.mjs
+++ b/packages/core/pattern.mjs
@@ -21,6 +21,8 @@ import {
numeralArgs,
parseNumeral,
pairs,
+ zipWith,
+ stringifyValues,
} from './util.mjs';
import drawLine from './drawLine.mjs';
import { logger } from './logger.mjs';
@@ -852,14 +854,29 @@ export class Pattern {
);
}
- log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) {
+ /**
+ * 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 })) {
return this.onTrigger((...args) => {
logger(func(...args), undefined, getData(...args));
}, false);
}
- logValues(func = id) {
- return this.log((_, hap) => func(hap.value));
+ /**
+ * 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));
}
//////////////////////////////////////////////////////////////////////
@@ -3400,3 +3417,69 @@ 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))));
+};
diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs
index 93c4168c..1df5c877 100644
--- a/packages/core/test/pattern.test.mjs
+++ b/packages/core/test/pattern.test.mjs
@@ -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 } from 'vitest';
+import { describe, it, expect, vi } from 'vitest';
import {
TimeSpan,
@@ -55,6 +55,8 @@ import {
expand,
} from '../index.mjs';
+import { log, logValues } from '../pattern.mjs';
+
import { steady } from '../signal.mjs';
import { n, s } from '../controls.mjs';
@@ -1306,4 +1308,40 @@ 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();
+ });
+ });
});
diff --git a/packages/core/timespan.mjs b/packages/core/timespan.mjs
index 0dbc74fc..446156bb 100644
--- a/packages/core/timespan.mjs
+++ b/packages/core/timespan.mjs
@@ -72,7 +72,7 @@ export class TimeSpan {
}
intersection(other) {
- // Intersection of two timespans, returns None if they don't intersect.
+ // Intersection of two timespans, returns undefined if they don't intersect.
const intersect_begin = this.begin.max(other.begin);
const intersect_end = this.end.min(other.end);
diff --git a/packages/core/util.mjs b/packages/core/util.mjs
index 756fac8e..2e7c6e02 100644
--- a/packages/core/util.mjs
+++ b/packages/core/util.mjs
@@ -487,3 +487,13 @@ 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;
+}
diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs
index 63643ebb..c09acf1a 100644
--- a/packages/superdough/helpers.mjs
+++ b/packages/superdough/helpers.mjs
@@ -209,7 +209,8 @@ 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 = audioContext.createConstantSource();
+ const constantNode = new ConstantSourceNode(audioContext);
+
constantNode.start(startTime);
constantNode.stop(stopTime);
constantNode.onended = () => {
@@ -264,7 +265,7 @@ export function applyFM(param, value, begin) {
modulator = fmmod.node;
stop = fmmod.stop;
- if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
+ if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) {
// no envelope by default
modulator.connect(param);
} else {
diff --git a/packages/superdough/logger.mjs b/packages/superdough/logger.mjs
index a20af1b3..b3c9c34f 100644
--- a/packages/superdough/logger.mjs
+++ b/packages/superdough/logger.mjs
@@ -1,5 +1,11 @@
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) => {
diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs
index 7d90eb15..eb1466d9 100644
--- a/packages/superdough/superdough.mjs
+++ b/packages/superdough/superdough.mjs
@@ -9,9 +9,9 @@ import './reverb.mjs';
import './vowel.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
-import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
+import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs';
import { map } from 'nanostores';
-import { logger } from './logger.mjs';
+import { logger, errorLogger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
export const DEFAULT_MAX_POLYPHONY = 128;
@@ -331,16 +331,18 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = clamp(delayfeedback, 0, 0.98);
- if (!delays[orbit]) {
+ if (!orbits[orbit].delayNode) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
- connectToDestination(dly, channels);
- delays[orbit] = dly;
+ connectToOrbit(dly, orbit);
+ orbits[orbit].delayNode = dly;
}
- 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];
+ orbits[orbit].delayNode.delayTime.value !== delaytime &&
+ orbits[orbit].delayNode.delayTime.setValueAtTime(delaytime, t);
+ orbits[orbit].delayNode.feedback.value !== delayfeedback &&
+ orbits[orbit].delayNode.feedback.setValueAtTime(delayfeedback, t);
+ return orbits[orbit].delayNode;
}
export function getLfo(audioContext, begin, end, properties = {}) {
@@ -365,22 +367,6 @@ export function getLfo(audioContext, begin, end, properties = {}) {
return getWorklet(audioContext, 'lfo-processor', lfoprops);
}
-export function getSyncedLfo(audioContext, time, end, cps, cycle, properties = {}) {
- const frequency = cycle / cps;
-
- return getWorklet(audioContext, 'lfo-processor', {
- frequency,
- depth: 1,
- skew: 0,
- phaseoffset: 0,
- time,
- end,
- shape: 1,
- dcoffset: -0.5,
- ...properties,
- });
-}
-
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
const ac = getAudioContext();
const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
@@ -412,31 +398,73 @@ function getFilterType(ftype) {
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
}
-let reverbs = {};
+//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 hasChanged = (now, before) => now !== undefined && now !== before;
-function getReverb(orbit, duration, fade, lp, dim, ir, channels) {
+function getReverb(orbit, duration, fade, lp, dim, ir) {
// If no reverb has been created for a given orbit, create one
- if (!reverbs[orbit]) {
+ if (!orbits[orbit].reverbNode) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
- connectToDestination(reverb, channels);
- reverbs[orbit] = reverb;
+ connectToOrbit(reverb, orbit);
+ orbits[orbit].reverbNode = reverb;
}
if (
- hasChanged(duration, reverbs[orbit].duration) ||
- hasChanged(fade, reverbs[orbit].fade) ||
- hasChanged(lp, reverbs[orbit].lp) ||
- hasChanged(dim, reverbs[orbit].dim) ||
- reverbs[orbit].ir !== ir
+ 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
) {
// 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
- reverbs[orbit].generate(duration, fade, lp, dim, ir);
+ orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir);
}
- return reverbs[orbit];
+ return orbits[orbit].reverbNode;
}
export let analysers = {},
@@ -479,8 +507,7 @@ function effectSend(input, effect, wet) {
}
export function resetGlobalEffects() {
- delays = {};
- reverbs = {};
+ orbits = {};
analysers = {};
analysersData = {};
}
@@ -533,6 +560,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
gain = getDefaultValue('gain'),
postgain = getDefaultValue('postgain'),
density = getDefaultValue('density'),
+ duckorbit,
+ duckattack,
+ duckdepth,
// filters
fanchor = getDefaultValue('fanchor'),
drive = 0.69,
@@ -571,6 +601,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
coarse,
crush,
+ dry,
shape,
shapevol = getDefaultValue('shapevol'),
distort,
@@ -604,7 +635,13 @@ 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);
@@ -791,7 +828,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// last gain
const post = new GainNode(ac, { gain: postgain });
chain.push(post);
- connectToDestination(post, channels);
// delay
let delaySend;
@@ -826,6 +862,14 @@ 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]);
diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs
index 4e876caf..71138ae4 100644
--- a/packages/superdough/synth.mjs
+++ b/packages/superdough/synth.mjs
@@ -201,10 +201,7 @@ 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);
- // 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
+ const fm = applyFM(o.parameters.get('frequency'), value, begin);
let envGain = gainNode(1);
envGain = o.connect(envGain);
@@ -216,7 +213,7 @@ export function registerSynthSounds() {
destroyAudioWorkletNode(o);
envGain.disconnect();
onended();
- // fm?.stop();
+ fm?.stop();
vibratoOscillator?.stop();
},
begin,
diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs
index e7928a48..2406d56d 100644
--- a/packages/superdough/worklets.mjs
+++ b/packages/superdough/worklets.mjs
@@ -8,18 +8,28 @@ 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;
-// adjust waveshape to remove frequencies above nyquist to prevent aliasing
+// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
function polyBlep(phase, dt) {
- // 0 <= phase < 1
+ dt = Math.min(dt, 1 - dt);
+ // Start of cycle
if (phase < dt) {
phase /= dt;
// 2 * (phase - phase^2/2 - 0.5)
return phase + phase - phase * phase - 1;
}
- // -1 < phase < 0
+ // End of cycle
else if (phase > 1 - dt) {
phase = (phase - 1) / dt;
// 2 * (phase^2/2 + phase + 0.5)
@@ -115,7 +125,6 @@ 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;
}
@@ -143,7 +152,6 @@ 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++) {
@@ -305,7 +313,6 @@ 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;
@@ -438,18 +445,13 @@ 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];
@@ -460,9 +462,6 @@ 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
@@ -470,21 +469,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] += dt;
-
- if (this.phase[n] > 1.0) {
- this.phase[n] = this.phase[n] - 1;
- }
+ this.phase[n] = wrapPhase(this.phase[n] + dt);
}
}
return true;
@@ -493,7 +492,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
-// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
+// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
const BUFFERED_BLOCK_SIZE = 2048;
function genHannWindow(length) {
diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap
index eb6c238d..39874935 100644
--- a/test/__snapshots__/examples.test.mjs.snap
+++ b/test/__snapshots__/examples.test.mjs.snap
@@ -3036,6 +3036,145 @@ 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 ]",
@@ -3271,6 +3410,39 @@ 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 ]",
@@ -5285,6 +5457,40 @@ 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 ]",
@@ -5964,6 +6170,48 @@ 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 ]",