Merge pull request 'supradough poc' (#1362) from supradough into main

Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1362
This commit is contained in:
Switch Angel AKA Jade Rose
2025-09-11 22:52:29 +02:00
17 changed files with 1493 additions and 63 deletions
+1
View File
@@ -42,6 +42,7 @@ export default [
'**/hydra.mjs',
'**/jsdoc-synonyms.js',
'packages/hs2js/src/hs2js.mjs',
'packages/supradough/dough-export.mjs',
'**/samples',
],
},
+27 -3
View File
@@ -309,6 +309,17 @@ 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
@@ -1097,14 +1108,27 @@ 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 delaytime
* @param {number | Pattern} seconds between 0 and Infinity
* @name delayspeed
* @param {number | Pattern} delayspeed controls the pitch of the delay feedback
* @synonyms delayt, dt
* @example
* s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
*
*/
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
+48 -43
View File
@@ -196,6 +196,52 @@ 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
@@ -217,49 +263,8 @@ function getSamplesPrefixHandler(url) {
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
if (typeof sampleMap === 'string') {
// 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 [json, base] = await fetchSampleMap(sampleMap);
return samples(json, baseUrl || base, options);
}
const { prebake, tag } = options;
processSampleMap(
+8 -2
View File
@@ -196,7 +196,7 @@ export const resetLoadedSounds = () => soundMap.set({});
let audioContext;
export const setDefaultAudioContext = () => {
audioContext = new AudioContext();
audioContext = new AudioContext({ latencyHint: 'playback' });
return audioContext;
};
@@ -212,11 +212,17 @@ export function getAudioContextCurrentTime() {
return getAudioContext().currentTime;
}
let externalWorklets = [];
export function registerWorklet(url) {
externalWorklets.push(url);
}
let workletsLoading;
function loadWorklets() {
if (!workletsLoading) {
const audioCtx = getAudioContext();
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
const allWorkletURLs = externalWorklets.concat([workletsUrl]);
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL)));
}
return workletsLoading;
+1
View File
@@ -0,0 +1 @@
pattern.wav
+3
View File
@@ -0,0 +1,3 @@
# supradough
platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.
+123
View File
@@ -0,0 +1,123 @@
// 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
@@ -0,0 +1,39 @@
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
@@ -0,0 +1,976 @@
// 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
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,37 @@
{
"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
View File
@@ -7,4 +7,5 @@ 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 -1
View File
@@ -35,7 +35,8 @@
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"superdough": "workspace:*"
"superdough": "workspace:*",
"supradough": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
+130
View File
@@ -0,0 +1,130 @@
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);
}
});
}
+6 -1
View File
@@ -5,7 +5,12 @@ 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 } from 'superdough';
import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough';
import './supradough.mjs';
import { workletUrl } from 'supradough';
registerWorklet(workletUrl);
const { Pattern, logger, repl } = strudel;
setLogger(logger);
+20
View File
@@ -517,6 +517,18 @@ 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':
@@ -625,6 +637,9 @@ importers:
superdough:
specifier: workspace:*
version: link:../superdough
supradough:
specifier: workspace:*
version: link:../supradough
devDependencies:
vite:
specifier: ^6.0.11
@@ -7537,6 +7552,9 @@ 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==}
@@ -15958,6 +15976,8 @@ snapshots:
walk-up-path@3.0.1: {}
wav-encoder@1.3.0: {}
wav@1.0.2:
dependencies:
buffer-alloc: 1.2.0
+67 -13
View File
@@ -1847,6 +1847,27 @@ 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 ]",
@@ -2585,6 +2606,52 @@ 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 ]",
@@ -2598,19 +2665,6 @@ 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 ]",