From 483843bd0a776ff0690712b5495499a0535c2c1f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Jun 2025 18:06:36 +0200 Subject: [PATCH 001/257] begin supradough --- packages/superdough/dough.mjs | 445 +++++++++++++++++++++++++++++++ packages/superdough/synth.mjs | 4 +- packages/superdough/worklets.mjs | 53 ++++ packages/webaudio/supradough.mjs | 39 +++ 4 files changed, 539 insertions(+), 2 deletions(-) create mode 100644 packages/superdough/dough.mjs create mode 100644 packages/webaudio/supradough.mjs diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs new file mode 100644 index 000000000..f3365b06c --- /dev/null +++ b/packages/superdough/dough.mjs @@ -0,0 +1,445 @@ +// this is dough, the superdough without dependencies + +const ISR = 1 / sampleRate; +// 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 / sampleRate) % 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 { + //phase = Math.random(); + phase = 0; + update(freq) { + const dt = freq / sampleRate; + 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; + } +} + +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 Lpf { + s0 = 0; + s1 = 0; + update(s, cutoff, resonance = 0) { + // Out of bound values can produce NaNs + cutoff = Math.min(cutoff, 1); + resonance = Math.max(resonance, 0); + var c = Math.pow(0.5, (1 - cutoff) / 0.125); + var r = Math.pow(0.5, (resonance + 0.125) / 0.125); + var mrc = 1 - r * c; + var v0 = this.s0; + var v1 = this.s1; + // Apply the filter to the sample + v0 = mrc * v0 - c * v1 + c * s; + v1 = mrc * v1 + c * v0; + s = v1; + this.s0 = v0; + this.s1 = v1; + return s; + } +} + +export class PulseOsc { + 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 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) { + if (x >= 1) return y1; + + return y0 + x * (y1 - y0); +} + +export class ADSR { + state = 'off'; + startTime = 0; + startVal = 0; + + 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); + } + case 'decay': { + let time = curTime - this.startTime; + let curVal = lerp(time / decay, 1, susVal); + 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); + 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 Delay { + writeIdx = 0; + readIdx = 0; + buffer = new Float32Array(MAX_DELAY_TIME * sampleRate); // .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(sampleRate * 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]; + } +} + +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; + } +} + +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]; + } +} + +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 getADSRValues = (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 oscillators = { + sine: SineOsc, + saw: SawOsc, + zaw: ZawOsc, + sawtooth: SawOsc, + zawtooth: ZawOsc, + tri: TriOsc, + triangle: TriOsc, + pulse: PulseOsc, + dust: Dust, + impulse: Impulse, +}; + +const defaultDefaultValues = { + s: 'triangle', + gain: 0.8, + postgain: 1, + density: '.03', + ftype: '12db', + fanchor: 0, + resonance: 1, + hresonance: 1, + bandq: 1, + channels: [1, 2], + phaserdepth: 0.75, + shapevol: 1, + distortvol: 1, + delay: 0, + byteBeatExpression: '0', + delayfeedback: 0.5, + delaytime: 0.25, + orbit: 1, + i: 1, + velocity: 1, + fft: 8, + z: 'triangle', +}; + +let getDefaultValue = (key) => defaultDefaultValues[key]; + +export class Dough { + init(value) { + // params without defaults: + /* + bank, + source, + cutoff, + lpenv, + lpattack, + lpdecay, + lpsustain, + lprelease, + hpenv, + hcutoff, + hpattack, + hpdecay, + hpsustain, + hprelease, + bpenv, + bandf, + bpattack, + bpdecay, + bpsustain, + bprelease, + phaserrate, + phasersweep, + phasercenter, + coarse, + crush, + shape, + distort, + pan, + vowel, + room, + roomfade, + roomlp, + roomdim, + roomsize, + ir, + analyze, + */ + Object.assign(this, value); + // params with defaults: + this.s = this.s ?? getDefaultValue('s'); + this.gain = this.gain ?? getDefaultValue('gain'); + this.postgain = this.postgain ?? getDefaultValue('postgain'); + this.density = this.density ?? getDefaultValue('density'); + this.fanchor = this.fanchor ?? getDefaultValue('fanchor'); + this.drive = this.drive ?? 0.69; + this.resonance = this.resonance ?? getDefaultValue('resonance'); + this.hresonance = this.hresonance ?? getDefaultValue('hresonance'); + this.bandq = this.bandq ?? getDefaultValue('bandq'); + this.phaserdepth = this.phaserdepth ?? getDefaultValue('phaserdepth'); + this.shapevol = this.shapevol ?? getDefaultValue('shapevol'); + this.distortvol = this.distortvol ?? getDefaultValue('distortvol'); + this.delay = this.delay ?? getDefaultValue('delay'); + this.delayfeedback = this.delayfeedback ?? getDefaultValue('delayfeedback'); + this.delaytime = this.delaytime ?? getDefaultValue('delaytime'); + this.orbit = this.orbit ?? getDefaultValue('orbit'); + this.i = this.i ?? getDefaultValue('i'); + this.velocity = this.velocity ?? getDefaultValue('velocity'); + this.fft = this.fft ?? getDefaultValue('fft'); + + [this.attack, this.decay, this.sustain, this.release] = getADSRValues([ + this.attack, + this.decay, + this.sustain, + this.release, + ]); + + const SourceClass = oscillators[this.s] ?? TriOsc; + this._sound = new SourceClass(); + this._lpf = this.cutoff ? new Lpf() : null; + this._adsr = new ADSR(); + } + update(t) { + if (!this._sound) { + return 0; + } + // sound source + let s = this._sound.update(this.freq); + // lpf + s = this._lpf ? this._lpf.update(s, this.cutoff, this.resonance) : s; + // not sure if gain is applied here + s = s * this.gain; + // envelope + let gate = Number(t >= this._begin && t <= this._end); + const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); + s = s * env; + s = s * this.postgain; + return s; + } +} diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 834e54c6d..7d175d6c4 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -12,7 +12,7 @@ import { } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; -const getFrequencyFromValue = (value) => { +export const getFrequencyFromValue = (value) => { let { note, freq } = value; note = note || 36; if (typeof note === 'string') { @@ -25,7 +25,7 @@ const getFrequencyFromValue = (value) => { return Number(freq); }; -function destroyAudioWorkletNode(node) { +export function destroyAudioWorkletNode(node) { if (node == null) { return; } diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 24f7434a9..43a21477b 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -4,6 +4,7 @@ import OLAProcessor from './ola-processor'; import FFT from './fft.js'; +import { Dough } from './dough.mjs'; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const _mod = (n, m) => ((n % m) + m) % m; @@ -895,3 +896,55 @@ class ByteBeatProcessor extends AudioWorkletProcessor { } registerProcessor('byte-beat-processor', ByteBeatProcessor); + +class DoughProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.dough = new Dough(); + this.port.onmessage = (event) => this.dough.init(event.data); + } + static get parameterDescriptors() { + return [ + { + name: 'begin', + defaultValue: 0, + max: Number.POSITIVE_INFINITY, + min: 0, + }, + { + name: 'end', + defaultValue: 0, + max: Number.POSITIVE_INFINITY, + min: 0, + }, + ]; + } + + process(inputs, outputs, params) { + if (this.disconnected) { + return false; + } + if (currentTime <= params.begin[0]) { + return true; + } + if (currentTime >= params.end[0]) { + return false; + } + if (this.t == null) { + this.t = params.begin[0] * sampleRate; + } + const output = outputs[0]; + for (let i = 0; i < output[0].length; i++) { + const out = this.dough.update(currentTime); + + for (let c = 0; c < output.length; c++) { + //prevent speaker blowout via clipping if threshold exceeds + output[c][i] = clamp(out, -1, 1); + } + this.t = this.t + 1; + } + return true; // keep the audio processing going + } +} + +registerProcessor('dough-processor', DoughProcessor); diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs new file mode 100644 index 000000000..514d5e606 --- /dev/null +++ b/packages/webaudio/supradough.mjs @@ -0,0 +1,39 @@ +import { Pattern } from '@strudel/core'; +import { connectToDestination, destroyAudioWorkletNode, getAudioContext } from 'superdough'; + +Pattern.prototype.supradough = function () { + return this.onTrigger((_, hap, __, cps, begin) => { + const { value } = hap; + value.freq = getFrequencyFromValue(hap.value); + const ac = getAudioContext(); + + const release = getADSRValues( + [value.attack, value.decay, value.sustain, value.release], + 'linear', + [0.001, 0.05, 0.6, 0.01], + )[3]; + + const duration = hap.duration / cps; + const holdend = begin + duration; + const end = holdend + release + 0.01; + value._begin = begin; // these are needed for the gate signal + value._end = end; + + let o = getWorklet( + ac, + 'dough-processor', + { + begin, // we might not need these, as we could send them via postMessage below + end, + }, + { + outputChannelCount: [2], + }, + ); + + o.port.postMessage(value); // send value to worklet + let timeoutNode = webAudioTimeout(ac, () => destroyAudioWorkletNode(o), begin, end); + timeoutNode.stop(end + 0.125); + connectToDestination(o); // channels? + }, 1); +}; From 5336569c25dfa0440396a61cc919d7b2cd29af70 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Jun 2025 21:20:28 +0200 Subject: [PATCH 002/257] proper lpf mapping + remove redundant worklet params, --- packages/superdough/dough.mjs | 19 ++++++++++++++--- packages/superdough/worklets.mjs | 36 +++++++++++--------------------- packages/webaudio/supradough.mjs | 5 +---- packages/webaudio/webaudio.mjs | 1 + 4 files changed, 30 insertions(+), 31 deletions(-) diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index f3365b06c..876cfdf11 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -351,7 +351,7 @@ const defaultDefaultValues = { let getDefaultValue = (key) => defaultDefaultValues[key]; export class Dough { - init(value) { + init(value, sampleRate) { // params without defaults: /* bank, @@ -424,6 +424,14 @@ export class Dough { this._sound = new SourceClass(); this._lpf = this.cutoff ? new Lpf() : null; this._adsr = new ADSR(); + + this.piOverSr = Math.PI / sampleRate; + this.eighthOverLogHalf = 0.125 / Math.log(0.5); + } + // credits to pulu: https://github.com/felixroos/kabelsalat/issues/35 + freq2cutoff(freq) { + const c = 2 * Math.sin(freq * this.piOverSr); + return 1 - Math.log(c) * this.eighthOverLogHalf; } update(t) { if (!this._sound) { @@ -432,14 +440,19 @@ export class Dough { // sound source let s = this._sound.update(this.freq); // lpf - s = this._lpf ? this._lpf.update(s, this.cutoff, this.resonance) : s; + if (this._lpf) { + const cutoff = this.freq2cutoff(this.cutoff); + s = this._lpf ? this._lpf.update(s, cutoff, this.resonance) : s; + } // not sure if gain is applied here s = s * this.gain; // envelope let gate = Number(t >= this._begin && t <= this._end); + /* Math.random() > 0.99 && console.log('gate', gate); */ const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); s = s * env; - s = s * this.postgain; + + s = s * this.postgain * 0.3; return s; } } diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 43a21477b..7633c6441 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -901,42 +901,30 @@ class DoughProcessor extends AudioWorkletProcessor { constructor() { super(); this.dough = new Dough(); - this.port.onmessage = (event) => this.dough.init(event.data); - } - static get parameterDescriptors() { - return [ - { - name: 'begin', - defaultValue: 0, - max: Number.POSITIVE_INFINITY, - min: 0, - }, - { - name: 'end', - defaultValue: 0, - max: Number.POSITIVE_INFINITY, - min: 0, - }, - ]; + this.port.onmessage = (event) => this.dough.init(event.data, sampleRate); } process(inputs, outputs, params) { if (this.disconnected) { return false; } - if (currentTime <= params.begin[0]) { + if (this.dough._begin === undefined) { return true; } - if (currentTime >= params.end[0]) { - return false; + if (currentTime <= this.dough._begin) { + return true; } - if (this.t == null) { - this.t = params.begin[0] * sampleRate; + if (currentTime >= this.dough._end + 1) { + return false; // this causes cracks for some reason (seems to kick in too early for some reason) + // it works with + 1 but not sure why this is needed + } + if (this.t === undefined) { + this.t = Math.floor(this.dough._begin * sampleRate); } const output = outputs[0]; for (let i = 0; i < output[0].length; i++) { - const out = this.dough.update(currentTime); - + // const out = this.dough.update(currentTime); + const out = this.dough.update(this.t / sampleRate); for (let c = 0; c < output.length; c++) { //prevent speaker blowout via clipping if threshold exceeds output[c][i] = clamp(out, -1, 1); diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index 514d5e606..8887a4132 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -22,10 +22,7 @@ Pattern.prototype.supradough = function () { let o = getWorklet( ac, 'dough-processor', - { - begin, // we might not need these, as we could send them via postMessage below - end, - }, + {}, { outputChannelCount: [2], }, diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 44a683480..8f7d041a9 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -6,6 +6,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 } from 'superdough'; +import './supradough.mjs'; const { Pattern, logger, repl } = strudel; setLogger(logger); From 5129fa667791abb0498f189a0ea3c7c506eac61c Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 4 Jun 2025 00:48:32 +0100 Subject: [PATCH 003/257] fix click --- packages/superdough/dough.mjs | 7 +++++-- packages/webaudio/supradough.mjs | 11 +++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index 876cfdf11..bff024150 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -146,6 +146,7 @@ export class ADSR { startVal = 0; update(curTime, gate, attack, decay, susVal, release) { + console.info('here') switch (this.state) { case 'off': { if (gate > 0) { @@ -352,6 +353,7 @@ let getDefaultValue = (key) => defaultDefaultValues[key]; export class Dough { init(value, sampleRate) { + // params without defaults: /* bank, @@ -447,12 +449,13 @@ export class Dough { // not sure if gain is applied here s = s * this.gain; // envelope - let gate = Number(t >= this._begin && t <= this._end); + let gate = Number(t >= this._begin && t <= this._holdEnd); + /* Math.random() > 0.99 && console.log('gate', gate); */ const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); s = s * env; - s = s * this.postgain * 0.3; + s = s * this.postgain * .3 return s; } } diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index 8887a4132..dcf2cf4c0 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -1,6 +1,5 @@ import { Pattern } from '@strudel/core'; -import { connectToDestination, destroyAudioWorkletNode, getAudioContext } from 'superdough'; - +import { connectToDestination, destroyAudioWorkletNode, getAudioContext, webAudioTimeout } from 'superdough'; Pattern.prototype.supradough = function () { return this.onTrigger((_, hap, __, cps, begin) => { const { value } = hap; @@ -14,10 +13,11 @@ Pattern.prototype.supradough = function () { )[3]; const duration = hap.duration / cps; - const holdend = begin + duration; - const end = holdend + release + 0.01; + const holdEnd = begin + duration; + const end = holdEnd + release + 0.01; value._begin = begin; // these are needed for the gate signal value._end = end; + value._holdEnd = holdEnd let o = getWorklet( ac, @@ -29,8 +29,7 @@ Pattern.prototype.supradough = function () { ); o.port.postMessage(value); // send value to worklet - let timeoutNode = webAudioTimeout(ac, () => destroyAudioWorkletNode(o), begin, end); - timeoutNode.stop(end + 0.125); + webAudioTimeout(ac, () => destroyAudioWorkletNode(o), begin, end); connectToDestination(o); // channels? }, 1); }; From ff4b7ca1718fc7ffd409f6bd62b3e1234d7acd38 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 4 Jun 2025 01:08:36 +0100 Subject: [PATCH 004/257] fix volume --- packages/superdough/dough.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index bff024150..32ca86ef7 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -146,7 +146,6 @@ export class ADSR { startVal = 0; update(curTime, gate, attack, decay, susVal, release) { - console.info('here') switch (this.state) { case 'off': { if (gate > 0) { @@ -191,6 +190,7 @@ export class ADSR { } case 'release': { let time = curTime - this.startTime; + if (time > release) { this.state = 'off'; return 0; @@ -353,7 +353,7 @@ let getDefaultValue = (key) => defaultDefaultValues[key]; export class Dough { init(value, sampleRate) { - + // params without defaults: /* bank, @@ -455,7 +455,7 @@ export class Dough { const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); s = s * env; - s = s * this.postgain * .3 + s = s * this.postgain * .2 return s; } } From 930dabe4d6e4952445b952cb88c04a0bef49ee1d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 5 Jun 2025 09:54:44 +0200 Subject: [PATCH 005/257] dough as a singleton worklet with voice allocation --- packages/superdough/dough.mjs | 78 ++++++++++++++++++++++++++++++-- packages/superdough/worklets.mjs | 25 +++------- packages/webaudio/supradough.mjs | 40 ++++++++++------ 3 files changed, 104 insertions(+), 39 deletions(-) diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index 32ca86ef7..65cc3793f 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -351,9 +351,8 @@ const defaultDefaultValues = { let getDefaultValue = (key) => defaultDefaultValues[key]; -export class Dough { - init(value, sampleRate) { - +export class DoughVoice { + constructor(value) { // params without defaults: /* bank, @@ -427,7 +426,7 @@ export class Dough { this._lpf = this.cutoff ? new Lpf() : null; this._adsr = new ADSR(); - this.piOverSr = Math.PI / sampleRate; + this.piOverSr = Math.PI / value.sampleRate; this.eighthOverLogHalf = 0.125 / Math.log(0.5); } // credits to pulu: https://github.com/felixroos/kabelsalat/issues/35 @@ -455,7 +454,76 @@ export class Dough { const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); s = s * env; - s = s * this.postgain * .2 + s = s * this.postgain * 0.2; return s; } } + +// 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 = []; + l = 0; // tbd + r = 0; // tbd + t = 0; + // sampleRate: number, currentTime: number (seconds) + constructor(sampleRate, currentTime) { + this.sampleRate = sampleRate; + this.t = Math.floor(currentTime * sampleRate); // samples + // console.log('init dough', this.sampleRate, this.t); + } + scheduleSpawn(value) { + const time = value._begin; // 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, value._holdDuration, 'voices:', this.voices.length); + // schedule removal + const endTime = Math.ceil(value._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 + let sum = 0; + for (let v = 0; v < this.voices.length; v++) { + sum += this.voices[v].update(this.t / this.sampleRate); + } + this.t++; + return sum; + } +} diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 7633c6441..5e5494255 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -900,36 +900,23 @@ registerProcessor('byte-beat-processor', ByteBeatProcessor); class DoughProcessor extends AudioWorkletProcessor { constructor() { super(); - this.dough = new Dough(); - this.port.onmessage = (event) => this.dough.init(event.data, sampleRate); + this.dough = new Dough(sampleRate, currentTime); + this.port.onmessage = (event) => { + event.data.sampleRate = sampleRate; + this.dough.scheduleSpawn(event.data); + }; } - process(inputs, outputs, params) { if (this.disconnected) { return false; } - if (this.dough._begin === undefined) { - return true; - } - if (currentTime <= this.dough._begin) { - return true; - } - if (currentTime >= this.dough._end + 1) { - return false; // this causes cracks for some reason (seems to kick in too early for some reason) - // it works with + 1 but not sure why this is needed - } - if (this.t === undefined) { - this.t = Math.floor(this.dough._begin * sampleRate); - } const output = outputs[0]; for (let i = 0; i < output[0].length; i++) { - // const out = this.dough.update(currentTime); - const out = this.dough.update(this.t / sampleRate); + const out = this.dough.update(); for (let c = 0; c < output.length; c++) { //prevent speaker blowout via clipping if threshold exceeds output[c][i] = clamp(out, -1, 1); } - this.t = this.t + 1; } return true; // keep the audio processing going } diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index dcf2cf4c0..f774b7180 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -1,10 +1,27 @@ import { Pattern } from '@strudel/core'; -import { connectToDestination, destroyAudioWorkletNode, getAudioContext, webAudioTimeout } from 'superdough'; +import { connectToDestination, getAudioContext, getWorklet } from 'superdough'; + +let doughWorklet; + +function initDoughWorklet() { + const ac = getAudioContext(); + doughWorklet = getWorklet( + ac, + 'dough-processor', + {}, + { + outputChannelCount: [2], + }, + ); + /* webAudioTimeout(ac, () => destroyAudioWorkletNode(doughWorklet), begin, end); */ + connectToDestination(doughWorklet); // channels? +} + Pattern.prototype.supradough = function () { return this.onTrigger((_, hap, __, cps, begin) => { const { value } = hap; + // todo: could these calculations be made inside dough as well? value.freq = getFrequencyFromValue(hap.value); - const ac = getAudioContext(); const release = getADSRValues( [value.attack, value.decay, value.sustain, value.release], @@ -17,19 +34,12 @@ Pattern.prototype.supradough = function () { const end = holdEnd + release + 0.01; value._begin = begin; // these are needed for the gate signal value._end = end; - value._holdEnd = holdEnd + value._holdEnd = holdEnd; + value._holdDuration = duration + release; - let o = getWorklet( - ac, - 'dough-processor', - {}, - { - outputChannelCount: [2], - }, - ); - - o.port.postMessage(value); // send value to worklet - webAudioTimeout(ac, () => destroyAudioWorkletNode(o), begin, end); - connectToDestination(o); // channels? + if (!doughWorklet) { + initDoughWorklet(); + } + doughWorklet.port.postMessage(value); }, 1); }; From 2cbd35f6d2a964331b199530ea2be80c1b6e5491 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 5 Jun 2025 10:26:14 +0200 Subject: [PATCH 006/257] refactor: make webaudio glue as minimal as possible --- packages/superdough/dough.mjs | 36 ++++++++++++++++++++++++++++++-- packages/webaudio/supradough.mjs | 22 +++---------------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index 65cc3793f..032bb5a69 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -311,6 +311,7 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => { 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 oscillators = { sine: SineOsc, saw: SawOsc, @@ -351,6 +352,33 @@ const defaultDefaultValues = { 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) => { + const [pc, acc = '', oct = defaultOctave] = + 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; + return (Number(oct) + 1) * 12 + chroma + offset; +}; +const getFrequency = (value) => { + let { note, freq } = value; + note = note || 36; + if (typeof note === 'string') { + note = note2midi(note); // e.g. c3 => 48 + } + if (!freq && typeof note === 'number') { + freq = Math.pow(2, (note - 69) / 12) * 440; + } + + return Number(freq); +}; + export class DoughVoice { constructor(value) { // params without defaults: @@ -392,6 +420,7 @@ export class DoughVoice { ir, analyze, */ + value.freq = getFrequency(value); Object.assign(this, value); // params with defaults: this.s = this.s ?? getDefaultValue('s'); @@ -421,6 +450,9 @@ export class DoughVoice { this.release, ]); + this._holdEnd = this._begin + this._duration; // needed for gate + this._end = this._holdEnd + this.release + 0.01; // needed for despawn + const SourceClass = oscillators[this.s] ?? TriOsc; this._sound = new SourceClass(); this._lpf = this.cutoff ? new Lpf() : null; @@ -482,9 +514,9 @@ export class Dough { value.id = this.vid++; const voice = new DoughVoice(value); this.voices.push(voice); - console.log('spawn', voice.id, value._holdDuration, 'voices:', this.voices.length); + console.log('spawn', voice.id, 'voices:', this.voices.length); // schedule removal - const endTime = Math.ceil(value._end * this.sampleRate); + const endTime = Math.ceil(voice._end * this.sampleRate); this.schedule({ time: endTime /* + 48000 */, type: 'despawn', arg: voice.id }); } despawn(vid) { diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index f774b7180..8df18bed6 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -13,33 +13,17 @@ function initDoughWorklet() { outputChannelCount: [2], }, ); - /* webAudioTimeout(ac, () => destroyAudioWorkletNode(doughWorklet), begin, end); */ connectToDestination(doughWorklet); // channels? } Pattern.prototype.supradough = function () { return this.onTrigger((_, hap, __, cps, begin) => { - const { value } = hap; - // todo: could these calculations be made inside dough as well? - value.freq = getFrequencyFromValue(hap.value); - - const release = getADSRValues( - [value.attack, value.decay, value.sustain, value.release], - 'linear', - [0.001, 0.05, 0.6, 0.01], - )[3]; - - const duration = hap.duration / cps; - const holdEnd = begin + duration; - const end = holdEnd + release + 0.01; - value._begin = begin; // these are needed for the gate signal - value._end = end; - value._holdEnd = holdEnd; - value._holdDuration = duration + release; + hap.value._begin = begin; + hap.value._duration = hap.duration / cps; if (!doughWorklet) { initDoughWorklet(); } - doughWorklet.port.postMessage(value); + doughWorklet.port.postMessage(hap.value); }, 1); }; From f0e6c5483c012de6d4c5de79fa2ac301a0c9ae94 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 5 Jun 2025 10:30:53 +0200 Subject: [PATCH 007/257] handle invalid spawns + remove logs --- packages/superdough/dough.mjs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index 032bb5a69..7261c4a5a 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -507,6 +507,12 @@ export class Dough { // console.log('init dough', this.sampleRate, this.t); } 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'); + } const time = value._begin; // set from supradough.mjs this.schedule({ time, type: 'spawn', arg: value }); } @@ -514,14 +520,14 @@ export class Dough { value.id = this.vid++; const voice = new DoughVoice(value); this.voices.push(voice); - console.log('spawn', voice.id, 'voices:', this.voices.length); + // 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); + // console.log('despawn', vid, 'voices:', this.voices.length); } // schedules a function call with a single argument // msg = {time:number,type:string, arg: any} From ccec7e725ab6344d8a4e58828f6b1cba1c85f776 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 5 Jun 2025 11:04:55 +0200 Subject: [PATCH 008/257] poc: render pattern audio with node script --- packages/superdough/.gitignore | 1 + packages/superdough/dough-export.mjs | 61 ++++++++++++++++++++++++++++ packages/superdough/dough.mjs | 15 +++---- packages/superdough/package.json | 3 +- packages/superdough/worklets.mjs | 5 +-- pnpm-lock.yaml | 8 ++++ 6 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 packages/superdough/.gitignore create mode 100644 packages/superdough/dough-export.mjs diff --git a/packages/superdough/.gitignore b/packages/superdough/.gitignore new file mode 100644 index 000000000..d21cbdf3e --- /dev/null +++ b/packages/superdough/.gitignore @@ -0,0 +1 @@ +pattern.wav diff --git a/packages/superdough/dough-export.mjs b/packages/superdough/dough-export.mjs new file mode 100644 index 000000000..5f8b9a8ee --- /dev/null +++ b/packages/superdough/dough-export.mjs @@ -0,0 +1,61 @@ +// 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.5; + +let pat = note('[c e g b]*3') + .add(note(7)) + .lpf(sine.rangex(200, 4000).slow(2)) + .lpq(0.3) + .s('*2') + .att(0.01) + .rel(0.2) + .clip(2) + .delay(0.5) + .jux(rev) + .sometimes(add(note(12))) + .gain(0.25) + .slow(1 / cps); + +let cycles = 4; +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...`); +const buffer = new Float32Array(seconds * sampleRate); +while (dough.t <= buffer.length) { + buffer[dough.t] = dough.update(); +} +console.log('done!'); + +const patternAudio = { + sampleRate, + channelData: [buffer], +}; + +WavEncoder.encode(patternAudio).then((buffer) => { + fs.writeFileSync('pattern.wav', new Float32Array(buffer)); +}); diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index 7261c4a5a..21918f95b 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -1,12 +1,12 @@ // this is dough, the superdough without dependencies - -const ISR = 1 / sampleRate; +const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000; +const ISR = 1 / SAMPLE_RATE; // 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 / sampleRate) % 1; + this.phase = (this.phase + freq / SAMPLE_RATE) % 1; return value; } } @@ -39,7 +39,7 @@ export class SawOsc { //phase = Math.random(); phase = 0; update(freq) { - const dt = freq / sampleRate; + const dt = freq / SAMPLE_RATE; let p = polyBlep(this.phase, dt); let s = 2 * this.phase - 1 - p; this.phase += dt; @@ -216,12 +216,12 @@ const MAX_DELAY_TIME = 10; export class Delay { writeIdx = 0; readIdx = 0; - buffer = new Float32Array(MAX_DELAY_TIME * sampleRate); // .fill(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(sampleRate * delayTime), this.buffer.length - 1); + 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; @@ -501,7 +501,7 @@ export class Dough { r = 0; // tbd t = 0; // sampleRate: number, currentTime: number (seconds) - constructor(sampleRate, currentTime) { + constructor(sampleRate = 48000, currentTime = 0) { this.sampleRate = sampleRate; this.t = Math.floor(currentTime * sampleRate); // samples // console.log('init dough', this.sampleRate, this.t); @@ -513,6 +513,7 @@ export class Dough { if (value._duration === undefined) { throw new Error('[dough]: scheduleSpawn expected _duration to be set'); } + value.sampleRate = this.sampleRate; const time = value._begin; // set from supradough.mjs this.schedule({ time, type: 'spawn', arg: value }); } diff --git a/packages/superdough/package.json b/packages/superdough/package.json index a835f252d..6b45144e4 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -33,7 +33,8 @@ "homepage": "https://github.com/tidalcycles/strudel#readme", "devDependencies": { "vite": "^6.0.11", - "vite-plugin-bundle-audioworklet": "workspace:*" + "vite-plugin-bundle-audioworklet": "workspace:*", + "wav-encoder": "^1.3.0" }, "dependencies": { "nanostores": "^0.11.3" diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 5e5494255..2edcc6f53 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -901,10 +901,7 @@ class DoughProcessor extends AudioWorkletProcessor { constructor() { super(); this.dough = new Dough(sampleRate, currentTime); - this.port.onmessage = (event) => { - event.data.sampleRate = sampleRate; - this.dough.scheduleSpawn(event.data); - }; + this.port.onmessage = (event) => this.dough.scheduleSpawn(event.data); } process(inputs, outputs, params) { if (this.disconnected) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a23c80924..ed8a7c014 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -485,6 +485,9 @@ importers: 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: @@ -7464,6 +7467,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==} @@ -15839,6 +15845,8 @@ snapshots: walk-up-path@3.0.1: {} + wav-encoder@1.3.0: {} + wav@1.0.2: dependencies: buffer-alloc: 1.2.0 From 87598e10d1c1af739cfc26f9efebe6e0039f6be1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 6 Jun 2025 08:17:20 +0200 Subject: [PATCH 009/257] coarse --- packages/superdough/dough.mjs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index 21918f95b..b6ded0d15 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -292,6 +292,19 @@ export class Sequence { } } +// 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; + } +} + export function _rangex(sig, min, max) { let logmin = Math.log(min); let range = Math.log(max) - logmin; @@ -457,6 +470,7 @@ export class DoughVoice { this._sound = new SourceClass(); this._lpf = this.cutoff ? new Lpf() : null; this._adsr = new ADSR(); + this._coarse = this.coarse ? new Coarse() : null; this.piOverSr = Math.PI / value.sampleRate; this.eighthOverLogHalf = 0.125 / Math.log(0.5); @@ -475,7 +489,10 @@ export class DoughVoice { // lpf if (this._lpf) { const cutoff = this.freq2cutoff(this.cutoff); - s = this._lpf ? this._lpf.update(s, cutoff, this.resonance) : s; + s = this._lpf.update(s, cutoff, this.resonance); + } + if (this._coarse) { + s = this._coarse.update(s, this.coarse); } // not sure if gain is applied here s = s * this.gain; From 0cc18a64682c7f984f549e0e0240694e0721811e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 6 Jun 2025 08:27:47 +0200 Subject: [PATCH 010/257] crush (might be wrong) --- packages/superdough/dough.mjs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index b6ded0d15..bd226cc72 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -305,6 +305,15 @@ export class Coarse { } } +// 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; + } +} + export function _rangex(sig, min, max) { let logmin = Math.log(min); let range = Math.log(max) - logmin; @@ -471,6 +480,7 @@ export class DoughVoice { this._lpf = this.cutoff ? new Lpf() : null; this._adsr = new ADSR(); this._coarse = this.coarse ? new Coarse() : null; + this._crush = this.crush ? new Crush() : null; this.piOverSr = Math.PI / value.sampleRate; this.eighthOverLogHalf = 0.125 / Math.log(0.5); @@ -494,6 +504,9 @@ export class DoughVoice { if (this._coarse) { s = this._coarse.update(s, this.coarse); } + if (this._crush) { + s = this._crush.update(s, this.crush); + } // not sure if gain is applied here s = s * this.gain; // envelope From 623bf9336865de8ff68bc8d2387a67e2ee412e6e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 6 Jun 2025 08:50:02 +0200 Subject: [PATCH 011/257] panning --- packages/superdough/dough.mjs | 25 +++++++++++++++++++------ packages/superdough/worklets.mjs | 4 ++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index bd226cc72..e6190f140 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -370,6 +370,7 @@ const defaultDefaultValues = { velocity: 1, fft: 8, z: 'triangle', + pan: 0.5, }; let getDefaultValue = (key) => defaultDefaultValues[key]; @@ -402,6 +403,8 @@ const getFrequency = (value) => { }; export class DoughVoice { + l = 0; + r = 0; constructor(value) { // params without defaults: /* @@ -464,6 +467,7 @@ export class DoughVoice { this.i = this.i ?? getDefaultValue('i'); this.velocity = this.velocity ?? getDefaultValue('velocity'); this.fft = this.fft ?? getDefaultValue('fft'); + this.pan = this.pan ?? getDefaultValue('pan'); [this.attack, this.decay, this.sustain, this.release] = getADSRValues([ this.attack, @@ -517,7 +521,15 @@ export class DoughVoice { s = s * env; s = s * this.postgain * 0.2; - return s; + + if (this.pan === 0.5) { + this.l = this.r = s; // mono + } else { + // stereo + const pos = (this.pan * Math.PI) / 2; + this.l = s * Math.cos(pos); + this.r = s * Math.sin(pos); + } } } @@ -527,8 +539,7 @@ export class Dough { voices = []; // DoughVoice[] vid = 0; q = []; - l = 0; // tbd - r = 0; // tbd + channels = [0, 0]; t = 0; // sampleRate: number, currentTime: number (seconds) constructor(sampleRate = 48000, currentTime = 0) { @@ -588,11 +599,13 @@ export class Dough { this.q.shift(); } // add active voices - let sum = 0; + this.channels[0] = 0; + this.channels[1] = 0; for (let v = 0; v < this.voices.length; v++) { - sum += this.voices[v].update(this.t / this.sampleRate); + this.voices[v].update(this.t / this.sampleRate); + this.channels[0] += this.voices[v].l; + this.channels[1] += this.voices[v].r; } this.t++; - return sum; } } diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 2edcc6f53..d8f189779 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -909,10 +909,10 @@ class DoughProcessor extends AudioWorkletProcessor { } const output = outputs[0]; for (let i = 0; i < output[0].length; i++) { - const out = this.dough.update(); + this.dough.update(); for (let c = 0; c < output.length; c++) { //prevent speaker blowout via clipping if threshold exceeds - output[c][i] = clamp(out, -1, 1); + output[c][i] = clamp(this.dough.channels[c], -1, 1); } } return true; // keep the audio processing going From 9ed690ce4551a471beb7784208f2fc275f7a808e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 6 Jun 2025 09:37:40 +0200 Subject: [PATCH 012/257] pulse osc + velocity --- packages/superdough/dough.mjs | 38 ++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index e6190f140..323e78d5c 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -82,7 +82,25 @@ export class Lpf { } } -export class PulseOsc { +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 / sampleRate; + 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; @@ -343,14 +361,16 @@ let oscillators = { tri: TriOsc, triangle: TriOsc, pulse: PulseOsc, + pulze: PulzeOsc, dust: Dust, impulse: Impulse, }; const defaultDefaultValues = { s: 'triangle', - gain: 0.8, + gain: 1, postgain: 1, + velocity: 1, density: '.03', ftype: '12db', fanchor: 0, @@ -367,7 +387,6 @@ const defaultDefaultValues = { delaytime: 0.25, orbit: 1, i: 1, - velocity: 1, fft: 8, z: 'triangle', pan: 0.5, @@ -450,6 +469,7 @@ export class DoughVoice { // params with defaults: this.s = this.s ?? getDefaultValue('s'); this.gain = this.gain ?? getDefaultValue('gain'); + this.velocity = this.velocity ?? getDefaultValue('velocity'); this.postgain = this.postgain ?? getDefaultValue('postgain'); this.density = this.density ?? getDefaultValue('density'); this.fanchor = this.fanchor ?? getDefaultValue('fanchor'); @@ -465,7 +485,6 @@ export class DoughVoice { this.delaytime = this.delaytime ?? getDefaultValue('delaytime'); this.orbit = this.orbit ?? getDefaultValue('orbit'); this.i = this.i ?? getDefaultValue('i'); - this.velocity = this.velocity ?? getDefaultValue('velocity'); this.fft = this.fft ?? getDefaultValue('fft'); this.pan = this.pan ?? getDefaultValue('pan'); @@ -498,8 +517,13 @@ export class DoughVoice { if (!this._sound) { return 0; } + let s = 0; // sound source - let s = this._sound.update(this.freq); + if (this.s === 'pulse') { + s = this._sound.update(this.freq, this.pw ?? 0.5); + } else { + s = this._sound.update(this.freq); + } // lpf if (this._lpf) { const cutoff = this.freq2cutoff(this.cutoff); @@ -511,8 +535,8 @@ export class DoughVoice { if (this._crush) { s = this._crush.update(s, this.crush); } - // not sure if gain is applied here - s = s * this.gain; + // not sure if gain/velocity is applied here + s = s * this.gain * this.velocity; // envelope let gate = Number(t >= this._begin && t <= this._holdEnd); From 794bf86904fe89e9213e75258a06f33a6b7e87f0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 6 Jun 2025 09:56:41 +0200 Subject: [PATCH 013/257] distort --- packages/superdough/dough.mjs | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index 323e78d5c..d687bb63c 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -332,6 +332,26 @@ export class Crush { } } +// (unused) overdrive-style distortion (adapted from noisecraft) +export class Overdrive { + update(input, amount = 0, postgain = 1) { + amount = Math.min(Math.max(amount, 0), 1); + amount -= 0.01; + const shape = (2 * amount) / (1 - amount); + return (((1 + shape) * input) / (1 + shape * Math.abs(input))) * postgain; + } +} + +// 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 function _rangex(sig, min, max) { let logmin = Math.log(min); let range = Math.log(max) - logmin; @@ -504,6 +524,7 @@ export class DoughVoice { this._adsr = new ADSR(); this._coarse = this.coarse ? new Coarse() : null; this._crush = this.crush ? new Crush() : null; + this._distort = this.distort ? new Distort() : null; this.piOverSr = Math.PI / value.sampleRate; this.eighthOverLogHalf = 0.125 / Math.log(0.5); @@ -529,12 +550,11 @@ export class DoughVoice { const cutoff = this.freq2cutoff(this.cutoff); s = this._lpf.update(s, cutoff, this.resonance); } - if (this._coarse) { - s = this._coarse.update(s, this.coarse); - } - if (this._crush) { - s = this._crush.update(s, this.crush); - } + + this._coarse && (s = this._coarse.update(s, this.coarse)); + this._crush && (s = this._crush.update(s, this.crush)); + this._distort && (s = this._distort.update(s, this.distort, this.distortvol)); + // not sure if gain/velocity is applied here s = s * this.gain * this.velocity; // envelope From 6cf0b6f6515910da199a939740296a694100e5bf Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 6 Jun 2025 10:12:49 +0200 Subject: [PATCH 014/257] hpf + bpf --- packages/superdough/dough.mjs | 58 +++++++++++++++++------------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index d687bb63c..dce26ab3b 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -60,7 +60,7 @@ export class TriOsc { } } -export class Lpf { +export class TwoPoleFilter { s0 = 0; s1 = 0; update(s, cutoff, resonance = 0) { @@ -70,15 +70,10 @@ export class Lpf { var c = Math.pow(0.5, (1 - cutoff) / 0.125); var r = Math.pow(0.5, (resonance + 0.125) / 0.125); var mrc = 1 - r * c; - var v0 = this.s0; - var v1 = this.s1; - // Apply the filter to the sample - v0 = mrc * v0 - c * v1 + c * s; - v1 = mrc * v1 + c * v0; - s = v1; - this.s0 = v0; - this.s1 = v1; - return s; + + 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 } } @@ -287,6 +282,7 @@ export class Slew { } } +// overdrive style distortion (adapted from noisecraft) currently unused export function applyDistortion(x, amount) { amount = Math.min(Math.max(amount, 0), 1); amount -= 0.01; @@ -332,16 +328,6 @@ export class Crush { } } -// (unused) overdrive-style distortion (adapted from noisecraft) -export class Overdrive { - update(input, amount = 0, postgain = 1) { - amount = Math.min(Math.max(amount, 0), 1); - amount -= 0.01; - const shape = (2 * amount) / (1 - amount); - return (((1 + shape) * input) / (1 + shape * Math.abs(input))) * postgain; - } -} - // this is the distort from superdough export class Distort { update(input, distort = 0, postgain = 1) { @@ -394,9 +380,12 @@ const defaultDefaultValues = { density: '.03', ftype: '12db', fanchor: 0, - resonance: 1, - hresonance: 1, - bandq: 1, + //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, @@ -470,11 +459,7 @@ export class DoughVoice { phaserrate, phasersweep, phasercenter, - coarse, - crush, shape, - distort, - pan, vowel, room, roomfade, @@ -520,7 +505,9 @@ export class DoughVoice { const SourceClass = oscillators[this.s] ?? TriOsc; this._sound = new SourceClass(); - this._lpf = this.cutoff ? new Lpf() : null; + this._lpf = this.cutoff ? new TwoPoleFilter() : null; + this._hpf = this.hcutoff ? new TwoPoleFilter() : null; + this._bpf = this.bandf ? new TwoPoleFilter() : null; this._adsr = new ADSR(); this._coarse = this.coarse ? new Coarse() : null; this._crush = this.crush ? new Crush() : null; @@ -548,7 +535,20 @@ export class DoughVoice { // lpf if (this._lpf) { const cutoff = this.freq2cutoff(this.cutoff); - s = this._lpf.update(s, cutoff, this.resonance); + this._lpf.update(s, cutoff, this.resonance); + s = this._lpf.s1; + } + // hpf + if (this._hpf) { + const cutoff = this.freq2cutoff(this.hcutoff); + this._hpf.update(s, cutoff, this.hresonance); + s = s - this._hpf.s1; + } + // bpf + if (this._bpf) { + const cutoff = this.freq2cutoff(this.bandf); + this._bpf.update(s, cutoff, this.bandq); + s = this._bpf.s0; } this._coarse && (s = this._coarse.update(s, this.coarse)); From e4cd5dc552bcd3038b25b2914d475626d2277423 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 6 Jun 2025 10:55:38 +0200 Subject: [PATCH 015/257] rough filter envelope implementation --- packages/superdough/dough.mjs | 60 ++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index dce26ab3b..09f85e153 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -505,10 +505,44 @@ export class DoughVoice { const SourceClass = oscillators[this.s] ?? TriOsc; this._sound = new SourceClass(); + + // filter setup this._lpf = this.cutoff ? new TwoPoleFilter() : null; + if (this.lpenv) { + this._lpenv = new ADSR(); + [this.lpattack, this.lpdecay, this.lpsustain, this.lprelease] = getADSRValues([ + this.lpattack, + this.lpdecay, + this.lpsustain, + this.lprelease, + ]); + } + this._hpf = this.hcutoff ? new TwoPoleFilter() : null; + if (this.hpenv) { + this._hpenv = new ADSR(); + [this.hpattack, this.hpdecay, this.hpsustain, this.hprelease] = getADSRValues([ + this.hpattack, + this.hpdecay, + this.hpsustain, + this.hprelease, + ]); + } this._bpf = this.bandf ? new TwoPoleFilter() : null; + if (this.bpenv) { + this._bpenv = new ADSR(); + [this.bpattack, this.bpdecay, this.bpsustain, this.bprelease] = getADSRValues([ + this.bpattack, + this.bpdecay, + this.bpsustain, + this.bprelease, + ]); + } + + // gain envelope this._adsr = new ADSR(); + + // fx setup this._coarse = this.coarse ? new Coarse() : null; this._crush = this.crush ? new Crush() : null; this._distort = this.distort ? new Distort() : null; @@ -532,21 +566,36 @@ export class DoughVoice { } else { s = this._sound.update(this.freq); } + let gate = Number(t >= this._begin && t <= this._holdEnd); + s = s * this.gain * this.velocity; + // lpf if (this._lpf) { - const cutoff = this.freq2cutoff(this.cutoff); + let cutoff = this.freq2cutoff(this.cutoff); + if (this._lpenv) { + const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease) ** 2; + cutoff = cutoff + env * this.lpenv * cutoff; // todo proper scaling + } this._lpf.update(s, cutoff, this.resonance); s = this._lpf.s1; } // hpf if (this._hpf) { - const cutoff = this.freq2cutoff(this.hcutoff); + let cutoff = this.freq2cutoff(this.hcutoff); + if (this._hpenv) { + const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease) ** 2; + cutoff = cutoff + env * this.hpenv * cutoff; // todo proper scaling + } this._hpf.update(s, cutoff, this.hresonance); s = s - this._hpf.s1; } // bpf if (this._bpf) { - const cutoff = this.freq2cutoff(this.bandf); + let cutoff = this.freq2cutoff(this.bandf); + if (this._bpenv) { + const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease) ** 2; + cutoff = cutoff + env * this.bpenv * cutoff; // todo proper scaling + } this._bpf.update(s, cutoff, this.bandq); s = this._bpf.s0; } @@ -555,11 +604,6 @@ export class DoughVoice { this._crush && (s = this._crush.update(s, this.crush)); this._distort && (s = this._distort.update(s, this.distort, this.distortvol)); - // not sure if gain/velocity is applied here - s = s * this.gain * this.velocity; - // envelope - let gate = Number(t >= this._begin && t <= this._holdEnd); - /* Math.random() > 0.99 && console.log('gate', gate); */ const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); s = s * env; From 998a0102471f63eec033f3bc52e330e1fcfc7b6e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 6 Jun 2025 11:21:01 +0200 Subject: [PATCH 016/257] better filter envelopes --- packages/superdough/dough.mjs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index 09f85e153..a292b0c8b 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -571,31 +571,34 @@ export class DoughVoice { // lpf if (this._lpf) { - let cutoff = this.freq2cutoff(this.cutoff); + let cutoff = this.cutoff; if (this._lpenv) { const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease) ** 2; - cutoff = cutoff + env * this.lpenv * cutoff; // todo proper scaling + cutoff = 2 ** this.lpenv * env * cutoff + cutoff; } + cutoff = this.freq2cutoff(cutoff); this._lpf.update(s, cutoff, this.resonance); s = this._lpf.s1; } // hpf if (this._hpf) { - let cutoff = this.freq2cutoff(this.hcutoff); + let cutoff = this.hcutoff; if (this._hpenv) { const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease) ** 2; - cutoff = cutoff + env * this.hpenv * cutoff; // todo proper scaling + cutoff = 2 ** this.hpenv * env * cutoff + cutoff; } + cutoff = this.freq2cutoff(cutoff); this._hpf.update(s, cutoff, this.hresonance); s = s - this._hpf.s1; } // bpf if (this._bpf) { - let cutoff = this.freq2cutoff(this.bandf); + let cutoff = this.bandf; if (this._bpenv) { const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease) ** 2; - cutoff = cutoff + env * this.bpenv * cutoff; // todo proper scaling + cutoff = 2 ** this.bpenv * env * cutoff + cutoff; } + cutoff = this.freq2cutoff(cutoff); this._bpf.update(s, cutoff, this.bandq); s = this._bpf.s0; } From ccc90d547cf85ec06b0433b6c02eb18c52112f1c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 6 Jun 2025 15:47:19 +0200 Subject: [PATCH 017/257] ignore dough-export to fix checks --- eslint.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 19d9bb390..0ba44da07 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -42,6 +42,7 @@ export default [ '**/hydra.mjs', '**/jsdoc-synonyms.js', 'packages/hs2js/src/hs2js.mjs', + 'packages/superdough/dough-export.mjs', '**/samples', ], }, From 43c05cb5041e18431dd297be5fb5a9b3044aaf0d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 6 Jun 2025 16:17:05 +0200 Subject: [PATCH 018/257] move supradough to separate package --- eslint.config.mjs | 2 +- packages/superdough/package.json | 3 +- packages/superdough/superdough.mjs | 9 ++++- packages/superdough/synth.mjs | 4 +- packages/superdough/worklets.mjs | 25 ------------- .../{superdough => supradough}/.gitignore | 0 packages/supradough/README.md | 3 ++ .../dough-export.mjs | 0 packages/supradough/dough-worklet.mjs | 27 ++++++++++++++ packages/{superdough => supradough}/dough.mjs | 0 packages/supradough/index.mjs | 4 ++ packages/supradough/package.json | 37 +++++++++++++++++++ packages/webaudio/package.json | 3 +- packages/webaudio/webaudio.mjs | 6 ++- pnpm-lock.yaml | 12 ++++++ 15 files changed, 102 insertions(+), 33 deletions(-) rename packages/{superdough => supradough}/.gitignore (100%) create mode 100644 packages/supradough/README.md rename packages/{superdough => supradough}/dough-export.mjs (100%) create mode 100644 packages/supradough/dough-worklet.mjs rename packages/{superdough => supradough}/dough.mjs (100%) create mode 100644 packages/supradough/index.mjs create mode 100644 packages/supradough/package.json diff --git a/eslint.config.mjs b/eslint.config.mjs index 0ba44da07..f2cf86307 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -42,7 +42,7 @@ export default [ '**/hydra.mjs', '**/jsdoc-synonyms.js', 'packages/hs2js/src/hs2js.mjs', - 'packages/superdough/dough-export.mjs', + 'packages/supradough/dough-export.mjs', '**/samples', ], }, diff --git a/packages/superdough/package.json b/packages/superdough/package.json index 6b45144e4..a835f252d 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -33,8 +33,7 @@ "homepage": "https://github.com/tidalcycles/strudel#readme", "devDependencies": { "vite": "^6.0.11", - "vite-plugin-bundle-audioworklet": "workspace:*", - "wav-encoder": "^1.3.0" + "vite-plugin-bundle-audioworklet": "workspace:*" }, "dependencies": { "nanostores": "^0.11.3" diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 1069d4e84..a6e54435c 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -190,11 +190,18 @@ 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]); + console.log('allWorkletURLs', allWorkletURLs); + workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL))); } return workletsLoading; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 7d175d6c4..834e54c6d 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -12,7 +12,7 @@ import { } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; -export const getFrequencyFromValue = (value) => { +const getFrequencyFromValue = (value) => { let { note, freq } = value; note = note || 36; if (typeof note === 'string') { @@ -25,7 +25,7 @@ export const getFrequencyFromValue = (value) => { return Number(freq); }; -export function destroyAudioWorkletNode(node) { +function destroyAudioWorkletNode(node) { if (node == null) { return; } diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index d8f189779..24f7434a9 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -4,7 +4,6 @@ import OLAProcessor from './ola-processor'; import FFT from './fft.js'; -import { Dough } from './dough.mjs'; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const _mod = (n, m) => ((n % m) + m) % m; @@ -896,27 +895,3 @@ class ByteBeatProcessor extends AudioWorkletProcessor { } registerProcessor('byte-beat-processor', ByteBeatProcessor); - -class DoughProcessor extends AudioWorkletProcessor { - constructor() { - super(); - this.dough = new Dough(sampleRate, currentTime); - this.port.onmessage = (event) => this.dough.scheduleSpawn(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.channels[c], -1, 1); - } - } - return true; // keep the audio processing going - } -} - -registerProcessor('dough-processor', DoughProcessor); diff --git a/packages/superdough/.gitignore b/packages/supradough/.gitignore similarity index 100% rename from packages/superdough/.gitignore rename to packages/supradough/.gitignore diff --git a/packages/supradough/README.md b/packages/supradough/README.md new file mode 100644 index 000000000..a8cfa84b3 --- /dev/null +++ b/packages/supradough/README.md @@ -0,0 +1,3 @@ +# supradough + +platform agnostic synth and sampler intended for live coding. a reimplementation of superdough. \ No newline at end of file diff --git a/packages/superdough/dough-export.mjs b/packages/supradough/dough-export.mjs similarity index 100% rename from packages/superdough/dough-export.mjs rename to packages/supradough/dough-export.mjs diff --git a/packages/supradough/dough-worklet.mjs b/packages/supradough/dough-worklet.mjs new file mode 100644 index 000000000..0361afa0d --- /dev/null +++ b/packages/supradough/dough-worklet.mjs @@ -0,0 +1,27 @@ +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) => this.dough.scheduleSpawn(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.channels[c], -1, 1); + } + } + return true; // keep the audio processing going + } +} + +registerProcessor('dough-processor', DoughProcessor); diff --git a/packages/superdough/dough.mjs b/packages/supradough/dough.mjs similarity index 100% rename from packages/superdough/dough.mjs rename to packages/supradough/dough.mjs diff --git a/packages/supradough/index.mjs b/packages/supradough/index.mjs new file mode 100644 index 000000000..1bce17f6d --- /dev/null +++ b/packages/supradough/index.mjs @@ -0,0 +1,4 @@ +import _workletUrl from './dough-worklet.mjs?audioworklet'; + +export * from './dough.mjs'; +export const workletUrl = _workletUrl; diff --git a/packages/supradough/package.json b/packages/supradough/package.json new file mode 100644 index 000000000..7e465c0a9 --- /dev/null +++ b/packages/supradough/package.json @@ -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 ", + "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": {} +} diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 5714fddf3..2617c2872 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -35,7 +35,8 @@ "dependencies": { "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", - "superdough": "workspace:*" + "superdough": "workspace:*", + "supradough": "workspace:*" }, "devDependencies": { "vite": "^6.0.11" diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 8f7d041a9..de53bb5cd 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -5,8 +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); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed8a7c014..805412ede 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -485,6 +485,15 @@ importers: vite-plugin-bundle-audioworklet: 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 @@ -597,6 +606,9 @@ importers: superdough: specifier: workspace:* version: link:../superdough + supradough: + specifier: workspace:* + version: link:../supradough devDependencies: vite: specifier: ^6.0.11 From 49f38aeddbf7ce2815805b7c9640df0ba848a0bb Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 6 Jun 2025 16:26:17 +0200 Subject: [PATCH 019/257] remove log --- packages/superdough/superdough.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a6e54435c..2629790df 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -200,7 +200,6 @@ function loadWorklets() { if (!workletsLoading) { const audioCtx = getAudioContext(); const allWorkletURLs = externalWorklets.concat([workletsUrl]); - console.log('allWorkletURLs', allWorkletURLs); workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL))); } From 1dac69635db5bf39324d2febdfc9aae45dc84706 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 7 Jun 2025 10:40:34 +0200 Subject: [PATCH 020/257] feedback delay (still hard coded feedback / time values) --- packages/supradough/dough.mjs | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index a292b0c8b..ba00d672f 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -229,7 +229,7 @@ const MAX_DELAY_TIME = 10; export class Delay { writeIdx = 0; readIdx = 0; - buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); // .fill(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; @@ -479,19 +479,13 @@ export class DoughVoice { this.density = this.density ?? getDefaultValue('density'); this.fanchor = this.fanchor ?? getDefaultValue('fanchor'); this.drive = this.drive ?? 0.69; - this.resonance = this.resonance ?? getDefaultValue('resonance'); - this.hresonance = this.hresonance ?? getDefaultValue('hresonance'); - this.bandq = this.bandq ?? getDefaultValue('bandq'); this.phaserdepth = this.phaserdepth ?? getDefaultValue('phaserdepth'); this.shapevol = this.shapevol ?? getDefaultValue('shapevol'); this.distortvol = this.distortvol ?? getDefaultValue('distortvol'); - this.delay = this.delay ?? getDefaultValue('delay'); - this.delayfeedback = this.delayfeedback ?? getDefaultValue('delayfeedback'); - this.delaytime = this.delaytime ?? getDefaultValue('delaytime'); - this.orbit = this.orbit ?? getDefaultValue('orbit'); this.i = this.i ?? getDefaultValue('i'); this.fft = this.fft ?? getDefaultValue('fft'); this.pan = this.pan ?? getDefaultValue('pan'); + this.orbit = this.orbit ?? getDefaultValue('orbit'); [this.attack, this.decay, this.sustain, this.release] = getADSRValues([ this.attack, @@ -508,6 +502,7 @@ export class DoughVoice { // filter setup this._lpf = this.cutoff ? new TwoPoleFilter() : null; + this.resonance = this.resonance ?? getDefaultValue('resonance'); if (this.lpenv) { this._lpenv = new ADSR(); [this.lpattack, this.lpdecay, this.lpsustain, this.lprelease] = getADSRValues([ @@ -519,6 +514,7 @@ export class DoughVoice { } this._hpf = this.hcutoff ? new TwoPoleFilter() : null; + this.hresonance = this.hresonance ?? getDefaultValue('hresonance'); if (this.hpenv) { this._hpenv = new ADSR(); [this.hpattack, this.hpdecay, this.hpsustain, this.hprelease] = getADSRValues([ @@ -529,6 +525,7 @@ export class DoughVoice { ]); } this._bpf = this.bandf ? new TwoPoleFilter() : null; + this.bandq = this.bandq ?? getDefaultValue('bandq'); if (this.bpenv) { this._bpenv = new ADSR(); [this.bpattack, this.bpdecay, this.bpsustain, this.bprelease] = getADSRValues([ @@ -547,6 +544,12 @@ export class DoughVoice { this._crush = this.crush ? new Crush() : null; this._distort = this.distort ? new Distort() : null; + // delay + this.delay = this.delay ?? getDefaultValue('delay'); + this.delayfeedback = this.delayfeedback ?? getDefaultValue('delayfeedback'); + this.delaytime = this.delaytime ?? getDefaultValue('delaytime'); + + // precalculated values this.piOverSr = Math.PI / value.sampleRate; this.eighthOverLogHalf = 0.125 / Math.log(0.5); } @@ -631,12 +634,17 @@ export class Dough { vid = 0; q = []; channels = [0, 0]; + delaysend = [0, 0]; + delaytime = getDefaultValue('delaytime'); + delayfeedback = getDefaultValue('delayfeedback'); 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 Delay(); + this._delayR = new Delay(); } scheduleSpawn(value) { if (value._begin === undefined) { @@ -696,7 +704,18 @@ export class Dough { this.voices[v].update(this.t / this.sampleRate); this.channels[0] += this.voices[v].l; this.channels[1] += this.voices[v].r; + if (this.voices[v].delay) { + this.delaysend[0] += this.voices[v].l * this.voices[v].delay; + this.delaysend[1] += this.voices[v].r * this.voices[v].delay; + } } + // todo: how to change delaytime / delayfeedback from a voice? + const delayL = this._delayL.update(this.delaysend[0], this.delaytime); + const delayR = this._delayR.update(this.delaysend[1], this.delaytime); + this.delaysend[0] = delayL * this.delayfeedback; + this.delaysend[1] = delayR * this.delayfeedback; + this.channels[0] += delayL; + this.channels[1] += delayR; this.t++; } } From d743570546d3659cd949441180e4a79df2fd023f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 7 Jun 2025 13:12:35 +0200 Subject: [PATCH 021/257] set delay time and feedback from voice --- packages/supradough/dough.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index ba00d672f..14bab22fe 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -707,6 +707,8 @@ export class Dough { if (this.voices[v].delay) { this.delaysend[0] += this.voices[v].l * this.voices[v].delay; this.delaysend[1] += this.voices[v].r * this.voices[v].delay; + this.delaytime = this.voices[v].delaytime; // we trust that these are initialized in the voice + this.delayfeedback = this.voices[v].delayfeedback; } } // todo: how to change delaytime / delayfeedback from a voice? From e54449f5f73cf0d41ff14f460bdd4ca9ccfa59eb Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 7 Jun 2025 13:24:06 +0200 Subject: [PATCH 022/257] white brown pink noise + crackle alias for dust --- packages/supradough/dough.mjs | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 14bab22fe..2050054ec 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -108,6 +108,51 @@ 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) { @@ -369,7 +414,11 @@ let oscillators = { pulse: PulseOsc, pulze: PulzeOsc, dust: Dust, + crackle: Dust, impulse: Impulse, + white: WhiteNoise, + brown: BrownNoise, + pink: PinkNoise, }; const defaultDefaultValues = { From 77463a199aaf68d2d32b4653b2c49443958be9f7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 7 Jun 2025 13:26:43 +0200 Subject: [PATCH 023/257] add square alias for pulse --- packages/supradough/dough.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 2050054ec..0c0e667d9 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -412,6 +412,7 @@ let oscillators = { tri: TriOsc, triangle: TriOsc, pulse: PulseOsc, + square: PulseOsc, pulze: PulzeOsc, dust: Dust, crackle: Dust, From b000f2297d23ddc4b46b8fa5ad4848d504652ea8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 7 Jun 2025 20:31:47 +0200 Subject: [PATCH 024/257] fm --- packages/supradough/dough.mjs | 36 ++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 0c0e667d9..08469e634 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -449,6 +449,7 @@ const defaultDefaultValues = { fft: 8, z: 'triangle', pan: 0.5, + fmh: 1, }; let getDefaultValue = (key) => defaultDefaultValues[key]; @@ -518,6 +519,8 @@ export class DoughVoice { roomsize, ir, analyze, + fmh, + fmi */ value.freq = getFrequency(value); Object.assign(this, value); @@ -550,6 +553,20 @@ export class DoughVoice { const SourceClass = oscillators[this.s] ?? TriOsc; this._sound = new SourceClass(); + if (this.fmi) { + this._fm = new SineOsc(); + this.fmh = this.fmh ?? getDefaultValue('fmh'); + if (this.fmenv) { + this._fmenv = new ADSR(); + [this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease] = getADSRValues([ + this.fmattack, + this.fmdecay, + this.fmsustain, + this.fmrelease, + ]); + } + } + // filter setup this._lpf = this.cutoff ? new TwoPoleFilter() : null; this.resonance = this.resonance ?? getDefaultValue('resonance'); @@ -613,13 +630,26 @@ export class DoughVoice { return 0; } let s = 0; + let gate = Number(t >= this._begin && t <= this._holdEnd); + + let freq = this.freq; + 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) ** 2; + fmi = /* 2 ** */ this.fmenv * env * fmi; // todo: find good scaling + } + const modfreq = freq * this.fmh; + const modgain = modfreq * fmi; + freq = freq + this._fm.update(modfreq) * modgain; + } + // sound source if (this.s === 'pulse') { - s = this._sound.update(this.freq, this.pw ?? 0.5); + s = this._sound.update(freq, this.pw ?? 0.5); } else { - s = this._sound.update(this.freq); + s = this._sound.update(freq); } - let gate = Number(t >= this._begin && t <= this._holdEnd); s = s * this.gain * this.velocity; // lpf From 2f2bf5bf29541ea1c4edee5e70302b109f54ce0b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 7 Jun 2025 22:36:54 +0200 Subject: [PATCH 025/257] fix: note2midi for notes without octave --- packages/supradough/dough.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 08469e634..2853f3971 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -457,7 +457,7 @@ 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) => { - const [pc, acc = '', oct = defaultOctave] = + let [pc, acc = '', oct = ''] = String(note) .match(/^([a-gA-G])([#bsf]*)([0-9]*)$/) ?.slice(1) || []; @@ -466,13 +466,14 @@ const note2midi = (note, defaultOctave = 3) => { } const chroma = chromas[pc.toLowerCase()]; const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0; - return (Number(oct) + 1) * 12 + chroma + offset; + oct = Number(oct || defaultOctave); + return (oct + 1) * 12 + chroma + offset; }; const getFrequency = (value) => { let { note, freq } = value; note = note || 36; if (typeof note === 'string') { - note = note2midi(note); // e.g. c3 => 48 + note = note2midi(note, 3); // e.g. c3 => 48 } if (!freq && typeof note === 'number') { freq = Math.pow(2, (note - 69) / 12) * 440; From 5988e3d3b48541444440f6c165ea1b37ed22ff02 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 7 Jun 2025 22:42:48 +0200 Subject: [PATCH 026/257] fix: hot reloading for dough.mjs (see comment) --- packages/supradough/index.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/supradough/index.mjs b/packages/supradough/index.mjs index 1bce17f6d..54a835495 100644 --- a/packages/supradough/index.mjs +++ b/packages/supradough/index.mjs @@ -1,4 +1,4 @@ -import _workletUrl from './dough-worklet.mjs?audioworklet'; +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; From ab052ce1ac6fe4232edd65e877dc2cb041df0d48 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 7 Jun 2025 23:15:09 +0200 Subject: [PATCH 027/257] fix: polyblepped sawtooth + fm = death --- packages/supradough/dough.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 2853f3971..826172a2f 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -450,6 +450,7 @@ const defaultDefaultValues = { z: 'triangle', pan: 0.5, fmh: 1, + fmenv: 0, // differs from superdough }; let getDefaultValue = (key) => defaultDefaultValues[key]; @@ -540,6 +541,7 @@ export class DoughVoice { this.fft = this.fft ?? getDefaultValue('fft'); this.pan = this.pan ?? getDefaultValue('pan'); this.orbit = this.orbit ?? getDefaultValue('orbit'); + this.fmenv = this.fmenv ?? getDefaultValue('fmenv'); [this.attack, this.decay, this.sustain, this.release] = getADSRValues([ this.attack, @@ -551,6 +553,9 @@ export class DoughVoice { this._holdEnd = this._begin + this._duration; // needed for gate this._end = this._holdEnd + this.release + 0.01; // needed for despawn + if (this.s === 'saw' || this.s === 'sawtooth') { + this.s = 'zaw'; // polyblepped saw when fm is applied + } const SourceClass = oscillators[this.s] ?? TriOsc; this._sound = new SourceClass(); @@ -638,7 +643,7 @@ export class DoughVoice { let fmi = this.fmi; if (this._fmenv) { const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease) ** 2; - fmi = /* 2 ** */ this.fmenv * env * fmi; // todo: find good scaling + fmi = this.fmenv * env * fmi; } const modfreq = freq * this.fmh; const modgain = modfreq * fmi; From ac7ee376e5a0ed121526096baa3839a2e117aebe Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 8 Jun 2025 00:43:58 +0200 Subject: [PATCH 028/257] pitch envelope --- packages/supradough/dough.mjs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 826172a2f..809b8d52b 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -559,6 +559,16 @@ export class DoughVoice { const SourceClass = oscillators[this.s] ?? TriOsc; this._sound = new SourceClass(); + if (this.penv) { + this._penv = new ADSR(); + [this.pattack, this.pdecay, this.psustain, this.prelease] = getADSRValues([ + this.pattack, + this.pdecay, + this.psustain, + this.prelease, + ]); + } + if (this.fmi) { this._fm = new SineOsc(); this.fmh = this.fmh ?? getDefaultValue('fmh'); @@ -650,6 +660,11 @@ export class DoughVoice { freq = freq + this._fm.update(modfreq) * modgain; } + if (this._penv) { + const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease) ** 2; + freq = freq + env * this.penv; + } + // sound source if (this.s === 'pulse') { s = this._sound.update(freq, this.pw ?? 0.5); From 0f2aa9569b9cabc3c1144e01c5586e4099baab0d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 8 Jun 2025 02:04:49 +0200 Subject: [PATCH 029/257] half working samples --- packages/supradough/dough-worklet.mjs | 14 ++- packages/supradough/dough.mjs | 41 ++++++-- packages/webaudio/index.mjs | 1 + packages/webaudio/supradough.mjs | 138 +++++++++++++++++++++++++- 4 files changed, 182 insertions(+), 12 deletions(-) diff --git a/packages/supradough/dough-worklet.mjs b/packages/supradough/dough-worklet.mjs index 0361afa0d..97a35c487 100644 --- a/packages/supradough/dough-worklet.mjs +++ b/packages/supradough/dough-worklet.mjs @@ -6,7 +6,19 @@ class DoughProcessor extends AudioWorkletProcessor { constructor() { super(); this.dough = new Dough(sampleRate, currentTime); - this.port.onmessage = (event) => this.dough.scheduleSpawn(event.data); + 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); + } else if (event.data.samples) { + event.data.samples.forEach(([name, channels]) => { + this.dough.loadSample(name, channels); + }); + } else { + console.log('unrecognized event type', event.data); + } + }; } process(inputs, outputs, params) { if (this.disconnected) { diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 809b8d52b..e42b4f8e8 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -383,6 +383,19 @@ export class Distort { } // distortion could be expressed as a function, because it's stateless +export class BufferPlayer { + channels = []; + pos = 0; + update(freq, channel = 0) { + if (this.pos >= this.channels[channel].length) { + return 0; + } + let s = this.channels[channel][this.pos]; + this.pos++; + return s; + } +} + export function _rangex(sig, min, max) { let logmin = Math.log(min); let range = Math.log(max) - logmin; @@ -403,7 +416,7 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => { return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)]; }; -let oscillators = { +let shapes = { sine: SineOsc, saw: SawOsc, zaw: ZawOsc, @@ -553,11 +566,20 @@ export class DoughVoice { this._holdEnd = this._begin + this._duration; // needed for gate this._end = this._holdEnd + this.release + 0.01; // needed for despawn + this.s ??= 'triangle'; if (this.s === 'saw' || this.s === 'sawtooth') { this.s = 'zaw'; // polyblepped saw when fm is applied } - const SourceClass = oscillators[this.s] ?? TriOsc; - this._sound = new SourceClass(); + if (shapes[this.s]) { + const SourceClass = shapes[this.s]; + this._sound = new SourceClass(); + } else if (value.samples.has(this.s)) { + this._sample = new BufferPlayer(); + this._sample.channels = value.samples.get(this.s); + this._sample.pos = 0; + } else { + console.warn('sound not found', this.s); + } if (this.penv) { this._penv = new ADSR(); @@ -642,7 +664,7 @@ export class DoughVoice { return 1 - Math.log(c) * this.eighthOverLogHalf; } update(t) { - if (!this._sound) { + if (!this._sound && !this._sample) { return 0; } let s = 0; @@ -666,10 +688,12 @@ export class DoughVoice { } // sound source - if (this.s === 'pulse') { + if (this._sound && this.s === 'pulse') { s = this._sound.update(freq, this.pw ?? 0.5); - } else { + } else if (this._sound) { s = this._sound.update(freq); + } else if (this._sample) { + s = this._sample.update(freq, 0); // tbd: stereo samples... } s = s * this.gain * this.velocity; @@ -738,6 +762,7 @@ export class Dough { delaysend = [0, 0]; delaytime = getDefaultValue('delaytime'); delayfeedback = getDefaultValue('delayfeedback'); + samples = new Map(); t = 0; // sampleRate: number, currentTime: number (seconds) constructor(sampleRate = 48000, currentTime = 0) { @@ -747,6 +772,9 @@ export class Dough { this._delayL = new Delay(); this._delayR = new Delay(); } + loadSample(name, channels) { + this.samples.set(name, channels); + } scheduleSpawn(value) { if (value._begin === undefined) { throw new Error('[dough]: scheduleSpawn expected _begin to be set'); @@ -760,6 +788,7 @@ export class Dough { } spawn(value) { value.id = this.vid++; + value.samples = this.samples; const voice = new DoughVoice(value); this.voices.push(voice); // console.log('spawn', voice.id, 'voices:', this.voices.length); diff --git a/packages/webaudio/index.mjs b/packages/webaudio/index.mjs index 59672b617..f89e12696 100644 --- a/packages/webaudio/index.mjs +++ b/packages/webaudio/index.mjs @@ -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'; diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index 8df18bed6..38b2330c3 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -20,10 +20,138 @@ Pattern.prototype.supradough = function () { return this.onTrigger((_, hap, __, cps, begin) => { hap.value._begin = begin; hap.value._duration = hap.duration / cps; - - if (!doughWorklet) { - initDoughWorklet(); - } - doughWorklet.port.postMessage(hap.value); + !doughWorklet && initDoughWorklet(); + doughWorklet.port.postMessage({ spawn: hap.value }); }, 1); }; + +async function loadSampleChannels(url) { + const buffer = await fetch(url) + .then((res) => res.arrayBuffer()) + .then((buf) => getAudioContext().decodeAudioData(buf)); + // console.log('buffer', buffer, buffer.numberOfChannels); + let channels = []; + for (let i = 0; i < buffer.numberOfChannels; i++) { + channels.push(buffer.getChannelData(i)); + } + return channels; +} + +let samples = { + casio: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/casio/high.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/casio/low.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/casio/noise.wav', + ], + crow: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/000_crow.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/001_crow2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/002_crow3.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/003_crow4.wav', + ], + insect: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/insect/000_everglades_conehead.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/insect/001_robust_shieldback.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/insect/002_seashore_meadow_katydid.wav', + ], + wind: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/000_wind1.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/001_wind10.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/002_wind2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/003_wind3.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/004_wind4.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/005_wind5.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/006_wind6.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/007_wind7.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/008_wind8.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/009_wind9.wav', + ], + jazz: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/000_BD.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/001_CB.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/002_FX.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/003_HH.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/004_OH.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/005_P1.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/006_P2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/007_SN.wav', + ], + metal: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/000_0.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/001_1.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/002_2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/003_3.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/004_4.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/005_5.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/006_6.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/007_7.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/008_8.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/009_9.wav', + ], + east: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/000_nipon_wood_block.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/001_ohkawa_mute.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/002_ohkawa_open.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/003_shime_hi.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/004_shime_hi_2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/005_shime_mute.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/006_taiko_1.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/007_taiko_2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/008_taiko_3.wav', + ], + space: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/000_0.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/001_1.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/002_11.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/003_12.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/004_13.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/005_14.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/006_15.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/007_16.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/008_17.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/009_18.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/010_2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/011_3.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/012_4.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/013_5.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/014_6.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/015_7.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/016_8.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/017_9.wav', + ], + numbers: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/0.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/1.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/3.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/4.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/5.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/6.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/7.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/8.wav', + ], + piano: ['https://raw.githubusercontent.com/felixroos/dough-samples/refs/heads/main/piano/A3v8.mp3'], + flute: ['https://raw.githubusercontent.com/felixroos/samples/refs/heads/main/flute/c4.mp3'], + bd: [ + 'https://raw.githubusercontent.com/geikha/tidal-drum-machines/15eac73c5e878550f91d864a4863e014799403f1/machines/RolandTR909/rolandtr909-bd/Bassdrum-01.wav', + ], +}; +// for some reason, only piano and flute work.. is it because mp3?? + +let loaded = false; +export async function doughsample() { + !doughWorklet && initDoughWorklet(); + if (loaded) { + return; + } + loaded = true; + const sampleMap = await Promise.all( + Object.entries(samples).map(async ([key, url]) => { + url = url[0]; + console.log(key, 'url', url); + return [key, await loadSampleChannels(url)]; + }), + ); + console.log('sampleMap', sampleMap); + doughWorklet.port.postMessage({ samples: sampleMap }); +} From 356d4360ecbbe09a21b697de7c563a4387b572cd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 8 Jun 2025 09:05:14 +0200 Subject: [PATCH 030/257] fix: scheduling + sample playback --- packages/supradough/dough.mjs | 14 ++++++-------- packages/webaudio/supradough.mjs | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index e42b4f8e8..dc083bac7 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -384,6 +384,7 @@ export class Distort { // distortion could be expressed as a function, because it's stateless export class BufferPlayer { + static samples = new Map(); channels = []; pos = 0; update(freq, channel = 0) { @@ -573,10 +574,9 @@ export class DoughVoice { if (shapes[this.s]) { const SourceClass = shapes[this.s]; this._sound = new SourceClass(); - } else if (value.samples.has(this.s)) { + } else if (BufferPlayer.samples.has(this.s)) { this._sample = new BufferPlayer(); - this._sample.channels = value.samples.get(this.s); - this._sample.pos = 0; + this._sample.channels = BufferPlayer.samples.get(this.s); } else { console.warn('sound not found', this.s); } @@ -735,7 +735,6 @@ export class DoughVoice { this._crush && (s = this._crush.update(s, this.crush)); this._distort && (s = this._distort.update(s, this.distort, this.distortvol)); - /* Math.random() > 0.99 && console.log('gate', gate); */ const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); s = s * env; @@ -762,7 +761,6 @@ export class Dough { delaysend = [0, 0]; delaytime = getDefaultValue('delaytime'); delayfeedback = getDefaultValue('delayfeedback'); - samples = new Map(); t = 0; // sampleRate: number, currentTime: number (seconds) constructor(sampleRate = 48000, currentTime = 0) { @@ -773,7 +771,7 @@ export class Dough { this._delayR = new Delay(); } loadSample(name, channels) { - this.samples.set(name, channels); + BufferPlayer.samples.set(name, channels); } scheduleSpawn(value) { if (value._begin === undefined) { @@ -783,12 +781,12 @@ export class Dough { throw new Error('[dough]: scheduleSpawn expected _duration to be set'); } value.sampleRate = this.sampleRate; - const time = value._begin; // set from supradough.mjs + // 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++; - value.samples = this.samples; const voice = new DoughVoice(value); this.voices.push(voice); // console.log('spawn', voice.id, 'voices:', this.voices.length); diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index 38b2330c3..317f3aa31 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -25,18 +25,6 @@ Pattern.prototype.supradough = function () { }, 1); }; -async function loadSampleChannels(url) { - const buffer = await fetch(url) - .then((res) => res.arrayBuffer()) - .then((buf) => getAudioContext().decodeAudioData(buf)); - // console.log('buffer', buffer, buffer.numberOfChannels); - let channels = []; - for (let i = 0; i < buffer.numberOfChannels; i++) { - channels.push(buffer.getChannelData(i)); - } - return channels; -} - let samples = { casio: [ 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/casio/high.wav', @@ -138,6 +126,17 @@ let samples = { }; // for some reason, only piano and flute work.. is it because mp3?? +async function loadSampleChannels(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; +} + let loaded = false; export async function doughsample() { !doughWorklet && initDoughWorklet(); @@ -148,8 +147,9 @@ export async function doughsample() { const sampleMap = await Promise.all( Object.entries(samples).map(async ([key, url]) => { url = url[0]; - console.log(key, 'url', url); - return [key, await loadSampleChannels(url)]; + const channels = await loadSampleChannels(url); + // console.log(key, 'url', url, channels); + return [key, channels]; }), ); console.log('sampleMap', sampleMap); From 55aef1885117a94c4a153368f3b29f762a698db8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 8 Jun 2025 14:43:23 +0200 Subject: [PATCH 031/257] buffers can now be repitched (still with aliasing) --- packages/supradough/dough-worklet.mjs | 6 +++--- packages/supradough/dough.mjs | 22 ++++++++++++++-------- packages/webaudio/supradough.mjs | 10 ++++------ 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/supradough/dough-worklet.mjs b/packages/supradough/dough-worklet.mjs index 97a35c487..dd2315628 100644 --- a/packages/supradough/dough-worklet.mjs +++ b/packages/supradough/dough-worklet.mjs @@ -10,10 +10,10 @@ class DoughProcessor extends AudioWorkletProcessor { if (event.data.spawn) { this.dough.scheduleSpawn(event.data.spawn); } else if (event.data.sample) { - this.dough.loadSample(event.data.sample, event.data.channels); + this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate); } else if (event.data.samples) { - event.data.samples.forEach(([name, channels]) => { - this.dough.loadSample(name, channels); + event.data.samples.forEach(([name, channels, sampleRate]) => { + this.dough.loadSample(name, channels, sampleRate); }); } else { console.log('unrecognized event type', event.data); diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index dc083bac7..b378ee70e 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -385,14 +385,16 @@ export class Distort { export class BufferPlayer { static samples = new Map(); - channels = []; + buffer; // { channels: Float32Array, sampleRate: number } pos = 0; + sampleFreq = 261.626; // middle c update(freq, channel = 0) { - if (this.pos >= this.channels[channel].length) { + if (this.pos >= this.buffer.channels[channel].length) { return 0; } - let s = this.channels[channel][this.pos]; - this.pos++; + const speed = ((freq / this.sampleFreq) * this.buffer.sampleRate) / SAMPLE_RATE; + let s = this.buffer.channels[channel][Math.floor(this.pos)]; + this.pos = this.pos + speed; return s; } } @@ -576,7 +578,8 @@ export class DoughVoice { this._sound = new SourceClass(); } else if (BufferPlayer.samples.has(this.s)) { this._sample = new BufferPlayer(); - this._sample.channels = BufferPlayer.samples.get(this.s); + const buffer = BufferPlayer.samples.get(this.s); + this._sample.buffer = buffer; } else { console.warn('sound not found', this.s); } @@ -738,7 +741,10 @@ export class DoughVoice { const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); s = s * env; - s = s * this.postgain * 0.2; + s = s * this.postgain; + if (!this._sample) { + s = s * 0.2; // turn down waveforms + } if (this.pan === 0.5) { this.l = this.r = s; // mono @@ -770,8 +776,8 @@ export class Dough { this._delayL = new Delay(); this._delayR = new Delay(); } - loadSample(name, channels) { - BufferPlayer.samples.set(name, channels); + loadSample(name, channels, sampleRate) { + BufferPlayer.samples.set(name, { channels, sampleRate }); } scheduleSpawn(value) { if (value._begin === undefined) { diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index 317f3aa31..269dcda95 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -118,7 +118,7 @@ let samples = { 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/7.wav', 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/8.wav', ], - piano: ['https://raw.githubusercontent.com/felixroos/dough-samples/refs/heads/main/piano/A3v8.mp3'], + piano: ['https://raw.githubusercontent.com/felixroos/dough-samples/refs/heads/main/piano/C3v8.mp3'], flute: ['https://raw.githubusercontent.com/felixroos/samples/refs/heads/main/flute/c4.mp3'], bd: [ 'https://raw.githubusercontent.com/geikha/tidal-drum-machines/15eac73c5e878550f91d864a4863e014799403f1/machines/RolandTR909/rolandtr909-bd/Bassdrum-01.wav', @@ -126,7 +126,7 @@ let samples = { }; // for some reason, only piano and flute work.. is it because mp3?? -async function loadSampleChannels(url) { +async function loadSampleChannels(key, url) { const buffer = await fetch(url) .then((res) => res.arrayBuffer()) .then((buf) => getAudioContext().decodeAudioData(buf)); @@ -134,7 +134,7 @@ async function loadSampleChannels(url) { for (let i = 0; i < buffer.numberOfChannels; i++) { channels.push(buffer.getChannelData(i)); } - return channels; + return [key, channels, buffer.sampleRate]; } let loaded = false; @@ -147,9 +147,7 @@ export async function doughsample() { const sampleMap = await Promise.all( Object.entries(samples).map(async ([key, url]) => { url = url[0]; - const channels = await loadSampleChannels(url); - // console.log(key, 'url', url, channels); - return [key, channels]; + return loadSampleChannels(key, url); }), ); console.log('sampleMap', sampleMap); From ec9109dd892386c65e3a374da48ed8eb0df53b59 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 8 Jun 2025 19:24:55 +0200 Subject: [PATCH 032/257] exponential lerp --- packages/supradough/dough.mjs | 38 ++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 08469e634..7ee353277 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -192,16 +192,30 @@ export class Hold { } } -function lerp(x, y0, y1) { +function lerp(x, y0, y1, exponent = 1) { + if (x <= 0) return y0; if (x >= 1) return y1; - return y0 + x * (y1 - y0); + 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 { - state = 'off'; - startTime = 0; - startVal = 0; + constructor(props = {}) { + this.state = 'off' + this.startTime = 0; + this.startVal = 0; + this.curve = props.curve ?? 1; + } update(curTime, gate, attack, decay, susVal, release) { switch (this.state) { @@ -220,11 +234,11 @@ export class ADSR { this.startTime = curTime; return 1; } - return lerp(time / attack, this.startVal, 1); + return lerp(time / attack, this.startVal, 1, this.curve); } case 'decay': { let time = curTime - this.startTime; - let curVal = lerp(time / decay, 1, susVal); + let curVal = lerp(time / decay, 1, susVal, -this.curve); if (gate <= 0) { this.state = 'release'; this.startTime = curTime; @@ -253,7 +267,7 @@ export class ADSR { this.state = 'off'; return 0; } - let curVal = lerp(time / release, this.startVal, 0); + let curVal = lerp(time / release, this.startVal, 0, -this.curve); if (gate > 0) { this.state = 'attack'; this.startTime = curTime; @@ -564,6 +578,7 @@ export class DoughVoice { this.fmsustain, this.fmrelease, ]); + } } @@ -571,7 +586,9 @@ export class DoughVoice { this._lpf = this.cutoff ? new TwoPoleFilter() : null; this.resonance = this.resonance ?? getDefaultValue('resonance'); if (this.lpenv) { - this._lpenv = new ADSR(); + + this._lpenv = new ADSR({curve: 4}); + [this.lpattack, this.lpdecay, this.lpsustain, this.lprelease] = getADSRValues([ this.lpattack, this.lpdecay, @@ -584,6 +601,7 @@ export class DoughVoice { this.hresonance = this.hresonance ?? getDefaultValue('hresonance'); if (this.hpenv) { this._hpenv = new ADSR(); + [this.hpattack, this.hpdecay, this.hpsustain, this.hprelease] = getADSRValues([ this.hpattack, this.hpdecay, @@ -604,7 +622,7 @@ export class DoughVoice { } // gain envelope - this._adsr = new ADSR(); + this._adsr = new ADSR({curve: 2}); // fx setup this._coarse = this.coarse ? new Coarse() : null; From 181e51ef0e1421031187231c8b131aca9e3570cc Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 8 Jun 2025 20:26:20 +0200 Subject: [PATCH 033/257] adjustable envelope curves --- packages/supradough/dough.mjs | 38 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index e16d11f8a..1b1be3e1d 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -214,7 +214,7 @@ export class ADSR { this.state = 'off' this.startTime = 0; this.startVal = 0; - this.curve = props.curve ?? 1; + this.decayCurve = props.decayCurve ?? 1; } update(curTime, gate, attack, decay, susVal, release) { @@ -234,11 +234,11 @@ export class ADSR { this.startTime = curTime; return 1; } - return lerp(time / attack, this.startVal, 1, this.curve); + return lerp(time / attack, this.startVal, 1, 1); } case 'decay': { let time = curTime - this.startTime; - let curVal = lerp(time / decay, 1, susVal, -this.curve); + let curVal = lerp(time / decay, 1, susVal, -this.decayCurve); if (gate <= 0) { this.state = 'release'; this.startTime = curTime; @@ -267,7 +267,7 @@ export class ADSR { this.state = 'off'; return 0; } - let curVal = lerp(time / release, this.startVal, 0, -this.curve); + let curVal = lerp(time / release, this.startVal, 0, -this.decayCurve); if (gate > 0) { this.state = 'attack'; this.startTime = curTime; @@ -599,7 +599,7 @@ export class DoughVoice { } if (this.penv) { - this._penv = new ADSR(); + this._penv = new ADSR({ decayCurve: 4 }); [this.pattack, this.pdecay, this.psustain, this.prelease] = getADSRValues([ this.pattack, this.pdecay, @@ -612,14 +612,14 @@ export class DoughVoice { this._fm = new SineOsc(); this.fmh = this.fmh ?? getDefaultValue('fmh'); if (this.fmenv) { - this._fmenv = new ADSR(); + this._fmenv = new ADSR({ decayCurve: 2 }); [this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease] = getADSRValues([ this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease, ]); - + } } @@ -628,8 +628,8 @@ export class DoughVoice { this.resonance = this.resonance ?? getDefaultValue('resonance'); if (this.lpenv) { - this._lpenv = new ADSR({curve: 4}); - + this._lpenv = new ADSR({ decayCurve: 4 }); + [this.lpattack, this.lpdecay, this.lpsustain, this.lprelease] = getADSRValues([ this.lpattack, this.lpdecay, @@ -641,8 +641,8 @@ export class DoughVoice { this._hpf = this.hcutoff ? new TwoPoleFilter() : null; this.hresonance = this.hresonance ?? getDefaultValue('hresonance'); if (this.hpenv) { - this._hpenv = new ADSR(); - + this._hpenv = new ADSR({ decayCurve: 4 }); + [this.hpattack, this.hpdecay, this.hpsustain, this.hprelease] = getADSRValues([ this.hpattack, this.hpdecay, @@ -653,7 +653,7 @@ export class DoughVoice { this._bpf = this.bandf ? new TwoPoleFilter() : null; this.bandq = this.bandq ?? getDefaultValue('bandq'); if (this.bpenv) { - this._bpenv = new ADSR(); + this._bpenv = new ADSR({ decayCurve: 4 }); [this.bpattack, this.bpdecay, this.bpsustain, this.bprelease] = getADSRValues([ this.bpattack, this.bpdecay, @@ -663,7 +663,7 @@ export class DoughVoice { } // gain envelope - this._adsr = new ADSR({curve: 2}); + this._adsr = new ADSR({ decayCurve: 2 }); // fx setup this._coarse = this.coarse ? new Coarse() : null; @@ -695,7 +695,7 @@ export class DoughVoice { 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) ** 2; + 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; @@ -704,7 +704,7 @@ export class DoughVoice { } if (this._penv) { - const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease) ** 2; + const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease); freq = freq + env * this.penv; } @@ -722,8 +722,8 @@ export class DoughVoice { if (this._lpf) { let cutoff = this.cutoff; if (this._lpenv) { - const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease) ** 2; - cutoff = 2 ** this.lpenv * env * cutoff + cutoff; + const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease); + cutoff = this.lpenv * env * cutoff + cutoff; } cutoff = this.freq2cutoff(cutoff); this._lpf.update(s, cutoff, this.resonance); @@ -733,7 +733,7 @@ export class DoughVoice { if (this._hpf) { let cutoff = this.hcutoff; if (this._hpenv) { - const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease) ** 2; + const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease); cutoff = 2 ** this.hpenv * env * cutoff + cutoff; } cutoff = this.freq2cutoff(cutoff); @@ -744,7 +744,7 @@ export class DoughVoice { if (this._bpf) { let cutoff = this.bandf; if (this._bpenv) { - const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease) ** 2; + const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease); cutoff = 2 ** this.bpenv * env * cutoff + cutoff; } cutoff = this.freq2cutoff(cutoff); From 73ef43dd2c85db8493c7a0d41fda2ec0cd824f58 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 8 Jun 2025 22:56:55 +0200 Subject: [PATCH 034/257] move things around in init + shorten things --- packages/supradough/dough.mjs | 226 ++++++++++++---------------------- 1 file changed, 79 insertions(+), 147 deletions(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 1b1be3e1d..f0665b779 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -211,7 +211,7 @@ function lerp(x, y0, y1, exponent = 1) { export class ADSR { constructor(props = {}) { - this.state = 'off' + this.state = 'off'; this.startTime = 0; this.startVal = 0; this.decayCurve = props.decayCurve ?? 1; @@ -398,7 +398,7 @@ export class Distort { // distortion could be expressed as a function, because it's stateless export class BufferPlayer { - static samples = new Map(); + static samples = new Map(); // string -> { channels, sampleRate } buffer; // { channels: Float32Array, sampleRate: number } pos = 0; sampleFreq = 261.626; // middle c @@ -421,7 +421,7 @@ export function _rangex(sig, min, max) { } // duplicate -export const getADSRValues = (params, curve = 'linear', defaultValues) => { +export const getADSR = (params, curve = 'linear', defaultValues) => { const envmin = curve === 'exponential' ? 0.001 : 0.001; const releaseMin = 0.01; const envmax = 1; @@ -517,167 +517,99 @@ export class DoughVoice { l = 0; r = 0; constructor(value) { - // params without defaults: - /* - bank, - source, - cutoff, - lpenv, - lpattack, - lpdecay, - lpsustain, - lprelease, - hpenv, - hcutoff, - hpattack, - hpdecay, - hpsustain, - hprelease, - bpenv, - bandf, - bpattack, - bpdecay, - bpsustain, - bprelease, - phaserrate, - phasersweep, - phasercenter, - shape, - vowel, - room, - roomfade, - roomlp, - roomdim, - roomsize, - ir, - analyze, - fmh, - fmi - */ value.freq = getFrequency(value); - Object.assign(this, value); - // params with defaults: - this.s = this.s ?? getDefaultValue('s'); - this.gain = this.gain ?? getDefaultValue('gain'); - this.velocity = this.velocity ?? getDefaultValue('velocity'); - this.postgain = this.postgain ?? getDefaultValue('postgain'); - this.density = this.density ?? getDefaultValue('density'); - this.fanchor = this.fanchor ?? getDefaultValue('fanchor'); - this.drive = this.drive ?? 0.69; - this.phaserdepth = this.phaserdepth ?? getDefaultValue('phaserdepth'); - this.shapevol = this.shapevol ?? getDefaultValue('shapevol'); - this.distortvol = this.distortvol ?? getDefaultValue('distortvol'); - this.i = this.i ?? getDefaultValue('i'); - this.fft = this.fft ?? getDefaultValue('fft'); - this.pan = this.pan ?? getDefaultValue('pan'); - this.orbit = this.orbit ?? getDefaultValue('orbit'); - this.fmenv = this.fmenv ?? getDefaultValue('fmenv'); + let $ = this; + Object.assign($, value); + $.s = $.s ?? getDefaultValue('s'); + $.gain = $.gain ?? getDefaultValue('gain'); + $.velocity = $.velocity ?? getDefaultValue('velocity'); + $.postgain = $.postgain ?? getDefaultValue('postgain'); + $.density = $.density ?? getDefaultValue('density'); + $.fanchor = $.fanchor ?? getDefaultValue('fanchor'); + $.drive = $.drive ?? 0.69; + $.phaserdepth = $.phaserdepth ?? getDefaultValue('phaserdepth'); + $.shapevol = $.shapevol ?? getDefaultValue('shapevol'); + $.distortvol = $.distortvol ?? getDefaultValue('distortvol'); + $.i = $.i ?? getDefaultValue('i'); + $.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'); - [this.attack, this.decay, this.sustain, this.release] = getADSRValues([ - this.attack, - this.decay, - this.sustain, - this.release, - ]); + [$.attack, $.decay, $.sustain, $.release] = getADSR([$.attack, $.decay, $.sustain, $.release]); - this._holdEnd = this._begin + this._duration; // needed for gate - this._end = this._holdEnd + this.release + 0.01; // needed for despawn + $._holdEnd = $._begin + $._duration; // needed for gate + $._end = $._holdEnd + $.release + 0.01; // needed for despawn - this.s ??= 'triangle'; - if (this.s === 'saw' || this.s === 'sawtooth') { - this.s = 'zaw'; // polyblepped saw when fm is applied + $.s ??= 'triangle'; + if ($.s === 'saw' || $.s === 'sawtooth') { + $.s = 'zaw'; // polyblepped saw when fm is applied } - if (shapes[this.s]) { - const SourceClass = shapes[this.s]; - this._sound = new SourceClass(); - } else if (BufferPlayer.samples.has(this.s)) { - this._sample = new BufferPlayer(); - const buffer = BufferPlayer.samples.get(this.s); - this._sample.buffer = buffer; + + if (shapes[$.s]) { + const SourceClass = shapes[$.s]; + $._sound = new SourceClass(); + $._channels = 1; + } else if (BufferPlayer.samples.has($.s)) { + $._sample = new BufferPlayer(); + const buffer = BufferPlayer.samples.get($.s); + $._sample.buffer = buffer; // {channels,sampleRate} + $._channels = $._sample.buffer.channels.length; } else { - console.warn('sound not found', this.s); + console.warn('sound not found', $.s); } - if (this.penv) { - this._penv = new ADSR({ decayCurve: 4 }); - [this.pattack, this.pdecay, this.psustain, this.prelease] = getADSRValues([ - this.pattack, - this.pdecay, - this.psustain, - this.prelease, - ]); + if ($.penv) { + $._penv = new ADSR({ decayCurve: 4 }); + [$.pattack, $.pdecay, $.psustain, $.prelease] = getADSR([$.pattack, $.pdecay, $.psustain, $.prelease]); } - if (this.fmi) { - this._fm = new SineOsc(); - this.fmh = this.fmh ?? getDefaultValue('fmh'); - if (this.fmenv) { - this._fmenv = new ADSR({ decayCurve: 2 }); - [this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease] = getADSRValues([ - this.fmattack, - this.fmdecay, - this.fmsustain, - this.fmrelease, - ]); - + 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]); } } - // filter setup - this._lpf = this.cutoff ? new TwoPoleFilter() : null; - this.resonance = this.resonance ?? getDefaultValue('resonance'); - if (this.lpenv) { - - this._lpenv = new ADSR({ decayCurve: 4 }); - - [this.lpattack, this.lpdecay, this.lpsustain, this.lprelease] = getADSRValues([ - this.lpattack, - this.lpdecay, - this.lpsustain, - this.lprelease, - ]); - } - - this._hpf = this.hcutoff ? new TwoPoleFilter() : null; - this.hresonance = this.hresonance ?? getDefaultValue('hresonance'); - if (this.hpenv) { - this._hpenv = new ADSR({ decayCurve: 4 }); - - [this.hpattack, this.hpdecay, this.hpsustain, this.hprelease] = getADSRValues([ - this.hpattack, - this.hpdecay, - this.hpsustain, - this.hprelease, - ]); - } - this._bpf = this.bandf ? new TwoPoleFilter() : null; - this.bandq = this.bandq ?? getDefaultValue('bandq'); - if (this.bpenv) { - this._bpenv = new ADSR({ decayCurve: 4 }); - [this.bpattack, this.bpdecay, this.bpsustain, this.bprelease] = getADSRValues([ - this.bpattack, - this.bpdecay, - this.bpsustain, - this.bprelease, - ]); - } - // gain envelope - this._adsr = new ADSR({ decayCurve: 2 }); - - // fx setup - this._coarse = this.coarse ? new Coarse() : null; - this._crush = this.crush ? new Crush() : null; - this._distort = this.distort ? new Distort() : null; - + $._adsr = new ADSR({ decayCurve: 2 }); // delay - this.delay = this.delay ?? getDefaultValue('delay'); - this.delayfeedback = this.delayfeedback ?? getDefaultValue('delayfeedback'); - this.delaytime = this.delaytime ?? getDefaultValue('delaytime'); + $.delay = $.delay ?? getDefaultValue('delay'); + $.delayfeedback = $.delayfeedback ?? getDefaultValue('delayfeedback'); + $.delaytime = $.delaytime ?? getDefaultValue('delaytime'); // precalculated values - this.piOverSr = Math.PI / value.sampleRate; - this.eighthOverLogHalf = 0.125 / Math.log(0.5); + $.piOverSr = Math.PI / value.sampleRate; + $.eighthOverLogHalf = 0.125 / Math.log(0.5); + + // 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]); + } + + // 1 per channel:: + $._lpf = $.cutoff ? new TwoPoleFilter() : null; + $._hpf = $.hcutoff ? new TwoPoleFilter() : null; + $._bpf = $.bandf ? new TwoPoleFilter() : null; + + // fx setup + $._coarse = $.coarse ? new Coarse() : null; + $._crush = $.crush ? new Crush() : null; + $._distort = $.distort ? new Distort() : null; } // credits to pulu: https://github.com/felixroos/kabelsalat/issues/35 freq2cutoff(freq) { From 1081fc69e9130bed6c28a516d8a17d6003be7e13 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 9 Jun 2025 09:52:29 +0200 Subject: [PATCH 035/257] refactor: stereo sample support + channelwise fx --- packages/supradough/dough-worklet.mjs | 2 +- packages/supradough/dough.mjs | 176 +++++++++++++++----------- 2 files changed, 101 insertions(+), 77 deletions(-) diff --git a/packages/supradough/dough-worklet.mjs b/packages/supradough/dough-worklet.mjs index dd2315628..18c316b2d 100644 --- a/packages/supradough/dough-worklet.mjs +++ b/packages/supradough/dough-worklet.mjs @@ -29,7 +29,7 @@ class DoughProcessor extends AudioWorkletProcessor { 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.channels[c], -1, 1); + output[c][i] = clamp(this.dough.out[c], -1, 1); } } return true; // keep the audio processing going diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index f0665b779..963bd9bdc 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -399,15 +399,20 @@ export class Distort { export class BufferPlayer { static samples = new Map(); // string -> { channels, sampleRate } - buffer; // { channels: Float32Array, sampleRate: number } + buffer; // Float32Array + sampleRate; pos = 0; sampleFreq = 261.626; // middle c - update(freq, channel = 0) { - if (this.pos >= this.buffer.channels[channel].length) { + constructor(buffer, sampleRate) { + this.buffer = buffer; + this.sampleRate = sampleRate; + } + update(freq) { + if (this.pos >= this.buffer.length) { return 0; } - const speed = ((freq / this.sampleFreq) * this.buffer.sampleRate) / SAMPLE_RATE; - let s = this.buffer.channels[channel][Math.floor(this.pos)]; + const speed = ((freq / this.sampleFreq) * this.sampleRate) / SAMPLE_RATE; + let s = this.buffer[Math.floor(this.pos)]; this.pos = this.pos + speed; return s; } @@ -514,8 +519,7 @@ const getFrequency = (value) => { }; export class DoughVoice { - l = 0; - r = 0; + out = [0, 0]; constructor(value) { value.freq = getFrequency(value); let $ = this; @@ -554,10 +558,12 @@ export class DoughVoice { $._sound = new SourceClass(); $._channels = 1; } else if (BufferPlayer.samples.has($.s)) { - $._sample = new BufferPlayer(); - const buffer = BufferPlayer.samples.get($.s); - $._sample.buffer = buffer; // {channels,sampleRate} - $._channels = $._sample.buffer.channels.length; + 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)); + } } else { console.warn('sound not found', $.s); } @@ -601,15 +607,21 @@ export class DoughVoice { [$.bpattack, $.bpdecay, $.bpsustain, $.bprelease] = getADSR([$.bpattack, $.bpdecay, $.bpsustain, $.bprelease]); } - // 1 per channel:: - $._lpf = $.cutoff ? new TwoPoleFilter() : null; - $._hpf = $.hcutoff ? new TwoPoleFilter() : null; - $._bpf = $.bandf ? new TwoPoleFilter() : null; - - // fx setup - $._coarse = $.coarse ? new Coarse() : null; - $._crush = $.crush ? new Crush() : null; - $._distort = $.distort ? new Distort() : null; + // channelwise effects setup + $._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()); + $._coarse?.push(new Coarse()); + $._crush?.push(new Crush()); + $._distort?.push(new Distort()); + } } // credits to pulu: https://github.com/felixroos/kabelsalat/issues/35 freq2cutoff(freq) { @@ -617,13 +629,13 @@ export class DoughVoice { return 1 - Math.log(c) * this.eighthOverLogHalf; } update(t) { - if (!this._sound && !this._sample) { + if (!this._sound && !this._buffers) { return 0; } - let s = 0; let gate = Number(t >= this._begin && t <= this._holdEnd); let freq = this.freq; + // frequency modulation if (this._fm) { let fmi = this.fmi; if (this._fmenv) { @@ -635,74 +647,86 @@ export class DoughVoice { freq = freq + this._fm.update(modfreq) * modgain; } + // 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; } - // sound source - if (this._sound && this.s === 'pulse') { - s = this._sound.update(freq, this.pw ?? 0.5); - } else if (this._sound) { - s = this._sound.update(freq); - } else if (this._sample) { - s = this._sample.update(freq, 0); // tbd: stereo samples... - } - s = s * this.gain * this.velocity; - - // lpf + // filters + let lpf = this.cutoff; if (this._lpf) { - let cutoff = this.cutoff; if (this._lpenv) { const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease); - cutoff = this.lpenv * env * cutoff + cutoff; + lpf = this.lpenv * env * lpf + lpf; } - cutoff = this.freq2cutoff(cutoff); - this._lpf.update(s, cutoff, this.resonance); - s = this._lpf.s1; + lpf = this.freq2cutoff(lpf); } - // hpf + let hpf = this.hcutoff; if (this._hpf) { - let cutoff = this.hcutoff; if (this._hpenv) { const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease); - cutoff = 2 ** this.hpenv * env * cutoff + cutoff; + hpf = 2 ** this.hpenv * env * hpf + hpf; } - cutoff = this.freq2cutoff(cutoff); - this._hpf.update(s, cutoff, this.hresonance); - s = s - this._hpf.s1; + hpf = this.freq2cutoff(hpf); } - // bpf + let bpf = this.bandf; if (this._bpf) { - let cutoff = this.bandf; if (this._bpenv) { const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease); - cutoff = 2 ** this.bpenv * env * cutoff + cutoff; + bpf = 2 ** this.bpenv * env * bpf + bpf; } - cutoff = this.freq2cutoff(cutoff); - this._bpf.update(s, cutoff, this.bandq); - s = this._bpf.s0; + bpf = this.freq2cutoff(bpf); } - - this._coarse && (s = this._coarse.update(s, this.coarse)); - this._crush && (s = this._crush.update(s, this.crush)); - this._distort && (s = this._distort.update(s, this.distort, this.distortvol)); - + // gain envelope const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); - s = s * env; - s = s * this.postgain; - if (!this._sample) { - s = s * 0.2; // turn down waveforms + // 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 ?? 0.5); + } 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._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], hcutoff, 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.pan === 0.5) { - this.l = this.r = s; // mono - } else { - // stereo - const pos = (this.pan * Math.PI) / 2; - this.l = s * Math.cos(pos); - this.r = s * Math.sin(pos); + 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); } } } @@ -713,7 +737,7 @@ export class Dough { voices = []; // DoughVoice[] vid = 0; q = []; - channels = [0, 0]; + out = [0, 0]; delaysend = [0, 0]; delaytime = getDefaultValue('delaytime'); delayfeedback = getDefaultValue('delayfeedback'); @@ -782,15 +806,15 @@ export class Dough { this.q.shift(); } // add active voices - this.channels[0] = 0; - this.channels[1] = 0; + 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.channels[0] += this.voices[v].l; - this.channels[1] += this.voices[v].r; + 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].l * this.voices[v].delay; - this.delaysend[1] += this.voices[v].r * 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.delayfeedback = this.voices[v].delayfeedback; } @@ -800,8 +824,8 @@ export class Dough { const delayR = this._delayR.update(this.delaysend[1], this.delaytime); this.delaysend[0] = delayL * this.delayfeedback; this.delaysend[1] = delayR * this.delayfeedback; - this.channels[0] += delayL; - this.channels[1] += delayR; + this.out[0] += delayL; + this.out[1] += delayR; this.t++; } } From d1b6b6be8541b833410f147672aa68071dda3f5f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 9 Jun 2025 09:54:24 +0200 Subject: [PATCH 036/257] fix: hpf --- packages/supradough/dough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 963bd9bdc..975840cb3 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -698,7 +698,7 @@ export class DoughVoice { this.out[i] = this._lpf[i].s1; } if (this._hpf) { - this._hpf[i].update(this.out[i], hcutoff, this.hresonance); + this._hpf[i].update(this.out[i], hpf, this.hresonance); this.out[i] = this.out[i] - this._hpf[i].s1; } if (this._bpf) { From 3ac0fdf619d824757aa9f2f6b0da8366b7b42463 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 9 Jun 2025 11:07:57 +0200 Subject: [PATCH 037/257] rudimentary sample map loading (doughsamples) --- packages/superdough/sampler.mjs | 91 ++++++++-------- packages/supradough/dough.mjs | 19 ++-- packages/webaudio/supradough.mjs | 178 ++++++++++++------------------- 3 files changed, 126 insertions(+), 162 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 18d1b7797..9188c17c3 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -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( diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 975840cb3..207638737 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -402,7 +402,7 @@ export class BufferPlayer { buffer; // Float32Array sampleRate; pos = 0; - sampleFreq = 261.626; // middle c + sampleFreq = note2freq(); constructor(buffer, sampleRate) { this.buffer = buffer; this.sampleRate = sampleRate; @@ -411,7 +411,7 @@ export class BufferPlayer { if (this.pos >= this.buffer.length) { return 0; } - const speed = ((freq / this.sampleFreq) * this.sampleRate) / SAMPLE_RATE; + const speed = ((freq / this.sampleFreq) * SAMPLE_RATE) / this.sampleRate; let s = this.buffer[Math.floor(this.pos)]; this.pos = this.pos + speed; return s; @@ -458,6 +458,7 @@ let shapes = { }; const defaultDefaultValues = { + note: 48, s: 'triangle', gain: 1, postgain: 1, @@ -505,23 +506,19 @@ const note2midi = (note, defaultOctave = 3) => { oct = Number(oct || defaultOctave); return (oct + 1) * 12 + chroma + offset; }; -const getFrequency = (value) => { - let { note, freq } = value; - note = note || 36; +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 } - if (!freq && typeof note === 'number') { - freq = Math.pow(2, (note - 69) / 12) * 440; - } - - return Number(freq); + return midi2freq(note); }; export class DoughVoice { out = [0, 0]; constructor(value) { - value.freq = getFrequency(value); + value.freq ??= note2freq(value.note); let $ = this; Object.assign($, value); $.s = $.s ?? getDefaultValue('s'); diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index 269dcda95..3b45415df 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -25,105 +25,58 @@ Pattern.prototype.supradough = function () { }, 1); }; -let samples = { - casio: [ - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/casio/high.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/casio/low.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/casio/noise.wav', - ], - crow: [ - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/000_crow.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/001_crow2.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/002_crow3.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/003_crow4.wav', - ], - insect: [ - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/insect/000_everglades_conehead.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/insect/001_robust_shieldback.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/insect/002_seashore_meadow_katydid.wav', - ], - wind: [ - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/000_wind1.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/001_wind10.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/002_wind2.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/003_wind3.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/004_wind4.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/005_wind5.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/006_wind6.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/007_wind7.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/008_wind8.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/009_wind9.wav', - ], - jazz: [ - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/000_BD.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/001_CB.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/002_FX.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/003_HH.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/004_OH.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/005_P1.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/006_P2.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/007_SN.wav', - ], - metal: [ - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/000_0.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/001_1.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/002_2.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/003_3.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/004_4.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/005_5.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/006_6.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/007_7.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/008_8.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/009_9.wav', - ], - east: [ - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/000_nipon_wood_block.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/001_ohkawa_mute.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/002_ohkawa_open.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/003_shime_hi.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/004_shime_hi_2.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/005_shime_mute.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/006_taiko_1.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/007_taiko_2.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/008_taiko_3.wav', - ], - space: [ - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/000_0.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/001_1.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/002_11.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/003_12.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/004_13.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/005_14.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/006_15.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/007_16.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/008_17.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/009_18.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/010_2.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/011_3.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/012_4.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/013_5.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/014_6.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/015_7.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/016_8.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/017_9.wav', - ], - numbers: [ - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/0.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/1.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/2.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/3.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/4.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/5.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/6.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/7.wav', - 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/8.wav', - ], - piano: ['https://raw.githubusercontent.com/felixroos/dough-samples/refs/heads/main/piano/C3v8.mp3'], - flute: ['https://raw.githubusercontent.com/felixroos/samples/refs/heads/main/flute/c4.mp3'], - bd: [ - 'https://raw.githubusercontent.com/geikha/tidal-drum-machines/15eac73c5e878550f91d864a4863e014799403f1/machines/RolandTR909/rolandtr909-bd/Bassdrum-01.wav', - ], -}; +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 loadSampleChannels(key, url) { @@ -138,18 +91,27 @@ async function loadSampleChannels(key, url) { } let loaded = false; -export async function doughsample() { +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); + } !doughWorklet && initDoughWorklet(); if (loaded) { return; } loaded = true; - const sampleMap = await Promise.all( - Object.entries(samples).map(async ([key, url]) => { - url = url[0]; - return loadSampleChannels(key, url); - }), - ); - console.log('sampleMap', sampleMap); - doughWorklet.port.postMessage({ samples: sampleMap }); + const json = ( + await Promise.all( + Object.entries(sampleMap).map(async ([key, url]) => { + if (key !== '_base') { + url = baseUrl + url[0]; + return loadSampleChannels(key, url); + } + }), + ) + ).filter(Boolean); + // console.log('sampleMap', json); + doughWorklet.port.postMessage({ samples: json }); } From aeaed50446e6f31e0a722ed3f19b8bd99e25efbb Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 9 Jun 2025 15:18:47 +0200 Subject: [PATCH 038/257] gaincurve --- packages/supradough/dough.mjs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 207638737..fe3786a05 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -1,6 +1,15 @@ // this is dough, the superdough without dependencies const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000; const ISR = 1 / SAMPLE_RATE; + +let gainCurveFunc = (val) => Math.pow(val, 2); + +function applyGainCurve(val) { + return gainCurveFunc(val); +} +// function setGainCurve(newGainCurveFunc) { +// gainCurveFunc = newGainCurveFunc; +// } // https://garten.salat.dev/audio-DSP/oscillators.html export class SineOsc { phase = 0; @@ -522,15 +531,15 @@ export class DoughVoice { let $ = this; Object.assign($, value); $.s = $.s ?? getDefaultValue('s'); - $.gain = $.gain ?? getDefaultValue('gain'); - $.velocity = $.velocity ?? getDefaultValue('velocity'); - $.postgain = $.postgain ?? getDefaultValue('postgain'); + $.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 = $.shapevol ?? getDefaultValue('shapevol'); - $.distortvol = $.distortvol ?? getDefaultValue('distortvol'); + $.shapevol = applyGainCurve($.shapevol ?? getDefaultValue('shapevol')); + $.distortvol = applyGainCurve($.distortvol ?? getDefaultValue('distortvol')); $.i = $.i ?? getDefaultValue('i'); $.fft = $.fft ?? getDefaultValue('fft'); $.pan = $.pan ?? getDefaultValue('pan'); @@ -582,7 +591,7 @@ export class DoughVoice { // gain envelope $._adsr = new ADSR({ decayCurve: 2 }); // delay - $.delay = $.delay ?? getDefaultValue('delay'); + $.delay = applyGainCurve($.delay ?? getDefaultValue('delay')); $.delayfeedback = $.delayfeedback ?? getDefaultValue('delayfeedback'); $.delaytime = $.delaytime ?? getDefaultValue('delaytime'); From 3d300077563dd4f978b8315f6beeb5b9abfe3a8c Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 9 Jun 2025 15:55:44 +0200 Subject: [PATCH 039/257] supersaw --- packages/supradough/dough.mjs | 59 +++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index fe3786a05..4af3b7118 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -45,8 +45,9 @@ function polyBlep(t, dt) { return 0; } export class SawOsc { - //phase = Math.random(); - phase = 0; + constructor(props = {}) { + this.phase = props.phase ?? 0 + } update(freq) { const dt = freq / SAMPLE_RATE; let p = polyBlep(this.phase, dt); @@ -59,6 +60,59 @@ export class SawOsc { } } +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 ?? .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) { @@ -453,6 +507,7 @@ let shapes = { zaw: ZawOsc, sawtooth: SawOsc, zawtooth: ZawOsc, + supersaw: SupersawOsc, tri: TriOsc, triangle: TriOsc, pulse: PulseOsc, From e8254735bb3ad281bbaa4c0838cbe9a2fac7d2d7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 9 Jun 2025 19:35:50 +0200 Subject: [PATCH 040/257] apply speed to synths and samples + add normalize flag (for fit) --- packages/supradough/dough.mjs | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 4af3b7118..b11de6c24 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -46,7 +46,7 @@ function polyBlep(t, dt) { } export class SawOsc { constructor(props = {}) { - this.phase = props.phase ?? 0 + this.phase = props.phase ?? 0; } update(freq) { const dt = freq / SAMPLE_RATE; @@ -66,7 +66,7 @@ function getUnisonDetune(unison, detune, voiceIndex) { } 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) { @@ -76,17 +76,17 @@ 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 ?? .2 + 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 + 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 freqAdjusted = applySemitoneDetuneToFrequency(freq, getUnisonDetune(this.voices, this.freqspread, n)); const dt = freqAdjusted / SAMPLE_RATE; const isOdd = (n & 1) == 1; let gainL = gain1; @@ -98,8 +98,8 @@ export class SupersawOsc { } let p = polyBlep(this.phase[n], dt); let s = 2 * this.phase[n] - 1 - p; - sl = sl + s * gainL - sr = sr + s * gainL + sl = sl + s * gainL; + sr = sr + s * gainL; this.phase[n] += dt; if (this.phase[n] > 1) { @@ -107,7 +107,7 @@ export class SupersawOsc { } } - return sl + sr + return sl + sr; //TODO: make stereo // return [sl, sr]; } @@ -466,15 +466,22 @@ export class BufferPlayer { sampleRate; pos = 0; sampleFreq = note2freq(); - constructor(buffer, sampleRate) { + 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) * SAMPLE_RATE) / this.sampleRate; + const speed = (freq / this.sampleFreq) * this.speed; let s = this.buffer[Math.floor(this.pos)]; this.pos = this.pos + speed; return s; @@ -551,6 +558,7 @@ const defaultDefaultValues = { pan: 0.5, fmh: 1, fmenv: 0, // differs from superdough + speed: 1, }; let getDefaultValue = (key) => defaultDefaultValues[key]; @@ -603,6 +611,7 @@ export class DoughVoice { $.resonance = $.resonance ?? getDefaultValue('resonance'); $.hresonance = $.hresonance ?? getDefaultValue('hresonance'); $.bandq = $.bandq ?? getDefaultValue('bandq'); + $.speed = $.speed ?? getDefaultValue('speed'); [$.attack, $.decay, $.sustain, $.release] = getADSR([$.attack, $.decay, $.sustain, $.release]); @@ -623,7 +632,7 @@ export class DoughVoice { $._buffers = []; $._channels = sample.channels.length; for (let i = 0; i < $._channels; i++) { - $._buffers.push(new BufferPlayer(sample.channels[i], sample.sampleRate)); + $._buffers.push(new BufferPlayer(sample.channels[i], sample.sampleRate, $.unit === 'c')); // tbd unit === 'c' } } else { console.warn('sound not found', $.s); @@ -695,7 +704,7 @@ export class DoughVoice { } let gate = Number(t >= this._begin && t <= this._holdEnd); - let freq = this.freq; + let freq = this.freq * this.speed; // frequency modulation if (this._fm) { let fmi = this.fmi; From c9f494d8657e5990ded89a02dbaf8ab80caa1e40 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 9 Jun 2025 22:10:09 +0200 Subject: [PATCH 041/257] fix: node export --- packages/supradough/dough-export.mjs | 3 ++- packages/supradough/dough.mjs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/supradough/dough-export.mjs b/packages/supradough/dough-export.mjs index 5f8b9a8ee..a9f5614b0 100644 --- a/packages/supradough/dough-export.mjs +++ b/packages/supradough/dough-export.mjs @@ -47,7 +47,8 @@ haps.forEach((hap) => { console.log(`render ${seconds}s long buffer...`); const buffer = new Float32Array(seconds * sampleRate); while (dough.t <= buffer.length) { - buffer[dough.t] = dough.update(); + dough.update(); + buffer[dough.t] = dough.out[0]; } console.log('done!'); diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index b11de6c24..aa2e1dfd5 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -150,7 +150,7 @@ class PulseOsc { return 2 * phase - 1 - p; } update(freq, pw = 0.5) { - const dt = freq / sampleRate; + 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; From e99adffc562e6495b02d19d8d66fc684935a77ec Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 9 Jun 2025 22:29:35 +0200 Subject: [PATCH 042/257] vibrato --- packages/supradough/dough.mjs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index aa2e1dfd5..8a648a449 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -643,6 +643,11 @@ export class DoughVoice { [$.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'); @@ -705,6 +710,7 @@ export class DoughVoice { let gate = Number(t >= this._begin && t <= this._holdEnd); let freq = this.freq * this.speed; + // frequency modulation if (this._fm) { let fmi = this.fmi; @@ -717,6 +723,11 @@ export class DoughVoice { 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); From 3963b807ebb35815890858e2d09bfadbc9e8a9f4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 9 Jun 2025 22:32:41 +0200 Subject: [PATCH 043/257] fix: calling doughsamples multiple times --- packages/webaudio/supradough.mjs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index 3b45415df..d9775642e 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -90,18 +90,12 @@ async function loadSampleChannels(key, url) { return [key, channels, buffer.sampleRate]; } -let loaded = false; 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); } - !doughWorklet && initDoughWorklet(); - if (loaded) { - return; - } - loaded = true; const json = ( await Promise.all( Object.entries(sampleMap).map(async ([key, url]) => { @@ -113,5 +107,6 @@ export async function doughsamples(sampleMap, baseUrl) { ) ).filter(Boolean); // console.log('sampleMap', json); + !doughWorklet && initDoughWorklet(); doughWorklet.port.postMessage({ samples: json }); } From 7e8206dcbc67570c5001869e86251199c0fd5ad7 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 10 Jun 2025 12:31:21 +0200 Subject: [PATCH 044/257] pitched delay --- packages/core/controls.mjs | 19 ++++++++++++--- packages/supradough/dough.mjs | 45 +++++++++++++++++++++++------------ 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 21c183da9..9c9bdbc8c 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -964,14 +964,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'); diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 8a648a449..8e3544c9f 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -44,6 +44,7 @@ function polyBlep(t, dt) { // 0 otherwise return 0; } + export class SawOsc { constructor(props = {}) { this.phase = props.phase ?? 0; @@ -349,21 +350,31 @@ export class ADSR { .out()*/ const MAX_DELAY_TIME = 10; 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; + constructor(_props = {}) { + this.buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); + this.writeIdx = 0; + this.readIdx = 0; + this.numSamples = 0; } - update(input, delayTime) { + 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); - return this.buffer[this.readIdx]; + 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; + } + return this.buffer[index]; } } @@ -550,6 +561,7 @@ const defaultDefaultValues = { delay: 0, byteBeatExpression: '0', delayfeedback: 0.5, + delayspeed: 1, delaytime: 0.25, orbit: 1, i: 1, @@ -662,6 +674,7 @@ export class DoughVoice { // delay $.delay = applyGainCurve($.delay ?? getDefaultValue('delay')); $.delayfeedback = $.delayfeedback ?? getDefaultValue('delayfeedback'); + $.delayspeed = $.delayspeed ?? getDefaultValue('delayspeed'); $.delaytime = $.delaytime ?? getDefaultValue('delaytime'); // precalculated values @@ -822,6 +835,7 @@ export class Dough { delaysend = [0, 0]; delaytime = getDefaultValue('delaytime'); delayfeedback = getDefaultValue('delayfeedback'); + delayspeed = getDefaultValue('delayspeed'); t = 0; // sampleRate: number, currentTime: number (seconds) constructor(sampleRate = 48000, currentTime = 0) { @@ -897,12 +911,13 @@ export class Dough { 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); - const delayR = this._delayR.update(this.delaysend[1], this.delaytime); + 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; From 43504d16b66e739590ec80c4fd18e10f54c31802 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 10 Jun 2025 16:03:42 +0200 Subject: [PATCH 045/257] add chorus --- packages/core/controls.mjs | 11 ++++++ packages/supradough/dough.mjs | 64 ++++++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 9c9bdbc8c..ed440b56d 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -285,6 +285,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 diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 8e3544c9f..6be0af78c 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -7,6 +7,22 @@ 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; // } @@ -349,7 +365,8 @@ export class ADSR { .add(x=>x.delay(.1).mul(.8)) .out()*/ const MAX_DELAY_TIME = 10; -export class Delay { +export class PitchDelay { + lpf = new TwoPoleFilter(); constructor(_props = {}) { this.buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); this.writeIdx = 0; @@ -374,7 +391,38 @@ export class Delay { } else { index = Math.floor(this.readIdx * speed) % this.numSamples; } - return this.buffer[index]; + 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); } } @@ -540,6 +588,7 @@ let shapes = { }; const defaultDefaultValues = { + chorus: 0, note: 48, s: 'triangle', gain: 1, @@ -616,6 +665,7 @@ export class DoughVoice { $.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'); @@ -696,6 +746,7 @@ export class DoughVoice { } // channelwise effects setup + $._chorus = $.chorus ? [] : null; $._lpf = $.cutoff ? [] : null; $._hpf = $.hcutoff ? [] : null; $._bpf = $.bandf ? [] : null; @@ -706,6 +757,7 @@ export class DoughVoice { $._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()); @@ -786,6 +838,10 @@ export class DoughVoice { 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); @@ -842,8 +898,8 @@ export class Dough { this.sampleRate = sampleRate; this.t = Math.floor(currentTime * sampleRate); // samples // console.log('init dough', this.sampleRate, this.t); - this._delayL = new Delay(); - this._delayR = new Delay(); + this._delayL = new PitchDelay(); + this._delayR = new PitchDelay(); } loadSample(name, channels, sampleRate) { BufferPlayer.samples.set(name, { channels, sampleRate }); From bbd7ed0f27f1b49eefb674afa70ea8fe1acbcbe8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 02:25:04 +0200 Subject: [PATCH 046/257] lazy sample loading --- packages/webaudio/supradough.mjs | 48 ++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index d9775642e..10664aa89 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -16,11 +16,35 @@ function initDoughWorklet() { 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); }; @@ -79,7 +103,7 @@ export async function fetchSampleMap(url) { // for some reason, only piano and flute work.. is it because mp3?? -async function loadSampleChannels(key, url) { +async function fetchSample(url) { const buffer = await fetch(url) .then((res) => res.arrayBuffer()) .then((buf) => getAudioContext().decodeAudioData(buf)); @@ -87,7 +111,7 @@ async function loadSampleChannels(key, url) { for (let i = 0; i < buffer.numberOfChannels; i++) { channels.push(buffer.getChannelData(i)); } - return [key, channels, buffer.sampleRate]; + return { channels, sampleRate: buffer.sampleRate }; } export async function doughsamples(sampleMap, baseUrl) { @@ -96,17 +120,11 @@ export async function doughsamples(sampleMap, baseUrl) { // console.log('json', json, 'base', base); return doughsamples(json, base); } - const json = ( - await Promise.all( - Object.entries(sampleMap).map(async ([key, url]) => { - if (key !== '_base') { - url = baseUrl + url[0]; - return loadSampleChannels(key, url); - } - }), - ) - ).filter(Boolean); - // console.log('sampleMap', json); - !doughWorklet && initDoughWorklet(); - doughWorklet.port.postMessage({ samples: json }); + 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); + } + }); } From 5cbd38a3e83b0b29414675bee6a1a961a13c93fa Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 02:25:46 +0200 Subject: [PATCH 047/257] improve filter performance making it accept hz directly --- packages/supradough/dough.mjs | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 6be0af78c..711922b63 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -1,5 +1,6 @@ // 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); @@ -145,11 +146,13 @@ export class TwoPoleFilter { s1 = 0; update(s, cutoff, resonance = 0) { // Out of bound values can produce NaNs - cutoff = Math.min(cutoff, 1); resonance = Math.max(resonance, 0); - var c = Math.pow(0.5, (1 - cutoff) / 0.125); - var r = Math.pow(0.5, (resonance + 0.125) / 0.125); - var mrc = 1 - r * c; + + 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 @@ -591,6 +594,7 @@ const defaultDefaultValues = { chorus: 0, note: 48, s: 'triangle', + bank: '', gain: 1, postgain: 1, velocity: 1, @@ -620,6 +624,7 @@ const defaultDefaultValues = { fmh: 1, fmenv: 0, // differs from superdough speed: 1, + pw: 0.5, }; let getDefaultValue = (key) => defaultDefaultValues[key]; @@ -674,14 +679,14 @@ export class DoughVoice { $.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 - $.s ??= 'triangle'; - if ($.s === 'saw' || $.s === 'sawtooth') { + if ($.fmi && ($.s === 'saw' || $.s === 'sawtooth')) { $.s = 'zaw'; // polyblepped saw when fm is applied } @@ -697,7 +702,7 @@ export class DoughVoice { $._buffers.push(new BufferPlayer(sample.channels[i], sample.sampleRate, $.unit === 'c')); // tbd unit === 'c' } } else { - console.warn('sound not found', $.s); + console.warn('sound not loaded', $.s); } if ($.penv) { @@ -727,10 +732,6 @@ export class DoughVoice { $.delayspeed = $.delayspeed ?? getDefaultValue('delayspeed'); $.delaytime = $.delaytime ?? getDefaultValue('delaytime'); - // precalculated values - $.piOverSr = Math.PI / value.sampleRate; - $.eighthOverLogHalf = 0.125 / Math.log(0.5); - // filter setup if ($.lpenv) { $._lpenv = new ADSR({ decayCurve: 4 }); @@ -763,11 +764,6 @@ export class DoughVoice { $._distort?.push(new Distort()); } } - // credits to pulu: https://github.com/felixroos/kabelsalat/issues/35 - freq2cutoff(freq) { - const c = 2 * Math.sin(freq * this.piOverSr); - return 1 - Math.log(c) * this.eighthOverLogHalf; - } update(t) { if (!this._sound && !this._buffers) { return 0; @@ -806,7 +802,6 @@ export class DoughVoice { const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease); lpf = this.lpenv * env * lpf + lpf; } - lpf = this.freq2cutoff(lpf); } let hpf = this.hcutoff; if (this._hpf) { @@ -814,7 +809,6 @@ export class DoughVoice { const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease); hpf = 2 ** this.hpenv * env * hpf + hpf; } - hpf = this.freq2cutoff(hpf); } let bpf = this.bandf; if (this._bpf) { @@ -822,7 +816,6 @@ export class DoughVoice { const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease); bpf = 2 ** this.bpenv * env * bpf + bpf; } - bpf = this.freq2cutoff(bpf); } // gain envelope const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); @@ -831,7 +824,7 @@ export class DoughVoice { 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 ?? 0.5); + this.out[i] = this._sound.update(freq, this.pw); } else if (this._sound) { this.out[i] = this._sound.update(freq); } else if (this._buffers) { From 3caf08e199a7f210094cae213e934ead30750933 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 02:26:08 +0200 Subject: [PATCH 048/257] up latency --- packages/superdough/superdough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 2629790df..ef43d7906 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -174,7 +174,7 @@ export const resetLoadedSounds = () => soundMap.set({}); let audioContext; export const setDefaultAudioContext = () => { - audioContext = new AudioContext(); + audioContext = new AudioContext({ latencyHint: 'playback' }); return audioContext; }; From 14193abe0315dcd54d17ba2680f85bdf0373ca9b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 02:26:50 +0200 Subject: [PATCH 049/257] improve export: add stereo + measure performance + add bigger pattern for testing --- packages/supradough/dough-export.mjs | 103 +++++++++++++++++++++------ 1 file changed, 82 insertions(+), 21 deletions(-) diff --git a/packages/supradough/dough-export.mjs b/packages/supradough/dough-export.mjs index a9f5614b0..a6c55bf46 100644 --- a/packages/supradough/dough-export.mjs +++ b/packages/supradough/dough-export.mjs @@ -16,23 +16,72 @@ await evalScope( miniAllStrings(); // allows using single quotes for mini notation / skip transpilation let sampleRate = 48000, - cps = 0.5; + cps = 0.4; -let pat = note('[c e g b]*3') - .add(note(7)) - .lpf(sine.rangex(200, 4000).slow(2)) - .lpq(0.3) - .s('*2') - .att(0.01) - .rel(0.2) - .clip(2) - .delay(0.5) - .jux(rev) - .sometimes(add(note(12))) - .gain(0.25) +/* await doughsamples('github:eddyflux/crate'); +await doughsamples('github:eddyflux/wax'); */ + +let pat = note('c,eb,g,') + .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('*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('*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('') + .voicing() + .s('') + .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 = 4; +let cycles = 30; let seconds = cycles + 1; // 1s release tail const haps = pat.queryArc(0, cycles); @@ -41,20 +90,32 @@ 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; + hap.value._duration = hap.duration/* / cps */; dough.scheduleSpawn(hap.value); }); -console.log(`render ${seconds}s long buffer...`); -const buffer = new Float32Array(seconds * sampleRate); -while (dough.t <= buffer.length) { +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(); - buffer[dough.t] = dough.out[0]; + buffers[0][dough.t] = dough.out[0]; + buffers[1][dough.t] = dough.out[1]; + if (dough.t % sampleRate === 0) { + process.stdout.write('.'); + } } -console.log('done!'); +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: [buffer], + channelData: buffers, }; WavEncoder.encode(patternAudio).then((buffer) => { From 8363ba5a41ca3210d6cd4b436ead3692d7d29d3c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 10:22:35 +0200 Subject: [PATCH 050/257] format --- packages/supradough/dough-export.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/supradough/dough-export.mjs b/packages/supradough/dough-export.mjs index a6c55bf46..bd4b530b3 100644 --- a/packages/supradough/dough-export.mjs +++ b/packages/supradough/dough-export.mjs @@ -90,7 +90,7 @@ 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 */; + hap.value._duration = hap.duration /* / cps */; dough.scheduleSpawn(hap.value); }); console.log(`render ${seconds}s long buffer, each dot is 1 second:`); From 239987af0b70169d6db451f277a3f24da59ca203 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 10:23:06 +0200 Subject: [PATCH 051/257] snapshot --- test/__snapshots__/examples.test.mjs.snap | 67 +++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 0eacfdfb0..e3df24d47 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -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 ]", @@ -2505,6 +2526,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 "delaytime" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:bd delay:0.25 delaytime:0.125 ]", From 5efcd14f4084295e287cf4a97364e7e932db994e Mon Sep 17 00:00:00 2001 From: anecondev Date: Wed, 18 Jun 2025 17:07:13 +0200 Subject: [PATCH 052/257] Update website/src/pages/learn/code.mdx Correct "appended" to "prepended" --- website/src/pages/learn/code.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/code.mdx b/website/src/pages/learn/code.mdx index ca2564dec..7057c4576 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -44,7 +44,7 @@ xxx("foo").yyy("bar") Generally, `xxx` and `yyy` are called [_functions_](), while `foo` and `bar` are called function [_arguments_ or _parameters_](). So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data. -The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is appended with a dot (`.`). +The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is prepended with a dot (`.`). Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code. You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects. From cbb4207bab08eb1a791d8e77f9508ddd37431324 Mon Sep 17 00:00:00 2001 From: dudymas Date: Fri, 4 Jul 2025 18:50:21 -0400 Subject: [PATCH 053/257] chore(docker): basic support added --- Dockerfile | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..b0c6618be --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM node:24 + +WORKDIR /app + +RUN npm install pnpm --global + +COPY pnpm-workspace.yaml ./ +COPY package.json pnpm-lock.yaml ./ +COPY packages/ ./packages/ +COPY examples/ ./examples/ +RUN mkdir -p website/public +COPY website/package.json ./website/ + +RUN pnpm install + + +COPY . . + +EXPOSE 4321 + +CMD ["pnpm", "dev"] From 31c1450edc8eb606716fc5e35af52614bf67c8c3 Mon Sep 17 00:00:00 2001 From: dudymas Date: Fri, 4 Jul 2025 18:50:21 -0400 Subject: [PATCH 054/257] docs(website/learn/xen): add tune() examples --- website/src/pages/learn/xen.mdx | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 website/src/pages/learn/xen.mdx diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx new file mode 100644 index 000000000..21db10eb4 --- /dev/null +++ b/website/src/pages/learn/xen.mdx @@ -0,0 +1,64 @@ +--- +title: Xen Harmonic Functions +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Xen Harmonic Functions + +These functions allow the use of scales other than your typical chromatic 12 based ones. + +### tune(scale) + + + +Here's an example of how to configure a basic hexany scale: + + + +Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3` + +For a full list of available scales from tunejs, see http://abbernie.github.io/tune/scales.html + +You can set your root to be a particular note with `getFreq` + + + +Some tunings become more pronounced with a longer reverb decay: + + -".tune("gumbeng") + .mul(getFreq('c3')) + .freq().clip(.8).room("3:10").rdim(10000).rfade(5)`} +/> + +Additionally, you can combo this with `fmap` so that the base note changes: + +".fmap(getFreq)) + .freq().legato("2 .7").room("1:15").rdim(8500).rlp(14000).rfade(8)`} +/> + +Combining this with various polyrhythm tricks can become very evocative: + + ~ ~,<-4 -5>" + .transpose(4) + .tune("iraq") + .mul("".fmap(getFreq)) + .freq().clip(.5).room(1).rfade(9)`} +/> From 37fde8f3e807a64289701ed67dd14d9d30713cc7 Mon Sep 17 00:00:00 2001 From: dudymas Date: Sat, 5 Jul 2025 13:43:09 -0400 Subject: [PATCH 055/257] docs(website/learn/xen): discussed strumming --- website/src/config.ts | 1 + website/src/pages/learn/xen.mdx | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/website/src/config.ts b/website/src/config.ts index 2e2e264da..f335b272e 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -103,6 +103,7 @@ export const SIDEBAR: Sidebar = { Understand: [ { text: 'Coding syntax', link: 'learn/code' }, { text: 'Pitch', link: 'understand/pitch' }, + { text: 'Xen Harmonic Functions', link: 'learn/xen' }, { text: 'Cycles', link: 'understand/cycles' }, { text: 'Voicings', link: 'understand/voicings' }, { text: 'Pattern Alignment', link: 'technical-manual/alignment' }, diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index 21db10eb4..32e2c0b43 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -62,3 +62,39 @@ Combining this with various polyrhythm tricks can become very evocative: .mul("".fmap(getFreq)) .freq().clip(.5).room(1).rfade(9)`} /> + +Another helpful trick when exploring new tunings is to strum them. +Many have a much more enchanting sound that was chosen over many generations of musicians for being strummed. + +Take the `sanza` tuning: + + +Notes 7 and 9 will clash quite a bit if you arp them normally. Many tunings will have this sort of sound, and it can feel distracting on its own. +See how close they are on the pitch wheel? + + + +This quality is often due to how the tunings were formed with instruments that were played differently than a piano. +As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical: + +") + .tune("sanza") + .mul(getFreq('c3')).freq() + .legato("3").room(1).rfade(5)`} +/> + +Note the legato and reverb effects make sure the sound of the strumming gets to wash together. Alternating the direction of the strum can make the +tones sound even more alive, too. + +The `tranh3` tuning has a similar set of notes, with two clashing. You might trying plugging that in above and see if you find a favorite strumming pattern. From 0722cf7ded81194dc91aef2690c30eb3c5cf4781 Mon Sep 17 00:00:00 2001 From: dudymas Date: Sat, 5 Jul 2025 13:43:42 -0400 Subject: [PATCH 056/257] feat(xen/tunejs): support sending a custom tuning --- packages/xen/tunejs.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/xen/tunejs.js b/packages/xen/tunejs.js index 6b5e7cb7c..5806cf596 100644 --- a/packages/xen/tunejs.js +++ b/packages/xen/tunejs.js @@ -139,10 +139,10 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) { /* Load a new scale */ -Tune.prototype.loadScale = function(name){ +Tune.prototype.loadScale = function(scale){ /* load the scale */ - var freqs = TuningList[name].frequencies + var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies this.scale = [] for (var i=0;i 0 && arg.every(item => typeof item === 'number' && !isNaN(item)); +} + +/* allow an array of values too */ +Tune.prototype.isValidScale = function(scale) { + return !!TuningList[scale] || isArrayOfNumbers(scale) ; } /* Return a collection of notes as an array */ From 5632d83afb124d679be292890a49ffd31b34e690 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 16 Jul 2025 02:21:39 +0200 Subject: [PATCH 057/257] fix: supradough onTrigger after breaking change --- packages/webaudio/supradough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index 10664aa89..f97251a07 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -20,7 +20,7 @@ const soundMap = new Map(); const loadedSounds = new Map(); Pattern.prototype.supradough = function () { - return this.onTrigger((_, hap, __, cps, begin) => { + return this.onTrigger((hap, __, cps, begin) => { hap.value._begin = begin; hap.value._duration = hap.duration / cps; !doughWorklet && initDoughWorklet(); From 10156f00670462b0280351c549c92b4728a95bff Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Tue, 15 Jul 2025 19:13:24 -0700 Subject: [PATCH 058/257] add basicSetup for keybindings --- packages/codemirror/basicSetup.mjs | 63 +++++++++++++++++++++++++++++ packages/codemirror/codemirror.mjs | 8 +++- packages/codemirror/keybindings.mjs | 5 ++- packages/codemirror/package.json | 2 +- pnpm-lock.yaml | 37 ++++++++++++----- 5 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 packages/codemirror/basicSetup.mjs diff --git a/packages/codemirror/basicSetup.mjs b/packages/codemirror/basicSetup.mjs new file mode 100644 index 000000000..3faf7d7ab --- /dev/null +++ b/packages/codemirror/basicSetup.mjs @@ -0,0 +1,63 @@ +import { + keymap, + highlightSpecialChars, + drawSelection, + highlightActiveLine, + dropCursor, + rectangularSelection, + crosshairCursor, + lineNumbers, + highlightActiveLineGutter, +} from '@codemirror/view'; +import { + defaultHighlightStyle, + syntaxHighlighting, + bracketMatching, + foldGutter, + foldKeymap, +} from '@codemirror/language'; +import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; +import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'; +import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete'; + +// Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts + +export const basicSetup = (() => [ + lineNumbers(), + highlightActiveLineGutter(), + highlightSpecialChars(), + history(), + foldGutter(), + drawSelection(), + dropCursor(), + // EditorState.allowMultipleSelections.of(true), + // indentOnInput(), + syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + // autocompletion(), + rectangularSelection(), + crosshairCursor(), + highlightActiveLine(), + highlightSelectionMatches(), + keymap.of([ + ...closeBracketsKeymap, + ...defaultKeymap, + ...searchKeymap, + ...historyKeymap, + ...foldKeymap, + ...completionKeymap, + ]), +])(); + +/// A minimal set of extensions to create a functional editor. Only +/// includes [the default keymap](#commands.defaultKeymap), [undo +/// history](#commands.history), [special character +/// highlighting](#view.highlightSpecialChars), [custom selection +/// drawing](#view.drawSelection), and [default highlight +/// style](#language.defaultHighlightStyle). +export const minimalSetup = (() => [ + highlightSpecialChars(), + history(), + drawSelection(), + syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + keymap.of([...defaultKeymap, ...historyKeymap]), +])(); diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index f47f6eba5..195971df9 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -2,7 +2,7 @@ import { closeBrackets } from '@codemirror/autocomplete'; export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands'; // import { search, highlightSelectionMatches } from '@codemirror/search'; import { history, indentWithTab } from '@codemirror/commands'; -import { javascript } from '@codemirror/lang-javascript'; +import { javascript, javascriptLanguage } from '@codemirror/lang-javascript'; import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language'; import { Compartment, EditorState, Prec } from '@codemirror/state'; import { @@ -24,6 +24,7 @@ import { initTheme, activateTheme, theme } from './themes.mjs'; import { sliderPlugin, updateSliderWidgets } from './slider.mjs'; import { widgetPlugin, updateWidgets } from './widget.mjs'; import { persistentAtom } from '@nanostores/persistent'; +import { basicSetup } from './basicSetup.mjs'; const extensions = { isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []), @@ -85,7 +86,12 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo /* search(), highlightSelectionMatches(), */ ...initialSettings, + basicSetup, mondo ? [] : javascript(), + javascriptLanguage.data.of({ + closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] }, + bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] }, + }), sliderPlugin, widgetPlugin, // indentOnInput(), // works without. already brought with javascript extension? diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index 1201959a2..a72b164eb 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -3,8 +3,9 @@ import { keymap, ViewPlugin } from '@codemirror/view'; // import { searchKeymap } from '@codemirror/search'; import { emacs } from '@replit/codemirror-emacs'; import { vim } from '@replit/codemirror-vim'; +// import { vim } from './vim_test.mjs'; import { vscodeKeymap } from '@replit/codemirror-vscode-keymap'; -import { defaultKeymap, historyKeymap } from '@codemirror/commands'; +import { defaultKeymap } from '@codemirror/commands'; const vscodePlugin = ViewPlugin.fromClass( class { @@ -27,5 +28,5 @@ const keymaps = { export function keybindings(name) { const active = keymaps[name]; - return [active ? active() : [], keymap.of(historyKeymap)]; + return [active ? active() : []]; } diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 4f8508c90..a802be2ed 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -42,7 +42,7 @@ "@lezer/highlight": "^1.2.1", "@nanostores/persistent": "^0.10.2", "@replit/codemirror-emacs": "^6.1.0", - "@replit/codemirror-vim": "^6.2.1", + "@replit/codemirror-vim": "^6.3.0", "@replit/codemirror-vscode-keymap": "^6.0.2", "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7aecd35bd..810ffb9d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,8 +201,8 @@ importers: specifier: ^6.1.0 version: 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) '@replit/codemirror-vim': - specifier: ^6.2.1 - version: 6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) + specifier: ^6.3.0 + version: 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) '@replit/codemirror-vscode-keymap': specifier: ^6.0.2 version: 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) @@ -215,6 +215,9 @@ importers: '@strudel/transpiler': specifier: workspace:* version: link:../transpiler + codemirror: + specifier: ^6.0.2 + version: 6.0.2 nanostores: specifier: ^0.11.3 version: 0.11.3 @@ -2232,14 +2235,14 @@ packages: '@codemirror/state': ^6.0.1 '@codemirror/view': ^6.3.0 - '@replit/codemirror-vim@6.2.1': - resolution: {integrity: sha512-qDAcGSHBYU5RrdO//qCmD8K9t6vbP327iCj/iqrkVnjbrpFhrjOt92weGXGHmTNRh16cUtkUZ7Xq7rZf+8HVow==} + '@replit/codemirror-vim@6.3.0': + resolution: {integrity: sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==} peerDependencies: - '@codemirror/commands': ^6.0.0 - '@codemirror/language': ^6.1.0 - '@codemirror/search': ^6.2.0 - '@codemirror/state': ^6.0.1 - '@codemirror/view': ^6.0.3 + '@codemirror/commands': 6.x.x + '@codemirror/language': 6.x.x + '@codemirror/search': 6.x.x + '@codemirror/state': 6.x.x + '@codemirror/view': 6.x.x '@replit/codemirror-vscode-keymap@6.0.2': resolution: {integrity: sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==} @@ -3445,6 +3448,9 @@ packages: resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -7657,6 +7663,7 @@ packages: workbox-google-analytics@7.0.0: resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} + deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained workbox-navigation-preload@7.0.0: resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==} @@ -9595,7 +9602,7 @@ snapshots: '@codemirror/state': 6.5.1 '@codemirror/view': 6.36.2 - '@replit/codemirror-vim@6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)': + '@replit/codemirror-vim@6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)': dependencies: '@codemirror/commands': 6.8.0 '@codemirror/language': 6.10.8 @@ -11043,6 +11050,16 @@ snapshots: cmd-shim@6.0.3: {} + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.18.4 + '@codemirror/commands': 6.8.0 + '@codemirror/language': 6.10.8 + '@codemirror/lint': 6.8.4 + '@codemirror/search': 6.5.8 + '@codemirror/state': 6.5.1 + '@codemirror/view': 6.36.2 + collapse-white-space@2.1.0: {} color-convert@2.0.1: From 06fafbd8ebfdda48c86848e00ae13a49641b2c5d Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Wed, 16 Jul 2025 15:03:55 -0700 Subject: [PATCH 059/257] Ensure no default extensions conflict with extensions in defaultSettings --- packages/codemirror/basicSetup.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/codemirror/basicSetup.mjs b/packages/codemirror/basicSetup.mjs index 3faf7d7ab..de5b30417 100644 --- a/packages/codemirror/basicSetup.mjs +++ b/packages/codemirror/basicSetup.mjs @@ -23,20 +23,20 @@ import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete' // Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts export const basicSetup = (() => [ - lineNumbers(), - highlightActiveLineGutter(), + // lineNumbers(), + // highlightActiveLineGutter(), highlightSpecialChars(), history(), - foldGutter(), + // foldGutter(), drawSelection(), dropCursor(), // EditorState.allowMultipleSelections.of(true), // indentOnInput(), - syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + // syntaxHighlighting(defaultHighlightStyle, { fallback: true }), // autocompletion(), rectangularSelection(), crosshairCursor(), - highlightActiveLine(), + // highlightActiveLine(), highlightSelectionMatches(), keymap.of([ ...closeBracketsKeymap, From 5448fc607748bceec0002b92c93be4a7814d3b3b Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Wed, 16 Jul 2025 15:17:34 -0700 Subject: [PATCH 060/257] removed duplication and unnecessary keymappings in codemirror config --- packages/codemirror/basicSetup.mjs | 16 ++++++++-------- packages/codemirror/codemirror.mjs | 3 +-- packages/codemirror/keybindings.mjs | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/codemirror/basicSetup.mjs b/packages/codemirror/basicSetup.mjs index de5b30417..02294b93a 100644 --- a/packages/codemirror/basicSetup.mjs +++ b/packages/codemirror/basicSetup.mjs @@ -27,24 +27,24 @@ export const basicSetup = (() => [ // highlightActiveLineGutter(), highlightSpecialChars(), history(), - // foldGutter(), - drawSelection(), + // foldGutter(), + // drawSelection(), dropCursor(), // EditorState.allowMultipleSelections.of(true), // indentOnInput(), - // syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + // syntaxHighlighting(defaultHighlightStyle, { fallback: true }), // autocompletion(), rectangularSelection(), crosshairCursor(), - // highlightActiveLine(), - highlightSelectionMatches(), + // highlightActiveLine(), + // highlightSelectionMatches(), keymap.of([ ...closeBracketsKeymap, ...defaultKeymap, - ...searchKeymap, + // ...searchKeymap, ...historyKeymap, - ...foldKeymap, - ...completionKeymap, + // ...foldKeymap, + // ...completionKeymap, ]), ])(); diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 195971df9..4dc23996f 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -1,7 +1,7 @@ import { closeBrackets } from '@codemirror/autocomplete'; export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands'; // import { search, highlightSelectionMatches } from '@codemirror/search'; -import { history, indentWithTab } from '@codemirror/commands'; +import { indentWithTab } from '@codemirror/commands'; import { javascript, javascriptLanguage } from '@codemirror/lang-javascript'; import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language'; import { Compartment, EditorState, Prec } from '@codemirror/state'; @@ -97,7 +97,6 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo // indentOnInput(), // works without. already brought with javascript extension? // bracketMatching(), // does not do anything syntaxHighlighting(defaultHighlightStyle), - history(), EditorView.updateListener.of((v) => onChange(v)), drawSelection({ cursorBlinkRate: 0 }), Prec.highest( diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index a72b164eb..ca5f34f4c 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -28,5 +28,5 @@ const keymaps = { export function keybindings(name) { const active = keymaps[name]; - return [active ? active() : []]; + return [active ? Prec.high(active()) : []]; } From c8cf1dc712847d9802b3a527ed337a8e75303943 Mon Sep 17 00:00:00 2001 From: Chandler Abraham Date: Tue, 22 Jul 2025 21:49:40 -0700 Subject: [PATCH 061/257] fix(midi): ensure midin initializes device state correctly The midin function only initialized the refs object for a device on the initial MIDI setup. This caused an error if a second MIDI device was connected in the same session, as its refs object would not exist, leading to a 'cannot read properties of undefined' error when accessed. This commit fixes the issue by ensuring that the refs object for a given MIDI input is initialized every time is called, not just on the first call. --- packages/midi/midi.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index ce7cdb0e2..faa45f32d 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -493,6 +493,9 @@ 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); From fe18aa770d566cccf7cf57618e6e00e3d4520d0b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 23 Jul 2025 18:34:34 -0400 Subject: [PATCH 062/257] test --- packages/core/controls.mjs | 53 ++++--- packages/core/logger.mjs | 2 +- packages/superdough/superdough.mjs | 108 +++++++++----- test/__snapshots__/examples.test.mjs.snap | 171 ++++++++++++++++++++++ 4 files changed, 276 insertions(+), 58 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 326343e10..631896e94 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -526,28 +526,39 @@ 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 + * 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)) + * + */ +export const { duck } = registerControl(['duckorbit', 'duckattack', 'duckdepth'], '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/logger.mjs b/packages/core/logger.mjs index 635505c59..4f2002319 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/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 7d90eb15d..c3054c1ba 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -13,6 +13,7 @@ import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; +import { errorLogger } from '@strudel/core'; export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; @@ -331,16 +332,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 +368,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 +399,63 @@ function getFilterType(ftype) { return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype; } -let reverbs = {}; +//type orbit { +// gain: number, +// reverb: reverbNode +// delay: +//} +const 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(target, t, attacktime = 0.1, duckdepth = 1) { + if (orbits[target] == null) { + errorLogger(new Error('duck target orbit does not exist'), 'superdough'); + } + + orbits[target].gain.gain.cancelAndHoldAtTime(t); + const currVal = orbits[target].gain.gain.value; + orbits[target].gain.gain.setValueAtTime(currVal, t); + orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - duckdepth, 0.01, currVal), t + 0.002); + orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime)); +} + 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 = {}, @@ -533,6 +552,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 +593,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) coarse, crush, + dry, shape, shapevol = getDefaultValue('shapevol'), distort, @@ -604,7 +627,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(duckorbit, t, duckattack, duckdepth); + } gain = applyGainCurve(nanFallback(gain, 1)); postgain = applyGainCurve(postgain); @@ -669,7 +698,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) logger('[webaudio] skip hap: still loading', ac.currentTime - t); return; } - const chain = []; // audio nodes that will be connected to each other sequentially + let chain = []; // audio nodes that will be connected to each other sequentially chain.push(sourceNode); stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); @@ -791,7 +820,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 +854,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/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index e6bfa44aa..a5dc0f415 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3036,6 +3036,177 @@ 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 ]", + "[ 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: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.2 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.2 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.2 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.2 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.2 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.2 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.2 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.2 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.2 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 "duration" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano duration:0.5 ]", From 2415210be6e02c46e3c09c661598d21103b1371f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 23 Jul 2025 18:41:20 -0400 Subject: [PATCH 063/257] fix lint --- packages/superdough/logger.mjs | 6 ++++++ packages/superdough/superdough.mjs | 12 +++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/superdough/logger.mjs b/packages/superdough/logger.mjs index a20af1b3c..b3c9c34f3 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 c3054c1ba..a98f55f3e 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -11,9 +11,8 @@ import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util import workletsUrl from './worklets.mjs?audioworklet'; import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs'; import { map } from 'nanostores'; -import { logger } from './logger.mjs'; +import { logger, errorLogger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; -import { errorLogger } from '@strudel/core'; export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; @@ -401,10 +400,10 @@ function getFilterType(ftype) { //type orbit { // gain: number, -// reverb: reverbNode -// delay: +// reverbNode: reverbNode +// delayNode: //} -const orbits = {}; +let orbits = {}; function connectToOrbit(node, orbit) { if (orbits[orbit] == null) { errorLogger(new Error('target orbit does not exist'), 'superdough'); @@ -498,8 +497,7 @@ function effectSend(input, effect, wet) { } export function resetGlobalEffects() { - delays = {}; - reverbs = {}; + orbits = {}; analysers = {}; analysersData = {}; } From fb7d76a2aba2c5aaa2d7415adc8c90fd510d6ef1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 24 Jul 2025 23:50:27 -0400 Subject: [PATCH 064/257] working --- packages/core/controls.mjs | 12 ++++ packages/superdough/helpers.mjs | 17 +++++- packages/superdough/noise.mjs | 2 +- packages/superdough/synth.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 69 +++++++++++++++++++++++ 5 files changed, 97 insertions(+), 5 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 326343e10..af0136e54 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -252,6 +252,18 @@ export const { fmenv } = registerControl('fmenv'); * */ export const { fmattack } = registerControl('fmattack'); + +/** + * Attack time for the FM envelope: time it takes to reach maximum modulation + * + * @name fmwave + * @param {number | Pattern} wave waveform + * @example + * n("0 1 2 3".fast(4)).chord("").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. * diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 6bde69373..63643ebbc 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,5 +1,8 @@ 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(); @@ -216,9 +219,17 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { } const mod = (freq, range = 1, type = 'sine') => { const ctx = getAudioContext(); - const osc = ctx.createOscillator(); - osc.type = type; - osc.frequency.value = freq; + 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; + } + osc.start(); const g = new GainNode(ctx, { gain: range }); osc.connect(g); // -range, range diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 247794702..5411f2f2f 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -4,7 +4,7 @@ import { getAudioContext } from './superdough.mjs'; let noiseCache = {}; // lazy generates noise buffers and keeps them forever -function getNoiseBuffer(type, density) { +export function getNoiseBuffer(type, density) { const ac = getAudioContext(); if (noiseCache[type]) { return noiseCache[type]; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 88e14e5ab..255e0342b 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -9,6 +9,7 @@ import { getVibratoOscillator, webAudioTimeout, getWorklet, + noises, } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; @@ -40,7 +41,6 @@ const waveformAliases = [ ['saw', 'sawtooth'], ['sin', 'sine'], ]; -const noises = ['pink', 'white', 'brown', 'crackle']; export function registerSynthSounds() { [...waveforms].forEach((s) => { diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index e6bfa44aa..2725e1d99 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3706,6 +3706,75 @@ exports[`runs examples > example "fmsustain" example index 0 1`] = ` ] `; +exports[`runs examples > example "fmwave" example index 0 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 ]", From 82f0dbb750c40ab04197b6d27666341fa59a0bc2 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 24 Jul 2025 23:59:38 -0400 Subject: [PATCH 065/257] add another example --- packages/core/controls.mjs | 4 +- test/__snapshots__/examples.test.mjs.snap | 69 +++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index af0136e54..24c75970c 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -254,11 +254,13 @@ export const { fmenv } = registerControl('fmenv'); export const { fmattack } = registerControl('fmattack'); /** - * Attack time for the FM envelope: time it takes to reach maximum modulation + * waveform of the modulator * * @name fmwave * @param {number | Pattern} wave waveform * @example + * n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("").fm(4).fmh(2.01) + * @example * n("0 1 2 3".fast(4)).chord("").voicing().s("sawtooth").fmwave("brown").fm(.6) * */ diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 2725e1d99..eb6c238d5 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3707,6 +3707,75 @@ 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 ]", From dae1560c409dab31828ecfe0e851348a524ed76b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 25 Jul 2025 00:00:23 -0400 Subject: [PATCH 066/257] description --- packages/core/controls.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 24c75970c..7071387af 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -254,7 +254,7 @@ export const { fmenv } = registerControl('fmenv'); export const { fmattack } = registerControl('fmattack'); /** - * waveform of the modulator + * waveform of the fm modulator * * @name fmwave * @param {number | Pattern} wave waveform From 51f0bab1f84922474354d4b289a12bdd0aeb1dd5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 26 Jul 2025 12:54:59 -0400 Subject: [PATCH 067/257] duckarray --- packages/core/controls.mjs | 2 +- packages/superdough/superdough.mjs | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 631896e94..741b66488 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -535,7 +535,7 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape'); * 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)) * */ -export const { duck } = registerControl(['duckorbit', 'duckattack', 'duckdepth'], 'duck'); +export const { duck } = registerControl('duckorbit', 'duck'); /** * the amount of ducking applied to target orbit diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a98f55f3e..011326c6a 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -419,16 +419,20 @@ function setOrbit(audioContext, orbit, channels) { connectToDestination(orbits[orbit].gain, channels); } } -function duckOrbit(target, t, attacktime = 0.1, duckdepth = 1) { - if (orbits[target] == null) { - errorLogger(new Error('duck target orbit does not exist'), 'superdough'); - } +function duckOrbit(targetOrbit, t, attacktime = 0.1, duckdepth = 1) { + const targetArr = [targetOrbit].flat(); - orbits[target].gain.gain.cancelAndHoldAtTime(t); - const currVal = orbits[target].gain.gain.value; - orbits[target].gain.gain.setValueAtTime(currVal, t); - orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - duckdepth, 0.01, currVal), t + 0.002); - orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime)); + targetArr.forEach((target) => { + if (orbits[target] == null) { + errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); + return; + } + orbits[target].gain.gain.cancelAndHoldAtTime(t); + const currVal = orbits[target].gain.gain.value; + orbits[target].gain.gain.setValueAtTime(currVal, t); + orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t + 0.002); + orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime)); + }); } let hasChanged = (now, before) => now !== undefined && now !== before; From 3b6e3624be36dc66482c03be0165215628b37cd0 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 27 Jul 2025 01:07:45 -0400 Subject: [PATCH 068/257] working --- packages/superdough/synth.mjs | 79 ++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 88e14e5ab..34f958f11 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -12,9 +12,9 @@ import { } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; -const getFrequencyFromValue = (value) => { +const getFrequencyFromValue = (value, defaultNote = 36) => { let { note, freq } = value; - note = note || 36; + note = note || defaultNote; if (typeof note === 'string') { note = noteToMidi(note); // e.g. c3 => 48 } @@ -42,6 +42,19 @@ const waveformAliases = [ ]; const noises = ['pink', 'white', 'brown', 'crackle']; +function makeDistortionCurve(amount) { + const k = typeof amount === 'number' ? amount : 50; + const n_samples = 44100; + const curve = new Float32Array(n_samples); + const deg = Math.PI / 180; + + for (let i = 0; i < n_samples; i++) { + const x = (i * 2) / n_samples - 1; + curve[i] = Math.tanh(x * k); + } + return curve; +} + export function registerSynthSounds() { [...waveforms].forEach((s) => { registerSound( @@ -84,6 +97,68 @@ export function registerSynthSounds() { { type: 'synth', prebake: true }, ); }); + + registerSound( + '909bd', + (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 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 = makeDistortionCurve(2); + + const mix = gainNode(1); + 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); + } + o.stop(end); + noise.stop(end); + + return { + node, + stop: (endTime) => { + o.stop(endTime); + }, + }; + }, + { type: 'synth', prebake: true }, + ); + registerSound( 'supersaw', (begin, value, onended) => { From 7691403fd08f23de09604e1e277619de6db4f148 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 27 Jul 2025 01:22:59 -0400 Subject: [PATCH 069/257] prevent clicks --- packages/superdough/synth.mjs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 34f958f11..e72e669eb 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -106,6 +106,7 @@ export function registerSynthSounds() { const attackhold = 0.02; const noiselvl = 1.2; const noisedecay = 0.025; + const mixGain = 1; const o = ctx.createOscillator(); o.type = 'triangle'; @@ -127,7 +128,8 @@ export function registerSynthSounds() { // tri to sine diode shaper emulation sat.curve = makeDistortionCurve(2); - const mix = gainNode(1); + const mix = gainNode(mixGain); + o.onended = () => { o.disconnect(); g.disconnect(); @@ -140,12 +142,17 @@ export function registerSynthSounds() { const node = o.connect(sat).connect(g).connect(mix); noise.node.connect(noiseGain).connect(mix); - const holdEnd = t + decay; + 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); From 4d13b981f16db4c281505708f8cfe09e904efe09 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 27 Jul 2025 01:28:50 -0400 Subject: [PATCH 070/257] variable srate --- packages/superdough/synth.mjs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index e72e669eb..8f4686a80 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -42,11 +42,9 @@ const waveformAliases = [ ]; const noises = ['pink', 'white', 'brown', 'crackle']; -function makeDistortionCurve(amount) { +function makeSaturationCurve(amount, n_samples) { const k = typeof amount === 'number' ? amount : 50; - const n_samples = 44100; const curve = new Float32Array(n_samples); - const deg = Math.PI / 180; for (let i = 0; i < n_samples; i++) { const x = (i * 2) / n_samples - 1; @@ -126,7 +124,7 @@ export function registerSynthSounds() { const sat = new WaveShaperNode(ctx); // tri to sine diode shaper emulation - sat.curve = makeDistortionCurve(2); + sat.curve = makeSaturationCurve(2, ctx.sampleRate); const mix = gainNode(mixGain); From 973ae4c7da4a036ea9147d61377c62f4513e5c28 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 27 Jul 2025 15:02:00 -0400 Subject: [PATCH 071/257] change 909bd to sbd --- packages/superdough/synth.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 8f4686a80..9446103f9 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -97,7 +97,7 @@ export function registerSynthSounds() { }); registerSound( - '909bd', + 'sbd', (t, value, onended) => { const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value; const ctx = getAudioContext(); From 30c80119e901a3afcd07491fd8f8ea77a8a3d8f4 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 28 Jul 2025 01:20:42 -0400 Subject: [PATCH 072/257] works --- website/src/repl/components/panel/PatternsTab.jsx | 2 +- website/src/user_pattern_utils.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index 8ced8a990..8a43e4a1b 100644 --- a/website/src/repl/components/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -125,7 +125,7 @@ function UserPatterns({ context }) { style={{ display: 'none' }} type="file" multiple - accept="text/plain,application/json" + accept="text/plain,text/x-markdown,application/json" onChange={(e) => importPatterns(e.target.files)} /> import diff --git a/website/src/user_pattern_utils.mjs b/website/src/user_pattern_utils.mjs index 791c6a8f9..f087c69e0 100644 --- a/website/src/user_pattern_utils.mjs +++ b/website/src/user_pattern_utils.mjs @@ -197,7 +197,7 @@ export async function importPatterns(fileList) { if (file.type === 'application/json') { const userPatterns = userPattern.getAll(); setUserPatterns({ ...userPatterns, ...parseJSON(content) }); - } else if (file.type === 'text/plain') { + } else if (['text/x-markdown', 'text/plain'].includes(file.type)) { const id = file.name.replace(/\.[^/.]+$/, ''); userPattern.update(id, { code: content }); } From ee3e1217b40f1f2b884e925602308df2a4ea9be9 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 28 Jul 2025 15:24:19 +0200 Subject: [PATCH 073/257] add euclidish --- packages/core/euclid.mjs | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 25e12b07c..38ae6f673 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, fastGap } from './pattern.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs'; import Fraction, { lcm } from './fraction.mjs'; @@ -196,3 +196,40 @@ 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,tri.slow(8)) + * .pan(tri.slow(8)) + * @example + * sound("bd").euclidish(7,12,slider(0,0.1,1)) + */ +export const [euclidish, eish] = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { + const b = bjork(pulses, steps); + let trues = 0; + const offs = []; + for (const [pos, step] of b.entries()) { + if (step) { + offs.push([trues++, pos]); + } + } + const tweened = offs.map(([n, pos]) => + Fraction(pos) + .div(steps) + .add(Fraction(n).div(pulses).sub(Fraction(pos).div(steps)).mul(perc)), + ); + return pat.struct(stack(...tweened.map((pos) => pure(true)._fastGap(steps)._late(pos)))).setSteps(steps); +}); From 784ee576da9be9114ae1fad2ac9317484f898c06 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 28 Jul 2025 15:33:18 +0200 Subject: [PATCH 074/257] fixes --- packages/core/euclid.mjs | 4 +-- test/__snapshots__/examples.test.mjs.snap | 33 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 38ae6f673..3dfb152c2 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -214,10 +214,8 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s * @example * sound("hh").euclidish(7,12,tri.slow(8)) * .pan(tri.slow(8)) - * @example - * sound("bd").euclidish(7,12,slider(0,0.1,1)) */ -export const [euclidish, eish] = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { +export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { const b = bjork(pulses, steps); let trues = 0; const offs = []; diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index e6bfa44aa..2f8404d05 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3271,6 +3271,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 ]", + "[ 1/6 → 1/4 | s:hh pan:0.041666666666666664 ]", + "[ 1/4 → 1/3 | s:hh pan:0.0625 ]", + "[ 5/12 → 1/2 | s:hh pan:0.10416666666666667 ]", + "[ 7/12 → 2/3 | s:hh pan:0.14583333333333334 ]", + "[ 2/3 → 3/4 | s:hh pan:0.16666666666666666 ]", + "[ 5/6 → 11/12 | s:hh pan:0.20833333333333334 ]", + "[ 1/1 → 13/12 | s:hh pan:0.25 ]", + "[ 65/56 → 209/168 | s:hh pan:0.29017857142857145 ]", + "[ 141/112 → 451/336 | s:hh pan:0.31473214285714285 ]", + "[ 159/112 → 505/336 | s:hh pan:0.3549107142857143 ]", + "[ 177/112 → 559/336 | s:hh pan:0.3950892857142857 ]", + "[ 47/28 → 37/21 | s:hh pan:0.41964285714285715 ]", + "[ 103/56 → 323/168 | s:hh pan:0.45982142857142855 ]", + "[ 2/1 → 25/12 | s:hh pan:0.5 ]", + "[ 181/84 → 47/21 | s:hh pan:0.5386904761904762 ]", + "[ 127/56 → 395/168 | s:hh pan:0.5669642857142857 ]", + "[ 407/168 → 421/168 | s:hh pan:0.6056547619047619 ]", + "[ 433/168 → 149/56 | s:hh pan:0.6443452380952381 ]", + "[ 113/42 → 233/84 | s:hh pan:0.6726190476190477 ]", + "[ 239/84 → 41/14 | s:hh pan:0.7113095238095238 ]", + "[ 3/1 → 37/12 | s:hh pan:0.75 ]", + "[ 529/168 → 181/56 | s:hh pan:0.7872023809523809 ]", + "[ 367/112 → 1129/336 | s:hh pan:0.8191964285714286 ]", + "[ 1151/336 → 393/112 | s:hh pan:0.8563988095238095 ]", + "[ 1201/336 → 1229/336 | s:hh pan:0.8936011904761905 ]", + "[ 311/84 → 53/14 | s:hh pan:0.9255952380952381 ]", + "[ 647/168 → 661/168 | s:hh pan:0.9627976190476191 ]", +] +`; + exports[`runs examples > example "every" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:g3 ]", From 03cbe4b6d4f567e763328b595ed5ef617d2a6e90 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 28 Jul 2025 15:48:09 +0200 Subject: [PATCH 075/257] delint --- packages/core/euclid.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 3dfb152c2..bf5a82b00 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, fastGap } from './pattern.mjs'; +import { timeCat, register, silence, stack, pure } from './pattern.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs'; import Fraction, { lcm } from './fraction.mjs'; From 6ee9dea2ff44591c191236aa495d141cec601caf Mon Sep 17 00:00:00 2001 From: samyk Date: Mon, 28 Jul 2025 20:26:34 +0200 Subject: [PATCH 076/257] Fix incorrect stack Mini Notation --- packages/core/pattern.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d59a1285d..0a9eccbe0 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1288,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: From d6045c943b188e780937c91222568edd7ecd1cfa Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 29 Jul 2025 18:20:13 -0400 Subject: [PATCH 077/257] working --- packages/repl/prebake.mjs | 2 +- website/public/uzu-drumkit.json | 76 +++++++++++++++++++++++++++++++++ website/src/repl/prebake.mjs | 2 +- 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 website/public/uzu-drumkit.json diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index 13027d2d8..3f421f985 100644 --- a/packages/repl/prebake.mjs +++ b/packages/repl/prebake.mjs @@ -36,7 +36,7 @@ export async function prebake() { samples(`${ds}/tidal-drum-machines.json`), samples(`${ds}/piano.json`), samples(`${ds}/Dirt-Samples.json`), - samples(`https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/strudel.json`), + samples(`${ds}/uzu-drumkit.json`), samples(`${ds}/vcsl.json`), samples(`${ds}/mridangam.json`), ]); diff --git a/website/public/uzu-drumkit.json b/website/public/uzu-drumkit.json new file mode 100644 index 000000000..b9c7250d5 --- /dev/null +++ b/website/public/uzu-drumkit.json @@ -0,0 +1,76 @@ +{ + "_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" + ] + } \ No newline at end of file diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index af4bdaed8..fac6f5bb6 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -28,7 +28,7 @@ export async function prebake() { prebake: true, tag: 'drum-machines', }), - samples(`https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/strudel.json`, undefined, { + samples(`${baseNoTrailing}/uzu-drumkit.json`, undefined, { prebake: true, tag: 'drum-machines', }), From 9017085fa301a549889b89412dba161315a6821b Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Fri, 1 Aug 2025 09:36:36 +0100 Subject: [PATCH 078/257] add trans alias for transpose --- packages/tonal/tonal.mjs | 73 +++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index a425782d2..5ac894f56 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -61,40 +61,7 @@ function scaleOffset(scale, offset, note) { return n + o; } -// Pattern.prototype._transpose = function (intervalOrSemitones: string | number) { -/** - * Change the pitch of each value by the given amount. Expects numbers or note strings as values. - * The amount can be given as a number of semitones or as a string in interval short notation. - * If you don't care about enharmonic correctness, just use numbers. Otherwise, pass the interval of - * the form: ST where S is the degree number and T the type of interval with - * - * - M = major - * - m = minor - * - P = perfect - * - A = augmented - * - d = diminished - * - * Examples intervals: - * - * - 1P = unison - * - 3M = major third - * - 3m = minor third - * - 4P = perfect fourth - * - 4A = augmented fourth - * - 5P = perfect fifth - * - 5d = diminished fifth - * - * @param {string | number} amount Either number of semitones or interval string. - * @returns Pattern - * @memberof Pattern - * @name transpose - * @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 = register('transpose', function (intervalOrSemitones, pat) { +function transposeFn(intervalOrSemitones, pat) { return pat.withHap((hap) => { const note = hap.value.note ?? hap.value; if (typeof note === 'number') { @@ -128,7 +95,43 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa } return hap.withValue(() => targetNote); }); -}); +} + +// Pattern.prototype._transpose = function (intervalOrSemitones: string | number) { +/** + * Change the pitch of each value by the given amount. Expects numbers or note strings as values. + * The amount can be given as a number of semitones or as a string in interval short notation. + * If you don't care about enharmonic correctness, just use numbers. Otherwise, pass the interval of + * the form: ST where S is the degree number and T the type of interval with + * + * - M = major + * - m = minor + * - P = perfect + * - A = augmented + * - d = diminished + * + * Examples intervals: + * + * - 1P = unison + * - 3M = major third + * - 3m = minor third + * - 4P = perfect fourth + * - 4A = augmented fourth + * - 5P = perfect fifth + * - 5d = diminished fifth + * + * @param {string | number} amount Either number of semitones or interval string. + * @returns Pattern + * @memberof Pattern + * @name transpose + * @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 = register('transpose', transposeFn); +export const trans = register('trans', transposeFn); // example: transpose(3).late(0.2) will be equivalent to compose(transpose(3), late(0.2)) // e.g. `stack(c3).superimpose(transpose(slowcat(7, 5)))` or From ef3cd56b0dca8f6f677f8b1e72386c2763464fa6 Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Fri, 1 Aug 2025 09:39:21 +0100 Subject: [PATCH 079/257] add synonym to docs --- packages/tonal/tonal.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 5ac894f56..da3a4d507 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -124,6 +124,7 @@ function transposeFn(intervalOrSemitones, pat) { * @returns Pattern * @memberof Pattern * @name transpose + * @synonyms trans * @example * "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note() * @example From 051efdc13bdd184daf328fcd146eaf109ea97243 Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Fri, 1 Aug 2025 09:39:33 +0100 Subject: [PATCH 080/257] flyby improvement: add dec synonym to docs --- packages/core/controls.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 326343e10..6b3433c31 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -306,6 +306,7 @@ 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) * From c2b8cc4b824c31590ffdfd7f7dda4f7032621264 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 1 Aug 2025 23:29:50 -0400 Subject: [PATCH 081/257] webtimeout --- packages/superdough/helpers.mjs | 3 ++- packages/superdough/superdough.mjs | 22 ++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 6bde69373..8a32fbf41 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -206,7 +206,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 = () => { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 011326c6a..e1c1b30ea 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -9,7 +9,7 @@ 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, errorLogger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; @@ -419,7 +419,7 @@ function setOrbit(audioContext, orbit, channels) { connectToDestination(orbits[orbit].gain, channels); } } -function duckOrbit(targetOrbit, t, attacktime = 0.1, duckdepth = 1) { +function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1) { const targetArr = [targetOrbit].flat(); targetArr.forEach((target) => { @@ -427,11 +427,17 @@ function duckOrbit(targetOrbit, t, attacktime = 0.1, duckdepth = 1) { errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); return; } - orbits[target].gain.gain.cancelAndHoldAtTime(t); - const currVal = orbits[target].gain.gain.value; - orbits[target].gain.gain.setValueAtTime(currVal, t); - orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t + 0.002); - orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime)); + 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 + 0.002); + orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime)); + }, + 0, + t, + ); }); } @@ -634,7 +640,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) setOrbit(ac, orbit, channels, t, cycle, cps); if (duckorbit != null) { - duckOrbit(duckorbit, t, duckattack, duckdepth); + duckOrbit(ac, duckorbit, t, duckattack, duckdepth); } gain = applyGainCurve(nanFallback(gain, 1)); From 39c45bc2b2077a2b7cdcd6365af85c2cc15e8395 Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Sat, 2 Aug 2025 10:17:33 +0100 Subject: [PATCH 082/257] pass an array of names instead --- packages/tonal/tonal.mjs | 73 +++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index da3a4d507..7cb987394 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -61,42 +61,6 @@ function scaleOffset(scale, offset, note) { return n + o; } -function transposeFn(intervalOrSemitones, pat) { - return pat.withHap((hap) => { - const note = hap.value.note ?? hap.value; - if (typeof note === 'number') { - // note is a number, so just add the number semitones of the interval - let semitones; - if (typeof intervalOrSemitones === 'number') { - semitones = intervalOrSemitones; - } else if (typeof intervalOrSemitones === 'string') { - semitones = Interval.semitones(intervalOrSemitones) || 0; - } - const targetNote = note + semitones; - if (typeof hap.value === 'object') { - return hap.withValue(() => ({ ...hap.value, note: targetNote })); - } - return hap.withValue(() => targetNote); - } - if (typeof note !== 'string' || !isNote(note)) { - logger(`[tonal] transpose: not a note "${note}"`, 'warning'); - return hap; - } - // note is a string, so we might be able to preserve harmonics if interval is a string as well - const interval = !isNaN(Number(intervalOrSemitones)) - ? Interval.fromSemitones(intervalOrSemitones) - : String(intervalOrSemitones); - // TODO: move simplify to player to preserve enharmonics - // tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3 - // TODO: check if this is still relevant.. - const targetNote = Note.simplify(Note.transpose(note, interval)); - if (typeof hap.value === 'object') { - return hap.withValue(() => ({ ...hap.value, note: targetNote })); - } - return hap.withValue(() => targetNote); - }); -} - // Pattern.prototype._transpose = function (intervalOrSemitones: string | number) { /** * Change the pitch of each value by the given amount. Expects numbers or note strings as values. @@ -131,8 +95,41 @@ function transposeFn(intervalOrSemitones, pat) { * "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note() */ -export const transpose = register('transpose', transposeFn); -export const trans = register('trans', transposeFn); +export const transpose = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) { + return pat.withHap((hap) => { + const note = hap.value.note ?? hap.value; + if (typeof note === 'number') { + // note is a number, so just add the number semitones of the interval + let semitones; + if (typeof intervalOrSemitones === 'number') { + semitones = intervalOrSemitones; + } else if (typeof intervalOrSemitones === 'string') { + semitones = Interval.semitones(intervalOrSemitones) || 0; + } + const targetNote = note + semitones; + if (typeof hap.value === 'object') { + return hap.withValue(() => ({ ...hap.value, note: targetNote })); + } + return hap.withValue(() => targetNote); + } + if (typeof note !== 'string' || !isNote(note)) { + logger(`[tonal] transpose: not a note "${note}"`, 'warning'); + return hap; + } + // note is a string, so we might be able to preserve harmonics if interval is a string as well + const interval = !isNaN(Number(intervalOrSemitones)) + ? Interval.fromSemitones(intervalOrSemitones) + : String(intervalOrSemitones); + // TODO: move simplify to player to preserve enharmonics + // tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3 + // TODO: check if this is still relevant.. + const targetNote = Note.simplify(Note.transpose(note, interval)); + if (typeof hap.value === 'object') { + return hap.withValue(() => ({ ...hap.value, note: targetNote })); + } + return hap.withValue(() => targetNote); + }); +}); // example: transpose(3).late(0.2) will be equivalent to compose(transpose(3), late(0.2)) // e.g. `stack(c3).superimpose(transpose(slowcat(7, 5)))` or From ad113b3888888fbc34b1742219fab93dadcd4b8d Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Sat, 2 Aug 2025 10:17:48 +0100 Subject: [PATCH 083/257] document ability to specify an array of names --- packages/core/pattern.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d59a1285d..4d0274c7a 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1569,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} name name of the function + * @param {string | string[]} name name of the function, or an array of names to be used as synonyms * @param {function} func function with 1 or more params, where last is the current pattern * @noAutocomplete * From efc2333360dd1528f471f1a131cc5f1ebfbe85b0 Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Sat, 2 Aug 2025 10:25:28 +0100 Subject: [PATCH 084/257] add synonyms for scaleTranspose --- packages/tonal/tonal.mjs | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index a425782d2..dbd9eb8b0 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -142,6 +142,7 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa * @name scaleTranspose * @param {offset} offset number of steps inside the scale * @returns Pattern + * @synonyms scaleTrans * @example * "-8 [2,4,6]" * .scale('C4 bebop major') @@ -149,22 +150,25 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa * .note() */ -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)); - }); -}); +export const scaleTranspose = 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)); + }); + }, +); /** * Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}. From d55ef8e84383f201585d0972de17d68c0b00b6cb Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Sat, 2 Aug 2025 10:26:36 +0100 Subject: [PATCH 085/257] add strans too --- packages/tonal/tonal.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index dbd9eb8b0..6d18f27b8 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -142,7 +142,7 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa * @name scaleTranspose * @param {offset} offset number of steps inside the scale * @returns Pattern - * @synonyms scaleTrans + * @synonyms scaleTrans, strans * @example * "-8 [2,4,6]" * .scale('C4 bebop major') From 450f9685f344f012281b8a87fddc26a20eb28dd9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 3 Aug 2025 12:54:19 -0400 Subject: [PATCH 086/257] fixed export --- packages/tonal/tonal.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index a2418c68a..0a5a9eefb 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -95,7 +95,7 @@ function scaleOffset(scale, offset, note) { * "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note() */ -export const transpose = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) { +export const {transpose, trans} = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) { return pat.withHap((hap) => { const note = hap.value.note ?? hap.value; if (typeof note === 'number') { @@ -151,7 +151,7 @@ export const transpose = register(['transpose', 'trans'], function transposeFn(i * .note() */ -export const scaleTranspose = register( +export const {scaleTranspose, scaleTrans, strans} = register( ['scaleTranspose', 'scaleTrans', 'strans'], function (offset /* : number | string */, pat) { return pat.withHap((hap) => { From 34b10b6c844bb4b5c75e74eef4f49937a8b0d0bf Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 3 Aug 2025 12:56:16 -0400 Subject: [PATCH 087/257] codeformat --- packages/tonal/tonal.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 0a5a9eefb..c4dd54880 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -95,7 +95,7 @@ function scaleOffset(scale, offset, note) { * "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, trans } = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) { return pat.withHap((hap) => { const note = hap.value.note ?? hap.value; if (typeof note === 'number') { @@ -151,7 +151,7 @@ export const {transpose, trans} = register(['transpose', 'trans'], function tran * .note() */ -export const {scaleTranspose, scaleTrans, strans} = register( +export const { scaleTranspose, scaleTrans, strans } = register( ['scaleTranspose', 'scaleTrans', 'strans'], function (offset /* : number | string */, pat) { return pat.withHap((hap) => { From 3b6777e25965fa6682b5f02574bbc7468b0f9208 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 5 Aug 2025 19:05:17 -0500 Subject: [PATCH 088/257] Fix signature of functions in log and logValues and add tests --- packages/core/pattern.mjs | 4 +-- packages/core/test/pattern.test.mjs | 40 ++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 4168645b8..d724bb853 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -852,14 +852,14 @@ export class Pattern { ); } - 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); } logValues(func = id) { - return this.log((_, hap) => func(hap.value)); + return this.log((hap) => func(hap.value)); } ////////////////////////////////////////////////////////////////////// diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 93c4168c9..51e0acaef 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); // query during first time arc + + // 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#").log() + const haps = pattern.queryArc(0, 1); // query during first time arc + + // Force a trigger + haps.forEach(hap => { + hap.context?.onTrigger?.(hap); + }); + + expect(mockConsoleLog).toHaveBeenCalledWith( + '%c[hap] 0/1 → 1/1: value:a note:c#', + 'background-color: black;color:white;border-radius:15px', + ); + mockConsoleLog.mockRestore(); + }); + }); }); From 23e5358ae424bfb851d23688c9e547ce2061ae4d Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 5 Aug 2025 20:46:00 -0500 Subject: [PATCH 089/257] Formatting --- packages/core/test/pattern.test.mjs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 51e0acaef..b8e7de504 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -55,7 +55,7 @@ import { expand, } from '../index.mjs'; -import { log, logValues } from '../pattern.mjs' +import { log, logValues } from '../pattern.mjs'; import { steady } from '../signal.mjs'; @@ -1311,11 +1311,11 @@ 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); // query during first time arc + const pattern = pure('a').log(); + const haps = pattern.queryArc(0, 1); // Force a trigger - haps.forEach(hap => { + haps.forEach((hap) => { hap.context?.onTrigger?.(hap); }); @@ -1329,11 +1329,11 @@ describe('Pattern', () => { describe('logValues', () => { it('logs values to console', () => { const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); - const pattern = pure('a').note("c#").log() - const haps = pattern.queryArc(0, 1); // query during first time arc + const pattern = pure('a').note('c#').log(); + const haps = pattern.queryArc(0, 1); // Force a trigger - haps.forEach(hap => { + haps.forEach((hap) => { hap.context?.onTrigger?.(hap); }); From de15d79edf39fea50159ec990c268c8aa56fcde3 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 5 Aug 2025 22:29:59 -0500 Subject: [PATCH 090/257] Add doscstrings and move stringifyValues into its own function for reuse --- packages/core/hap.mjs | 9 ++------- packages/core/pattern.mjs | 19 ++++++++++++++++++- packages/core/test/pattern.test.mjs | 4 ++-- packages/core/util.mjs | 10 ++++++++++ 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/core/hap.mjs b/packages/core/hap.mjs index a6e3c55ad..5f820d644 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/pattern.mjs b/packages/core/pattern.mjs index d724bb853..f64761f4f 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -21,6 +21,7 @@ import { numeralArgs, parseNumeral, pairs, + stringifyValues, } from './util.mjs'; import drawLine from './drawLine.mjs'; import { logger } from './logger.mjs'; @@ -852,13 +853,29 @@ export class Pattern { ); } + /** + * Writes the content of the current event to the console, which is visible in the side menu + * or as the developer console. + * @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) { + /** + * A simplified version of `log` which writes all "values" (various configurable parameters) + * within the event to the console, which is visible in the side menu or as the developer console. + * @name logValues + * @memberof Pattern + * @example + * s("bd sd").gain("0.25 0.5 1").n("2 1 0").logValues() + */ + logValues(func = (value) => `${stringifyValues(value, true)}`) { return this.log((hap) => func(hap.value)); } diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index b8e7de504..b82d39b9d 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1329,7 +1329,7 @@ describe('Pattern', () => { describe('logValues', () => { it('logs values to console', () => { const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); - const pattern = pure('a').note('c#').log(); + const pattern = pure('a').note('c#').logValues(); const haps = pattern.queryArc(0, 1); // Force a trigger @@ -1338,7 +1338,7 @@ describe('Pattern', () => { }); expect(mockConsoleLog).toHaveBeenCalledWith( - '%c[hap] 0/1 → 1/1: value:a note:c#', + '%cvalue:a note:c#', 'background-color: black;color:white;border-radius:15px', ); mockConsoleLog.mockRestore(); diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 756fac8e8..2e7c6e026 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; +} From f22f89347e9dc8da263a56d30bde6f41187b5506 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 5 Aug 2025 22:40:11 -0500 Subject: [PATCH 091/257] Final docstring cleanup & adding hap tag to logValues for consistency --- packages/core/pattern.mjs | 7 +++---- packages/core/test/pattern.test.mjs | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index f64761f4f..dbf1b17f7 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -854,8 +854,7 @@ export class Pattern { } /** - * Writes the content of the current event to the console, which is visible in the side menu - * or as the developer console. + * Writes the content of the current event to the console (visible in the side menu). * @name log * @memberof Pattern * @example @@ -869,13 +868,13 @@ export class Pattern { /** * A simplified version of `log` which writes all "values" (various configurable parameters) - * within the event to the console, which is visible in the side menu or as the developer console. + * 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) => `${stringifyValues(value, true)}`) { + logValues(func = (value) => `[hap] ${stringifyValues(value, true)}`) { return this.log((hap) => func(hap.value)); } diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index b82d39b9d..1df5c8776 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1338,7 +1338,7 @@ describe('Pattern', () => { }); expect(mockConsoleLog).toHaveBeenCalledWith( - '%cvalue:a note:c#', + '%c[hap] value:a note:c#', 'background-color: black;color:white;border-radius:15px', ); mockConsoleLog.mockRestore(); From a4ab8e1b06e80c9092b75fb5bd8107aa24f288a0 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 6 Aug 2025 00:45:01 -0500 Subject: [PATCH 092/257] First pass at extending scale function to include notes --- packages/tonal/tonal.mjs | 113 ++++++++++++++++++++++------------- packages/tonal/tonleiter.mjs | 14 ++++- 2 files changed, 84 insertions(+), 43 deletions(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index c4dd54880..2aca7e9cb 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -6,7 +6,8 @@ This program is free software: you can redistribute it and/or modify it under th import { Note, Interval, Scale } from '@tonaljs/tonal'; import { register, _mod, silence, logger, pure, isNote } from '@strudel/core'; -import { stepInNamedScale } from './tonleiter.mjs'; +import { stepInNamedScale, scaleToChromas } from './tonleiter.mjs'; +import { noteToMidi } from '../core/util.mjs' const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P'; @@ -171,8 +172,52 @@ export const { scaleTranspose, scaleTrans, strans } = register( }, ); +// Converts a step value, which is a number optionally decorated with sharps and flats, +// to a number and an `offset` number of semitones +function _convertStepToNumberAndOffset(step) { + if (isNote(step)) { + // legacy.. + return pure(step); + } + let asNumber = Number(step); + let offset = 0; + if (isNaN(asNumber)) { + step = String(step); + // Check to see if the format correctly matches the expected one of + // Optionally starting with + or - + // A number + // Some number of sharps or flats (but not both) + const match = /^[-+]?(\d+)((#{1,})|(b{1,}))?$/.exec(step); + + if (!match) { + logger( + `[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`, + 'error', + ); + return silence; + } + asNumber = Number(match[2]); + // The number of semitones will be given by either the total number of sharps (match 3) + // or the negative of the total number of flats (match 4) + offset = match[3].length > 0 ? match[3].length : -match[4].length; + } + return [asNumber, offset]; +} + +// Finds the nearest (named) scale note to `note` (a string which is then converted to a midi number) +function _getNearestScaleNote(scaleName, note) { + let midiNote = noteToMidi(note); + const octave = (midiNote / 12) >> 0; + const goal = midiNote % 12; + const chromas = scaleToChromas(scaleName); + return chromas.reduce((prev, curr) => { + return Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev; + }) + octave * 12; +} + /** - * Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}. + * Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale. + * Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}. * * A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts). * @@ -200,58 +245,42 @@ export const scale = register( if (Array.isArray(scale)) { scale = scale.flat().join(' '); } - return ( + let output = ( pat .fmap((value) => { const isObject = typeof value === 'object'; - let step = isObject ? value.n : value; - if (isObject) { + // The case where the note has been defined via `n` + if ((isObject && 'n' in value) || !isObject) { + let step = isObject ? value.n : value; + debugger; delete value.n; // remove n so it won't cause trouble - } - if (isNote(step)) { - // legacy.. - return pure(step); - } - let asNumber = Number(step); - let semitones = 0; - if (isNaN(asNumber)) { - step = String(step); - if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) { - logger( - `[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`, - 'error', - ); - return silence; - } - const isharp = step.indexOf('#'); - if (isharp >= 0) { - asNumber = Number(step.substring(0, isharp)); - semitones = step.length - isharp; - } else { - const iflat = step.indexOf('b'); - asNumber = Number(step.substring(0, iflat)); - semitones = iflat - step.length; + let [number, offset] = _convertStepToNumberAndOffset(step); + try { + let note; + if (isObject && value.anchor) { + note = stepInNamedScale(number, scale, value.anchor); + } else { + note = scaleStep(number, scale); + } + if (offset != 0) note = Note.transpose(note, Interval.fromSemitones(offset)); + value = pure(isObject ? { ...value, note } : note); + } catch (err) { + logger(`[tonal] ${err.message}`, 'error'); + value = silence; } + return value; } - try { - let note; - if (isObject && value.anchor) { - note = stepInNamedScale(asNumber, scale, value.anchor); - } else { - note = scaleStep(asNumber, scale); - } - if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones)); - value = pure(isObject ? { ...value, note } : note); - } catch (err) { - logger(`[tonal] ${err.message}`, 'error'); - value = silence; + // The case where the note has been defined via `note` + else { + let note = _getNearestScaleNote(scale, value.note); + return pure(isObject ? { ...value, note } : note); } - return value; }) .outerJoin() // legacy: .withHap((hap) => hap.setContext({ ...hap.context, scale })) ); + return output; }, true, true, // preserve step count diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index 3814394f6..63ae9c942 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -222,6 +222,7 @@ export const Note = { }; // TODO: support octave numbers +// Example: Note("Bb3").transpose("c3") export function transpose(note, step) { // example: E, 3 const stepNumber = Step.tokenize(step)[1]; // 3 @@ -236,4 +237,15 @@ export function transpose(note, step) { return [targetNote, offsetAccidentals].join(''); } -//Note("Bb3").transpose("c3") +// Converts a `scaleName` into a corresponding list of chromas between 0 and 12 +export function scaleToChromas (scaleName) { + if (Array.isArray(scaleName)) { + scaleName = scaleName.flat().join(' '); + } + const [tonic, name] = Scale.tokenize(scaleName); + const rootMidi = noteToMidi(tonic); + const chroma = rootMidi % 12; + const intervals = Scale.get(name).intervals; + const scaleSteps = intervals.map(Interval.semitones); + return scaleSteps.map(s => (s + chroma) % 12); +} From 7046c1f54669f26a93252349af28e6adca4b6310 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 6 Aug 2025 10:17:48 -0500 Subject: [PATCH 093/257] Cleanup and simplification; improved docstrings --- packages/tonal/tonal.mjs | 43 +++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 2aca7e9cb..5b0c63b97 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -183,11 +183,10 @@ function _convertStepToNumberAndOffset(step) { let offset = 0; if (isNaN(asNumber)) { step = String(step); - // Check to see if the format correctly matches the expected one of - // Optionally starting with + or - - // A number - // Some number of sharps or flats (but not both) - const match = /^[-+]?(\d+)((#{1,})|(b{1,}))?$/.exec(step); + // Check to see if the step matches the expected format: + // - A number (possibly negative) + // - Some number of sharps or flats (but not both) + const match = /^(-?\d+)(#+|b+)?$/.exec(step); if (!match) { logger( @@ -196,27 +195,33 @@ function _convertStepToNumberAndOffset(step) { ); return silence; } - asNumber = Number(match[2]); - // The number of semitones will be given by either the total number of sharps (match 3) - // or the negative of the total number of flats (match 4) - offset = match[3].length > 0 ? match[3].length : -match[4].length; + asNumber = Number(match[1]); + // These decorations will determine the semitone offset based on the number of + // sharps or flats + const decorations = match[2] || ''; + offset = decorations[0] === '#' ? decorations.length : -decorations.length; } return [asNumber, offset]; } -// Finds the nearest (named) scale note to `note` (a string which is then converted to a midi number) +// Finds the nearest scale note to `note` function _getNearestScaleNote(scaleName, note) { - let midiNote = noteToMidi(note); + let midiNote = typeof note === 'string' ? noteToMidi(note) : note; const octave = (midiNote / 12) >> 0; - const goal = midiNote % 12; - const chromas = scaleToChromas(scaleName); - return chromas.reduce((prev, curr) => { - return Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev; + const targetChroma = midiNote % 12; + const scaleChromas = scaleToChromas(scaleName); + return scaleChromas.reduce((prev, curr) => { + // Include equality so ties are broken upwards + return Math.abs(curr - targetChroma) <= Math.abs(prev - targetChroma) ? curr : prev; }) + octave * 12; } /** * Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale. + * + * When describing notes via numbers, note that negative numbers can be used to wrap backwards + * in the scale as well as sharps or flats (but not both) to produce notes outside of the scale. + * * Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}. * * A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts). @@ -236,6 +241,10 @@ function _getNearestScaleNote(scaleName, note) { * n(rand.range(0,12).segment(8)) * .scale("C:ritusen") * .s("piano") + * @example + * n("<[0,7b] [-4# -4] [-2,7##] 4 [0,7] [-4# -4b] [-2,7###] 4b>*4") + * .scale("C:/2") + * .s("piano") */ export const scale = register( @@ -245,14 +254,13 @@ export const scale = register( if (Array.isArray(scale)) { scale = scale.flat().join(' '); } - let output = ( + return ( pat .fmap((value) => { const isObject = typeof value === 'object'; // The case where the note has been defined via `n` if ((isObject && 'n' in value) || !isObject) { let step = isObject ? value.n : value; - debugger; delete value.n; // remove n so it won't cause trouble let [number, offset] = _convertStepToNumberAndOffset(step); try { @@ -280,7 +288,6 @@ export const scale = register( // legacy: .withHap((hap) => hap.setContext({ ...hap.context, scale })) ); - return output; }, true, true, // preserve step count From a4c040e10134c8978fdeda318bddb93caf5fa267 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 6 Aug 2025 12:37:55 -0500 Subject: [PATCH 094/257] Add new tests, organize old tests, fix issue with note --- packages/tonal/test/tonal.test.mjs | 171 +++++++++++++++++------------ packages/tonal/tonal.mjs | 32 +++--- packages/tonal/tonleiter.mjs | 4 +- 3 files changed, 120 insertions(+), 87 deletions(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index cd8b3c88b..5486d5135 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -7,80 +7,113 @@ This program is free software: you can redistribute it and/or modify it under th // import { strict as assert } from 'assert'; import '../tonal.mjs'; // need to import this to add prototypes -import { pure, n, seq, note } from '@strudel/core'; +import { pure, n, seq, note, noteToMidi } from '@strudel/core'; import { describe, it, expect } from 'vitest'; import { mini } from '../../mini/mini.mjs'; describe('tonal', () => { - it('Should run tonal functions ', () => { - expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']); + describe('scaleTranspose', () => { + it('transposes notes by scale degrees', () => { + expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']); + }); }); - it('scale with plain values', () => { - expect( - seq(0, 1, 2) - .scale('C major') - .note() - .firstCycleValues.map((h) => h.note), - ).toEqual(['C3', 'D3', 'E3']); + describe('scale', () => { + it('converts plain values', () => { + expect( + seq(0, 1, 2) + .scale('C major') + .note() + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts n values', () => { + expect( + n(seq(0, 1, 2)) + .scale('C major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts n values (mini notation)', () => { + expect( + n(seq(0, 1, 2)) + .scale('C:major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts n values (no tonic)', () => { + expect( + n(seq(0, 1, 2)) + .scale('major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts n values (explicit mini notation)', () => { + expect( + n(seq(0, 1, 2)) + .scale(mini('C:major')) + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts decorated n values', () => { + expect( + n(seq('0b', '1#', '-2', '3##', '4bb')) + .scale('C major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']); + }); + it('produces silence for mixed sharps and flats', () => { + expect( + n(seq('0b#', '1#b', '2#b#')) + .scale('C major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['', '', '']); + }); + it('snaps notes (upwards) to scale', () => { + const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb']; + let expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3']; + + // Notes are converted to midi by scale + expectedNotes = expectedNotes.map((note) => noteToMidi(note)); + + expect( + note(seq(inputNotes)) + .scale('C major') + .firstCycleValues.map((h) => h.note), + ).toEqual(expectedNotes); + }); }); - it('scale with n values', () => { - expect( - n(seq(0, 1, 2)) - .scale('C major') - .firstCycleValues.map((h) => h.note), - ).toEqual(['C3', 'D3', 'E3']); - }); - it('scale with colon', () => { - expect( - n(seq(0, 1, 2)) - .scale('C:major') - .firstCycleValues.map((h) => h.note), - ).toEqual(['C3', 'D3', 'E3']); - }); - it('scale without tonic', () => { - expect( - n(seq(0, 1, 2)) - .scale('major') - .firstCycleValues.map((h) => h.note), - ).toEqual(['C3', 'D3', 'E3']); - }); - it('scale with mininotation colon', () => { - expect( - n(seq(0, 1, 2)) - .scale(mini('C:major')) - .firstCycleValues.map((h) => h.note), - ).toEqual(['C3', 'D3', 'E3']); - }); - it('transposes note numbers with interval numbers', () => { - expect( - note(seq(40, 40, 40)) - .transpose(0, 1, 2) - .firstCycleValues.map((h) => h.note), - ).toEqual([40, 41, 42]); - expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]); - }); - it('transposes note numbers with interval strings', () => { - expect( - note(seq(40, 40, 40)) - .transpose('1P', '2M', '3m') - .firstCycleValues.map((h) => h.note), - ).toEqual([40, 42, 43]); - expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]); - }); - it('transposes note strings with interval numbers', () => { - expect( - note(seq('c', 'c', 'c')) - .transpose(0, 1, 2) - .firstCycleValues.map((h) => h.note), - ).toEqual(['C', 'Db', 'D']); - expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']); - }); - it('transposes note strings with interval strings', () => { - expect( - note(seq('c', 'c', 'c')) - .transpose('1P', '2M', '3m') - .firstCycleValues.map((h) => h.note), - ).toEqual(['C', 'D', 'Eb']); - expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']); + describe('transpose', () => { + it('transposes note numbers with interval numbers', () => { + expect( + note(seq(40, 40, 40)) + .transpose(0, 1, 2) + .firstCycleValues.map((h) => h.note), + ).toEqual([40, 41, 42]); + expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]); + }); + it('transposes note numbers with interval strings', () => { + expect( + note(seq(40, 40, 40)) + .transpose('1P', '2M', '3m') + .firstCycleValues.map((h) => h.note), + ).toEqual([40, 42, 43]); + expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]); + }); + it('transposes note strings with interval numbers', () => { + expect( + note(seq('c', 'c', 'c')) + .transpose(0, 1, 2) + .firstCycleValues.map((h) => h.note), + ).toEqual(['C', 'Db', 'D']); + expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']); + }); + it('transposes note strings with interval strings', () => { + expect( + note(seq('c', 'c', 'c')) + .transpose('1P', '2M', '3m') + .firstCycleValues.map((h) => h.note), + ).toEqual(['C', 'D', 'Eb']); + expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']); + }); }); }); diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 5b0c63b97..5e505c75a 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th import { Note, Interval, Scale } from '@tonaljs/tonal'; import { register, _mod, silence, logger, pure, isNote } from '@strudel/core'; import { stepInNamedScale, scaleToChromas } from './tonleiter.mjs'; -import { noteToMidi } from '../core/util.mjs' +import { noteToMidi } from '../core/util.mjs'; const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P'; @@ -175,25 +175,18 @@ export const { scaleTranspose, scaleTrans, strans } = register( // Converts a step value, which is a number optionally decorated with sharps and flats, // to a number and an `offset` number of semitones function _convertStepToNumberAndOffset(step) { - if (isNote(step)) { - // legacy.. - return pure(step); - } let asNumber = Number(step); let offset = 0; if (isNaN(asNumber)) { step = String(step); // Check to see if the step matches the expected format: - // - A number (possibly negative) - // - Some number of sharps or flats (but not both) + // - A number (possibly negative) + // - Some number of sharps or flats (but not both) const match = /^(-?\d+)(#+|b+)?$/.exec(step); if (!match) { - logger( - `[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`, - 'error', - ); - return silence; + logger(`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`, 'error'); + return [silence, 0]; } asNumber = Number(match[1]); // These decorations will determine the semitone offset based on the number of @@ -210,10 +203,13 @@ function _getNearestScaleNote(scaleName, note) { const octave = (midiNote / 12) >> 0; const targetChroma = midiNote % 12; const scaleChromas = scaleToChromas(scaleName); - return scaleChromas.reduce((prev, curr) => { - // Include equality so ties are broken upwards - return Math.abs(curr - targetChroma) <= Math.abs(prev - targetChroma) ? curr : prev; - }) + octave * 12; + return ( + scaleChromas.reduce((prev, curr) => { + // Include equality so ties are broken upwards + return Math.abs(curr - targetChroma) <= Math.abs(prev - targetChroma) ? curr : prev; + }) + + octave * 12 + ); } /** @@ -262,6 +258,10 @@ export const scale = register( if ((isObject && 'n' in value) || !isObject) { let step = isObject ? value.n : value; delete value.n; // remove n so it won't cause trouble + if (isNote(step)) { + // legacy.. + return pure(step); + } let [number, offset] = _convertStepToNumberAndOffset(step); try { let note; diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index 63ae9c942..39c819cd7 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -238,7 +238,7 @@ export function transpose(note, step) { } // Converts a `scaleName` into a corresponding list of chromas between 0 and 12 -export function scaleToChromas (scaleName) { +export function scaleToChromas(scaleName) { if (Array.isArray(scaleName)) { scaleName = scaleName.flat().join(' '); } @@ -247,5 +247,5 @@ export function scaleToChromas (scaleName) { const chroma = rootMidi % 12; const intervals = Scale.get(name).intervals; const scaleSteps = intervals.map(Interval.semitones); - return scaleSteps.map(s => (s + chroma) % 12); + return scaleSteps.map((s) => (s + chroma) % 12); } From 3292f888108cd4633ac869ba625809ef07e6d761 Mon Sep 17 00:00:00 2001 From: robase <11038379+robase@users.noreply.github.com> Date: Sun, 27 Jul 2025 18:50:37 +0100 Subject: [PATCH 095/257] fix: repl autocomplete not rendering correctly --- packages/codemirror/autocomplete.mjs | 123 ++++++++++++++----------- website/src/repl/Repl.css | 131 +++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 53 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 203ab8556..d3091d874 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -1,68 +1,91 @@ import jsdoc from '../../doc.json'; -// import { javascriptLanguage } from '@codemirror/lang-javascript'; import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; -function plaintext(str) { +const escapeHtml = (str) => { const div = document.createElement('div'); div.innerText = str; return div.innerHTML; -} +}; -const getDocLabel = (doc) => doc.name || doc.longname; -const getInnerText = (html) => { - var div = document.createElement('div'); +const stripHtml = (html) => { + const div = document.createElement('div'); div.innerHTML = html; return div.textContent || div.innerText || ''; }; -export function Autocomplete({ doc, label }) { - return h`
-

${label || getDocLabel(doc)}

-${doc.description} -
    - ${doc.params?.map( - ({ name, type, description }) => - `
  • ${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}
  • `, - )} -
-
- ${doc.examples?.map((example) => `
${plaintext(example)}
`)} -
-
`[0]; - /* -
 {
-  console.log('ola!');
-  navigator.clipboard.writeText(example);
-  e.stopPropagation();
-}}
->
-{example}
-
-*/ -} +const getDocLabel = (doc) => doc.name || doc.longname; + +const buildParamsList = (params) => + params?.length + ? ` +
+

Parameters

+
    + ${params + .map( + ({ name, type, description }) => ` +
  • + ${name} + ${type.names?.join(' | ')} + ${description ? `
    ${stripHtml(description)}
    ` : ''} +
  • + `, + ) + .join('')} +
+
+ ` + : ''; + +const buildExamples = (examples) => + examples?.length + ? ` +
+

Examples

+ ${examples + .map( + (example) => ` +
${escapeHtml(example)}
+ `, + ) + .join('')} +
+ ` + : ''; + +export const Autocomplete = ({ doc, label }) => + h` +
+

${label || getDocLabel(doc)}

+ ${doc.description ? `

${doc.description}

` : ''} + ${buildParamsList(doc.params)} + ${buildExamples(doc.examples)} +
+`[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)); const jsdocCompletions = jsdoc.docs - .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) /*: Completion */ => ({ + .filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc)) + // https://codemirror.net/docs/ref/#autocomplete.Completion + .map((doc) => ({ 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 /* : CompletionContext */) => { - let word = context.matchBefore(/\w*/); - if (word.from == word.to && !context.explicit) return null; +export const strudelAutocomplete = (context) => { + const word = context.matchBefore(/\w*/); + if (word.from === word.to && !context.explicit) return null; + return { from: word.from, options: jsdocCompletions, @@ -74,11 +97,5 @@ export const strudelAutocomplete = (context /* : CompletionContext */) => { }; }; -export function isAutoCompletionEnabled(on) { - return on - ? [ - autocompletion({ override: [strudelAutocomplete] }), - //javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }), - ] - : []; // autocompletion({ override: [] }) -} +export const isAutoCompletionEnabled = (enabled) => + enabled ? [autocompletion({ override: [strudelAutocomplete] })] : []; diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index 3e13ff5a2..5f1d427e6 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -69,3 +69,134 @@ 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: 0 !important; + background: #1e1e1e !important; + border: 1px solid #3a3a3a !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; +} + +/* Main tooltip container */ +.autocomplete-info-tooltip { + padding: 12px; + color: #d4d4d4; + font-family: 'SF Mono', 'Monaco', monospace; + font-size: 13px; + line-height: 1.4; + overflow-y: auto; + max-width: 600px; + max-height: 400px; +} + +/* Function name */ +.autocomplete-info-function-name { + font-size: 15px; + font-weight: 600; + color: #ffffff; + margin: 0 0 8px 0; +} + +/* Function description */ +.autocomplete-info-function-description { + margin: 0 0 12px 0; + color: #b4b4b4; + line-height: 1.5; +} + +/* Section titles */ +.autocomplete-info-section-title { + font-size: 12px; + font-weight: 600; + color: #ffffff; + margin: 16px 0 6px 0; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.autocomplete-info-section-title:first-child { + margin-top: 0; +} + +/* Parameters */ +.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: #2a2a2a; + border-radius: 3px; + border-left: 2px solid #555; +} + +.autocomplete-info-param-item:last-child { + margin-bottom: 0; +} + +.autocomplete-info-param-name { + font-weight: 600; + color: #ffffff; + margin-right: 8px; +} + +.autocomplete-info-param-type { + color: #888; + font-size: 12px; + background: #333; + padding: 1px 4px; + border-radius: 2px; +} + +.autocomplete-info-param-desc { + color: #b4b4b4; + font-size: 10px; + margin-top: 4px; + line-height: 1.4; +} + +/* Examples */ +.autocomplete-info-examples-section { + margin-top: 12px; +} + +.autocomplete-info-example-code { + background: #2a2a2a; + color: #d4d4d4; + padding: 8px; + border-radius: 3px; + 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 #3a3a3a; +} + +/* Scrollbar */ +.autocomplete-info-tooltip::-webkit-scrollbar { + width: 4px; +} + +.autocomplete-info-tooltip::-webkit-scrollbar-track { + background: #2a2a2a; +} + +.autocomplete-info-tooltip::-webkit-scrollbar-thumb { + background: #555; + border-radius: 2px; +} From e7839a09a1c12fe3d803b747fe41289280a8ebac Mon Sep 17 00:00:00 2001 From: robase <11038379+robase@users.noreply.github.com> Date: Sun, 27 Jul 2025 22:02:06 +0100 Subject: [PATCH 096/257] fix: tooltip should respect themes --- website/src/repl/Repl.css | 57 ++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index 5f1d427e6..3a5a6eb23 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -71,50 +71,54 @@ } /* Override default styles from the codemirror inline css for autocomplete info tooltip*/ +/* Override default styles from the c odemirror inline css for autocomplete info tooltip*/ .cm-tooltip.cm-completionInfo { - padding: 0 !important; - background: #1e1e1e !important; - border: 1px solid #3a3a3a !important; + 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 { - padding: 12px; - color: #d4d4d4; - font-family: 'SF Mono', 'Monaco', monospace; - font-size: 13px; + 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; - overflow-y: auto; max-width: 600px; max-height: 400px; + min-width: 400px; } /* Function name */ .autocomplete-info-function-name { font-size: 15px; font-weight: 600; - color: #ffffff; + color: var(--foreground); margin: 0 0 8px 0; } /* Function description */ .autocomplete-info-function-description { margin: 0 0 12px 0; - color: #b4b4b4; + color: var(--foreground); line-height: 1.5; + opacity: 0.8; } /* Section titles */ .autocomplete-info-section-title { font-size: 12px; font-weight: 600; - color: #ffffff; + color: var(--foreground); margin: 16px 0 6px 0; text-transform: uppercase; letter-spacing: 0.5px; @@ -138,9 +142,9 @@ .autocomplete-info-param-item { margin-bottom: 8px; padding: 8px; - background: #2a2a2a; + background-color: var(--lineBackground); border-radius: 3px; - border-left: 2px solid #555; + border-left: 2px solid var(--foreground, #555); } .autocomplete-info-param-item:last-child { @@ -149,23 +153,24 @@ .autocomplete-info-param-name { font-weight: 600; - color: #ffffff; + color: var(--variable, var(--foreground)); margin-right: 8px; } .autocomplete-info-param-type { - color: #888; + color: var(--comment); font-size: 12px; - background: #333; + background-color: var(--gutterForeground); padding: 1px 4px; border-radius: 2px; } .autocomplete-info-param-desc { - color: #b4b4b4; + color: var(--foreground); font-size: 10px; margin-top: 4px; line-height: 1.4; + opacity: 0.7; } /* Examples */ @@ -174,29 +179,33 @@ } .autocomplete-info-example-code { - background: #2a2a2a; - color: #d4d4d4; + background: var(--lineBackground); + color: var(--foreground); padding: 8px; border-radius: 3px; - font-family: 'SF Mono', 'Monaco', monospace; + 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 #3a3a3a; + border: 1px solid var(--foreground, #3a3a3a); } -/* Scrollbar */ +/* Scrollbar - using theme colors */ .autocomplete-info-tooltip::-webkit-scrollbar { width: 4px; } .autocomplete-info-tooltip::-webkit-scrollbar-track { - background: #2a2a2a; + /* background: var(--lineBackground, var(--background)); */ } .autocomplete-info-tooltip::-webkit-scrollbar-thumb { - background: #555; + /* background: var(--border, #555); */ border-radius: 2px; } + +.autocomplete-info-tooltip::-webkit-scrollbar-thumb:hover { + /* background: var(--selection, var(--border, #777)); */ +} From 612efad3eb47ce376281a7fd3043338538ab842f Mon Sep 17 00:00:00 2001 From: robase <11038379+robase@users.noreply.github.com> Date: Sun, 27 Jul 2025 22:09:58 +0100 Subject: [PATCH 097/257] clean up comments --- website/src/repl/Repl.css | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index 3a5a6eb23..62e7dcf24 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -71,7 +71,6 @@ } /* Override default styles from the codemirror inline css for autocomplete info tooltip*/ -/* Override default styles from the c odemirror inline css for autocomplete info tooltip*/ .cm-tooltip.cm-completionInfo { padding: 12px !important; padding-bottom: 12px !important; @@ -98,7 +97,6 @@ min-width: 400px; } -/* Function name */ .autocomplete-info-function-name { font-size: 15px; font-weight: 600; @@ -106,7 +104,6 @@ margin: 0 0 8px 0; } -/* Function description */ .autocomplete-info-function-description { margin: 0 0 12px 0; color: var(--foreground); @@ -114,7 +111,6 @@ opacity: 0.8; } -/* Section titles */ .autocomplete-info-section-title { font-size: 12px; font-weight: 600; @@ -128,7 +124,6 @@ margin-top: 0; } -/* Parameters */ .autocomplete-info-params-section { margin-top: 12px; } @@ -173,7 +168,6 @@ opacity: 0.7; } -/* Examples */ .autocomplete-info-examples-section { margin-top: 12px; } @@ -192,20 +186,17 @@ border: 1px solid var(--foreground, #3a3a3a); } -/* Scrollbar - using theme colors */ .autocomplete-info-tooltip::-webkit-scrollbar { width: 4px; } .autocomplete-info-tooltip::-webkit-scrollbar-track { - /* background: var(--lineBackground, var(--background)); */ } .autocomplete-info-tooltip::-webkit-scrollbar-thumb { - /* background: var(--border, #555); */ border-radius: 2px; } .autocomplete-info-tooltip::-webkit-scrollbar-thumb:hover { - /* background: var(--selection, var(--border, #777)); */ + } From 2e8a8b50df239d92fd7d14e0858a3e88e341ef94 Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 8 Aug 2025 22:27:05 -0500 Subject: [PATCH 098/257] Seemingly working version of supersaw FM and light cleanup --- eslint.config.mjs | 10 +++++++++ packages/superdough/synth.mjs | 7 ++---- packages/superdough/worklets.mjs | 37 ++++++++++++++++---------------- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 19d9bb390..e30b8e8a6 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/superdough/synth.mjs b/packages/superdough/synth.mjs index 4e876caff..71138ae41 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 e7928a48f..0ab71cbfa 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -8,18 +8,27 @@ 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 + // 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 +124,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 +151,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 +312,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 +444,14 @@ 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 frequency = applySemitoneDetuneToFrequency(params.frequency[0], params.detune[0] / 100); const output = outputs[0]; const voices = params.voices[0]; @@ -470,8 +472,9 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { gainL = gain2; gainR = gain1; } - // eslint-disable-next-line no-undef - const dt = freq / sampleRate; + // We must wrap this here because it is passed into sawblep below which + // has domain [0, 1] + const dt = wrapPhase(freq / sampleRate); for (let i = 0; i < output[0].length; i++) { this.phase[n] = this.phase[n] ?? Math.random(); @@ -480,11 +483,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { 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) { From 218952b8b6f9a0e3ebf6cf2771d58e9f9ce6bf08 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 9 Aug 2025 16:25:11 -0500 Subject: [PATCH 099/257] Use the per-sample frequency and properly wrap polyBlep --- packages/superdough/worklets.mjs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 0ab71cbfa..2406d56dc 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -18,9 +18,10 @@ function wrapPhase(phase, maxPhase = 1) { return phase; } const blockSize = 128; -// Smooth waveshape near discontinuities to remove frequencies above nyquist and 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) { + dt = Math.min(dt, 1 - dt); // Start of cycle if (phase < dt) { phase /= dt; @@ -451,7 +452,6 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { // this.port.postMessage({ type: 'onended' }); return false; } - const frequency = applySemitoneDetuneToFrequency(params.frequency[0], params.detune[0] / 100); const output = outputs[0]; const voices = params.voices[0]; @@ -462,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 @@ -472,11 +469,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { gainL = gain2; gainR = gain1; } - // We must wrap this here because it is passed into sawblep below which - // has domain [0, 1] - const dt = wrapPhase(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); From 3dd667e82523564ef9cd8a70d985e705d10c4d32 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 10 Aug 2025 17:11:59 -0500 Subject: [PATCH 100/257] Fix for a zero appearing (first) in FM's ADSR --- packages/superdough/helpers.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 63643ebbc..cca897913 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -264,7 +264,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 { From 2ab2cc5fc824c1abb6a8358e5e7a15680c2497db Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 15 Aug 2025 13:27:08 -0500 Subject: [PATCH 101/257] Add example tests --- test/__snapshots__/examples.test.mjs.snap | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index eb6c238d5..51dff68a1 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -5285,6 +5285,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 ]", From c8c6a2ce08e0fca7857323156ba2a8adf877cd5c Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 15 Aug 2025 13:34:43 -0500 Subject: [PATCH 102/257] Add example test and update fastChunk test (which was previously broken) --- test/__snapshots__/examples.test.mjs.snap | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index eb6c238d5..1b5e73d34 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3418,6 +3418,8 @@ exports[`runs examples > example "fast" example index 0 1`] = ` exports[`runs examples > example "fastChunk" example index 0 1`] = ` [ + "[ 0/1 → 1/4 | color:red note:0 ]", + "[ 1/4 → 1/2 | color:red note:1 ]", "[ 1/2 → 3/4 | note:E2 ]", "[ 3/4 → 1/1 | note:F2 ]", "[ 1/1 → 5/4 | note:G2 ]", @@ -3426,6 +3428,8 @@ exports[`runs examples > example "fastChunk" example index 0 1`] = ` "[ 7/4 → 2/1 | note:C3 ]", "[ 2/1 → 9/4 | note:D3 ]", "[ 9/4 → 5/2 | note:D2 ]", + "[ 5/2 → 11/4 | color:red note:2 ]", + "[ 11/4 → 3/1 | color:red note:3 ]", "[ 3/1 → 13/4 | note:G2 ]", "[ 13/4 → 7/2 | note:A2 ]", "[ 7/2 → 15/4 | note:B2 ]", @@ -8414,6 +8418,39 @@ exports[`runs examples > example "scale" example index 2 1`] = ` ] `; +exports[`runs examples > example "scale" example index 3 1`] = ` +[ + "[ 0/1 → 1/4 | note:C3 s:piano ]", + "[ 0/1 → 1/4 | note:B3 s:piano ]", + "[ 1/4 → 3/8 | note:Gb2 s:piano ]", + "[ 3/8 → 1/2 | note:F2 s:piano ]", + "[ 1/2 → 3/4 | note:A2 s:piano ]", + "[ 1/2 → 3/4 | note:D4 s:piano ]", + "[ 3/4 → 1/1 | note:G3 s:piano ]", + "[ 1/1 → 5/4 | note:C3 s:piano ]", + "[ 1/1 → 5/4 | note:C4 s:piano ]", + "[ 5/4 → 11/8 | note:Gb2 s:piano ]", + "[ 11/8 → 3/2 | note:E2 s:piano ]", + "[ 3/2 → 7/4 | note:A2 s:piano ]", + "[ 3/2 → 7/4 | note:Eb4 s:piano ]", + "[ 7/4 → 2/1 | note:F#3 s:piano ]", + "[ 2/1 → 9/4 | note:C3 s:piano ]", + "[ 2/1 → 9/4 | note:B3 s:piano ]", + "[ 9/4 → 19/8 | note:Gb2 s:piano ]", + "[ 19/8 → 5/2 | note:F2 s:piano ]", + "[ 5/2 → 11/4 | note:Ab2 s:piano ]", + "[ 5/2 → 11/4 | note:D4 s:piano ]", + "[ 11/4 → 3/1 | note:G3 s:piano ]", + "[ 3/1 → 13/4 | note:C3 s:piano ]", + "[ 3/1 → 13/4 | note:C4 s:piano ]", + "[ 13/4 → 27/8 | note:Gb2 s:piano ]", + "[ 27/8 → 7/2 | note:E2 s:piano ]", + "[ 7/2 → 15/4 | note:Ab2 s:piano ]", + "[ 7/2 → 15/4 | note:Eb4 s:piano ]", + "[ 15/4 → 4/1 | note:F#3 s:piano ]", +] +`; + exports[`runs examples > example "scaleTranspose" example index 0 1`] = ` [ "[ 0/1 → 1/2 | note:C3 ]", From a20468b85f2fc8ccb869f2d5bf13c36929e00531 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 16 Aug 2025 23:19:34 -0400 Subject: [PATCH 103/257] minor changes --- packages/superdough/superdough.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index e1c1b30ea..1dd73fe03 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -401,7 +401,7 @@ function getFilterType(ftype) { //type orbit { // gain: number, // reverbNode: reverbNode -// delayNode: +// delayNode: delayNode //} let orbits = {}; function connectToOrbit(node, orbit) { @@ -706,7 +706,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) logger('[webaudio] skip hap: still loading', ac.currentTime - t); return; } - let chain = []; // audio nodes that will be connected to each other sequentially + const chain = []; // audio nodes that will be connected to each other sequentially chain.push(sourceNode); stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); From 5ff59a878d8c48da0b803ae0d6e493fd48e18d18 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 16 Aug 2025 23:38:03 -0400 Subject: [PATCH 104/257] fix lookahead --- packages/core/controls.mjs | 3 ++- packages/superdough/superdough.mjs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 174c6ecfd..e6181d453 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -547,7 +547,8 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape'); * @name duckorbit * @param {number | Pattern} orbit target orbit * @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)) + * $: 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'); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 1dd73fe03..eb1466d91 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -432,11 +432,11 @@ function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1 () => { 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 + 0.002); + 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, + t - 0.01, ); }); } From 018649c4f3759d5e315cefd1295848a608192c6b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 16 Aug 2025 23:42:14 -0400 Subject: [PATCH 105/257] fix test --- test/__snapshots__/examples.test.mjs.snap | 32 ----------------------- 1 file changed, 32 deletions(-) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 16369872d..bb0489a91 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3153,57 +3153,25 @@ exports[`runs examples > example "duckdepth" example index 0 1`] = ` exports[`runs examples > example "duckorbit" 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: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.2 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.2 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.2 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.2 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.2 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.2 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.2 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.2 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.2 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 ]", ] `; From e9aef9e4d49cb451e7e96fd989101949c72c9121 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 17 Aug 2025 16:49:10 -0400 Subject: [PATCH 106/257] working --- packages/core/controls.mjs | 22 ++++++++++++++++++++++ packages/core/pattern.mjs | 2 +- packages/superdough/reverb.mjs | 20 ++++++++++++++------ packages/superdough/superdough.mjs | 17 +++++++++++------ 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index e6181d453..9f07f8c76 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1516,6 +1516,28 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade'); * */ export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse'); + +/** + * Sets the sample to use as an impulse response for the reverb. + * @name iresponse + * @param {string | Pattern} sample to use as an impulse response + * @synonyms ir + * @example + * s("bd sd [~ bd] sd").room(.8).ir("") + * + */ +export const { irspeed } = registerControl('irspeed'); + +/** + * Sets the sample to use as an impulse response for the reverb. + * @name iresponse + * @param {string | Pattern} sample to use as an impulse response + * @synonyms ir + * @example + * s("bd sd [~ bd] sd").room(.8).ir("") + * + */ +export const { irbegin } = registerControl('irbegin'); /** * Sets the room size of the reverb, see `room`. * When this property is changed, the reverb will be recaculated, so only change this sparsely.. diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index dbf1b17f7..56f2c16e3 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3276,7 +3276,7 @@ export const slice = register( * @memberof Pattern * @returns Pattern * @example - * s("bd!8").onTriggerTime((hap) => {console.info(hap)}) + * s("bd!8").onTriggerTime((hap) => {console.log(hap)}) */ Pattern.prototype.onTriggerTime = function (func) { return this.onTrigger((hap, currentTime, _cps, targetTime) => { diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs index 0f638ca80..4d62aafdc 100644 --- a/packages/superdough/reverb.mjs +++ b/packages/superdough/reverb.mjs @@ -1,7 +1,8 @@ import reverbGen from './reverbGen.mjs'; if (typeof AudioContext !== 'undefined') { - AudioContext.prototype.adjustLength = function (duration, buffer) { + AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetSeconds = 0) { + const offset = offsetSeconds * buffer.sampleRate; const newLength = buffer.sampleRate * duration; const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate); for (let channel = 0; channel < buffer.numberOfChannels; channel++) { @@ -9,22 +10,29 @@ if (typeof AudioContext !== 'undefined') { let newData = newBuffer.getChannelData(channel); for (let i = 0; i < newLength; i++) { - newData[i] = oldData[i] || 0; + let position = (offset + i * Math.abs(speed)) % oldData.length; + if (speed < 1) { + position = position * -1; + } + + newData[i] = oldData.at(position) || 0; } } return newBuffer; }; - AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir) { + AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) { const convolver = this.createConvolver(); - convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir) => { + convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir, irspeed, irbegin) => { convolver.duration = d; convolver.fade = fade; convolver.lp = lp; convolver.dim = dim; convolver.ir = ir; + convolver.irspeed = irspeed; + convolver.irbegin = irbegin; if (ir) { - convolver.buffer = this.adjustLength(d, ir); + convolver.buffer = this.adjustLength(d, ir, irspeed, irbegin); } else { reverbGen.generateReverb( { @@ -41,7 +49,7 @@ if (typeof AudioContext !== 'undefined') { ); } }; - convolver.generate(duration, fade, lp, dim, ir); + convolver.generate(duration, fade, lp, dim, ir, irspeed, irbegin); return convolver; }; } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index eb1466d91..ccdb15abf 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -326,7 +326,7 @@ export const panic = () => { channelMerger == null; }; -function getDelay(orbit, delaytime, delayfeedback, t, channels) { +function getDelay(orbit, delaytime, delayfeedback, t) { if (delayfeedback > maxfeedback) { //logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`); } @@ -442,19 +442,22 @@ function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1 } let hasChanged = (now, before) => now !== undefined && now !== before; -function getReverb(orbit, duration, fade, lp, dim, ir) { +function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) { // If no reverb has been created for a given orbit, create one if (!orbits[orbit].reverbNode) { const ac = getAudioContext(); - const reverb = ac.createReverb(duration, fade, lp, dim, ir); + const reverb = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin); connectToOrbit(reverb, orbit); orbits[orbit].reverbNode = 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) || + hasChanged(irspeed, orbits[orbit].reverbNode.irspeed) || + hasChanged(irbegin, orbits[orbit].reverbNode.irbegin) || orbits[orbit].reverbNode.ir !== ir ) { // only regenerate when something has changed @@ -462,7 +465,7 @@ function getReverb(orbit, duration, fade, lp, dim, ir) { // 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); + orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin); } return orbits[orbit].reverbNode; } @@ -619,6 +622,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) roomdim, roomsize, ir, + irspeed, + irbegin, i = getDefaultValue('i'), velocity = getDefaultValue('velocity'), analyze, // analyser wet @@ -832,7 +837,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // delay let delaySend; if (delay > 0 && delaytime > 0 && delayfeedback > 0) { - const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels); + const delayNode = getDelay(orbit, delaytime, delayfeedback, t); delaySend = effectSend(post, delayNode, delay); audioNodes.push(delaySend); } @@ -850,7 +855,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } roomIR = await loadBuffer(url, ac, ir, 0); } - const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels); + const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); reverbSend = effectSend(post, reverbNode, room); audioNodes.push(reverbSend); } From b7176155589f6533346d0557172bd1ef72e859f3 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 17 Aug 2025 22:13:26 +0100 Subject: [PATCH 107/257] optimise euclidish, creating new `morph` function in the process --- packages/core/euclid.mjs | 22 ++--- packages/core/pattern.mjs | 67 ++++++++++++++++ packages/core/timespan.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 98 ++++++++++++++++------- 4 files changed, 143 insertions(+), 46 deletions(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index bf5a82b00..44ab07f11 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, stack, pure } 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'; @@ -212,22 +212,10 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s * @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,tri.slow(8)) - * .pan(tri.slow(8)) + * 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 b = bjork(pulses, steps); - let trues = 0; - const offs = []; - for (const [pos, step] of b.entries()) { - if (step) { - offs.push([trues++, pos]); - } - } - const tweened = offs.map(([n, pos]) => - Fraction(pos) - .div(steps) - .add(Fraction(n).div(pulses).sub(Fraction(pos).div(steps)).mul(perc)), - ); - return pat.struct(stack(...tweened.map((pos) => pure(true)._fastGap(steps)._late(pos)))).setSteps(steps); + const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc); + return pat.struct(morphed).setSteps(steps); }); diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 4168645b8..ada13f65b 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -21,6 +21,7 @@ import { numeralArgs, parseNumeral, pairs, + zipWith, } from './util.mjs'; import drawLine from './drawLine.mjs'; import { logger } from './logger.mjs'; @@ -3400,3 +3401,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/timespan.mjs b/packages/core/timespan.mjs index 0dbc74fc8..446156bbf 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/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 83dd8c029..079e6ff3b 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3273,34 +3273,34 @@ 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 ]", - "[ 1/6 → 1/4 | s:hh pan:0.041666666666666664 ]", - "[ 1/4 → 1/3 | s:hh pan:0.0625 ]", - "[ 5/12 → 1/2 | s:hh pan:0.10416666666666667 ]", - "[ 7/12 → 2/3 | s:hh pan:0.14583333333333334 ]", - "[ 2/3 → 3/4 | s:hh pan:0.16666666666666666 ]", - "[ 5/6 → 11/12 | s:hh pan:0.20833333333333334 ]", - "[ 1/1 → 13/12 | s:hh pan:0.25 ]", - "[ 65/56 → 209/168 | s:hh pan:0.29017857142857145 ]", - "[ 141/112 → 451/336 | s:hh pan:0.31473214285714285 ]", - "[ 159/112 → 505/336 | s:hh pan:0.3549107142857143 ]", - "[ 177/112 → 559/336 | s:hh pan:0.3950892857142857 ]", - "[ 47/28 → 37/21 | s:hh pan:0.41964285714285715 ]", - "[ 103/56 → 323/168 | s:hh pan:0.45982142857142855 ]", - "[ 2/1 → 25/12 | s:hh pan:0.5 ]", - "[ 181/84 → 47/21 | s:hh pan:0.5386904761904762 ]", - "[ 127/56 → 395/168 | s:hh pan:0.5669642857142857 ]", - "[ 407/168 → 421/168 | s:hh pan:0.6056547619047619 ]", - "[ 433/168 → 149/56 | s:hh pan:0.6443452380952381 ]", - "[ 113/42 → 233/84 | s:hh pan:0.6726190476190477 ]", - "[ 239/84 → 41/14 | s:hh pan:0.7113095238095238 ]", - "[ 3/1 → 37/12 | s:hh pan:0.75 ]", - "[ 529/168 → 181/56 | s:hh pan:0.7872023809523809 ]", - "[ 367/112 → 1129/336 | s:hh pan:0.8191964285714286 ]", - "[ 1151/336 → 393/112 | s:hh pan:0.8563988095238095 ]", - "[ 1201/336 → 1229/336 | s:hh pan:0.8936011904761905 ]", - "[ 311/84 → 53/14 | s:hh pan:0.9255952380952381 ]", - "[ 647/168 → 661/168 | s:hh pan:0.9627976190476191 ]", + "[ 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 ]", ] `; @@ -5997,6 +5997,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 ]", From e7e636886d8ca04dc35e5540cd3239f153a2105a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 17 Aug 2025 17:21:36 -0400 Subject: [PATCH 108/257] add tests --- packages/core/controls.mjs | 19 +++--- packages/core/logger.mjs | 2 +- packages/superdough/reverb.mjs | 8 ++- test/__snapshots__/examples.test.mjs.snap | 74 +++++++++++++++++++++++ 4 files changed, 90 insertions(+), 13 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 9f07f8c76..58a30652c 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1518,23 +1518,24 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade'); export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse'); /** - * Sets the sample to use as an impulse response for the reverb. - * @name iresponse - * @param {string | Pattern} sample to use as an impulse response - * @synonyms ir + * Sets speed of the sample for the impulse response. + * @name irspeed + * @param {string | Pattern} speed * @example - * s("bd sd [~ bd] sd").room(.8).ir("") + * samples('github:switchangel/pad') + * $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.2).irspeed("<2 1 .5>/2").irbegin(.5).roomsize(.5) * */ export const { irspeed } = registerControl('irspeed'); /** - * Sets the sample to use as an impulse response for the reverb. - * @name iresponse - * @param {string | Pattern} sample to use as an impulse response + * Sets the beginning of the IR response sample + * @name irbegin + * @param {string | Pattern} begin between 0 and 1 * @synonyms ir * @example - * s("bd sd [~ bd] sd").room(.8).ir("") + * samples('github:switchangel/pad') + * $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.65).irspeed("-2").irbegin("<0 .5 .75>/2").roomsize(.6) * */ export const { irbegin } = registerControl('irbegin'); diff --git a/packages/core/logger.mjs b/packages/core/logger.mjs index 4f2002319..488bce5e8 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/superdough/reverb.mjs b/packages/superdough/reverb.mjs index 4d62aafdc..2960b597c 100644 --- a/packages/superdough/reverb.mjs +++ b/packages/superdough/reverb.mjs @@ -1,8 +1,9 @@ import reverbGen from './reverbGen.mjs'; +import { clamp } from './util.mjs'; if (typeof AudioContext !== 'undefined') { - AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetSeconds = 0) { - const offset = offsetSeconds * buffer.sampleRate; + AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) { + const sampleOffset = Math.floor(clamp(offsetAmount, 0, 1) * buffer.length); const newLength = buffer.sampleRate * duration; const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate); for (let channel = 0; channel < buffer.numberOfChannels; channel++) { @@ -10,7 +11,8 @@ if (typeof AudioContext !== 'undefined') { let newData = newBuffer.getChannelData(channel); for (let i = 0; i < newLength; i++) { - let position = (offset + i * Math.abs(speed)) % oldData.length; + // loop the buffer around to prevent + let position = (sampleOffset + i * Math.abs(speed)) % oldData.length; if (speed < 1) { position = position * -1; } diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index bb0489a91..3409ce1b9 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -4842,6 +4842,43 @@ exports[`runs examples > example "irand" example index 0 1`] = ` ] `; +exports[`runs examples > example "irbegin" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", +] +`; + exports[`runs examples > example "iresponse" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd room:0.8 ir:shaker_large i:0 ]", @@ -4863,6 +4900,43 @@ exports[`runs examples > example "iresponse" example index 0 1`] = ` ] `; +exports[`runs examples > example "irspeed" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", +] +`; + exports[`runs examples > example "isaw" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:c3 clip:1 ]", From f17a4d045fce0c10ae8e1d40f73d4e6da950fbfb Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 18 Aug 2025 11:46:29 -0400 Subject: [PATCH 109/257] Publish - @strudel/codemirror@1.2.3 - @strudel/core@1.2.3 - @strudel/csound@1.2.4 - @strudel/draw@1.2.3 - @strudel/embed@1.1.1 - @strudel/gamepad@1.2.3 - @strudel/hydra@1.2.3 - @strudel/midi@1.2.4 - @strudel/mini@1.2.3 - mondolang@1.1.1 - @strudel/mondo@1.1.1 - @strudel/motion@1.2.3 - @strudel/mqtt@1.2.3 - @strudel/osc@1.2.3 - @strudel/reference@1.2.1 - @strudel/repl@1.2.4 - @strudel/serial@1.2.3 - @strudel/soundfonts@1.2.4 - superdough@1.2.4 - @strudel/tonal@1.2.3 - @strudel/transpiler@1.2.3 - @strudel/web@1.2.4 - @strudel/webaudio@1.2.4 - @strudel/xen@1.2.3 --- packages/codemirror/package.json | 2 +- packages/core/package.json | 2 +- packages/csound/package.json | 2 +- packages/draw/package.json | 2 +- packages/embed/package.json | 2 +- packages/gamepad/package.json | 2 +- packages/hydra/package.json | 2 +- packages/midi/package.json | 2 +- packages/mini/package.json | 2 +- packages/mondo/package.json | 2 +- packages/mondough/package.json | 2 +- packages/motion/package.json | 2 +- packages/mqtt/package.json | 2 +- packages/osc/package.json | 2 +- packages/reference/package.json | 2 +- packages/repl/package.json | 2 +- packages/serial/package.json | 2 +- packages/soundfonts/package.json | 2 +- packages/superdough/package.json | 2 +- packages/tonal/package.json | 2 +- packages/transpiler/package.json | 2 +- packages/web/package.json | 2 +- packages/webaudio/package.json | 2 +- packages/xen/package.json | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 4f8508c90..797a4312b 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "1.2.2", + "version": "1.2.3", "description": "Codemirror Extensions for Strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/core/package.json b/packages/core/package.json index f4170f2b5..33540c8bf 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/core", - "version": "1.2.2", + "version": "1.2.3", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", "type": "module", diff --git a/packages/csound/package.json b/packages/csound/package.json index 04a5ff246..19b1dd6fd 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/csound", - "version": "1.2.3", + "version": "1.2.4", "description": "csound bindings for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/draw/package.json b/packages/draw/package.json index ee1b8dd00..f63367402 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/draw", - "version": "1.2.2", + "version": "1.2.3", "description": "Helpers for drawing with Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/embed/package.json b/packages/embed/package.json index a0cc33de1..6a00ee904 100644 --- a/packages/embed/package.json +++ b/packages/embed/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/embed", - "version": "1.1.0", + "version": "1.1.1", "description": "Embeddable Web Component to load a Strudel REPL into an iframe", "main": "embed.js", "type": "module", diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json index 3efb2e084..53fad18a6 100644 --- a/packages/gamepad/package.json +++ b/packages/gamepad/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/gamepad", - "version": "1.2.2", + "version": "1.2.3", "description": "Gamepad Inputs for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/hydra/package.json b/packages/hydra/package.json index b022de87d..1553264d4 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/hydra", - "version": "1.2.2", + "version": "1.2.3", "description": "Hydra integration for strudel", "main": "hydra.mjs", "type": "module", diff --git a/packages/midi/package.json b/packages/midi/package.json index 4efd329d8..8dde35984 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/midi", - "version": "1.2.3", + "version": "1.2.4", "description": "Midi API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mini/package.json b/packages/mini/package.json index 5d94301d4..e8fca4ac6 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mini", - "version": "1.2.2", + "version": "1.2.3", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mondo/package.json b/packages/mondo/package.json index 277bddb1c..a59bfc344 100644 --- a/packages/mondo/package.json +++ b/packages/mondo/package.json @@ -1,6 +1,6 @@ { "name": "mondolang", - "version": "1.1.0", + "version": "1.1.1", "description": "a language for functional composition that translates to js", "main": "mondo.mjs", "type": "module", diff --git a/packages/mondough/package.json b/packages/mondough/package.json index a034424c0..99a87f098 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mondo", - "version": "1.1.0", + "version": "1.1.1", "description": "mondo notation for strudel", "main": "mondough.mjs", "type": "module", diff --git a/packages/motion/package.json b/packages/motion/package.json index a7db05680..acacc154f 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/motion", - "version": "1.2.2", + "version": "1.2.3", "description": "DeviceMotion API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index f522e3354..d5ac6dd71 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mqtt", - "version": "1.2.2", + "version": "1.2.3", "description": "MQTT API for strudel", "main": "mqtt.mjs", "type": "module", diff --git a/packages/osc/package.json b/packages/osc/package.json index 7d19fbbfc..6e8b57813 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/osc", - "version": "1.2.2", + "version": "1.2.3", "description": "OSC messaging for strudel", "main": "osc.mjs", "type": "module", diff --git a/packages/reference/package.json b/packages/reference/package.json index 8dc966cc2..634057e4c 100644 --- a/packages/reference/package.json +++ b/packages/reference/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/reference", - "version": "1.2.0", + "version": "1.2.1", "description": "Headless reference of all strudel functions", "main": "index.mjs", "type": "module", diff --git a/packages/repl/package.json b/packages/repl/package.json index bfa404c75..aabd1b8c7 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "1.2.3", + "version": "1.2.4", "description": "Strudel REPL as a Web Component", "module": "index.mjs", "publishConfig": { diff --git a/packages/serial/package.json b/packages/serial/package.json index c04a69cd0..8abbe7692 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/serial", - "version": "1.2.2", + "version": "1.2.3", "description": "Webserial API for strudel", "main": "serial.mjs", "type": "module", diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index 2c87a6e05..e5c1780ed 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/soundfonts", - "version": "1.2.3", + "version": "1.2.4", "description": "Soundsfont support for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/superdough/package.json b/packages/superdough/package.json index 439b83718..809da0227 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -1,6 +1,6 @@ { "name": "superdough", - "version": "1.2.3", + "version": "1.2.4", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "main": "index.mjs", "type": "module", diff --git a/packages/tonal/package.json b/packages/tonal/package.json index 614e86f74..de02e4b1f 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/tonal", - "version": "1.2.2", + "version": "1.2.3", "description": "Tonal functions for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 2a5e39776..f9ebdfde1 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/transpiler", - "version": "1.2.2", + "version": "1.2.3", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "main": "index.mjs", "type": "module", diff --git a/packages/web/package.json b/packages/web/package.json index 0feddc82d..3f9cc50d1 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/web", - "version": "1.2.3", + "version": "1.2.4", "description": "Easy to setup, opiniated bundle of Strudel for the browser.", "module": "web.mjs", "publishConfig": { diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 5cc0a5538..78988340a 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/webaudio", - "version": "1.2.3", + "version": "1.2.4", "description": "Web Audio helpers for Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/xen/package.json b/packages/xen/package.json index 88c2bb082..1cb751157 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/xen", - "version": "1.2.2", + "version": "1.2.3", "description": "Xenharmonic API for strudel", "main": "index.mjs", "type": "module", From cf55d4c8d19c47d4ae31c7d47a10e10ea001effb Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 18 Aug 2025 11:49:11 -0400 Subject: [PATCH 110/257] rm var --- packages/superdough/superdough.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index ccdb15abf..a518f49b6 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -282,7 +282,6 @@ export async function initAudioOnFirstClick(options) { return audioReady; } -let delays = {}; const maxfeedback = 0.98; let channelMerger, destinationGain; From 7d24e4569279d567b10789102391a49bf858dffc Mon Sep 17 00:00:00 2001 From: fyynn Date: Mon, 18 Aug 2025 23:36:39 +0200 Subject: [PATCH 111/257] Added scrub() to Learn Docs Signed-off-by: fyynn --- website/src/pages/learn/samples.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index bbe650cf5..dbcb8acb2 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -361,6 +361,10 @@ Sampler effects are functions that can be used to change the behaviour of sample +### scrub + + + ### speed From f7f1bd63e868126090adff8861ed8ea9c1e189f0 Mon Sep 17 00:00:00 2001 From: fyynn Date: Mon, 18 Aug 2025 23:49:43 +0200 Subject: [PATCH 112/257] Adding the Duck/Sidechain effect to Learn Docs (learn/effects.mdx) Added the duck Effect to the section global effects. Signed-off-by: fyynn --- website/src/pages/learn/effects.mdx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index 8f6a3b8aa..89f36fe96 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -339,4 +339,18 @@ global effects use the same chain for all events of the same orbit: +## Duck + +### duckorbit + + + +### duckattack + + + +### duckdepth + + + Next, we'll look at input / output via [MIDI, OSC and other methods](/learn/input-output). From d009b9592ec7b8975294182399d9a0891855e863 Mon Sep 17 00:00:00 2001 From: fyynn Date: Tue, 19 Aug 2025 00:09:41 +0200 Subject: [PATCH 113/257] Added Refrence to arp() arpWidth() and hush to Docs Signed-off-by: fyynn --- website/src/pages/learn/conditional-modifiers.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/pages/learn/conditional-modifiers.mdx b/website/src/pages/learn/conditional-modifiers.mdx index c2e22595b..b408883a9 100644 --- a/website/src/pages/learn/conditional-modifiers.mdx +++ b/website/src/pages/learn/conditional-modifiers.mdx @@ -34,11 +34,11 @@ import { JsDoc } from '../../docs/JsDoc'; ## arp - + ## arpWith 🧪 - + ## struct @@ -58,7 +58,7 @@ import { JsDoc } from '../../docs/JsDoc'; ## hush - + ## invert From 189daa3942aa8518f6da85c3f263015bb1859a88 Mon Sep 17 00:00:00 2001 From: fyynn Date: Tue, 19 Aug 2025 00:21:32 +0200 Subject: [PATCH 114/257] Added Refrence to arp() arpWidth() to Docs Signed-off-by: fyynn --- website/src/pages/learn/conditional-modifiers.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/pages/learn/conditional-modifiers.mdx b/website/src/pages/learn/conditional-modifiers.mdx index b408883a9..ea4b92f06 100644 --- a/website/src/pages/learn/conditional-modifiers.mdx +++ b/website/src/pages/learn/conditional-modifiers.mdx @@ -34,11 +34,11 @@ import { JsDoc } from '../../docs/JsDoc'; ## arp - + ## arpWith 🧪 - + ## struct From 36441e7b73a2b1d8dad141bde27da5807714961e Mon Sep 17 00:00:00 2001 From: fyynn Date: Tue, 19 Aug 2025 00:25:31 +0200 Subject: [PATCH 115/257] Fixing hush refrence to code documentation Signed-off-by: fyynn --- website/src/pages/learn/conditional-modifiers.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/conditional-modifiers.mdx b/website/src/pages/learn/conditional-modifiers.mdx index ea4b92f06..d31ab0cf0 100644 --- a/website/src/pages/learn/conditional-modifiers.mdx +++ b/website/src/pages/learn/conditional-modifiers.mdx @@ -58,7 +58,7 @@ import { JsDoc } from '../../docs/JsDoc'; ## hush - + ## invert From d6ee10e05c99b08e5e8e2c0645c2183b0301b307 Mon Sep 17 00:00:00 2001 From: fyynn Date: Tue, 19 Aug 2025 00:44:46 +0200 Subject: [PATCH 116/257] fixed new scrub refrence --- website/src/pages/learn/samples.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index dbcb8acb2..475319748 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -363,7 +363,7 @@ Sampler effects are functions that can be used to change the behaviour of sample ### scrub - + ### speed From 32fe73aba2949a4725f458844a558e275afd9591 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 19 Aug 2025 11:10:34 -0500 Subject: [PATCH 117/257] Update effects documentation to include information on signal flow --- website/public/img/strudel-signal-flow.png | Bin 0 -> 37017 bytes website/src/pages/learn/effects.mdx | 118 +++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 website/public/img/strudel-signal-flow.png diff --git a/website/public/img/strudel-signal-flow.png b/website/public/img/strudel-signal-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..c5099c917c0e569dd8b5f78bb4eb36e0f7c77baf GIT binary patch literal 37017 zcmeFa1z45o{w@s5rGmK#K?S50q@+u_8$n3{X%H5R?o^PL5J4IdP^6@#Q30j9OHfKe zknZ!muyC9`bM~J9KG*e~+53DtW4!P4KKaZ0cRw@!it-X?PhCERfq`*WN>Wq_0|Q2Z zfr0rA>jZehJwG9HOmbXl#MMlt0*-Ki>{?5qk*Ze zv9&JJhS9>%7CZv)b&U-z&<*5J&Zg$(y3{PZ7FI4UZWcZ6Lzw`7WoChX0)c=I6sdbO z47w{D6I}z8!{NvWkN$W7N5^1|LV=n5a@vP4e*G2F4iQ6hNR*FG06h+nxc~XLKljo4 zrzZWMbZ4#ZWF+ixM~X#6?!LSz7ps#xD;onl*p>qw&^5P1pB6O>hdEfOun`LAFAIk; z^k)TYJxKEoUx3B@`tGm;8(XI%T{$=!L+Egr!KV%;rnZL4mb&^-_YOdk!7~$Eq&fOE zuq);wD09@IQn4E78gd!wgC;h%)+jT>U*2%&a~bLxfj7F~ydh=P2fG64qOiHHo}sxS z%Er|8P>X-Q0E%sE3ItB}9~$VPY;93U@a0hhA#|6vC}?)j;(i%e{NM+8j@~KIsUcbi z|Kx-Y9{qDMIg%)#*oMGF*;+dRfjR1tg_V&LZ6J>Hn)wDJC$M(VF**FEC~P-47|{() zeo+%nw20`UwbS_L5YYF3MGiq5v>5;MW&DY>|G9{<{fUS{llett3=M#3KuZwH+SUYR zjIz)*7yIq0@NYr|?5yx_ZDdiC7_wKcW>{iOazhldt1``_$? zoSm(yh4G<*Md~_&gS14MJ6W2bEPx#U$+;eA-|x@j4=VhRiik8dFo4X7u(hF$sk5%$ zfoXvb-Vy}@hXc`O7p7(x1y2sNU-+j>I8s~-lm+zt|5%sN-VQw^SP)vDEOmdg!hb3O z><6Oq&sFA6B>=7YAVA?_Wj)+MwB{ei1S~iHvW)zd(SMtRBD9y__>N4^@5e@4oFh?0 z85!9eg?9hiiX07cU`u{lxqm)ARuIMg+m`2#Hbzm`7PtqCKZxX?`J+Q;a}Ln8FAqsRXP-TBiEI1&x7>o(^zpwDX?6WX)p(EO(RN(&` zyZ@hT+K*K67fDpMwKlZXH~E8&`8yeR&Lb2DebUTF2+n_?c}E8bzns7yw*EgW4PZrU z;lEA;4sx=8L+AhQ=KYU}wYaG{NUUxCaI$}ArD9=YVPyFuhWa0pUbCSwsQ)6pLPHq; zTCe^~aaC6I2K;iQVn#-w%=RCqSKR;6dW8zD_VL;Li_LYrL1U~{w3lC#3@@~cm5Q#{X3`htpC|5{U3Ep6ykw_jO15d z)Zga-n2$gRG)&3H`X8P79RyatBP=KZny$6p-}nUBZlE!P|Be&*BLE@_sBnL06=2~+ zOU-}90UYtHe{lsufOF~O3^=uaUo*u1H#b8EB)0zzRUA!l`-RQ_Yk+a^_UQh=;S2N! z(AA4WYU$3hrZffy0z*nv=(Z!~T>Qzn+ufV%iRuKABgfN~#8R*yJb(pcBDzfU{Er7E zUzEd=rbp5H>&eQ}d$8+?DN^eB-+CdQn>Kya+&FRZSP*6%hJF%TonXMyNf^Qt15SqF1; zj)Nm8xcx29V`2$nfaikmvB*SmAH_T(!@$IXhhPwhd^Q0sBq&9Bz!wLNtSCJZ{zMwr zA7Q{BNMHn#V|cyC74*Z8ytqkz*Z@Jp48sb;6_FA{U|qqa2kpod?}8`exG6V}IuX0- ziJ)cSIduXW;$UDYJVCz*wp-sG3Swdh5pCz;l-DPDGF~MDA7g-zVS=ab6U;pp@UqgE z@U6vqCqq0gaNPtG_Kw06fgM?u%Ylu3hKQr-5zftbWZ1svwH|m*VUvj_Gk%4^QYAho zxGXRWTeH$svr1hNO@S8C z@(@Fq>iiASgSmnh2oKO=gEj3q)Z!hbXDxz;4AWZ@Qz-20(e8#{150#D>%zip#ho6h zDDx41Zer5)Dr|SN)uSk$*J0AEGtaPbE|_z~Z_;Udz8JYQUfaHnarYhtSh6VSp5noa zi|`%k&yRT*-q_9d^bZu8A)B5tuk}{#b!)rK7rHjbvl>4hcHf?Fzq#dAq@Hgi_Pyj_ z`h;LA+6I2Uf`Zt$)0Jw!b*1Zr4hj?aTn5AlwG8w>KDJ|{Vq~n`(90l_=lx#3Y~GO~ zRb)_)OT=UQ%Fw{zSqIxg3SH~vX3oL4R$t6VE8XN%*ia)w^8A}$U%leC{OlQT-9Wqc z{xp+XECm^&2#9! zhi`9u3CTi@R%T~6GF1)T?|yCgy=uQQ-ts-R{fD!hT zh0@u%lqd3eW#20Y83u1IRv)wPl;e#4wz0F^%D~GzXw>v?qm45a68YtWrrl>cQh9KI?j!;&+33oBcHAo& zw=KLOSu9Xicd!V#)}^eV9(Lzh(J^v zXLVe>$4QyzvTo5C%N)?kbUQ<(nYwCwp=_aVmVAS5|B}^OtJy6y9!UX41<@N#&C8+N&8Er3K2!$+0&l znE1am>&;&K(a2OLm!V;FYqOPqyJ_OffkhzKtD^yOc}g%zCW_I1p=3(q;fb@EuA4tB zNc~5$+ta!plkk>aUReudEGW4{q`o>x0Y1)@i|6TP_)ep>dWM)wvHvcX&$&b#t1xC? zMVkGUXPI|}F<@7yz$}wGr&-|RV)uT0txp@psn6O|q%qjv9^E(J{4rs%CW0!O_H9aV z{UMd1Q&pLzQP^PA5;qet;<3M*b}8aCQ+eA!F$(2e>o8L8RHRpXs`yr+d4FD-I$j0$ zi{PB=w{!Kx*00KSrpejOKIphJGHeOf;VS&uk`+PWyZrea48e5@6M3~eJTTAHD8+g8 zTdD1P3VQ|rk!SvM>!H(avz2FwIBcD`A=~PQ5cCt3w1mtd*p(VDj60JG806ssq)JD2 z(~lD9E%BHVJ7k6ko?uT%GDp$)k zGB`{SgB%y*n!a8;c1@Xqa}`~4h|tiENtAgw5{@O9^c8pp>BrRULP?77j_tEpfe4Qi zUW;XJd0}8V_=#Ym8)w{ZA4`MQ3U&!s1}si(q~OHS)X#Vco({Ws4m!94Bdd4{IF?k5 z_^ardV^W1?)MJI+%|&1zeL?{_BcW)H1J6U|2!Hcd5PWm|z3A0LZ-oIL7XouW|GEqM z1Zr>*CI%b=EiT8YBU`SB3qGM13gZELnnD#Cj>~d9@FVFrZw$D!H<+{N4JB;&DTU8< zv(EjbzAL$xyRTnS*(iEvT(3-{Nz^XDfQ?K8@M+|)VCcB107T(y1Q^~UMO$SFn(-1UfDowrLK-WeQ8>yL4-7<{YulrFU& z1ND?qT78Ln5=1@mC0Ma zW_&FNLkF{wl%nJpLU#3$lHT8Dx+=rWSc|tJzW4ET%hK0S={Y>TQAU*gZOkaag1D=( zG{IHzOhQMxKdtustMlJ9YfnwYoh>QM*un>X^;a+$9PnN;h(KA|Papj_AR zltEuzy??Ev>g8bN?#WE!;z-79%_t*|iN@Ns_Koi!kazOFaYsI+|43R$r@K74Us<#p zeR3|P=qz%`dy`{o`;I)E)}`tV8EvdaT>=qb?N#^OHu;&pnnJ$iSVIoW&w}fgsLt03 zaxq11dHMNws4n03NDVOMxZi}YS!Derosl}>$w(ass^9L-{x-|}(29M)<43`<4x+{n zq~lDifoPUQ;{Rfa5_Q)xtt2VP69@C(#fv( z4KJVCt1mwP-tq{&W|@0z`juvlxUTFl)5wXyXqmaP<7VcY zuba68zdG*^@30pcY>I2l7fK#J?1u zb4pK^7I%Z6{+tFIcil>}%jy8bSpUNzUS;Z=%V91JW4SILD8!g?>3Mqj(gF-?ndg2Bejt4c_r^`lCMp4MB`RI4#$p!gFRYvjiJGgAg9TB+-r-Y-35$^f_b0f~SRAQZ zWHE?z#$r-)-S}Xmy&>eGm*H$7VluIqCzf(jCTibAA+SntiU7)nV_x86JVeK-=}O zls%oTz!r!r`b@w3R=tRO4q8j*hHeXYq?3x z`vc^P_{v4oZsJ&+%$Y}sHRfwgPy&A_b}dwCB{>54oB)x}OH*}&9}`<`?%Z6?ky=|+ z{xVcp^Xb?J0sG#8s>PaDt@HI$*ZRItejF50a33FEMegSs3$P-ux{Y-+%>_FcM;o1^ zn233{pB24yWQ<6zojvESHtuCOa59m&FqZ(&p4}Wk@vr32KsUDOjYC{ zSBN_2!(Iy5b`rbOk-1%^;BFA{gg^#pI;aqg8vmt zuh?-WWNn%?(Px{tF~M0kxngZJdhB*Swld+sVpw9C83{0@{UeunHKUx0W^tjwXcPAn z6ApPeDM<%eC>(CTFkpO&yDYha>=gGGn-NrhX(LY&(xg|X`vc@Wz7Z?eBD-!^UhbwF zxP~`5n36slda5v8WkPScK?ND!+|1Ezf9A4fDW1}GB2D?;fJXJi zahuZ(FYSv+W3cNP6sQ(V=EZ+lfq?832*i{ejUqffUt*c;*LE_=7ogtDuxX@SVmEs2 zLL@HkdL_ZDEiW`1?-G@JPp$bQ29rUJZ$9gapdKLqWqzS}1-NkJUC@hMu2 z*@p!|gAJS6J@NK_T`&m}m#P#rUD^=B!Ie_I36d30L3$9O(x1%+{Zimbes`GAHKfcZ zk!Dj-+_f;r;X-bhFOt^Cg`hB zO}X8@oT>z3KPPPbBx49Bpg0W-%?;QLH~4UbFq6OJ5JrEsqlGQn$5tV`ty&<4y0Pl{S? z&%h9pS7FD$pB7H}ocRgcs~X`r74MKK7t5hiY@r3F6La?Bjp~EkOr!9%{teW~&DrjZ zs4{_x2xaqz2wFyMr@gJ-s#0Jx9LN2qWgC)&9^NIn{?gg^%q6{(=ZF0FXXLAkYlq1;{ zYZ_%=9`njl7y`tEQEvVzgJutv9{=u>}!|m^Ou=PM|!HCtLdnZ%-8V&Ap8;@-!x< zldm(f^Cp{Pd6#mnzK9bExR*=aE8p9i?Wu5a=wx~9vyU$-IOOuZ!ouEgy6t&SstloK zfk_y{-M4Mn@liVN{fs4Faf$d{9p7^7frI-^evVTmP2g5AKuP|Sw|ylxFDrgD&~YFK z*vj_zwvqjLhW6wB*E9gmk<4C%8Xf7$(j=@1xjQ#l%w#>&5=_KtZ%k4^ekY#}@0xtU zmk@!G`@Pv&Cdyh14@7YBlFSe^+(5p=^`+zxi^nmWl_aT5xW!M*G)F_Yjr(ebPDy_t z=(3FZA~r#)v9a-B9Jh_EiAOH8ENn7y*#?tYfd#;V zQq&teK~E7hH@u9RW7Ok$?1o}l02pF3QMte8QcPv@qyCy^vBmJ+p-*9y@s57wQk3M$ zm(Dj^(yQmyfG-ROR=%-J*|hz5Y^0jtGL#P&*xbse-n%&`foTz#8SyX#-&r364b3Bk~2<2bC`2KWcWaMN_$6i=-EazZXy0V;$WOm8qWx||GIP>J~ zKLSP~+vohWXYM4!A88-!DSTk0bUP%QTA0JU?=wfvH@VBrHE&c!4X_Z40s!LKcMI}C z(BNV=TNaqKzu!%elx_$kz0jSZS`09%QXQROq@WWKo5{WBiepA>1?qQDQ{KWpbcBN( zy$Dawbu8k|nQ=a%X=ZqUnCJV$9Q>y`9{@ElFQ7WuLx}2ohh|0a2f!?-LYStp4{=*E zOSmhOC~WH188nLF=m%0>k(T6)J?f6;l?G?5l6=I0WgL55%`-R2{-^z z1j!bF(Xx_|J$A5W@a0$FF~bQjjgup8!qm<{J&l1D*b?#&k6MTVhe<)jb@C@Xoesg% zrKhJ4(HlXsx8Q?!OMH=NG$R}UHB>Wqg?%CO4Q$Xm^3OWtxWuG-w1+!aoe$b4*pZO< z9(F)=-YaGYaczY;%)@FC}+%6&6g1NTCKr-w(Z?QBM{FE>9K@?QZ^m?XYegebcxFPPgHR)OiAR*)8F zY0aLHz>niXhMgtdEUDP3iz?FXJXe;a4k$X&nmDah`p)=f#}nEV4zGrp}JDCW)^rM|^!2ibVw(U+nGa_g?azjbFBU1*3Z z^w*)!yZ*E@)4A}JuX+(mCvAS9qE#hpSTCoX`vZ2Q_DfRKbHz^$Q3{34n)U;GO~)8_ zheM18-v^y-|Dk>|PfPu&PKA9&*5I4wjt-?4GbLJVDC?kD!xH3T|5~Gse1MS@UDSO5 zy{D%+HiyTxXQi@RsCAw=M{IJ3E~;2Z%Q8=EEwE)RKYu{9acAt&h`nr9gT3_2tM(u6 z@Xa|KYqiX0dzi!h;kpXf+la0l2coi7hnegV84s(xd^!!G$AOPlUze#T3hgQsLqHDl z^ThDnvw=IZ9&DX2oG*ANI~u)mdNL5eQM+38N~YlKs-_bPyZ91qM!xxLjns0w55N@p zod?q4`c{b!=y%d~dNXqSaRtYG26g1qr^uHMrU}MwG3@=N$dK+=D(%?e-zRf&<>!e8 zNwWnSw{%w)6YoPe|``=#Z@poU0f*?jZ9 z8`7|Gw?H68!m#PIM&v_bysnE_G(V7&D0!U>;HPZ=dF<3tc{V`g=Th1(K}dyMRK}J zt3%VY*$auQJna+RvaKT((Q6HwtjNwtE&H(&lK5A*_JB0T8hNW^_nXJ_UNN5KG%udJ zF{}ImCgoG+gY2)`_e2g>N)!W(giIdjffY=quwIs|WE)^vnNlw_ii}(NivojgEtt1wkKz0%P`BrOU7uh@O z_eP2+zxyIr7tMpNtbGrPM_y`lFOV8DB=3GK0B*gyfqe0j*Z=%_f@_Axwivh;Zgw&sIv2Kv&{pDuT0OiH^lvr^t9Di}_4kJe2$Jm-$ThggtU7RkTb z{K2&{zIb2)D#X-;RKN3ZTgpg`0r9pl?sQw4*Nj)RMo~~9|DM`l@psRsr=xF{!)(th9lF?D}7RrfDx@oPhk%5x>{Pv)1x zz982Zc6)M>lu?m`&&3_n#Fk>l+SE7gR3v5ShzN+)2~G)ozCb{)Z7dUsyseb|T-Ybi zx0VzDq+%3o`FN*BO{64P@m?$F&NKvbaNab-m2X)8?sZI^k&mIgiON03|CXo`m-gzb zJ*U&X1CQffE+BCoOGEEO% zigdr6BU^9%}O1&ak%g$7l?n$_wj$8gvk#7`E!Txwq zr-t~lxOYvZTXg%|zQ9$Y;@B%A0cPzh&lY9t-?`zJs;cF=zL+5&&DWISbg`Cx`2~Oe z0bIepsQ+YQlO5~nkDM9$-(s(98NMGEjb5ql&8RVKNKxyRKN)3#pQyTFv>>fKvNUEG z9rCf&CZEFY-27(fy>PdZr^w#Y%2e;cuDpnva3cve%6z_?gUc3 z`iV-LoT=EOFI#9>ut@t-HD>q(ppd1h*lvN}0rodIoi3LW&%~HZ;_UzVxnHS*IaYkIvSo@H8zQjRcm0lRJ{{wnUY?kXP{AhKR4iNnnlj2ZHE9CT|6?%2Wk9ZKW@0VemB-X8 zCO^OR<~yDO)9GxRD^ypy#OD{So3_7H+STK|Z)A(P1Av)S4^{^su^OKN>u9~Lr0srP zO=^=gk3!J{rEY<{(uj`B27Mz(NWivE>{l;_AlDq|&YOviPLn$Sz$1LQ(zp0^8Bf-Q z{;u9elBx@-+ZyY3Z`I3wfPfV4yWEeH<146H95>T>Cd{R|lC!4r$^!f8On&45Fm%bL zVSx(QT8dwOXuPSv$NFK9TdzFbE!Fhl6yA%geO20tMlb4Q3klz^((Qp7?wgPA-P$Hq z7<{MijN>rLJ&x1@NT(Fl^aZ^M)i$NDwdW=r6F#a{Z;bcY>+8xlQ|lu?SqB&y_aOVy zKHVkOKf(g$e2+if6sXAFNo1NIds7h@wKv^Sdka~$Hkvc%e=BhiU>;?T69CuQ|Jcy2 znY>1GE7KrxE6W82A_g5G2^&9B?Xe;*WBS9J>G9K!vA>t$3-h&Wv37P5%{e_4)Vo`{ z$nwZNQMC6`;G|l9d@sG8hX~rMf&n7c3UL0!9}khkMZBhRTla?UKKbl7|2f8P(EP4r z-BpgqAc3yt1xh)xbb${_iOG{ljbZ^N$Ve+HM8*hzRp1RGJ?V=y-9KwJmS71F@+q$U zEP&NSgSe{2GyMw8{aR9U`j@)OIS1C<^T65R0cRIit;7I-Ae^KgxE7gfQZ($YuhU2? zX`86&iktu348$Q1zyP>s;wH&HOF)sPtx+%$FAu%AxMtmZZeQcDm%zbGPhp$$9Xg=nEgJ6_G`JI);fw{8Vu3xq4V%P=Jgv2NeM}h^+QfJsWc3Th!fpYtw7~{Ag^^zYs>tW7%Y^pWjgL4y>nNuSQ8kpTt-6WHELM@ax#Ldz$ns%GW~JuQt%XB~ye z_yAx?!3g~XgVu6Tdh-XshSV8j4X?wG1jz`&w%koZ02u0kN&;aHAXeuYGQDsVtKR^_ zT!eW*f%v9o+|#FqGLiJm@yo61*6AR_zu^;}5f%d`zy$IT$9FO-I52FA(CBDW&QaHV zkoL5KLi}V)uZ~B>d|&Q|)q4X4rW}S1_zqgnM1&f4Czw2#3-3I0u0M^5q8ip)(x{UaY#$6Y|GJqPOJ1;wN8gW^FKbzew? z6rH@{Sgq|icDxK!+~)_(a)Mh7$~VRXd!CD&G#e=|*)CLTe)E<7CD(3>+iw z_xzYaA-xd56eCUG!yyV%puYQl;rm2B*xqIsN1)T5%z!U&X}*BG1@It7H403075Fz(+yGH! zZKYFzE(S=&#xucy5ab6ofRly7x7NRZH{yrr91g>FQFhYE<{w#tWjLQ_C`iDeE` zQ|ty$bI@P=7v6KCZKV?-Hmq-vjWI{fN^Pi1>C7Zd)@i zRVrPbI2*$>6{xtBl+L*a(W||wreYC(9_j-a6$beLMW765P81l`D7GL8t{Qtplxf_W zu)t4YX{%g#|IT2AOEDv;NP31~6Ys#6a=o5`v=u*25WrS*-Ksiv#{od~B~sU-Y@Q<) zuy6t!zbob&$!&ij-8bQNSg_RvQo-TAvq2dUXWJoVp?)oVu}2ft-xSR{jJ$vUHLnfG z@xo&S{q4_@Dw&knby zUThON-=F*O$}C6FebW^C`2i65URh5IIt6GY9~cHrNF{0T^FHMT`Ror_H!}3R(G^iu zK~fP#^?gDd%PJ=!J@PZ6a>#6h1gue?#q$--SIG%)_`^<0#Y{Ie7c3rC?eSbp38 z5C$QixwK*0qI z1Qr&s|ANFMH(7JJuE3R)gnM2Ioz1vQ%W$Pz83X|cK`;Xh9+D^Iiuf=A-i+X3jt%oN zP!QzUh+&;O&$&_rks{nZTDY=OhaI(YcJe}jmcjwarRLf(3|2Bw<#=g(Uyo#f?M>^; zoqOlc?Ds+Cr?t6ZR8-t3?Wj>zbXmvV`cm=cBsmT^dJZs~H@hLL2pWV}Cl6=-Jn!1T zin@%}o}8wAKhgK5Ja)5U@hDTqZ9`)yedkyko|+Fk`Q3U`gCj%V_>r-g?F;pYJwiJx zC$@ZR@#4sUs{pFNy!QDPIj&`nExA#RVo_eG+35Vrv2m*O+J1%kPk_m62y>GT7z=0H0?@d{9O>vTOO+4(oKd zz>s4WFoPLa2S^xE z#O)0Edou9bW9fT)=gqR@=NoP@E3H4fzLu|jO}sI_EiW5n>4VyCE@^VxKg{SfIoMB(CdNN)M1q$Nz#`F|Sn|krx%h7%iTh6_=QOG;sx_oOajdoWy0Bnbr6jRoyxBUpD zireDeAaXe}%YG}pSxaMX=%YfffL4htLgK`6bIL zvmGkgBjeh~>^X_06EXNa1!+P55frX5k34&^=0@^IBk4y?T-HfSPKs_z_GAuTW;bo@ z8E!XYgyN~603-%x6>c>n&^3PmIEH_^WPAKjgAr^%@{^p1T^=4N7AOfZgCoY}_02}d z$~yq{9CtW>vX%dUV2goy9@O6)KdIZIL#xl6!0P0n+(iSDf@F`tUY|G-bz~oEaQ*h#g{k&}On(}cn_!MVFhtl63Tz;Oz^A7yA^VI8 zvt?caYVB_}LJ^ z^rGUiaQ~W*lXA1NFoT*e#p?M4BmuXXd~k(4Imi^Zz$dBSk2zytwnBA6!>&g`KKb(* zGo&DVe<;HNuwl{8x1ejXP{8e^x`wj=Asl!n_MpIL{BGrBN^eqF}q zd*FEu`|DMnj_9*XDNHkLa9rRd!zBj_j(d6IN}B6)gD5=utW@SPle4fZpdh;R>dQ0- zJUmP=b3+ou`j@ms6Ujnt!K|dFaCvUuhG}1qdTLi4$(S%(7K$sKyLC2Jb1K`-%~-QW z=SO^0-DNi)!90K)QPlBefah?5II>8;%XY}dUbTuT2T)lE)9G^ zN<=kbmHPd~(_UBjoG+4ZUfM7$*F6aj4+g92zbWhilejlt<1a4he!`3B*~K$}zr7fQ zkV2e~`5AC~fw^IO%PFG|F$5v0#)(6LEg~m!bgW-N1+`wv+vPwEAZ9ZoJJ=f~5rq#& zEyzSK9suPBCOwo5TJ4upJW!MmU}x}!x8$PhW59Ptcr6Tbe!L9FH=c0UXv}C1csm zR0`ss(y0!#Jr{|;R{}z7tsPK~oChIMAqY4$KtMbv<01Cl0_-&QN~&c-UzwKJ8LCAj*yU2Qll{_0?ZXGzZnJJg@QF@J0Odhw}mjps+0V0W2cH zOcRjdph^$KMvnj7W?mn3iPgNu4<|Aq!2$&7Rd}?C?px_}Oz$Yh+A~y{H1iDnk$rF8 zb#!#Z`9-sthLdnvRv%DuH=t~2pTABR3qQIVg!*GfbZq+17eQQ4_=*vjZgGZLKsyE` zUn^D`Tw;PfN~`3zi>c|sr!HJqE3==^6bmF+Yvvq%7U>Zo4)kva)MUo_NIUhhsQA1< z22jF(Kk-vbJg=M(D;1%ZZCGFkpGnE&ix7Tm)UZineJHsIL>9|ce+%L)SI92uPw8C- zx+us>%xgbqnX__Vz;6JkKtXC++Q7!rmn=XVwonmW${kbi*m}YT_}?ly+I+L4RaG(r z`Nkm!!Ss#-*`^ODka_wb7OA`xY4GPAKuik}i(M)ygn_Bay zVZc9Cl6skUMFQ1iviy3a&9o!M-zD~{Qtxy6IAEhmf#PoTl{`D)HI8_8(!;m0I-?cXtTvOfs-igy_aA*7L{$}n98PSuLz6v*)++k_Nu(Oc z>oU??*v)$0sy_VCz5{R&X@9}wSTR4U7_NDt>W}b(Gs>ybPha_fGOabL6VT`>FGmfE zb-%uo1ya1Uosv|%lfTmGAuZ&(j>!@M1uLd5o@+<+S#YBQ`^_>l$$^s6KN8FX4uL9G zVj3U@z~cy#LDX4HYmPMG!&nX>X%CdkU>&i$%!j!w~YVK>z{~(1EIv;Z{#lK`;fbMB7<(kct37>Vs)JbFjKYo0kIFyl)pv(exQCh--l; z4+=hF&)fvG-Eka=b~!96IuprQ;N`QBg>!a~rJE2#(=+L#9y3$VUQgDs>ZpaAagJ>t;)MIX4o-~%%c_6Ntu z;QY#Y*cGzS*lFO&YK|qm2t?3mfthqnD9j=j&w9n>Q_M6y+NdkO2=j8pl@uq(Rd7is z0yrC`m)cTTi@7ARlrlSf2jDm3q&j;3zE}o^ByUm!r?wbVyp=NDKTP(NnCXo66D&o! zPCb7vu1XGW?WD#YCs!)nG384Fp?yOi>wbA{liW10kg>b>9?EA5%;gwO&Ha#L@=;vF#*T4O4q8sbx4=Bo$b z6-f1qA>eIZz!`lRY7tah{ay|embd6zStI%YUu1ruu+i=+xN%Zs-anoVaZM035SdBW zKa3Q{f2|GBIZpcsOL&uuJmPAU2)=Cik=Q1vV&_LAhufB|S$ne2E`T!ZOnvxdV>z1B zWTTb(^uB`l%N6Kbcvnk`4T6RQ<}hUC3$EZ06M!o!lRT7|*1cK|fSJ8awEgvoLMA|P zAgn4%rSF5s-ZH5RxO;L+GiV3GDZqZXewCF4JSLo1;2sP+a$x0+BJh~ofVg6j=&@H4 ze3AM6-pWgX{>8BmNN`=k9ztHybX=FjX4{@WWC!wbmH zdJ%-{kGO9mRa>XqU(9=*ymDirWF{>((_*-^CryqBP*>LPs-FY1sKQR6dg8PKqFBju zFjk|cH>x^rt8d-_)=L?1&hrpH*2q}a1QA+zr{Zj6Ww>!K{TUjyP_^%F+;*08=w2Wp zQxSwfK{sEDBRT;$=2vJdI?|PZUhROuqh(cl!G3RZDt0aT?I|vBHkD8Wmu1F$2JkNz z0nF78ys4`ia9~;Bjz!1tX~fgM8#5 z$M$;F{=5<2O5!$zCt6Ve+O9fq;%|<3Fl)wiCXX8?v^IXFd({ zzY0N5Pdxhp0f;v9Zb?U72r23#i9_6MfYEK+FaWA5++6Y&Te$EtN!3$eYnoEC$UOQi z2~SsZW>YMuf)pGp;tb|}DQRF0QfyDhKM3UhP1xJrE_(}H z$XiwrTvb;Ah1^?Zh!jft1i#|8a;x~HD5_fYeOB?vN`zq`+6T79WwltVR`4;0ihl+o){4pmUU@V05?>unA<(KpZ0t{5sD=*^T) z(LUhz?m_2}gUUSv!@dTH+?i6293n)@BF__z7gkW!C{2OT&sfMP>;*kavJYv<%% zJjfGWf%6=fDF*q&p`#-LJ6u0=&G`ZuY)jw{){~)qh9^zLFg7Y?Kz#%gm7=k@{IEI< z?z}c<>iyN&GG)pSY`D_K+hwz3j9HDcIpgQE2Snkh&PHIA+A~jJPwWWsWA_P;mn*9?tEzAXse%XCY`_(ukS?4E#`0uwbdLXG&q1Ik<0k z>nb?_FIK`qO@$j62r7e9Ig5_tRAxbD_4$h%N zujbx+3P?~9ig{M#8C{&fLkp^epO$2zlR9Y zd;a8tNqJ)ZA(O?`)l4rSA3bjJO7A*+91Mo*P{W4dwR7ao$vl|CPXYWK0##pqr8((^ zO96RvIz=*_bnENn4$6S|8wy>qJ&CYD>jFZO7@WztTBp(&7NjeX9HB3$feW$+5v@8n z#PC|ym1Boe@EckPy~Tjl3nvwLOXl?{2plwroV6IRaZ26_od_&kFX&3`kBP>I1}sIw z0DhrDsRG@ZC2~ID^%n7G_jL)N2gz6q3 z#Ug8E34^b$f|~|GL)K7+BMF38pQP+=knNngR+h+P|i50{`BSQYwd!`=0g`^qXU)Yxog>8JoknO ziE^C7UmFTcJL|yBS3W3TgxmrP3(Fix=2eP5ya)9K!fRA)$CSW5NablzQVPDpH?TH8 zNZ1Vd#o^n=P&s5W7WKF#GKiSF17c>ntj#g68iF*2G0{jZf*{NDG_d>-`>obit1g*a z+*9aQoglNXc5Z+Sd=A7@fS76lY6Wkg5Ci)W2sWUkJyY1{)Y`j~q;I*feOG6DV@0bU z96x<`=Yr~8T~Mp4ypy9neB$ioucFQI!0n1ArYR&^Tql+4s{%PNS=Cl|PTXh39k7>X zA$)7kVyT@#Tr+g08PrEIsABnCiqhx771mg0{YQKopC7JGzX;)ZZaZw(%k4GJa&=J= zU*}`hnf((L4P0I#oTjnDe)yg2lvk>n0hwLSQ|S4}!^xiqm(8!=dHvOhujS^lEYXd7 zp0Vv00kJm=_}>E8Wa_7&;@4WZJo7TKv_Aw~0#(z{h>^0D1p$IqF>H2OfD^-H7%#ilhc zaO+EHpspQb#NIUt?#&|nttrG|A`e9ES`!5XVj`D?iOen#&LkRH+9rNdR1?lrUJ#^JZ6C|2z=iD9&#$k13YYkS^Csz=J`Ig4qA1*D-QpiKnSX2~0%fHnw6DUfsC5nF}=!!jEco!RUcR|8jX6w#vzMa)NIrDNOS-tg`nzR{>8Gw?p z`4bFJopBBnr=Y~=;rd|J#QgD@sPy4&k3H~TnxM5y>^OuTJq0a+y?-$4LXU>5BY3>L zPlqWLKRbtxGsylCt)#JnN$HRSVy>3x$)gYp9j-M|!dEz% z0UsLfEsw1o1#WmlvwQ$Lz`fn7&xu|GHW*O)9X8w1)$wW}(7{7uE_47%1}>Z9LM^Vx zpcyTKKZ9BH1N>0Wk9p*xHz#kD0=@_&QNIf!p}3R}s<~L5>~KSetx;g(%IEmD5+b0O zF_-KlH?@BIoFMC`#(h2IIl<$I#YPG+D+!x1)sD!`zkgi5;0#4-l=ZejltI~*Pa9oQ%BCH&npConUic_VIs zWqNWG+o1QU9zqgiN3gVY(E9{~VzEU`&rP&6bO88Yipp&OAan;keug^uU}*_>P0*4( z1u3v2ULpDuN%ATLd<5?{ste^`a#<#w3i99Cgxl@Fk$0z^$YQY(T$jD{PKU*-(-u*u zmS;858<#cQ$(w7qbL;|~%Y;K8wWlA)943&M^f)mo_%z@7mAh@rLao^S3VxiJs2&{y z^1N%jweAH!R3@Hj*o*h#&8Qm^Z3ew<{35LZGrzOXw~%aNDODDAua9mKIf7H z0r&F6YqNQ-zOk7!RnuCUIGsS`>>hzn@NMa&m+)zvv&q&^Gt~AMn;=aA6`zg2Gh1LHepjk?hFo$*vox_JqmK!C&FR-cck?`6tjh zSHc^uGn=KLcC5Sxcm7us=N(Vw|G#moIJVdiWL}s>-y=jq* z>{*gB4*A$yWN+E>d*9Ue@$;YKIL>k3@B4nguIqVS*Apu=!}TAzSMieU1pdnm-uL-_RuL9HS&v;})|NTAIo zotRukjzZ&QA`v=4wHq zWam~=`&XY6_3WC~Y`o7UEn{?kp>D2r)HOmKi+lf5`WbRogk7*_yk@%MlY4g376Wx> za#sc9kZydccaZVt_NtGhbcm%MYVyXl0tM@XDqoo1`Jz)^7MJ z>vZZ6I~`p{u~O}2CV*_YFn+SpcdRXf5~kcE?SFk$`nrSv$19tBGZ6vc z_q3jvB%&Q+-QrKOlfZW;#)V_w-lm zYpr}gX>7`)-6hJ?eMZ8oCtSCo#N>F5n_E}~dS2y*{`-uzkqVA==f0Lc*tpM1i3?X( zhRDuhYHY$YOn5dHO5Wcpenl6UwWn*Y0Ts|7d~&!`5^(khLi@%%hD={N`ZJ%(<$2tE@kW@6{no&6_7P-Rbe6b|8rL>*LEUgM8BTo0dUf_Q62e zRbdyUSSFL z%OmqUC=?9K1uTQ}EURW3L%9bwP06?50EAg+qupC`EYjXpP^Q|iw_{>OB~1yb4?8mTkZ6FEx2UEKuUN($D-0NJVz^F zQ*&Fw4unb9zP}}TLNB#e*}s|>$x{y0crzI-7-6{o%m@F3dMMUGICmK z9P{E~Eocl=m%&hd1SNSS{WKzHCp!}_X$M}p74dzv?xc!*f;9CUv3@Vf%RtVouz3%} zdQGp92%0V$|G;PTo4%Zr?6QlU?{&@Y$&2UW4G6_ep+JKv#|RAp%cErA2e<%2p!7KB zn|?*F8G@qLMNy6L!p|jABK8(P;M)r<@eA8UGe4b<+e+ zb+z9P&{wXd9|!SUv|zo{d6PiNL`R)=?f7?Tz#8&lnZQ*be$-IjI6Bd^gpET{x7`wB zH^|G+gTa_{!cK0KGUl^9!@?pBfVu-4tbA%#R27>2PxC4a;(5e(x&(?wU=b8wxJ~Q+ zyQi(awpKmmDUb^_JqJPIEq4qW5ZB;`G~4G zCsk7d0b1R7hGtIM1Cg9??xVqC*V4M7$~wa3F%Qm14_EDK^^fl&N*^x2d9{on0dki6 z&{4+H=*M@?<28K~?n=bi5wMR1hS#|p?Lt*L?aO?Fftpdk(5;9sKnlFN4AYVd7r5|p|iQtchMRx+7Cgo3? zWbC7jkcXQ~OCee8T8u38NrJc1D~!RVQ<;O1yo~m*?Rq3Z+Ldls z20JRbLo;B&RhQ|ys@P4%7Y{z;;P>2p`n>A{#3w8vNJh00wW&a}8WSV0U0?{n8THNO zr86?qIsmejn(8%CMWdl|zd2F#pnpOL5p|l4A`o|Mq>e{0%*tVzw*-n7LbB*>v%HHS zR-w=_SuOjjNKeC=5iYNgA7 z;?hP)hnx%<&LKGcbCx{TuJy{TF25`FbUZUkTcXPv=*_})HX4vq@$dsT5bq!wNxvH7 zo_<`dDSur_Oc!e+C$=-qpjkE$TKjrLSGcGv@mS*D%%`2uy&zZ`udNtUEnrIm@<=b* zMNjvzW4qCeY_K?WcDF0O=HN5qpXoi8r3$5{A%F5hM=Esz)}w96Gpu%1*fJv~h|l*@ zj~S%P5Tb?h@N3w;bU8u{Z|yh{Xzlw{JBqkE)k7xk(PMG19fLqzx_iHR935$|mk)Rs zm;E|P&dS4^R`%drc_M8WT@9|SMW<{&1okBQ=_84;Wz2-%7-e%m+j6qr{Lisuh0&S8 zj^*vPLqK2;;pn)y$Ffc}nY62pT?SNVr_2Lfq3Zqlm*#a_`543#+8+jJT9LNQr#3R+ zQsC9U5a4tl4m0xX?^1FCM;Aeu+7>za;lJ`?o39^rN?+0ozAN<0?Uj*QskkGn>jv(i zxf(@=`53ZG+rC3|<3u%rA(Y!H;j^vv)9cRTOj?AEtd9 zegBAxfiOg^)QtzS#uN(4k|0_9#a+pzZ9YAQ&;rShmp-?|s$Ljn4=n`l5zh&e>f9k? znjfI&t<&$7Q~zlL*dM5BuOHE{N4OYR&|vgOOA=-}v0_}>9*6Wv^VX(A@c}C4)ks7} ziG6IWU;fax$6!kMTwkMKaVq87MBHX?lT5dhy`J1L(XIEpv=rC0fjnJpZWfQ-(X66^>2Ix5f{b!8l(4jpRTG&`w)>Jmko8r- zRnzt~(`7p^Ki@Eo3WV^VHO9Ul<_{>BwTKRG%E4e1j0vr*|F)z5^ItBuqnwsFl5a^- zvn;k&gcsXS;BmourC}oydq^RPtwUz{Bwi#YpE|M*}b9yPxMk+by6DMrt{4-22gGr zTb`HAq$!Q_=UKdk zgV=N$ggBQdnO*L%c2>DeWg;?*D9>Mg+8Jm953H>XQ#W_mq*Rr5qylkZ6qcN>hE)#_ zW9wMYr5@0wpfDY_xg(EBveK2cG$-e*A^$e%YK}2pBFg7Gw>Hsm0*%rpnAF|xVc9oY z%V<=0m^Cd>_jnSW=gY~4+e)bxbg8%gDogo5t&qo1O6bR=r4l!Bfafv8lPdN{Pog}8 z9`_kd0MHQ_fRR`s&lIIGWD<(kL$WzqE0B@yESj7sIN^{cw_~h&*2m$dtmf-}U(GN6 zanh{>i?z86VOcC8J_m?mAGCL(bnaK#kxjUI_lJa2pLKrU<9?AUm!F=oX&Tk2I6It5 z{6GuvPGm7e62iMDz3!|jtRF;$aYCq=-?HA~y7Sg{t!j;4Xe8jX)!YDCQ#o(5sY|53 zOXUE472KkQ2I@-2k-WSs-s>)d!I$9Jg_I`f-2$B3tb!3kz5HG7Zs*u>~>SW8y{coQ0q)G9QgaZwhbEhoRD3fmX11C53reLp_m1 z`y{f}s_Uus1b>^Eu^DlFkc+iB3vzRYt+Y6`6r7+9%lJ!c^38~u9i-GLP-hj&|s=>r&T13e@{ zsY9iq-ok4A{SQ*(y<`3!t>4a@4nT9J@mJ4ibjhz_QQfs)Q+rjF=r@W5*`V+7w0SCf zZI+(Fc*4g+YSI*nKrxX+MaN7&SJR0Um=J^`uJ2b}t^iT$aeGe{e)d6ImcLtGiD`59 zQO3yVXohC9=5KC;${xv+RbTxg!xy82wV&PAMK$rGAkyz42(-+R6KIfkL`uJm`D$3Z z+0`y~e_{CKLwH9|57-v%4|kp&{@7dVtjsbsHUJ@d7g9{}?#e{F%c*JIK&f%J?&B?t zSltNOmrOS!RqCM+W*#dPJpCX1Jyv|s+WMZGw`^+!0px^*7X$$fg0c5qb76umnxD(d z%bmA=kc1^AF&Q?xyt5yHAR*AcQ+ygh;xv%PU3H5~C$V;&PuF?0i>=D}E0RN$Rrb2B zio3Ot%cy;)o(ungfd4j*H3zgV2^azsTMVOG$vIgApU<$R;^LR1x!D5YgdutM70t%h zrA-q3pd@W&w#ct`m|es80VXJ}s0>O5s~T~49Qpo{V&;e2PE`a5t#Uk&Yv*Zin**ii_+MiZ>1?p$8FJSjo_vxzSmG`q_QqMR|3>2W%&B%rG*35> z%Z*d8QDPn;XpD7mcpHvFpkj}sJ<+~Uvw&ohnmI&2 z@z)e#+u>g&^?3shKkawf{j@54*tGqftp@0Y;es=Zi&F}!gY}N{Zqy(M0s=x&(la4S zEm)R3c=8r2RTFz4B5vZf|5;>jRrSRxXuSKF^iKH3NM-8pz!}D|gn9ajM*sWr`?1jn zU1mr7k9@8ETUZxXN<09CjO*1(j~YRAMjrzLBn8GGh7E2v05-wvnWr&fz!CFY{|I## zfb2ezN%UmY2!JHB4h6^_q%L<0;6jC`Ik1BXui)@0iKr)k7a}VJ$5<`&-hx(tE68QZ zfRzbL`&tGe>?BT!M;{T%@{@N)IK|ZKsM!e?lwLqHbUrqpe z`HL5YE)ReFl>Q;Jf6El;8{YmUjhHlV + +The signal chain in Strudel is as follows: + +- An sound-generating event is triggered by a pattern + - This has a start time and a duration, which is usually +controlled by the note length and ADSR parameters + - If we exceed the max polyphony, old sounds begin to die off + - Muted sounds (one whose `s` value is `-`, `~`, or `_`) are skipped +- A sound is produced (through, say, a sample or an oscillator) + - This is where detune-based effects (like `detune`, `penv`, etc. occur) +- The following will only occur if their respective parameters are turned on. Note that all of these are +_single use_ effects, meaning that multiple occurrences of them in a pattern will simply override the values +(i.e. you can't (currently) do `s("bd").lpf(100).distort(2).lpf(800)` to lowpass, distort, and then lowpass +again) + - Phase vocoder (`stretch`) + - Gain is applied (`gain`) + - This is where the main (volume) ADSR happens + - A lowpass filter (`lpf`) + - A highpass filter (`hpf`) + - A bandpass filter (`bandpass`) + - A vowel filter (`vowel`) + - Sample rate reduction (`coarse`) + - Bit crushing (`crush`) + - Waveshape distortion (`shape`) + - Normal distortion (`distort`) + - Tremolo (`tremolo`) + - Compressor (`compressor`) + - Panning (`pan`) + - Phaser (`phaser`) + - Postgain (`post`) +- The sound is then split into multiple destinations + - Main output (amount controlled by `dry` parameter) + - This is where the `duck` function will apply sidechain + - Analyzer (used for tooling like `scope` and `spectrum`) + - Per-orbit effects (see the section below) + - Delay send (amount controlled by `delay` parameter) + - Reverb send (amount controlled by `delay` parameter) + +## Orbits + +Orbits are the way in which outputs are handled in Strudel. If you are listening in +normal circumstances, you will just hear all of them mixed down to stereo at the output. However you can also +use routers like Blackhole 16 to retrieve and record all of the split channels in a DAW for later processing. +By default all orbits are mono, however with the "Multi Channel Orbits" setting (under settings at the right) +you can use them as 2 channel stereo outs. + +The default orbit is `1` and it is set with `orbit`. You may send a sound to multiple orbits via mininotation + + + +but please be careful as this will create three copies of the sound behind the scenes, meaning that if they are mixed +down to a single output, they will triple the volume. We've reduced the gain here to save your ears. + +⚠️ There is only one delay and reverb per orbit, so please be aware that if you attempt to change the parameters on two +patterns pointing to the same orbit, it can lead to unpredictable results. Compare, for example, this pretty pluck +with a large reverb: + + + +versus the same pluck with a muted kick drum coming in and overwriting the `roomsize` value (occasionally) + + +This is due to them sharing the same orbit (the default of `1`). It can be corrected simply by updating the orbits to be +distinct: + + + +## Continuous changes + +As all of the above is triggered by a _sound occurring_, it is often the case that parameters may not be +modified continuously in time. For example, + + + +Will not produce a continually LFO'd low-pass filter due to the `tri` only being sample every time the note hits +(in this case the default of once per cycle). You can fake it by introducing more sound-generating events, e.g.: + + + +Some parameters _do_ induce continuous variations in time, though: +* The ADSR curve (governed by `attack`, `sustain`, `decay`, `release`) +* The pitch envelope curve (governed by `penv` and its associated ADSR) +* The FM curve (`fmenv`) +* The filter envelopes (`lpenv`, `hpenv`, `bpenv`) +* Tremolo +* Phaser + # Filters Filters are an essential building block of [subtractive synthesis](https://en.wikipedia.org/wiki/Subtractive_synthesis). From ce7cff2c3b6a25994e595b96849cd289474fb472 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 19 Aug 2025 11:26:25 -0500 Subject: [PATCH 118/257] Clean up wording on orbit routing --- website/src/pages/learn/effects.mdx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index 5e41eb92d..70218b78f 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -54,11 +54,9 @@ again) ## Orbits -Orbits are the way in which outputs are handled in Strudel. If you are listening in -normal circumstances, you will just hear all of them mixed down to stereo at the output. However you can also -use routers like Blackhole 16 to retrieve and record all of the split channels in a DAW for later processing. -By default all orbits are mono, however with the "Multi Channel Orbits" setting (under settings at the right) -you can use them as 2 channel stereo outs. +Orbits are the way in which outputs are handled in Strudel. By default all orbits are mixed down to channels `1` and `2` in stereo, however with the "Multi Channel Orbits" setting +(under settings at the right) you can use them as individual 2 channel stereo outs (orbit `i` will be mapped to +to channels `2i` and `2i + 1`). You can then use routers like Blackhole 16 to retrieve and record all of the channels in a DAW for later processing. The default orbit is `1` and it is set with `orbit`. You may send a sound to multiple orbits via mininotation From 59c8d707149ceeddda26aaa69bbdf7f7a42a4f40 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 19 Aug 2025 11:27:56 -0500 Subject: [PATCH 119/257] Code format --- website/src/pages/learn/effects.mdx | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index 70218b78f..4194757ef 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -19,15 +19,15 @@ The signal chain in Strudel is as follows: - An sound-generating event is triggered by a pattern - This has a start time and a duration, which is usually -controlled by the note length and ADSR parameters + controlled by the note length and ADSR parameters - If we exceed the max polyphony, old sounds begin to die off - Muted sounds (one whose `s` value is `-`, `~`, or `_`) are skipped - A sound is produced (through, say, a sample or an oscillator) - This is where detune-based effects (like `detune`, `penv`, etc. occur) - The following will only occur if their respective parameters are turned on. Note that all of these are -_single use_ effects, meaning that multiple occurrences of them in a pattern will simply override the values -(i.e. you can't (currently) do `s("bd").lpf(100).distort(2).lpf(800)` to lowpass, distort, and then lowpass -again) + _single use_ effects, meaning that multiple occurrences of them in a pattern will simply override the values + (i.e. you can't (currently) do `s("bd").lpf(100).distort(2).lpf(800)` to lowpass, distort, and then lowpass + again) - Phase vocoder (`stretch`) - Gain is applied (`gain`) - This is where the main (volume) ADSR happens @@ -77,13 +77,14 @@ $: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor') /> versus the same pluck with a muted kick drum coming in and overwriting the `roomsize` value (occasionally) + This is due to them sharing the same orbit (the default of `1`). It can be corrected simply by updating the orbits to be @@ -95,7 +96,7 @@ distinct: $: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor') .room(1).roomsize(10).orbit(2) -$: s("bd*4").room(0.01).roomsize(0.01).postgain(0)`} +$: s("bd\*4").room(0.01).roomsize(0.01).postgain(0)`} /> ## Continuous changes @@ -115,17 +116,17 @@ Will not produce a continually LFO'd low-pass filter due to the `tri` only being Some parameters _do_ induce continuous variations in time, though: -* The ADSR curve (governed by `attack`, `sustain`, `decay`, `release`) -* The pitch envelope curve (governed by `penv` and its associated ADSR) -* The FM curve (`fmenv`) -* The filter envelopes (`lpenv`, `hpenv`, `bpenv`) -* Tremolo -* Phaser + +- The ADSR curve (governed by `attack`, `sustain`, `decay`, `release`) +- The pitch envelope curve (governed by `penv` and its associated ADSR) +- The FM curve (`fmenv`) +- The filter envelopes (`lpenv`, `hpenv`, `bpenv`) +- Tremolo +- Phaser # Filters From 34e8a574726fa01e036a8147ddc729d090003dfb Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 19 Aug 2025 11:32:07 -0500 Subject: [PATCH 120/257] Typos grammar etc --- website/src/pages/learn/effects.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index 4194757ef..268d7071a 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -87,7 +87,7 @@ $: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor') $: s("bd\*4").room(0.01).roomsize(0.01).postgain(0)`} /> -This is due to them sharing the same orbit (the default of `1`). It can be corrected simply by updating the orbits to be +This is due to them sharing the same orbit: the default of `1`. It can be corrected simply by updating the orbits to be distinct: -Will not produce a continually LFO'd low-pass filter due to the `tri` only being sample every time the note hits +Will not produce a continually LFO'd low-pass filter due to the `tri` only being sampled every time the note hits (in this case the default of once per cycle). You can fake it by introducing more sound-generating events, e.g.: Date: Tue, 19 Aug 2025 16:06:18 -0500 Subject: [PATCH 121/257] Mention order of effects --- website/src/pages/learn/effects.mdx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index 268d7071a..7a57b898f 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -24,9 +24,9 @@ The signal chain in Strudel is as follows: - Muted sounds (one whose `s` value is `-`, `~`, or `_`) are skipped - A sound is produced (through, say, a sample or an oscillator) - This is where detune-based effects (like `detune`, `penv`, etc. occur) -- The following will only occur if their respective parameters are turned on. Note that all of these are - _single use_ effects, meaning that multiple occurrences of them in a pattern will simply override the values - (i.e. you can't (currently) do `s("bd").lpf(100).distort(2).lpf(800)` to lowpass, distort, and then lowpass +- The following will occur _in order_ and only if they've been called in the pattern. Note that all of these are + single use effects, meaning that multiple occurrences of them in a pattern will simply override the values + (e.g. you can't do `s("bd").lpf(100).distort(2).lpf(800)` to lowpass, distort, and then lowpass again) - Phase vocoder (`stretch`) - Gain is applied (`gain`) @@ -127,6 +127,7 @@ Some parameters _do_ induce continuous variations in time, though: - The filter envelopes (`lpenv`, `hpenv`, `bpenv`) - Tremolo - Phaser +- Ducking (`duckorbit`) # Filters From 215ab878092605c71867096b4dcab619dd9edc29 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 21 Aug 2025 09:41:21 +0200 Subject: [PATCH 122/257] fix: prettier --- packages/codemirror/autocomplete.mjs | 2 +- website/src/pages/learn/samples.mdx | 2 +- website/src/repl/Repl.css | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index d3091d874..c0845895e 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -74,7 +74,7 @@ const hasExcludedTags = (doc) => const jsdocCompletions = jsdoc.docs .filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc)) - // https://codemirror.net/docs/ref/#autocomplete.Completion + // https://codemirror.net/docs/ref/#autocomplete.Completion .map((doc) => ({ label: getDocLabel(doc), // detail: 'xxx', // An optional short piece of information to show (with a different style) after the label. diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index 475319748..0deb60704 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -363,7 +363,7 @@ Sampler effects are functions that can be used to change the behaviour of sample ### scrub - +{' '} ### speed diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index 62e7dcf24..b8443081f 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -198,5 +198,4 @@ } .autocomplete-info-tooltip::-webkit-scrollbar-thumb:hover { - } From 8465517c76aef62b081190cb33419b755f72fa9b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 21 Aug 2025 10:00:41 +0200 Subject: [PATCH 123/257] remove hs2js postinstall --- website/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/package.json b/website/package.json index 499758147..87f4025e4 100644 --- a/website/package.json +++ b/website/package.json @@ -8,8 +8,7 @@ "start": "astro dev", "build": "astro build", "preview": "astro preview --port 3009 --host 0.0.0.0", - "astro": "astro", - "postinstall": "cp node_modules/hs2js/dist/tree-sitter.wasm public && cp node_modules/hs2js/dist/tree-sitter-haskell.wasm public" + "astro": "astro" }, "dependencies": { "@algolia/client-search": "^5.20.0", From 4f15d681b0cdccf1410c1107567c1f194d08e130 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 21 Aug 2025 13:12:36 -0500 Subject: [PATCH 124/257] Add release parameter, avoid clicks, some cleanup --- packages/core/controls.mjs | 56 ++++++++++++-------- packages/superdough/superdough.mjs | 84 ++++++++++++++++++------------ 2 files changed, 87 insertions(+), 53 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index e6181d453..4d6f8d8b7 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -141,8 +141,8 @@ export const { note } = registerControl(['note', 'n']); */ export const { accelerate } = registerControl('accelerate'); /** - * * Sets the velocity from 0 to 1. Is multiplied together with gain. + * * @name velocity * @example * s("hh*8") @@ -254,7 +254,7 @@ export const { fmenv } = registerControl('fmenv'); export const { fmattack } = registerControl('fmattack'); /** - * waveform of the fm modulator + * Waveform of the fm modulator * * @name fmwave * @param {number | Pattern} wave waveform @@ -377,7 +377,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], ' // ['bpq'], export const { bandq, bpq } = registerControl('bandq', 'bpq'); /** - * a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample. + * A pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample. * * @memberof Pattern * @name begin @@ -438,7 +438,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb'); */ export const { loopEnd, loope } = registerControl('loopEnd', 'loope'); /** - * bit crusher effect. + * Bit crusher effect. * * @name crush * @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction). @@ -449,7 +449,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope'); // ['clhatdecay'], export const { crush } = registerControl('crush'); /** - * fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers + * Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers * * @name coarse * @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on. @@ -460,7 +460,7 @@ export const { crush } = registerControl('crush'); export const { coarse } = registerControl('coarse'); /** - * modulate the amplitude of a sound with a continuous waveform + * Modulate the amplitude of a sound with a continuous waveform * * @name tremolo * @synonyms trem @@ -472,7 +472,7 @@ export const { coarse } = registerControl('coarse'); export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem'); /** - * modulate the amplitude of a sound with a continuous waveform + * Modulate the amplitude of a sound with a continuous waveform * * @name tremolosync * @synonyms tremsync @@ -487,7 +487,7 @@ export const { tremolosync } = registerControl( ); /** - * depth of amplitude modulation + * Depth of amplitude modulation * * @name tremolodepth * @synonyms tremdepth @@ -498,7 +498,7 @@ export const { tremolosync } = registerControl( */ export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth'); /** - * alter the shape of the modulation waveform + * Alter the shape of the modulation waveform * * @name tremoloskew * @synonyms tremskew @@ -510,7 +510,7 @@ export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth'); export const { tremoloskew } = registerControl('tremoloskew', 'tremskew'); /** - * alter the phase of the modulation waveform + * Alter the phase of the modulation waveform * * @name tremolophase * @synonyms tremphase @@ -522,7 +522,7 @@ export const { tremoloskew } = registerControl('tremoloskew', 'tremskew'); export const { tremolophase } = registerControl('tremolophase', 'tremphase'); /** - * shape of amplitude modulation + * Shape of amplitude modulation * * @name tremoloshape * @param {number | Pattern} shape tri | square | sine | saw | ramp @@ -532,7 +532,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase'); */ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape'); /** - * filter overdrive for supported filter types + * Filter overdrive for supported filter types * * @name drive * @param {number | Pattern} amount @@ -542,7 +542,7 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape'); */ /** - * modulate the amplitude of an orbit to create a "sidechain" like effect + * Modulate the amplitude of an orbit to create a "sidechain" like effect * * @name duckorbit * @param {number | Pattern} orbit target orbit @@ -554,7 +554,7 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape'); export const { duck } = registerControl('duckorbit', 'duck'); /** - * the amount of ducking applied to target orbit + * The amount of ducking applied to target orbit * * @name duckdepth * @param {number | Pattern} depth depth of modulation from 0 to 1 @@ -566,16 +566,30 @@ export const { duck } = registerControl('duckorbit', 'duck'); export const { duckdepth } = registerControl('duckdepth'); /** - * the attack time of the duck effect + * The attack time of the duck effect. Can be used to prevent clicking or for creative rhythmic effects * * @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)) + * sound: n(run(8)).scale("c:minor").s("sawtooth").lpf(200).delay(.7).orbit(2) + * duckerWithClick: s("bd*4").duckorbit(2).duckattack(0).postgain(0) + * _duckerWithoutClick: s("bd*4").duckorbit(2).duckattack(0.003).postgain(0) * */ export const { duckattack } = registerControl('duckattack', 'duckatt'); +/** + * The release time of the duck effect + * + * @name duckrelease + * @param {number | Pattern} time + * @example + * sound: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2) + * ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckrelease("<0.2 0 0.4>").duckdepth(1) + * + */ +export const { duckrelease } = registerControl('duckrelease', 'duckrelease'); + export const { drive } = registerControl('drive'); /** @@ -618,7 +632,7 @@ export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime', export const { channels, ch } = registerControl('channels', 'ch'); /** - * controls the pulsewidth of the pulse oscillator + * Controls the pulsewidth of the pulse oscillator * * @name pw * @param {number | Pattern} pulsewidth @@ -630,7 +644,7 @@ export const { channels, ch } = registerControl('channels', 'ch'); export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']); /** - * controls the lfo rate for the pulsewidth of the pulse oscillator + * Controls the lfo rate for the pulsewidth of the pulse oscillator * * @name pwrate * @param {number | Pattern} rate @@ -642,7 +656,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']); export const { pwrate } = registerControl('pwrate'); /** - * controls the lfo sweep for the pulsewidth of the pulse oscillator + * Controls the lfo sweep for the pulsewidth of the pulse oscillator * * @name pwsweep * @param {number | Pattern} sweep @@ -683,7 +697,7 @@ export const { phaserrate, ph, phaser } = registerControl( export const { phasersweep, phs } = registerControl('phasersweep', 'phs'); /** - * The center frequency of the phaser in HZ. Defaults to 1000 + * The center frequency of the phaser in HZ. Defaults to 1000 * * @name phasercenter * @synonyms phc @@ -711,7 +725,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc'); export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp'); /** - * choose the channel the pattern is sent to in superdirt + * Choose the channel the pattern is sent to in superdirt * * @name channel * @param {number | Pattern} channel channel number diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index eb1466d91..a75727240 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -331,18 +331,19 @@ 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) { + let delayNode = orbits[orbit].delayNode; + if (delayNode === undefined) { 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; + delayNode = 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; + delayNode.delayTime.value !== delaytime && + delayNode.delayTime.setValueAtTime(delaytime, t); + delayNode.feedback.value !== delayfeedback && + delayNode.feedback.setValueAtTime(delayfeedback, t); + return delayNode; } export function getLfo(audioContext, begin, end, properties = {}) { @@ -398,42 +399,59 @@ function getFilterType(ftype) { return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype; } -//type orbit { -// gain: number, -// reverbNode: reverbNode -// delayNode: delayNode -//} +// type orbit { +// output: GainNode, +// reverbNode: ConvolverNode +// delayNode: FeedbackDelayNode +// } let orbits = {}; function connectToOrbit(node, orbit) { if (orbits[orbit] == null) { errorLogger(new Error('target orbit does not exist'), 'superdough'); } - node.connect(orbits[orbit].gain); + node.connect(orbits[orbit].output); } function setOrbit(audioContext, orbit, channels) { if (orbits[orbit] == null) { orbits[orbit] = { - gain: new GainNode(audioContext, { gain: 1 }), + // Setup output node through which all audio filters prior to hitting + // the destination (and thus allows for global volume automation) + output: new GainNode(audioContext, { gain: 1 }), }; - connectToDestination(orbits[orbit].gain, channels); + connectToDestination(orbits[orbit].output, channels); } } -function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1) { - const targetArr = [targetOrbit].flat(); - targetArr.forEach((target) => { +function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.003, releasetime = 0.1, duckdepth = 1) { + const targetArr = [targetOrbit].flat(); + const attackArr = [attacktime].flat(); + const releaseArr = [releasetime].flat(); + const depthArr = [duckdepth].flat(); + + targetArr.forEach((target, idx) => { if (orbits[target] == null) { errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); return; } + const attack = attackArr[idx] ?? attackArr[0]; + const release = Math.max(releaseArr[idx] ?? releaseArr[0], 0.002); + const depth = depthArr[idx] ?? depthArr[0]; + const gainParam = orbits[target].output.gain; 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)); + gainParam.cancelScheduledValues(t); + const currVal = gainParam.value; + const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal); + + // Guarantees the value is set to currVal at time t. This in conjunction with + // cancelScheduledValues above emulates cancelAndHoldAtTime on browsers which lack + // that method + gainParam.setValueAtTime(currVal, t); + + gainParam.exponentialRampToValueAtTime(duckedVal, t + attack); + gainParam.exponentialRampToValueAtTime(1, t + attack + release); }, 0, t - 0.01, @@ -444,27 +462,28 @@ function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1 let hasChanged = (now, before) => now !== undefined && now !== before; function getReverb(orbit, duration, fade, lp, dim, ir) { // If no reverb has been created for a given orbit, create one - if (!orbits[orbit].reverbNode) { + let reverbNode = orbits[orbit].reverbNode; + if (reverbNode === undefined) { const ac = getAudioContext(); const reverb = ac.createReverb(duration, fade, lp, dim, ir); connectToOrbit(reverb, orbit); - orbits[orbit].reverbNode = reverb; + reverbNode = 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, reverbNode.duration) || + hasChanged(fade, reverbNode.fade) || + hasChanged(lp, reverbNode.lp) || + hasChanged(dim, reverbNode.dim) || + 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 - orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir); + reverbNode.generate(duration, fade, lp, dim, ir); } - return orbits[orbit].reverbNode; + return reverbNode; } export let analysers = {}, @@ -562,6 +581,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) density = getDefaultValue('density'), duckorbit, duckattack, + duckrelease, duckdepth, // filters fanchor = getDefaultValue('fanchor'), @@ -640,7 +660,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) setOrbit(ac, orbit, channels, t, cycle, cps); if (duckorbit != null) { - duckOrbit(ac, duckorbit, t, duckattack, duckdepth); + duckOrbit(ac, duckorbit, t, duckattack, duckrelease, duckdepth); } gain = applyGainCurve(nanFallback(gain, 1)); From bf5d9917ab8366e2d82289945405cc3721b448f6 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 21 Aug 2025 13:20:12 -0500 Subject: [PATCH 125/257] Codeformat and examples --- packages/codemirror/autocomplete.mjs | 2 +- packages/superdough/superdough.mjs | 6 +-- test/__snapshots__/examples.test.mjs.snap | 57 ----------------------- website/src/pages/learn/samples.mdx | 2 +- website/src/repl/Repl.css | 1 - 5 files changed, 4 insertions(+), 64 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index d3091d874..c0845895e 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -74,7 +74,7 @@ const hasExcludedTags = (doc) => const jsdocCompletions = jsdoc.docs .filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc)) - // https://codemirror.net/docs/ref/#autocomplete.Completion + // https://codemirror.net/docs/ref/#autocomplete.Completion .map((doc) => ({ label: getDocLabel(doc), // detail: 'xxx', // An optional short piece of information to show (with a different style) after the label. diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a75727240..b8975a852 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -339,10 +339,8 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) { connectToOrbit(dly, orbit); delayNode = dly; } - delayNode.delayTime.value !== delaytime && - delayNode.delayTime.setValueAtTime(delaytime, t); - delayNode.feedback.value !== delayfeedback && - delayNode.feedback.setValueAtTime(delayfeedback, t); + delayNode.delayTime.value !== delaytime && delayNode.delayTime.setValueAtTime(delaytime, t); + delayNode.feedback.value !== delayfeedback && delayNode.feedback.setValueAtTime(delayfeedback, t); return delayNode; } diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 398749359..6002fa849 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3036,63 +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 ]", diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index 475319748..0deb60704 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -363,7 +363,7 @@ Sampler effects are functions that can be used to change the behaviour of sample ### scrub - +{' '} ### speed diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index 62e7dcf24..b8443081f 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -198,5 +198,4 @@ } .autocomplete-info-tooltip::-webkit-scrollbar-thumb:hover { - } From 66aa3ac1da2d1151d5bf50222152ec21966376ae Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 21 Aug 2025 20:30:02 +0200 Subject: [PATCH 126/257] add --json flag to strudel sampler --- packages/sampler/README.md | 10 ++++++++++ packages/sampler/sample-server.mjs | 26 ++++++++++++++++++-------- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/sampler/README.md b/packages/sampler/README.md index c495c2add..c7b44610a 100644 --- a/packages/sampler/README.md +++ b/packages/sampler/README.md @@ -20,3 +20,13 @@ 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 @strudel/sampler --json > strudel.json +``` + +this is useful if you want to create a sample pack from the current folder. \ No newline at end of file diff --git a/packages/sampler/sample-server.mjs b/packages/sampler/sample-server.mjs index d1e56108f..08456add9 100644 --- a/packages/sampler/sample-server.mjs +++ b/packages/sampler/sample-server.mjs @@ -10,14 +10,6 @@ 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 }); @@ -60,8 +52,26 @@ 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); From b12707316ac2c0493d9330a9cec9a3a349b7a326 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 21 Aug 2025 20:30:32 +0200 Subject: [PATCH 127/257] bump @strudel/sampler to 0.2.1 --- packages/sampler/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sampler/package.json b/packages/sampler/package.json index b0f27f86a..765f2962d 100644 --- a/packages/sampler/package.json +++ b/packages/sampler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/sampler", - "version": "0.2.0", + "version": "0.2.1", "description": "", "keywords": [ "tidalcycles", From da283eb55a94d4ee9ba9bd68a12c850be6eba439 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 21 Aug 2025 20:36:17 +0200 Subject: [PATCH 128/257] doc: generate strudel.json --- website/src/pages/learn/samples.mdx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index 0deb60704..a87b8f7dd 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -178,6 +178,16 @@ 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: From 42a903ecc080b1615f6b097e8f478aa21ae8837e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 21 Aug 2025 20:36:42 +0200 Subject: [PATCH 129/257] add --yes flag to readme, making sure first run works --- packages/sampler/README.md | 2 +- packages/sampler/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sampler/README.md b/packages/sampler/README.md index c7b44610a..1142176b7 100644 --- a/packages/sampler/README.md +++ b/packages/sampler/README.md @@ -26,7 +26,7 @@ PORT=5555 npx @strudel/sampler # changes port when running with `--json`, you will simply get the json logged back: ```sh -npx @strudel/sampler --json > strudel.json +npx --yes @strudel/sampler --json > strudel.json ``` this is useful if you want to create a sample pack from the current folder. \ No newline at end of file diff --git a/packages/sampler/package.json b/packages/sampler/package.json index 765f2962d..dd701bcfb 100644 --- a/packages/sampler/package.json +++ b/packages/sampler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/sampler", - "version": "0.2.1", + "version": "0.2.2", "description": "", "keywords": [ "tidalcycles", From 9a9fe83f9c341d5689475854c78de7ace8043d0a Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 21 Aug 2025 14:13:01 -0500 Subject: [PATCH 130/257] Example tests --- test/__snapshots__/examples.test.mjs.snap | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 6002fa849..85079269e 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3036,6 +3036,27 @@ exports[`runs examples > example "dry" example index 0 1`] = ` ] `; +exports[`runs examples > example "duckattack" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", +] +`; + exports[`runs examples > example "duckdepth" example index 0 1`] = ` [ "[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", @@ -3118,6 +3139,31 @@ exports[`runs examples > example "duckorbit" example index 0 1`] = ` ] `; +exports[`runs examples > example "duckrelease" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckrelease:0 duckdepth:1 ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckrelease:0 duckdepth:1 ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckrelease:0 duckdepth:1 ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckrelease:0 duckdepth:1 ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckrelease:0 duckdepth:1 ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckrelease:0.4 duckdepth:1 ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckrelease:0.4 duckdepth:1 ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckrelease:0.4 duckdepth:1 ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckrelease:0.4 duckdepth:1 ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckrelease:0.4 duckdepth:1 ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", +] +`; + exports[`runs examples > example "duration" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano duration:0.5 ]", From a0fc52b1ec2d1d8786ae8f711fe777aadf15be8a Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 21 Aug 2025 14:42:52 -0500 Subject: [PATCH 131/257] Final tweaks --- website/src/pages/learn/effects.mdx | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index 7a57b898f..d9efe8f8a 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -45,17 +45,21 @@ The signal chain in Strudel is as follows: - Phaser (`phaser`) - Postgain (`post`) - The sound is then split into multiple destinations - - Main output (amount controlled by `dry` parameter) - - This is where the `duck` function will apply sidechain - - Analyzer (used for tooling like `scope` and `spectrum`) - - Per-orbit effects (see the section below) - - Delay send (amount controlled by `delay` parameter) - - Reverb send (amount controlled by `delay` parameter) + - Dry output (amount controlled by `dry` parameter) + - The sends + - Analyzers + - These are used for tooling like `scope` and `spectrum` and their setup usually happens behind the scenes + - Delay (amount controlled by `delay` parameter) + - Reverb (amount controlled by `room` parameter) +- The dry output, delay, and reverb are joined into what is called the "orbit" of the pattern (see more in the section below) + - The `duck` effect affects the volume of all signals in the orbit + - The orbit is then sent to the mixer ## Orbits -Orbits are the way in which outputs are handled in Strudel. By default all orbits are mixed down to channels `1` and `2` in stereo, however with the "Multi Channel Orbits" setting -(under settings at the right) you can use them as individual 2 channel stereo outs (orbit `i` will be mapped to +Orbits are the way in which outputs are handled in Strudel. They also prescribe which delay and reverb to associate with the dry signal. +By default, all orbits are mixed down to channels `1` and `2` in stereo, however with the "Multi Channel Orbits" setting +(under Settings at the right) you can use them as individual 2 channel stereo outs (orbit `i` will be mapped to to channels `2i` and `2i + 1`). You can then use routers like Blackhole 16 to retrieve and record all of the channels in a DAW for later processing. The default orbit is `1` and it is set with `orbit`. You may send a sound to multiple orbits via mininotation @@ -76,7 +80,7 @@ $: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor') .room(1).roomsize(10)`} /> -versus the same pluck with a muted kick drum coming in and overwriting the `roomsize` value (occasionally) +versus the same pluck with a muted kick drum coming in and overwriting the `roomsize` value: Date: Fri, 22 Aug 2025 11:25:13 -0500 Subject: [PATCH 132/257] Working version with note names --- packages/tonal/test/tonal.test.mjs | 25 ++++++++-- packages/tonal/tonal.mjs | 79 ++++++++++++++++++++---------- packages/tonal/tonleiter.mjs | 17 +------ 3 files changed, 75 insertions(+), 46 deletions(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 5486d5135..a64b455b9 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -70,10 +70,7 @@ describe('tonal', () => { }); it('snaps notes (upwards) to scale', () => { const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb']; - let expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3']; - - // Notes are converted to midi by scale - expectedNotes = expectedNotes.map((note) => noteToMidi(note)); + const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3']; expect( note(seq(inputNotes)) @@ -81,6 +78,26 @@ describe('tonal', () => { .firstCycleValues.map((h) => h.note), ).toEqual(expectedNotes); }); + it('snaps notes to the correct octave', () => { + const inputNotes = ['Cb0', 'Eb4', 'G1', 'A#19', 'Bb8']; + const expectedNotes = ['B#0', 'D#4', 'G#1', 'A#19', 'A#8']; + + expect( + note(seq(inputNotes)) + .scale('A# minor') // A#, B#, C#, D#, E#, F#, G# + .firstCycleValues.map((h) => h.note), + ).toEqual(expectedNotes); + }); + it('handles scale names provided with colons', () => { + const inputNotes = ['Cb', 'E', 'G', 'A#', 'Bb']; + const expectedNotes = ['A#2', 'D#3', 'G#3', 'A#3', 'A#3']; + + expect( + note(seq(inputNotes)) + .scale('F#:pentatonic') // F#, G#, A#, C#, and D# + .firstCycleValues.map((h) => h.note), + ).toEqual(expectedNotes); + }); }); describe('transpose', () => { it('transposes note numbers with interval numbers', () => { diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 5e505c75a..28e2606e1 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -6,20 +6,28 @@ This program is free software: you can redistribute it and/or modify it under th import { Note, Interval, Scale } from '@tonaljs/tonal'; import { register, _mod, silence, logger, pure, isNote } from '@strudel/core'; -import { stepInNamedScale, scaleToChromas } from './tonleiter.mjs'; +import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs'; import { noteToMidi } from '../core/util.mjs'; const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P'; -function scaleStep(step, scale) { - scale = scale.replaceAll(':', ' '); - step = Math.ceil(step); - let { intervals, tonic, empty } = Scale.get(scale); - if ((empty && isNote(scale)) || (empty && !tonic)) { - throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`); +function getScale(scaleName) { + scaleName = scaleName.replaceAll(':', ' '); + const scale = Scale.get(scaleName); + const { tonic, empty } = scale; + if ((empty && isNote(scaleName)) || (empty && !tonic)) { + throw new Error( + `Scale name ${scaleName} is incomplete. Make sure to use ":" instead of spaces, example: .scale("C:major")`, + ); } else if (empty) { - throw new Error(`invalid scale "${scale}"`); + throw new Error(`Invalid scale name "${scaleName}"`); } + return scale; +} + +function scaleStep(step, scale) { + step = Math.ceil(step); + let { intervals, tonic } = getScale(scale); tonic = tonic || 'C'; const { pc, oct = 3 } = Note.get(tonic); const octaveOffset = Math.floor(step / intervals.length); @@ -31,8 +39,7 @@ function scaleStep(step, scale) { // transpose note inside scale by offset steps // function scaleOffset(scale: string, offset: number, note: string) { function scaleOffset(scale, offset, note) { - let [tonic, scaleName] = Scale.tokenize(scale); - let { notes } = Scale.get(`${tonic} ${scaleName}`); + let { notes } = getScale(scale); notes = notes.map((note) => Note.get(note).pc); // use only pc! offset = Number(offset); if (isNaN(offset)) { @@ -197,19 +204,37 @@ function _convertStepToNumberAndOffset(step) { return [asNumber, offset]; } +let scaleToMidisAndNotes = {}; // Finds the nearest scale note to `note` -function _getNearestScaleNote(scaleName, note) { - let midiNote = typeof note === 'string' ? noteToMidi(note) : note; - const octave = (midiNote / 12) >> 0; - const targetChroma = midiNote % 12; - const scaleChromas = scaleToChromas(scaleName); - return ( - scaleChromas.reduce((prev, curr) => { - // Include equality so ties are broken upwards - return Math.abs(curr - targetChroma) <= Math.abs(prev - targetChroma) ? curr : prev; - }) + - octave * 12 - ); +function _getNearestScaleNote(scaleName, note, preferHigher = true) { + let noteMidi = typeof note === 'string' ? noteToMidi(note) : note; + noteMidi = Math.max(noteMidi, 24); // we will not play notes below C0 + if (scaleToMidisAndNotes[scaleName] === undefined) { + const { intervals, tonic } = getScale(scaleName); + const { pc } = Note.get(tonic); + const expandedIntervals = intervals.concat('8P'); // add the octave for wrapping + const sNotes = expandedIntervals.map((interval) => Note.transpose(pc + '0', interval)); + const sMidi = sNotes.map(noteToMidi); + // Cache + scaleToMidisAndNotes[scaleName] = [sMidi, sNotes]; + } + const [scaleMidis, scaleNotes] = scaleToMidisAndNotes[scaleName]; + const rootMidi = scaleMidis[0]; + const octaveDiff = Math.floor((noteMidi - rootMidi) / 12); + let filteredNotes = []; // we must filter the notes to avoid negative octave values + let filteredMidis = []; + for (let i = 0; i < scaleMidis.length; i++) { + const newMidi = scaleMidis[i] + 12 * octaveDiff; + if (newMidi < 24) { + continue; + } + filteredMidis.push(newMidi); + const oldNote = scaleNotes[i]; + const newNote = Note.transpose(oldNote, Interval.fromSemitones(12 * octaveDiff)); + filteredNotes.push(newNote); + } + const noteIdx = nearestNumberIndex(noteMidi, filteredMidis, preferHigher); + return filteredNotes[noteIdx]; } /** @@ -254,15 +279,15 @@ export const scale = register( pat .fmap((value) => { const isObject = typeof value === 'object'; - // The case where the note has been defined via `n` - if ((isObject && 'n' in value) || !isObject) { - let step = isObject ? value.n : value; + // The case where the note has been defined via `n` or `pure` + if (!isObject || (isObject && ('n' in value || 'value' in value))) { + const step = isObject ? (value.n ?? value.value) : value; delete value.n; // remove n so it won't cause trouble if (isNote(step)) { // legacy.. return pure(step); } - let [number, offset] = _convertStepToNumberAndOffset(step); + const [number, offset] = _convertStepToNumberAndOffset(step); try { let note; if (isObject && value.anchor) { @@ -280,7 +305,7 @@ export const scale = register( } // The case where the note has been defined via `note` else { - let note = _getNearestScaleNote(scale, value.note); + const note = _getNearestScaleNote(scale, value.note); return pure(isObject ? { ...value, note } : note); } }) diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index 39c819cd7..233129641 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -101,11 +101,11 @@ export function nearestNumberIndex(target, numbers, preferHigher) { let scaleSteps = {}; // [scaleName]: semitones[] export function stepInNamedScale(step, scale, anchor, preferHigher) { - let [root, scaleName] = Scale.tokenize(scale); + const [root, scaleName] = Scale.tokenize(scale); const rootMidi = x2midi(root); const rootChroma = midi2chroma(rootMidi); if (!scaleSteps[scaleName]) { - let { intervals } = Scale.get(`C ${scaleName}`); + const { intervals } = Scale.get(`C ${scaleName}`); // cache result scaleSteps[scaleName] = intervals.map(step2semitones); } @@ -236,16 +236,3 @@ export function transpose(note, step) { const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E" return [targetNote, offsetAccidentals].join(''); } - -// Converts a `scaleName` into a corresponding list of chromas between 0 and 12 -export function scaleToChromas(scaleName) { - if (Array.isArray(scaleName)) { - scaleName = scaleName.flat().join(' '); - } - const [tonic, name] = Scale.tokenize(scaleName); - const rootMidi = noteToMidi(tonic); - const chroma = rootMidi % 12; - const intervals = Scale.get(name).intervals; - const scaleSteps = intervals.map(Interval.semitones); - return scaleSteps.map((s) => (s + chroma) % 12); -} From 79453ac2c3c71a1ba4d2ce34471cd125a2744cd5 Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 22 Aug 2025 13:17:13 -0500 Subject: [PATCH 133/257] Allow negatives and multi-accidentals --- packages/core/util.mjs | 4 ++-- packages/superdough/util.mjs | 2 +- packages/tonal/test/tonal.test.mjs | 2 +- packages/tonal/tonal.mjs | 24 +++++------------------- 4 files changed, 9 insertions(+), 23 deletions(-) diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 2e7c6e026..ef3f1e961 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -8,12 +8,12 @@ import { logger } from './logger.mjs'; // returns true if the given string is a note export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name); -export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name); +export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]?$/.test(name); export const tokenizeNote = (note) => { if (typeof note !== 'string') { return []; } - const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || []; + const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || []; if (!pc) { return []; } diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index f4d59024e..764ebb43e 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -7,7 +7,7 @@ export const tokenizeNote = (note) => { if (typeof note !== 'string') { return []; } - const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || []; + const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || []; if (!pc) { return []; } diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index a64b455b9..cb856fa99 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -80,7 +80,7 @@ describe('tonal', () => { }); it('snaps notes to the correct octave', () => { const inputNotes = ['Cb0', 'Eb4', 'G1', 'A#19', 'Bb8']; - const expectedNotes = ['B#0', 'D#4', 'G#1', 'A#19', 'A#8']; + const expectedNotes = ['B#-1', 'D#4', 'G#1', 'A#19', 'A#8']; expect( note(seq(inputNotes)) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 28e2606e1..2ec698c38 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -128,10 +128,7 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr const interval = !isNaN(Number(intervalOrSemitones)) ? Interval.fromSemitones(intervalOrSemitones) : String(intervalOrSemitones); - // TODO: move simplify to player to preserve enharmonics - // tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3 - // TODO: check if this is still relevant.. - const targetNote = Note.simplify(Note.transpose(note, interval)); + const targetNote = Note.transpose(note, interval); if (typeof hap.value === 'object') { return hap.withValue(() => ({ ...hap.value, note: targetNote })); } @@ -208,7 +205,6 @@ let scaleToMidisAndNotes = {}; // Finds the nearest scale note to `note` function _getNearestScaleNote(scaleName, note, preferHigher = true) { let noteMidi = typeof note === 'string' ? noteToMidi(note) : note; - noteMidi = Math.max(noteMidi, 24); // we will not play notes below C0 if (scaleToMidisAndNotes[scaleName] === undefined) { const { intervals, tonic } = getScale(scaleName); const { pc } = Note.get(tonic); @@ -221,20 +217,10 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) { const [scaleMidis, scaleNotes] = scaleToMidisAndNotes[scaleName]; const rootMidi = scaleMidis[0]; const octaveDiff = Math.floor((noteMidi - rootMidi) / 12); - let filteredNotes = []; // we must filter the notes to avoid negative octave values - let filteredMidis = []; - for (let i = 0; i < scaleMidis.length; i++) { - const newMidi = scaleMidis[i] + 12 * octaveDiff; - if (newMidi < 24) { - continue; - } - filteredMidis.push(newMidi); - const oldNote = scaleNotes[i]; - const newNote = Note.transpose(oldNote, Interval.fromSemitones(12 * octaveDiff)); - filteredNotes.push(newNote); - } - const noteIdx = nearestNumberIndex(noteMidi, filteredMidis, preferHigher); - return filteredNotes[noteIdx]; + const alignedMidis = scaleMidis.map((m) => m + 12 * octaveDiff); + const noteIdx = nearestNumberIndex(noteMidi, alignedMidis, preferHigher); + const noteMatch = scaleNotes[noteIdx]; + return Note.transpose(noteMatch, Interval.fromSemitones(12 * octaveDiff)); } /** From b8c46d6b26f7b3d5864c3f3b02e1ec90468a510d Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 22 Aug 2025 13:23:41 -0500 Subject: [PATCH 134/257] Added some description and examples of multi-accidentals and negative octaves --- packages/core/controls.mjs | 4 +- packages/tonal/tonal.mjs | 2 + test/__snapshots__/examples.test.mjs.snap | 90 +++++++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index e6181d453..de152d417 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -113,7 +113,7 @@ export const { n } = registerControl('n'); * * - a letter (a-g or A-G) * - optional accidentals (b or #) - * - optional octave number (0-9). Defaults to 3 + * - optional (possibly negative) octave number (0-9). Defaults to 3 * * Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5` * @@ -126,6 +126,8 @@ export const { n } = registerControl('n'); * note("c4 a4 f4 e4") * @example * note("60 69 65 64") + * @example + * note("fbb1 a#0 cbbb-1 e##-2").sound("saw") */ export const { note } = registerControl(['note', 'n']); diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 2ec698c38..4f189e3d6 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -252,6 +252,8 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) { * n("<[0,7b] [-4# -4] [-2,7##] 4 [0,7] [-4# -4b] [-2,7###] 4b>*4") * .scale("C:/2") * .s("piano") + * @example + * note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3) */ export const scale = register( diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 09482375e..77c2773ce 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -6436,6 +6436,27 @@ exports[`runs examples > example "note" example index 2 1`] = ` ] `; +exports[`runs examples > example "note" example index 3 1`] = ` +[ + "[ 0/1 → 1/4 | note:fbb1 s:saw ]", + "[ 1/4 → 1/2 | note:a#0 s:saw ]", + "[ 1/2 → 3/4 | note:cbbb-1 s:saw ]", + "[ 3/4 → 1/1 | note:e##-2 s:saw ]", + "[ 1/1 → 5/4 | note:fbb1 s:saw ]", + "[ 5/4 → 3/2 | note:a#0 s:saw ]", + "[ 3/2 → 7/4 | note:cbbb-1 s:saw ]", + "[ 7/4 → 2/1 | note:e##-2 s:saw ]", + "[ 2/1 → 9/4 | note:fbb1 s:saw ]", + "[ 9/4 → 5/2 | note:a#0 s:saw ]", + "[ 5/2 → 11/4 | note:cbbb-1 s:saw ]", + "[ 11/4 → 3/1 | note:e##-2 s:saw ]", + "[ 3/1 → 13/4 | note:fbb1 s:saw ]", + "[ 13/4 → 7/2 | note:a#0 s:saw ]", + "[ 7/2 → 15/4 | note:cbbb-1 s:saw ]", + "[ 15/4 → 4/1 | note:e##-2 s:saw ]", +] +`; + exports[`runs examples > example "nrpnn" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", @@ -8699,6 +8720,75 @@ exports[`runs examples > example "scale" example index 3 1`] = ` ] `; +exports[`runs examples > example "scale" example index 4 1`] = ` +[ + "[ 0/1 → 1/16 | note:Gb1 ]", + "[ 1/16 → 1/8 | note:Gb1 ]", + "[ 1/8 → 3/16 | note:Gb1 ]", + "[ 3/16 → 1/4 | note:Gb1 ]", + "[ 1/4 → 5/16 | note:Gb1 ]", + "[ 5/16 → 3/8 | note:Gb1 ]", + "[ 3/8 → 7/16 | note:Gb1 ]", + "[ 7/16 → 1/2 | note:Gb1 ]", + "[ 1/2 → 9/16 | note:Gb1 ]", + "[ 9/16 → 5/8 | note:Gb1 ]", + "[ 5/8 → 11/16 | note:Gb1 ]", + "[ 11/16 → 3/4 | note:Gb1 ]", + "[ 3/4 → 13/16 | note:Gb1 ]", + "[ 13/16 → 7/8 | note:Gb1 ]", + "[ 7/8 → 15/16 | note:Gb1 ]", + "[ 15/16 → 1/1 | note:Gb1 ]", + "[ 1/1 → 17/16 | note:Cb3 ]", + "[ 17/16 → 9/8 | note:Cb3 ]", + "[ 9/8 → 19/16 | note:Cb3 ]", + "[ 19/16 → 5/4 | note:Cb3 ]", + "[ 5/4 → 21/16 | note:Cb3 ]", + "[ 21/16 → 11/8 | note:Cb3 ]", + "[ 11/8 → 23/16 | note:Cb3 ]", + "[ 23/16 → 3/2 | note:Cb3 ]", + "[ 3/2 → 25/16 | note:Cb3 ]", + "[ 25/16 → 13/8 | note:Cb3 ]", + "[ 13/8 → 27/16 | note:Cb3 ]", + "[ 27/16 → 7/4 | note:Cb3 ]", + "[ 7/4 → 29/16 | note:Cb3 ]", + "[ 29/16 → 15/8 | note:Cb3 ]", + "[ 15/8 → 31/16 | note:Cb3 ]", + "[ 31/16 → 2/1 | note:Cb3 ]", + "[ 2/1 → 33/16 | note:Eb4 ]", + "[ 33/16 → 17/8 | note:Eb4 ]", + "[ 17/8 → 35/16 | note:Eb4 ]", + "[ 35/16 → 9/4 | note:Eb4 ]", + "[ 9/4 → 37/16 | note:Eb4 ]", + "[ 37/16 → 19/8 | note:Eb4 ]", + "[ 19/8 → 39/16 | note:Eb4 ]", + "[ 39/16 → 5/2 | note:Eb4 ]", + "[ 5/2 → 41/16 | note:Eb4 ]", + "[ 41/16 → 21/8 | note:Eb4 ]", + "[ 21/8 → 43/16 | note:Eb4 ]", + "[ 43/16 → 11/4 | note:Eb4 ]", + "[ 11/4 → 45/16 | note:Eb4 ]", + "[ 45/16 → 23/8 | note:Eb4 ]", + "[ 23/8 → 47/16 | note:Eb4 ]", + "[ 47/16 → 3/1 | note:Eb4 ]", + "[ 3/1 → 49/16 | note:Db2 ]", + "[ 49/16 → 25/8 | note:Db2 ]", + "[ 25/8 → 51/16 | note:Db2 ]", + "[ 51/16 → 13/4 | note:Db2 ]", + "[ 13/4 → 53/16 | note:Db2 ]", + "[ 53/16 → 27/8 | note:Db2 ]", + "[ 27/8 → 55/16 | note:Db2 ]", + "[ 55/16 → 7/2 | note:Db2 ]", + "[ 7/2 → 57/16 | note:Db2 ]", + "[ 57/16 → 29/8 | note:Db2 ]", + "[ 29/8 → 59/16 | note:Db2 ]", + "[ 59/16 → 15/4 | note:Db2 ]", + "[ 15/4 → 61/16 | note:Db2 ]", + "[ 61/16 → 31/8 | note:Db2 ]", + "[ 31/8 → 63/16 | note:Db2 ]", + "[ 63/16 → 4/1 | note:Db2 ]", +] +`; + exports[`runs examples > example "scaleTranspose" example index 0 1`] = ` [ "[ 0/1 → 1/2 | note:C3 ]", From b8c98736c5024fc213ed4e84edb92c7a2e5c3c0b Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 23 Aug 2025 17:29:15 +0100 Subject: [PATCH 135/257] fix benchmarks --- packages/core/bench/pattern.bench.mjs | 12 ++++++------ packages/mini/bench/mini.bench.mjs | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/bench/pattern.bench.mjs b/packages/core/bench/pattern.bench.mjs index 1b5be0b9c..56a849801 100644 --- a/packages/core/bench/pattern.bench.mjs +++ b/packages/core/bench/pattern.bench.mjs @@ -1,11 +1,11 @@ import { describe, bench } from 'vitest'; -import { calculateTactus, sequence, stack } from '../index.mjs'; +import { calculateSteps, sequence, stack } from '../index.mjs'; const pat64 = sequence(...Array(64).keys()); describe('steps', () => { - calculateTactus(true); + calculateSteps(true); bench( '+tactus', () => { @@ -14,7 +14,7 @@ describe('steps', () => { { time: 1000 }, ); - calculateTactus(false); + calculateSteps(false); bench( '-tactus', () => { @@ -25,7 +25,7 @@ describe('steps', () => { }); describe('stack', () => { - calculateTactus(true); + calculateSteps(true); bench( '+tactus', () => { @@ -34,7 +34,7 @@ describe('stack', () => { { time: 1000 }, ); - calculateTactus(false); + calculateSteps(false); bench( '-tactus', () => { @@ -43,4 +43,4 @@ describe('stack', () => { { time: 1000 }, ); }); -calculateTactus(true); +calculateSteps(true); diff --git a/packages/mini/bench/mini.bench.mjs b/packages/mini/bench/mini.bench.mjs index 782ac86ba..e7471bf22 100644 --- a/packages/mini/bench/mini.bench.mjs +++ b/packages/mini/bench/mini.bench.mjs @@ -1,10 +1,10 @@ import { describe, bench } from 'vitest'; -import { calculateTactus } from '../../core/index.mjs'; +import { calculateSteps } from '../../core/index.mjs'; import { mini } from '../index.mjs'; describe('mini', () => { - calculateTactus(true); + calculateSteps(true); bench( '+tactus', () => { @@ -13,7 +13,7 @@ describe('mini', () => { { time: 1000 }, ); - calculateTactus(false); + calculateSteps(false); bench( '-tactus', () => { @@ -21,5 +21,5 @@ describe('mini', () => { }, { time: 1000 }, ); - calculateTactus(true); + calculateSteps(true); }); From 64f7bc444288a98410da299b6aa7b1e62b9a3110 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 19 Aug 2025 09:12:29 -0500 Subject: [PATCH 136/257] Working version of duck on iOS safari --- packages/superdough/helpers.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index c09acf1a5..1217b5163 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -211,7 +211,14 @@ export function getVibratoOscillator(param, value, t) { export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { const constantNode = new ConstantSourceNode(audioContext); + // Safari 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); constantNode.start(startTime); + + // Schedule the `onComplete` callback to occur at `stopTime` constantNode.stop(stopTime); constantNode.onended = () => { onComplete(); From 659071a4ee6b7a67f0e1a5c46a10c0f854bd3ffa Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 19 Aug 2025 16:18:48 -0500 Subject: [PATCH 137/257] Ensure zeroGain is cleaned up after use; move start/stop to after callback assigned for safety --- packages/superdough/helpers.mjs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 1217b5163..53922aa2c 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -211,18 +211,25 @@ export function getVibratoOscillator(param, value, t) { export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { const constantNode = new ConstantSourceNode(audioContext); - // Safari requires audio nodes to be connected in order for their onended events + // 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); - constantNode.start(startTime); // Schedule the `onComplete` callback to occur at `stopTime` - constantNode.stop(stopTime); constantNode.onended = () => { + // Ensure garbage collection + try { + zeroGain.disconnect(); + } catch {} + try { + constantNode.disconnect(); + } catch {} onComplete(); }; + constantNode.start(startTime); + constantNode.stop(stopTime); return constantNode; } const mod = (freq, range = 1, type = 'sine') => { From d64a0ef0eb8d79d90ff8fb51dce0ef47631f6d1b Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 19 Aug 2025 16:21:00 -0500 Subject: [PATCH 138/257] Lint requires no empty blocks: pass comment --- packages/superdough/helpers.mjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 53922aa2c..81bec9399 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -222,10 +222,14 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { // Ensure garbage collection try { zeroGain.disconnect(); - } catch {} + } catch { + // pass + } try { constantNode.disconnect(); - } catch {} + } catch { + // pass + } onComplete(); }; constantNode.start(startTime); From 3d25aa1b91ec75532682c3d786ca5d862c0c374f Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 26 Aug 2025 12:06:10 -0500 Subject: [PATCH 139/257] Rename params, more examples --- packages/core/controls.mjs | 74 ++++++--- packages/superdough/superdough.mjs | 16 +- test/__snapshots__/examples.test.mjs.snap | 191 +++++++++++++++++----- 3 files changed, 217 insertions(+), 64 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 4d6f8d8b7..4deb6f77f 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -542,13 +542,19 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape'); */ /** - * Modulate the amplitude of an orbit to create a "sidechain" like effect + * Modulate the amplitude of an orbit to create a "sidechain" like effect. + * + * Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")` * * @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) + * @example + * $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2) + * $: s("hh*16").orbit(3) + * $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth(1) * */ export const { duck } = registerControl('duckorbit', 'duck'); @@ -556,40 +562,70 @@ export const { duck } = registerControl('duckorbit', 'duck'); /** * The amount of ducking applied to target orbit * + * Can vary across orbits with the ':' mininotation, e.g. `duckdepth("0.3:0.1")`. + * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. + * * @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>")) + * @example + * $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2) + * $: s("hh*16").orbit(3) + * $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth("1:0.5") * */ - export const { duckdepth } = registerControl('duckdepth'); /** - * The attack time of the duck effect. Can be used to prevent clicking or for creative rhythmic effects + * The time required for the ducked signal(s) to reach their lowest volume. + * Can be used to prevent clicking or for creative rhythmic effects. + + * Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`. + * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. + * + * @name duckonset + * @synonyms duckons + * + * @param {number | Pattern} time The onset time in seconds + * @example + * // Clicks + * sound: n(run(8)).scale("c:minor").s("sawtooth").lpf(200).delay(.7).orbit(2) + * duckerWithClick: s("bd*4").duckorbit(2).duckonset(0).postgain(0) + * @example + * // No clicks + * sound: n(run(8)).scale("c:minor").s("sawtooth").lpf(200).delay(.7).orbit(2) + * duckerWithoutClick: s("bd*4").duckorbit(2).duckonset(0.003).postgain(0) + * @example + * // Rhythmic + * noise: s("pink").distort("2:1").orbit(4) // used rhythmically with 0.3 onset below + * hhat: s("hh*16").orbit(7) + * ducker: s("bd*4").bank("tr909").duckorbit("4:7").duckonset("0.3:0.003").duckattack(0.25) + * + */ +export const { duckonset } = registerControl('duckonset', 'duckons'); + +/** + * The time required for the ducked signal(s) to return to their normal volume. + + * Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`. + * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. * * @name duckattack - * @param {number | Pattern} time + * @synonyms duckatt + * + * @param {number | Pattern} time The attack time in seconds * @example - * sound: n(run(8)).scale("c:minor").s("sawtooth").lpf(200).delay(.7).orbit(2) - * duckerWithClick: s("bd*4").duckorbit(2).duckattack(0).postgain(0) - * _duckerWithoutClick: s("bd*4").duckorbit(2).duckattack(0.003).postgain(0) + * sound: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2) + * ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1) + * @example + * moreduck: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2) + * lessduck: s("hh*16").orbit(5) + * ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1") * */ export const { duckattack } = registerControl('duckattack', 'duckatt'); -/** - * The release time of the duck effect - * - * @name duckrelease - * @param {number | Pattern} time - * @example - * sound: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2) - * ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckrelease("<0.2 0 0.4>").duckdepth(1) - * - */ -export const { duckrelease } = registerControl('duckrelease', 'duckrelease'); - export const { drive } = registerControl('drive'); /** diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index b8975a852..98ffae16a 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -421,10 +421,10 @@ function setOrbit(audioContext, orbit, channels) { } } -function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.003, releasetime = 0.1, duckdepth = 1) { +function duckOrbit(audioContext, targetOrbit, t, onsettime = 0.003, attacktime = 0.1, duckdepth = 1) { const targetArr = [targetOrbit].flat(); + const onsetArr = [onsettime].flat(); const attackArr = [attacktime].flat(); - const releaseArr = [releasetime].flat(); const depthArr = [duckdepth].flat(); targetArr.forEach((target, idx) => { @@ -432,8 +432,8 @@ function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.003, releasetime errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); return; } - const attack = attackArr[idx] ?? attackArr[0]; - const release = Math.max(releaseArr[idx] ?? releaseArr[0], 0.002); + const onset = onsetArr[idx] ?? onsetArr[0]; + const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002); const depth = depthArr[idx] ?? depthArr[0]; const gainParam = orbits[target].output.gain; webAudioTimeout( @@ -448,8 +448,8 @@ function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.003, releasetime // that method gainParam.setValueAtTime(currVal, t); - gainParam.exponentialRampToValueAtTime(duckedVal, t + attack); - gainParam.exponentialRampToValueAtTime(1, t + attack + release); + gainParam.exponentialRampToValueAtTime(duckedVal, t + onset); + gainParam.exponentialRampToValueAtTime(1, t + onset + attack); }, 0, t - 0.01, @@ -578,8 +578,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) postgain = getDefaultValue('postgain'), density = getDefaultValue('density'), duckorbit, + duckonset, duckattack, - duckrelease, duckdepth, // filters fanchor = getDefaultValue('fanchor'), @@ -658,7 +658,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) setOrbit(ac, orbit, channels, t, cycle, cps); if (duckorbit != null) { - duckOrbit(ac, duckorbit, t, duckattack, duckrelease, duckdepth); + duckOrbit(ac, duckorbit, t, duckonset, duckattack, duckdepth); } gain = applyGainCurve(nanFallback(gain, 1)); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 85079269e..4655b5cd9 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3038,22 +3038,51 @@ exports[`runs examples > example "dry" example index 0 1`] = ` exports[`runs examples > example "duckattack" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", - "[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.003 postgain:0 ]", + "[ 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 duckdepth:1 ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.4 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 "duckattack" example index 1 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", ] `; @@ -3114,6 +3143,94 @@ exports[`runs examples > example "duckdepth" example index 0 1`] = ` ] `; +exports[`runs examples > example "duckdepth" example index 1 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", +] +`; + +exports[`runs examples > example "duckonset" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 1/4 → 1/2 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 1/2 → 3/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 3/4 → 1/1 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 1/1 → 5/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 5/4 → 3/2 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 3/2 → 7/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 7/4 → 2/1 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 2/1 → 9/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 9/4 → 5/2 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 5/2 → 11/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 11/4 → 3/1 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 3/1 → 13/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 13/4 → 7/2 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 7/2 → 15/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 15/4 → 4/1 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", +] +`; + +exports[`runs examples > example "duckonset" example index 1 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 1/4 → 1/2 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 1/2 → 3/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 3/4 → 1/1 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 1/1 → 5/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 5/4 → 3/2 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 3/2 → 7/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 7/4 → 2/1 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 2/1 → 9/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 9/4 → 5/2 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 5/2 → 11/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 11/4 → 3/1 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 3/1 → 13/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 13/4 → 7/2 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 7/2 → 15/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 15/4 → 4/1 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", +] +`; + +exports[`runs examples > example "duckonset" example index 2 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 1/4 → 1/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 1/2 → 3/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 3/4 → 1/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 1/1 → 5/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 5/4 → 3/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 3/2 → 7/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 7/4 → 2/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 2/1 → 9/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 9/4 → 5/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 5/2 → 11/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 11/4 → 3/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 3/1 → 13/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 13/4 → 7/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 7/2 → 15/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 15/4 → 4/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", +] +`; + exports[`runs examples > example "duckorbit" example index 0 1`] = ` [ "[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", @@ -3139,28 +3256,28 @@ exports[`runs examples > example "duckorbit" example index 0 1`] = ` ] `; -exports[`runs examples > example "duckrelease" example index 0 1`] = ` +exports[`runs examples > example "duckorbit" example index 1 1`] = ` [ - "[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", - "[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", - "[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", - "[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", - "[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", - "[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckrelease:0 duckdepth:1 ]", - "[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckrelease:0 duckdepth:1 ]", - "[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckrelease:0 duckdepth:1 ]", - "[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckrelease:0 duckdepth:1 ]", - "[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckrelease:0 duckdepth:1 ]", - "[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckrelease:0.4 duckdepth:1 ]", - "[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckrelease:0.4 duckdepth:1 ]", - "[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckrelease:0.4 duckdepth:1 ]", - "[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckrelease:0.4 duckdepth:1 ]", - "[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckrelease:0.4 duckdepth:1 ]", - "[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", - "[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", - "[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", - "[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", - "[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckrelease:0.2 duckdepth:1 ]", + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", ] `; From 022ac95bbd169b9a490e3bbccaa9933d0ba61f8e Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 26 Aug 2025 12:09:12 -0500 Subject: [PATCH 140/257] Typo --- packages/core/controls.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 4deb6f77f..25d2e52f0 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -580,7 +580,7 @@ export const { duckdepth } = registerControl('duckdepth'); /** * The time required for the ducked signal(s) to reach their lowest volume. * Can be used to prevent clicking or for creative rhythmic effects. - + * * Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`. * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. * @@ -607,7 +607,7 @@ export const { duckonset } = registerControl('duckonset', 'duckons'); /** * The time required for the ducked signal(s) to return to their normal volume. - + * * Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`. * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. * From 18b1739b890b4659d936b80e17ccce74ffb1952e Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 26 Aug 2025 12:53:38 -0500 Subject: [PATCH 141/257] Add a space after paragraphs in descriptions and add padding to hover tooltips --- website/src/repl/Repl.css | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index b8443081f..b74eff218 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -72,8 +72,7 @@ /* Override default styles from the codemirror inline css for autocomplete info tooltip*/ .cm-tooltip.cm-completionInfo { - padding: 12px !important; - padding-bottom: 12px !important; + padding: 0 !important; border: 1px solid var(--foreground) !important; border-radius: 4px !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important; @@ -87,6 +86,8 @@ /* Main tooltip container */ .autocomplete-info-tooltip { + padding: 12px !important; + padding-bottom: 12px !important; border-radius: 4px !important; color: var(--foreground); font-family: var(--font-family, 'SF Mono', 'Monaco', monospace); @@ -97,6 +98,10 @@ min-width: 400px; } +.autocomplete-info-tooltip p { + margin-bottom: 1em; +} + .autocomplete-info-function-name { font-size: 15px; font-weight: 600; From 877bc95a5836bd4d45755dcc5ac673af1cc7e05b Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 26 Aug 2025 12:54:39 -0500 Subject: [PATCH 142/257] Switch to more specific class and fix p->div --- packages/codemirror/autocomplete.mjs | 2 +- website/src/repl/Repl.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index c0845895e..646107236 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -58,7 +58,7 @@ export const Autocomplete = ({ doc, label }) => h`

${label || getDocLabel(doc)}

- ${doc.description ? `

${doc.description}

` : ''} + ${doc.description ? `
${doc.description}
` : ''} ${buildParamsList(doc.params)} ${buildExamples(doc.examples)}
diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index b74eff218..ad8b6a40d 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -98,7 +98,7 @@ min-width: 400px; } -.autocomplete-info-tooltip p { +.autocomplete-info-function-description p { margin-bottom: 1em; } From a5886bb9d46259dea09539d29e7dc5bc9017e507 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 26 Aug 2025 14:14:00 -0500 Subject: [PATCH 143/257] Use white-space instead; fix scrolling; don't close autocomplete tooltips on click --- packages/codemirror/autocomplete.mjs | 14 ++++++++------ website/src/repl/Repl.css | 15 +++++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 646107236..59ca8adf2 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -56,11 +56,13 @@ const buildExamples = (examples) => export const Autocomplete = ({ doc, label }) => h` -
-

${label || getDocLabel(doc)}

- ${doc.description ? `
${doc.description}
` : ''} - ${buildParamsList(doc.params)} - ${buildExamples(doc.examples)} +
+
+

${label || getDocLabel(doc)}

+ ${doc.description ? `
${doc.description}
` : ''} + ${buildParamsList(doc.params)} + ${buildExamples(doc.examples)} +
`[0]; @@ -98,4 +100,4 @@ export const strudelAutocomplete = (context) => { }; export const isAutoCompletionEnabled = (enabled) => - enabled ? [autocompletion({ override: [strudelAutocomplete] })] : []; + enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : []; diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index ad8b6a40d..498679090 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -79,15 +79,12 @@ 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 { +.autocomplete-info-container { padding: 12px !important; - padding-bottom: 12px !important; border-radius: 4px !important; color: var(--foreground); font-family: var(--font-family, 'SF Mono', 'Monaco', monospace); @@ -96,10 +93,16 @@ max-width: 600px; max-height: 400px; min-width: 400px; + white-space: normal !important; + overflow-y: auto !important; } -.autocomplete-info-function-description p { - margin-bottom: 1em; +.autocomplete-info-tooltip { + overflow-y: auto !important; +} + +.autocomplete-info-function-description { + white-space: pre-wrap !important; } .autocomplete-info-function-name { From 02cd79a6d981df25f7d75ed6a47024565db29ed5 Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 25 Aug 2025 08:44:18 -0500 Subject: [PATCH 144/257] Add wavetable oscillator with scanning, warps, and detune --- packages/core/controls.mjs | 39 +++ packages/sampler/sample-server.mjs | 39 ++- packages/superdough/helpers.mjs | 28 +- packages/superdough/index.mjs | 1 + packages/superdough/superdough.mjs | 12 +- packages/superdough/synth.mjs | 27 +- packages/superdough/wavetable.mjs | 253 +++++++++++++++++++ packages/superdough/worklets.mjs | 393 ++++++++++++++++++++++++++--- 8 files changed, 715 insertions(+), 77 deletions(-) create mode 100644 packages/superdough/wavetable.mjs diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index e6181d453..ba77f1a35 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -87,6 +87,45 @@ export function registerControl(names, ...aliases) { */ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound'); +/** + * Position in the wavetable of the wavetable oscillator + * + * @name wtPos + * @param {number | Pattern} position Position in the wavetable from 0 to 1 + * @synonyms wavetablePosition + * + */ +export const { wtPos, wavetablePosition } = registerControl('wtPos', 'wavetablePosition'); + +/** + * Amount of warp (alteration of the waveform) to apply to the wavetable oscillator + * + * @name wtWarp + * @param {number | Pattern} amount Warp of the wavetable from 0 to 1 + * @synonyms wavetableWarp + * + */ +export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWarp'); + +/** + * Amount of warp (alteration of the waveform) to apply to the wavetable oscillator. + * + * The current options are: + * 0 = asym + * 1 = mirror + * 2 = bend+ + * 3 = bend- + * 4 = bend+/- + * 5 = sync + * 6 = quantize + * + * @name wtWarpMode + * @param {number | Pattern} mode Warp mode: an integer + * @synonyms wavetableWarpMode + * + */ +export const { wtWarpMode, wavetableWarpMode } = registerControl('wtWarpMode', 'wavetableWarpMode'); + /** * Define a custom webaudio node to use as a sound source. * diff --git a/packages/sampler/sample-server.mjs b/packages/sampler/sample-server.mjs index 08456add9..2832741aa 100644 --- a/packages/sampler/sample-server.mjs +++ b/packages/sampler/sample-server.mjs @@ -1,10 +1,10 @@ #!/usr/bin/env node import cowsay from 'cowsay'; -import { createReadStream, existsSync } from 'fs'; +import { createReadStream, existsSync, writeFileSync } from 'fs'; import { readdir } from 'fs/promises'; import http from 'http'; -import { join, sep } from 'path'; +import { join, sep, resolve } from 'path'; import os from 'os'; // eslint-disable-next-line @@ -36,17 +36,19 @@ async function getFilesInDirectory(directory) { return files; } -async function getBanks(directory) { +async function getBanks(directory, flat = false) { let files = await getFilesInDirectory(directory); let banks = {}; directory = directory.split(sep).join('/'); files = files.map((path) => { path = path.split(sep).join('/'); - const [bank] = path.split('/').slice(-2); + const subDir = path.replace(directory, ''); + const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore + const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension + let bank = flat ? subDirFlatStem : subDir.split('/')[0]; banks[bank] = banks[bank] || []; - const relativeUrl = path.replace(directory, ''); - banks[bank].push(relativeUrl); - return relativeUrl; + banks[bank].push(subDir); + return subDir; }); banks._base = `http://localhost:5432`; return { banks, files }; @@ -54,14 +56,25 @@ async function getBanks(directory) { const args = process.argv.slice(2); +function getArgValue(flag) { + const i = args.indexOf(flag); + if (i !== -1) { + const nextIsFlag = args[i + 1]?.startsWith('--') ?? true; + if (nextIsFlag) return true; + return args[i + 1]; + } +} + // eslint-disable-next-line -const directory = process.cwd(); +let directory = getArgValue('--dir') || process.cwd(); +directory = resolve(directory); if (args.includes('--json')) { - const { banks, files } = await getBanks(directory); + const { banks } = await getBanks(directory, getArgValue('--flat')); const json = JSON.stringify(banks); - console.log(json); - process.exit(0); + const outFile = resolve(directory, 'strudel.json'); + writeFileSync(outFile, json, 'utf8'); + console.log(`Wrote json to ${outFile}`); } console.log( @@ -74,7 +87,7 @@ console.log( const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); - const { banks, files } = await getBanks(directory); + const { banks, files } = await getBanks(directory, getArgValue('--flat')); if (req.url === '/') { res.setHeader('Content-Type', 'application/json'); return res.end(JSON.stringify(banks)); @@ -82,7 +95,7 @@ const server = http.createServer(async (req, res) => { let subpath = decodeURIComponent(req.url); const filePath = join(directory, subpath.split('/').join(sep)); - //console.log('GET:', filePath); + // console.log('GET:', filePath); const isFound = existsSync(filePath); if (!isFound) { res.statusCode = 404; diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 81bec9399..7879cff00 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,5 +1,5 @@ import { getAudioContext } from './superdough.mjs'; -import { clamp, nanFallback } from './util.mjs'; +import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs'; import { getNoiseBuffer } from './noise.mjs'; export const noises = ['pink', 'white', 'brown', 'crackle']; @@ -21,7 +21,9 @@ const getSlope = (y1, y2, x1, x2) => { export function getWorklet(ac, processor, params, config) { const node = new AudioWorkletNode(ac, processor, config); Object.entries(params).forEach(([key, value]) => { - node.parameters.get(key).value = value; + if (value !== undefined) { + node.parameters.get(key).value = value; + } }); return node; } @@ -307,3 +309,25 @@ export function applyFM(param, value, begin) { } return { stop }; } + +export const getFrequencyFromValue = (value, defaultNote = 36) => { + let { note, freq } = value; + note = note || defaultNote; + if (typeof note === 'string') { + note = noteToMidi(note); // e.g. c3 => 48 + } + // get frequency + if (!freq && typeof note === 'number') { + freq = midiToFreq(note); // + 48); + } + + return Number(freq); +}; + +export const destroyAudioWorkletNode = (node) => { + if (node == null) { + return; + } + node.disconnect(); + node.parameters.get('end')?.setValueAtTime(0, 0); +}; diff --git a/packages/superdough/index.mjs b/packages/superdough/index.mjs index fd49fe338..a7e87ffae 100644 --- a/packages/superdough/index.mjs +++ b/packages/superdough/index.mjs @@ -11,3 +11,4 @@ export * from './synth.mjs'; export * from './zzfx.mjs'; export * from './logger.mjs'; export * from './dspworklet.mjs'; +export * from './wavetable.mjs'; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index eb1466d91..37579005a 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -555,6 +555,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) tremolophase = 0, tremoloshape, s = getDefaultValue('s'), + wt, bank, source, gain = getDefaultValue('gain'), @@ -681,8 +682,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) let sourceNode; if (source) { sourceNode = source(t, value, hapDuration, cps); - } else if (getSound(s)) { - const { onTrigger } = getSound(s); + } else { + const soundSource = wt ?? s; + const sound = getSound(soundSource); + if (!sound) { + throw new Error(`sound ${soundSource} not found! Is it loaded?`); + } + const { onTrigger } = sound; const onEnded = () => { audioNodes.forEach((n) => n?.disconnect()); activeSoundSources.delete(chainID); @@ -693,8 +699,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) sourceNode = soundHandle.node; activeSoundSources.set(chainID, soundHandle); } - } else { - throw new Error(`sound ${s} not found! Is it loaded?`); } if (!sourceNode) { // if onTrigger does not return anything, we will just silently skip diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 71138ae41..5b1b4edf1 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,39 +1,20 @@ -import { clamp, midiToFreq, noteToMidi } from './util.mjs'; +import { clamp } from './util.mjs'; import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs'; import { applyFM, + destroyAudioWorkletNode, gainNode, getADSRValues, + getFrequencyFromValue, getParamADSR, getPitchEnvelope, getVibratoOscillator, - webAudioTimeout, getWorklet, noises, + webAudioTimeout, } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; -const getFrequencyFromValue = (value, defaultNote = 36) => { - let { note, freq } = value; - note = note || defaultNote; - if (typeof note === 'string') { - note = noteToMidi(note); // e.g. c3 => 48 - } - // get frequency - if (!freq && typeof note === 'number') { - freq = midiToFreq(note); // + 48); - } - - return Number(freq); -}; -function destroyAudioWorkletNode(node) { - if (node == null) { - return; - } - node.disconnect(); - node.parameters.get('end')?.setValueAtTime(0, 0); -} - const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; const waveformAliases = [ ['tri', 'triangle'], diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs new file mode 100644 index 000000000..d18f1fdff --- /dev/null +++ b/packages/superdough/wavetable.mjs @@ -0,0 +1,253 @@ +import { getAudioContext, registerSound } from './index.mjs'; +import { clamp, getSoundIndex, valueToMidi } from './util.mjs'; +import { + destroyAudioWorkletNode, + getADSRValues, + getFrequencyFromValue, + getParamADSR, + getPitchEnvelope, + getVibratoOscillator, + getWorklet, + webAudioTimeout, +} from './helpers.mjs'; +import { logger } from './logger.mjs'; + +const WT_MAX_MIP_LEVELS = 6; +export const WarpMode = Object.freeze({ + NONE: 0, + ASYM: 1, + MIRROR: 2, + BENDP: 3, + BENDM: 4, + BENDMP: 5, + SYNC: 6, + QUANT: 7, + FOLD: 8, + PWM: 9, + ORBIT: 10, + SPIN: 11, + CHAOS: 12, + PRIMES: 13, + BINARY: 14, + BROWNIAN: 15, + RECIPROCAL: 16, + WORMHOLE: 17, + LOGISTIC: 18, + SIGMOID: 19, + FRACTAL: 20, + FLIP: 21, +}); + +async function loadWavetableFrames(url, label, frameLen = 256) { + const ac = getAudioContext(); + const buf = await loadBuffer(url, ac, label); + const ch0 = buf.getChannelData(0); + const total = ch0.length; + const numFrames = Math.floor(total / frameLen); + const frames = new Array(numFrames); + for (let i = 0; i < numFrames; i++) { + const start = i * frameLen; + frames[i] = ch0.subarray(start, start + frameLen); + } + + // build mipmaps + const mipmaps = [frames]; + let levelFrames = frames; + for (let level = 1; level < WT_MAX_MIP_LEVELS; level++) { + const prevLen = levelFrames[0].length; + if (prevLen <= 32) break; + const nextLen = prevLen >> 1; + const next = levelFrames.map((src) => { + const out = new Float32Array(nextLen); + for (let j = 0; j < nextLen; j++) { + out[j] = (src[2 * j] + src[2 * j + 1]) / 2; + } + return out; + }); + mipmaps.push(next); + levelFrames = next; + } + return { frames, mipmaps, frameLen, numFrames }; +} + +const loadCache = {}; + +function humanFileSize(bytes, si) { + var thresh = si ? 1000 : 1024; + if (bytes < thresh) return bytes + ' B'; + var units = si + ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + var u = -1; + do { + bytes /= thresh; + ++u; + } while (bytes >= thresh); + return bytes.toFixed(1) + ' ' + units[u]; +} + +export function getTableInfo(hapValue, bank) { + const { wt, n = 0 } = hapValue; + let midi = valueToMidi(hapValue, 36); + let transpose = midi - 36; // C3 is middle C; + const index = getSoundIndex(n, bank.length); + const tableUrl = bank[index]; + const label = `${wt}:${index}`; + return { transpose, tableUrl, index, midi, label }; +} + +const loadBuffer = (url, ac, wt, n = 0) => { + const label = wt ? `table "${wt}:${n}"` : 'table'; + url = url.replace('#', '%23'); + if (!loadCache[url]) { + logger(`[wavetable] load ${label}..`, 'load-table', { url }); + const timestamp = Date.now(); + loadCache[url] = fetch(url) + .then((res) => res.arrayBuffer()) + .then(async (res) => { + const took = Date.now() - timestamp; + const size = humanFileSize(res.byteLength); + logger(`[wavetable] load ${label}... done! loaded ${size} in ${took}ms`, 'loaded-table', { url }); + const decoded = await ac.decodeAudioData(res); + return decoded; + }); + } + return loadCache[url]; +}; + +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}`; +} + +const _processTables = (json, baseUrl, frameLen) => { + return Object.entries(json).forEach(([key, value]) => { + if (typeof value === 'string') { + value = [value]; + } + if (typeof value !== 'object') { + throw new Error('wrong json format for ' + key); + } + baseUrl = value._base || baseUrl; + if (baseUrl.startsWith('github:')) { + baseUrl = githubPath(baseUrl, ''); + } + value = value.map((v) => baseUrl + v); + registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, value, frameLen), { + type: 'wavetable', + tables: value, + baseUrl, + frameLen, + }); + }); +}; + +/** + * Loads a collection of wavetables to use with `wt` + * + * @name tables + */ +export const tables = async (url, frameLen, json) => { + if (json !== undefined) return _processTables(json, url, frameLen); + if (url.startsWith('github:')) { + url = githubPath(url, 'strudel.json'); + } + if (url.startsWith('local:')) { + url = `http://localhost:5432`; + } + 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; + } + return fetch(url) + .then((res) => res.json()) + .then((json) => _processTables(json, url, frameLen)) + .catch((error) => { + console.error(error); + throw new Error(`error loading "${url}"`); + }); +}; + +async function onTriggerSynth(t, value, onended, bank, frameLen) { + let { s, n = 0, duration } = value; + const ac = getAudioContext(); + let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); + let sourceDesc, holdEnd, envEnd; + let { unison = 5, spread = 0.6, detune, wtPos, wtWarp, wtWarpMode } = value; + if (typeof wtWarpMode === 'string') { + wtWarpMode = WarpMode[wtWarpMode.toUpperCase()] ?? WarpMode.NONE; + } + detune = detune ?? 0.18; + const frequency = getFrequencyFromValue(value); + const voices = clamp(unison, 1, 100); + let { tableUrl, label } = getTableInfo(value, bank); + const payload = await loadWavetableFrames(tableUrl, label, frameLen); + holdEnd = t + duration; + envEnd = holdEnd + release + 0.01; + const worklet = getWorklet( + ac, + 'wavetable-oscillator-processor', + { + begin: t, + end: envEnd, + frequency, + detune, + position: wtPos, + warp: wtWarp, + warpMode: wtWarpMode, + voices, + spread, + }, + { outputChannelCount: [2] }, + ); + worklet.port.postMessage({ type: 'tables', payload }); + sourceDesc = { source: worklet }; + const { source } = sourceDesc; + if (ac.currentTime > t) { + logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight'); + return; + } + if (!source) { + logger(`[wavetable] could not load "${s}:${n}"`, 'error'); + return; + } + let vibratoOscillator = getVibratoOscillator(source.detune, value, t); + const envGain = ac.createGain(); + const node = source.connect(envGain); + getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); + getPitchEnvelope(source.detune, value, t, holdEnd); + + const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... + node.connect(out); + let handle = { node: out, bufferSource: source }; + let timeoutNode = webAudioTimeout( + ac, + () => { + source.disconnect(); + destroyAudioWorkletNode(source); + vibratoOscillator?.stop(); + node.disconnect(); + out.disconnect(); + onended(); + }, + t, + envEnd, + ); + handle.stop = (time) => { + timeoutNode.stop(time); + }; + return handle; +} diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 2406d56dc..7775803d9 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -6,7 +6,21 @@ import OLAProcessor from './ola-processor'; 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; +const mod = (n, m) => ((n % m) + m) % m; +const lerp = (a, b, n) => n * (b - a) + a; +const pv = (arr, n) => arr[n] ?? arr[0]; +const frac = (x) => x - Math.floor(x); +const ffloor = (x) => x | 0; // fast floor for non-negative + +const getUnisonDetune = (unison, detune, voiceIndex) => { + if (unison < 2) { + return 0; + } + return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1)); +}; +const applySemitoneDetuneToFrequency = (frequency, detune) => { + return frequency * Math.pow(2, detune / 12); +}; // Restrict phase to the range [0, maxPhase) via wrapping function wrapPhase(phase, maxPhase = 1) { @@ -150,7 +164,7 @@ class LFOProcessor extends AudioWorkletProcessor { const blockSize = output[0].length ?? 0; if (this.phase == null) { - this.phase = _mod(time * frequency + phaseoffset, 1); + this.phase = mod(time * frequency + phaseoffset, 1); } const dt = frequency / sampleRate; for (let n = 0; n < blockSize; n++) { @@ -378,21 +392,6 @@ class DistortProcessor extends AudioWorkletProcessor { registerProcessor('distort-processor', DistortProcessor); // SUPERSAW -function lerp(a, b, n) { - return n * (b - a) + a; -} - -function getUnisonDetune(unison, detune, voiceIndex) { - if (unison < 2) { - return 0; - } - return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1)); -} - -function applySemitoneDetuneToFrequency(frequency, detune) { - return frequency * Math.pow(2, detune / 12); -} - class SuperSawOscillatorProcessor extends AudioWorkletProcessor { constructor() { super(); @@ -454,29 +453,31 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { } const output = outputs[0]; - const voices = params.voices[0]; - const freqspread = params.freqspread[0]; - const panspread = params.panspread[0] * 0.5 + 0.5; - const gain1 = Math.sqrt(1 - panspread); - const gain2 = Math.sqrt(panspread); - for (let n = 0; n < voices; n++) { - const isOdd = (n & 1) == 1; - let gainL = gain1; - let gainR = gain2; - // invert right and left gain - if (isOdd) { - gainL = gain2; - gainR = gain1; - } - for (let i = 0; i < output[0].length; i++) { - // Main detuning - let freq = applySemitoneDetuneToFrequency(params.frequency[i] ?? params.frequency[0], params.detune[0] / 100); + for (let i = 0; i < output[0].length; i++) { + const detune = pv(params.detune, i); + const voices = pv(params.voices, i); + const freqspread = pv(params.freqspread, i); + const panspread = pv(params.panspread, i) * 0.5 + 0.5; + const gain1 = Math.sqrt(1 - panspread); + const gain2 = Math.sqrt(panspread); + let freq = pv(params.frequency, i); + // Main detuning + freq = applySemitoneDetuneToFrequency(freq, detune / 100); + for (let n = 0; n < voices; n++) { + const isOdd = (n & 1) == 1; + let gainL = gain1; + let gainR = gain2; + // invert right and left gain + if (isOdd) { + gainL = gain2; + gainR = gain1; + } // 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); + const dt = mod(freq / sampleRate, 1); this.phase[n] = this.phase[n] ?? Math.random(); const v = waveshapes.sawblep(this.phase[n], dt); @@ -907,3 +908,325 @@ class ByteBeatProcessor extends AudioWorkletProcessor { } registerProcessor('byte-beat-processor', ByteBeatProcessor); + + +export const WarpMode = Object.freeze({ + NONE: 0, + ASYM: 1, + MIRROR: 2, + BENDP: 3, + BENDM: 4, + BENDMP: 5, + SYNC: 6, + QUANT: 7, + FOLD: 8, + PWM: 9, + ORBIT: 10, + SPIN: 11, + CHAOS: 12, + PRIMES: 13, + BINARY: 14, + BROWNIAN: 15, + RECIPROCAL: 16, + WORMHOLE: 17, + LOGISTIC: 18, + SIGMOID: 19, + FRACTAL: 20, + FLIP: 21, +}); + +function hash32(u) { + u = u + 0x7ed55d16 + (u << 12); + u = u ^ 0xc761c23c ^ (u >>> 19); + u = u + 0x165667b1 + (u << 5); + u = (u + 0xd3a2646c) ^ (u << 9); + u = u + 0xfd7046c5 + (u << 3); + u = u ^ 0xb55a4f09 ^ (u >>> 16); + return u >>> 0; +} +const hash01 = (i) => (hash32(i) >>> 8) / 0x01000000; + +function bitReverse(i, n) { + let r = 0; + for (let b = 0; b < n; b++) { + r = (r << 1) | (i & 1); + i >>>= 1; + } + return r; +} + +function noise(x) { + const i = Math.floor(x), + f = x - i; + const a = hash01(i), + b = hash01(i + 1); + return a + (b - a) * f; +} + +function brownian(x, oct = 4) { + let amp = 0.5, + sum = 0, + norm = 0, + freq = 1; + for (let o = 0; o < oct; o++) { + sum += amp * noise(x * freq); + norm += amp; + amp *= 0.5; + freq *= 2; + } + return (sum / norm) * 2 - 1; +} + +class WavetableOscillatorProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return [ + { name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, + { name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, + { name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 }, + { name: 'detune', defaultValue: 0 }, + { name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 }, + { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, + { name: 'warpMode', defaultValue: 0 }, + { name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 }, + { name: 'spread', defaultValue: 0, minValue: 0, maxValue: 1 }, + ]; + } + + constructor(options) { + super(options); + this.tables = null; + this.frameLen = 0; + this.numFrames = 0; + this.phase = []; + this.syncRatio = 1; + + this.port.onmessage = (e) => { + const { type, payload } = e.data || {}; + if (type === 'tables') { + this.tables = payload.mipmaps; + this.frameLen = payload.frameLen; + this.numFrames = this.tables[0].length; + } + }; + this.lfoPhase = 0; + } + + _chooseMip(dphi) { + const approxHarm = Math.min(64, 1 / Math.max(1e-6, dphi)); + let level = 0; + while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) { + level++; + } + return level; + } + + _mirror(x) { + return 1 - Math.abs(2 * x - 1); + } + + _toBits(amt, min = 2, max = 12) { + const b = max + (min - max) * amt; + return { b, n: Math.round(Math.pow(2, b)) }; + } + + _warpPhase(phase, amt, mode) { + switch (mode) { + case WarpMode.NONE: { + return phase; + } + case WarpMode.ASYM: { + const a = 0.01 + 0.99 * amt; + return phase < a ? (0.5 * phase) / a : 0.5 + (0.5 * (phase - a)) / (1 - a); + } + case WarpMode.MIRROR: { + // Asym, then mirror + return this._mirror(this._warpPhase(phase, amt, WarpMode.ASYM)); + } + case WarpMode.BENDP: { + return Math.pow(phase, 1 + 3 * amt); + } + case WarpMode.BENDM: { + return Math.pow(phase, 1 / (1 + 3 * amt)); + } + case WarpMode.BENDMP: { + return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2); + } + case WarpMode.SYNC: { + const syncRatio = Math.pow(16, amt * amt); + return (phase * syncRatio) % 1; + } + case WarpMode.QUANT: { + const { n } = this._toBits(amt); + return ffloor(phase * n) / n; + } + case WarpMode.FOLD: { + const K = 7; + const k = 1 + Math.max(1, Math.round(K * amt)); + return Math.abs(frac(k * phase) - 0.5) * 2; + } + case WarpMode.PWM: { + const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1); + if (phase < w) return (phase / w) * 0.5; + return 0.5 + ((phase - w) / (1 - w)) * 0.5; + } + case WarpMode.ORBIT: { + const depth = 0.5 * amt; + const n = 3; + return frac(phase + depth * Math.sin(2 * Math.PI * n * phase)); + } + case WarpMode.SPIN: { + const depth = 0.5 * amt; + const { n } = this._toBits(amt, 1, 6); + return frac(phase + depth * Math.sin(2 * Math.PI * n * phase)); + } + case WarpMode.CHAOS: { + const r = 3.7 + 0.3 * amt; + const logistic = r * phase * (1 - phase); + return clamp((1 - amt) * phase + amt * logistic, 0, 1); + } + case WarpMode.PRIMES: { + const isPrime = (n) => { + if (n < 2) return false; + if (n % 2 === 0) return n === 2; + for (let d = 3; d * d <= n; d += 2) if (n % d === 0) return false; + return true; + }; + let { n } = this._toBits(amt, 3); + while (!isPrime(n)) n++; + return ffloor(phase * n) / n; + } + case WarpMode.BINARY: { + let { b } = this._toBits(amt, 3); + b = Math.round(b); + const n = 1 << b; + const idx = ffloor(phase * n); + const ridx = bitReverse(idx, b); + return ridx / n; + } + case WarpMode.MODULAR: { + const { n } = this._toBits(amt); + const depth = 0.5 * amt; + const jump = frac(phase * n) / n; + return frac(phase + depth * jump); + } + case WarpMode.BROWNIAN: { + const disp = 0.25 * amt * brownian(64 * phase, 4); + return frac(phase + disp); + } + case WarpMode.RECIPROCAL: { + const g = 2 + 4 * amt; + const num = phase * g; + const den = phase + (1 - phase) * g; + const y = den > 1e-12 ? num / den : 0; + return clamp(y, 0, 1); + } + case WarpMode.WORMHOLE: { + const gap = clamp(0.8 * amt, 0, 1); + const a = 0.5 * (1 - gap); + const b = 0.5 * (1 + gap); + if (phase < a) return (phase / a) * 0.5; + if (phase > b) return 0.5 * (1 + (phase - b) / (1 - b)); + return 0.5; + } + case WarpMode.LOGISTIC: { + let x = phase; + const r = 3.6 + 0.4 * amt; + const iters = 1 + Math.round(2 * amt); + for (let i = 0; i < iters; i++) x = r * x * (1 - x); + return clamp(x, 0, 1); + } + case WarpMode.SIGMOID: { + const k = 1 + 10 * amt; + const x = phase - 0.5; + const y = 1 / (1 + Math.exp(-k * x)); + const y0 = 1 / (1 + Math.exp(0.5 * k)); + const y1 = 1 / (1 + Math.exp(-0.5 * k)); + return (y - y0) / (y1 - y0); + } + case WarpMode.FRACTAL: { + const d = 0.5 * Math.sin(2 * Math.PI * phase) * amt; + return frac(phase + d); + } + case WarpMode.FLIP: { + return phase; + } + default: + return phase; + } + } + + _sampleFrame(frame, phase) { + const pos = phase * (frame.length - 1); + const i = pos | 0; + const frac = pos - i; + const a = frame[i]; + const b = frame[(i + 1) % frame.length]; + return a + (b - a) * frac; + } + + process(_inputs, outputs, parameters) { + if (currentTime >= parameters.end[0]) { + return false; + } + if (currentTime <= parameters.begin[0]) { + return true; + } + const outL = outputs[0][0]; + const outR = outputs[0][1] || outputs[0][0]; + + if (!this.tables) { + outL.fill(0); + if (outR !== outL) outR.set(outL); + return true; + } + + for (let i = 0; i < outL.length; i++) { + const detune = pv(parameters.detune, i); + const spread = pv(parameters.spread, i) * 0.5 + 0.5; + const tablePos = pv(parameters.position, i); //Math.sin(2 * Math.PI * this.lfoPhase); + // morph across frames + const idx = tablePos * (this.numFrames - 1); + const fIdx = idx | 0; + const frac = idx - fIdx; + const warpAmount = 0.5 * Math.sin(2 * Math.PI * this.lfoPhase) + 0.5; // pv(parameters.warp, i); + const warpMode = pv(parameters.warpMode, i); + const voices = pv(parameters.voices, i); + const gain1 = Math.sqrt(1 - spread); + const gain2 = Math.sqrt(spread); + let f = pv(parameters.frequency, i); + f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune + for (let n = 0; n < voices; n++) { + const isOdd = (n & 1) == 1; + let gainL = gain1; + let gainR = gain2; + // invert right and left gain + if (isOdd) { + gainL = gain2; + gainR = gain1; + } + let fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune + const dPhase = fVoice / sampleRate; + const level = this._chooseMip(dPhase); + const bank = this.tables[level]; + + // warp phase then sample + this.phase[n] = this.phase[n] ?? Math.random(); + let ph = this._warpPhase(this.phase[n], warpAmount, warpMode); + const s0 = this._sampleFrame(bank[fIdx], ph); + const s1 = this._sampleFrame(bank[Math.min(this.numFrames - 1, fIdx + 1)], ph); + let s = s0 + (s1 - s0) * frac; + if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { + s = -s; + } + outL[i] += (s * gainL) / Math.sqrt(voices); + outR[i] += (s * gainR) / Math.sqrt(voices); + this.phase[n] = wrapPhase(this.phase[n] + dPhase); + } + this.lfoPhase += 1 / sampleRate; + if (this.lfoPhase >= 1) this.lfoPhase -= 1; + } + return true; + } +} + +registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor); From d1869c18ba526e8ca251fdcc3aaff048d8f9ba4c Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 28 Aug 2025 16:23:40 -0500 Subject: [PATCH 145/257] Remove internal LFO --- packages/superdough/wavetable.mjs | 2 +- packages/superdough/worklets.mjs | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index d18f1fdff..9b9e325f7 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -232,7 +232,7 @@ async function onTriggerSynth(t, value, onended, bank, frameLen) { const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... node.connect(out); - let handle = { node: out, bufferSource: source }; + let handle = { node: out, bufferSource: source, oscillator: worklet }; let timeoutNode = webAudioTimeout( ac, () => { diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 7775803d9..ce0e7ac80 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1008,7 +1008,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { this.numFrames = this.tables[0].length; } }; - this.lfoPhase = 0; } _chooseMip(dphi) { @@ -1183,12 +1182,11 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { for (let i = 0; i < outL.length; i++) { const detune = pv(parameters.detune, i); const spread = pv(parameters.spread, i) * 0.5 + 0.5; - const tablePos = pv(parameters.position, i); //Math.sin(2 * Math.PI * this.lfoPhase); - // morph across frames + const tablePos = pv(parameters.position, i); const idx = tablePos * (this.numFrames - 1); const fIdx = idx | 0; const frac = idx - fIdx; - const warpAmount = 0.5 * Math.sin(2 * Math.PI * this.lfoPhase) + 0.5; // pv(parameters.warp, i); + const warpAmount = pv(parameters.warp, i); const warpMode = pv(parameters.warpMode, i); const voices = pv(parameters.voices, i); const gain1 = Math.sqrt(1 - spread); @@ -1222,8 +1220,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { outR[i] += (s * gainR) / Math.sqrt(voices); this.phase[n] = wrapPhase(this.phase[n] + dPhase); } - this.lfoPhase += 1 / sampleRate; - if (this.lfoPhase >= 1) this.lfoPhase -= 1; } return true; } From fe46e1da5372ddf3c86e023566e9984012fd070e Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 28 Aug 2025 16:28:33 -0500 Subject: [PATCH 146/257] Update docstring to include new warp modes; save sample server for a separate PR --- packages/core/controls.mjs | 10 ++------ packages/sampler/sample-server.mjs | 39 ++++++++++-------------------- 2 files changed, 15 insertions(+), 34 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index ba77f1a35..1099fe9ec 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -110,14 +110,8 @@ export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWar /** * Amount of warp (alteration of the waveform) to apply to the wavetable oscillator. * - * The current options are: - * 0 = asym - * 1 = mirror - * 2 = bend+ - * 3 = bend- - * 4 = bend+/- - * 5 = sync - * 6 = quantize + * The current options are: none, asym, bendp, bendm, bendmp, sync, quant, fold, pwm, orbit, + * spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip * * @name wtWarpMode * @param {number | Pattern} mode Warp mode: an integer diff --git a/packages/sampler/sample-server.mjs b/packages/sampler/sample-server.mjs index 2832741aa..08456add9 100644 --- a/packages/sampler/sample-server.mjs +++ b/packages/sampler/sample-server.mjs @@ -1,10 +1,10 @@ #!/usr/bin/env node import cowsay from 'cowsay'; -import { createReadStream, existsSync, writeFileSync } from 'fs'; +import { createReadStream, existsSync } from 'fs'; import { readdir } from 'fs/promises'; import http from 'http'; -import { join, sep, resolve } from 'path'; +import { join, sep } from 'path'; import os from 'os'; // eslint-disable-next-line @@ -36,19 +36,17 @@ async function getFilesInDirectory(directory) { return files; } -async function getBanks(directory, flat = false) { +async function getBanks(directory) { let files = await getFilesInDirectory(directory); let banks = {}; directory = directory.split(sep).join('/'); files = files.map((path) => { path = path.split(sep).join('/'); - const subDir = path.replace(directory, ''); - const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore - const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension - let bank = flat ? subDirFlatStem : subDir.split('/')[0]; + const [bank] = path.split('/').slice(-2); banks[bank] = banks[bank] || []; - banks[bank].push(subDir); - return subDir; + const relativeUrl = path.replace(directory, ''); + banks[bank].push(relativeUrl); + return relativeUrl; }); banks._base = `http://localhost:5432`; return { banks, files }; @@ -56,25 +54,14 @@ async function getBanks(directory, flat = false) { const args = process.argv.slice(2); -function getArgValue(flag) { - const i = args.indexOf(flag); - if (i !== -1) { - const nextIsFlag = args[i + 1]?.startsWith('--') ?? true; - if (nextIsFlag) return true; - return args[i + 1]; - } -} - // eslint-disable-next-line -let directory = getArgValue('--dir') || process.cwd(); -directory = resolve(directory); +const directory = process.cwd(); if (args.includes('--json')) { - const { banks } = await getBanks(directory, getArgValue('--flat')); + const { banks, files } = await getBanks(directory); const json = JSON.stringify(banks); - const outFile = resolve(directory, 'strudel.json'); - writeFileSync(outFile, json, 'utf8'); - console.log(`Wrote json to ${outFile}`); + console.log(json); + process.exit(0); } console.log( @@ -87,7 +74,7 @@ console.log( const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); - const { banks, files } = await getBanks(directory, getArgValue('--flat')); + const { banks, files } = await getBanks(directory); if (req.url === '/') { res.setHeader('Content-Type', 'application/json'); return res.end(JSON.stringify(banks)); @@ -95,7 +82,7 @@ const server = http.createServer(async (req, res) => { let subpath = decodeURIComponent(req.url); const filePath = join(directory, subpath.split('/').join(sep)); - // console.log('GET:', filePath); + //console.log('GET:', filePath); const isFound = existsSync(filePath); if (!isFound) { res.statusCode = 404; From 189e650a733b85d183babc680b8e14fc49b1c486 Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 29 Aug 2025 12:18:28 -0500 Subject: [PATCH 147/257] Add flat flag, automatic json saving; filter out non-audio at base dir --- packages/sampler/sample-server.mjs | 74 ++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/packages/sampler/sample-server.mjs b/packages/sampler/sample-server.mjs index 08456add9..31b83f5a3 100644 --- a/packages/sampler/sample-server.mjs +++ b/packages/sampler/sample-server.mjs @@ -1,14 +1,20 @@ #!/usr/bin/env node import cowsay from 'cowsay'; -import { createReadStream, existsSync } from 'fs'; +import { createReadStream, existsSync, writeFileSync } from 'fs'; import { readdir } from 'fs/promises'; import http from 'http'; -import { join, sep } from 'path'; +import { join, resolve, sep } from 'path'; +import readline from 'readline'; import os from 'os'; -// eslint-disable-next-line const LOG = !!process.env.LOG || false; +const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg']; + +const isAudioFile = (f) => { + const ext = f.split('.').slice(-1)[0].toLowerCase(); + return VALID_AUDIO_EXTENSIONS.includes(ext); +}; async function getFilesInDirectory(directory) { let files = []; @@ -21,32 +27,32 @@ async function getFilesInDirectory(directory) { continue; } try { - const subFiles = (await getFilesInDirectory(fullPath)).filter((f) => - ['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()), - ); + const subFiles = (await getFilesInDirectory(fullPath)).filter(isAudioFile); files = files.concat(subFiles); LOG && console.log(`${dirent.name} (${subFiles.length})`); } catch (err) { LOG && console.warn(`skipped due to error: ${fullPath}`); } } else { - files.push(fullPath); + isAudioFile(fullPath) && files.push(fullPath); } } return files; } -async function getBanks(directory) { +async function getBanks(directory, flat = false) { let files = await getFilesInDirectory(directory); let banks = {}; directory = directory.split(sep).join('/'); files = files.map((path) => { path = path.split(sep).join('/'); - const [bank] = path.split('/').slice(-2); + const subDir = path.replace(directory, ''); + const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore + const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension + let bank = flat ? subDirFlatStem : path.split('/').slice(-2)[0]; banks[bank] = banks[bank] || []; - const relativeUrl = path.replace(directory, ''); - banks[bank].push(relativeUrl); - return relativeUrl; + banks[bank].push(subDir); + return subDir; }); banks._base = `http://localhost:5432`; return { banks, files }; @@ -54,14 +60,44 @@ async function getBanks(directory) { const args = process.argv.slice(2); -// eslint-disable-next-line -const directory = process.cwd(); +function getArgValue(flag) { + const i = args.indexOf(flag); + if (i !== -1) { + const nextIsFlag = args[i + 1]?.startsWith('--') ?? true; + if (nextIsFlag) return true; + return args[i + 1]; + } +} +function getInput(query) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => + rl.question(query, (response) => { + rl.close(); + resolve(response); + }), + ); +} + +let directory = getArgValue('--dir') || process.cwd(); +directory = resolve(directory); if (args.includes('--json')) { - const { banks, files } = await getBanks(directory); + const { banks } = await getBanks(directory, getArgValue('--flat')); const json = JSON.stringify(banks); - console.log(json); - process.exit(0); + const outFile = resolve(directory, 'strudel.json'); + if (existsSync(outFile)) { + const answer = await getInput(`Warning: File already exists at ${outFile}. Overwrite? (y/N): `); + if (answer.toLowerCase() !== 'y') { + console.log('Aborted.'); + process.exit(0); + } + } + writeFileSync(outFile, json, 'utf8'); + console.log(`Wrote json to ${outFile}`); } console.log( @@ -74,7 +110,7 @@ console.log( const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); - const { banks, files } = await getBanks(directory); + const { banks, files } = await getBanks(directory, getArgValue('--flat')); if (req.url === '/') { res.setHeader('Content-Type', 'application/json'); return res.end(JSON.stringify(banks)); @@ -82,7 +118,7 @@ const server = http.createServer(async (req, res) => { let subpath = decodeURIComponent(req.url); const filePath = join(directory, subpath.split('/').join(sep)); - //console.log('GET:', filePath); + // console.log('GET:', filePath); const isFound = existsSync(filePath); if (!isFound) { res.statusCode = 404; From 9eb3c9410de3734fabee94a007f910d8d45b43ff Mon Sep 17 00:00:00 2001 From: yaxu Date: Sat, 30 Aug 2025 12:46:00 +0200 Subject: [PATCH 148/257] Add verify link to mastodon a/c --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 4aa90fd38..21c5b8b6b 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ Live coding patterns on the web https://strudel.cc/ - -Development is moving to https://codeberg.org/uzu/strudel +Mastodon: social.toplap.org/@strudel - Try it here: - Docs: From dc6b766ab7420fbd8eb025a73d69e7f2f2644e9f Mon Sep 17 00:00:00 2001 From: yaxu Date: Sat, 30 Aug 2025 12:47:06 +0200 Subject: [PATCH 149/257] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 21c5b8b6b..baaac82b2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ Live coding patterns on the web https://strudel.cc/ -Mastodon: social.toplap.org/@strudel - Try it here: - Docs: @@ -46,3 +45,5 @@ There is a #strudel channel on the TidalCycles discord: The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project. + +We also have a mastodon account: social.toplap.org/@strudel From 6a09f54b25ebaa12abca4178412e94548f78ebac Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 31 Aug 2025 01:03:09 -0500 Subject: [PATCH 150/257] Add vibrato to list of continuous modulators --- website/src/pages/learn/effects.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index d9efe8f8a..15df506c8 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -129,8 +129,9 @@ Some parameters _do_ induce continuous variations in time, though: - The pitch envelope curve (governed by `penv` and its associated ADSR) - The FM curve (`fmenv`) - The filter envelopes (`lpenv`, `hpenv`, `bpenv`) -- Tremolo -- Phaser +- Tremolo (`tremolo`) +- Phaser (`phaser`) +- Vibrato (`vib`) - Ducking (`duckorbit`) # Filters From 9b9176325b04d6607e831e37bc32efef8f497d87 Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 22 Aug 2025 15:38:50 -0500 Subject: [PATCH 151/257] Update restore defaults to not delete patterns --- website/src/repl/components/panel/SettingsTab.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index cf2978a1a..8f02ca8b8 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -311,7 +311,8 @@ export function SettingsTab({ started }) { onClick={() => { confirmDialog('Sure?').then((r) => { if (r) { - settingsMap.set(defaultSettings); + const { userPatterns } = settingsMap.get(); // keep current patterns + settingsMap.set({...defaultSettings, userPatterns}); } }); }} From 142160d79ae7a2eef1a6f38996b30352134b6cfc Mon Sep 17 00:00:00 2001 From: James Walker Date: Mon, 1 Sep 2025 21:08:06 +0100 Subject: [PATCH 152/257] Add examples for ? and | operators --- website/src/pages/learn/mini-notation.mdx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/website/src/pages/learn/mini-notation.mdx b/website/src/pages/learn/mini-notation.mdx index 7dbf67ce2..c7327bb86 100644 --- a/website/src/pages/learn/mini-notation.mdx +++ b/website/src/pages/learn/mini-notation.mdx @@ -168,6 +168,16 @@ Using "!" we can repeat without speeding up: *2")`} punchcard /> +## Randomness + +Events with a "?" placed after them will have a 50% chance of playing: + + + +Events separated by a "|" will be chosen from at random: + + + ## Mini-notation review To recap what we've learned so far, compare the following patterns: @@ -179,6 +189,8 @@ To recap what we've learned so far, compare the following patterns: *2")`} /> *2")`} /> *2")`} /> +*2")`} /> +*2")`} /> ## Euclidian rhythms From 4e2e79086446e7b82c5bf382125ef69b09ceba16 Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 1 Sep 2025 20:45:53 -0500 Subject: [PATCH 153/257] Set delay and reverb nodes properly --- packages/superdough/superdough.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 0c205bcca..290796956 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -336,7 +336,7 @@ function getDelay(orbit, delaytime, delayfeedback, t) { const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback); dly.start?.(t); // for some reason, this throws when audion extension is installed.. connectToOrbit(dly, orbit); - delayNode = dly; + orbits[orbit].delayNode = dly; } delayNode.delayTime.value !== delaytime && delayNode.delayTime.setValueAtTime(delaytime, t); delayNode.feedback.value !== delayfeedback && delayNode.feedback.setValueAtTime(delayfeedback, t); @@ -464,7 +464,7 @@ function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) { const ac = getAudioContext(); const reverb = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin); connectToOrbit(reverb, orbit); - reverbNode = reverb; + orbits[orbit].reverbNode = reverb; } if ( From 439a7dc5e6b825a5270958ff04b869d52b37387b Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 2 Sep 2025 16:05:40 -0500 Subject: [PATCH 154/257] Include synonyms in autocomplete --- packages/codemirror/autocomplete.mjs | 29 +++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 59ca8adf2..bfbf7b333 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -74,15 +74,26 @@ const isValidDoc = (doc) => { const hasExcludedTags = (doc) => ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); -const jsdocCompletions = jsdoc.docs - .filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc)) - // https://codemirror.net/docs/ref/#autocomplete.Completion - .map((doc) => ({ - 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 - })); +const jsdocCompletions = (() => { + const seen = new Set(); // avoid repetition + const completions = []; + for (const doc of jsdoc.docs) { + if (!isValidDoc(doc) || hasExcludedTags(doc)) continue; + let labels = [getDocLabel(doc), ...(doc.synonyms || [])]; + for (const label of labels) { + // https://codemirror.net/docs/ref/#autocomplete.Completion + if (label && !seen.has(label)) { + seen.add(label); + completions.push({ + label, + info: () => Autocomplete({ doc }), + type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type + }); + } + } + } + return completions; +})(); export const strudelAutocomplete = (context) => { const word = context.matchBefore(/\w*/); From 052d09e892f2ec8022381e72d690a4c3905b23ea Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 2 Sep 2025 16:14:38 -0500 Subject: [PATCH 155/257] Add synonyms to reference --- .../src/repl/components/panel/Reference.jsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index 1b617341b..2e2ef4cbc 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -2,9 +2,28 @@ import { useMemo, useState } from 'react'; import jsdocJson from '../../../../../doc.json'; import { Textbox } from '../textbox/Textbox'; -const availableFunctions = jsdocJson.docs - .filter(({ name, description }) => name && !name.startsWith('_') && !!description) - .sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name)); + +const isValid = ({ name, description }) => + name && !name.startsWith('_') && !!description; + +const availableFunctions = (() => { + const seen = new Set(); // avoid repetition + const functions = []; + for (const doc of jsdocJson.docs) { + if (!isValid(doc)) continue; + let docAndSynonyms = [doc.name, ...(doc.synonyms || [])]; + for (const s of docAndSynonyms) { + if (!s || seen.has(s)) continue; + seen.add(s); + functions.push({ + ...doc, + name: s, // update names for the synonym + longname: s, + }); + } + } + return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name)); +})(); const getInnerText = (html) => { var div = document.createElement('div'); From 8d2a368da91d84307779e8e969bb0828c649c2a2 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 2 Sep 2025 16:14:50 -0500 Subject: [PATCH 156/257] Add some more synonyms to controls docs --- packages/core/controls.mjs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 58a30652c..1c025313d 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -91,6 +91,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound'); * Define a custom webaudio node to use as a sound source. * * @name source + * @synonyms src * @param {function} getSource * @synonyms src * @@ -525,6 +526,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase'); * shape of amplitude modulation * * @name tremoloshape + * @synonyms tremshape * @param {number | Pattern} shape tri | square | sine | saw | ramp * @example * note("{f g c d}%16").tremsync(4).tremoloshape("").s("sawtooth") @@ -540,11 +542,13 @@ 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>") * */ +export const { drive } = registerControl('drive'); /** * modulate the amplitude of an orbit to create a "sidechain" like effect * * @name duckorbit + * @synonyms duck * @param {number | Pattern} orbit target orbit * @example * $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2) @@ -569,6 +573,7 @@ export const { duckdepth } = registerControl('duckdepth'); * the attack time of the duck effect * * @name duckattack + * @synonyms duckatt * @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)) @@ -576,8 +581,6 @@ export const { duckdepth } = registerControl('duckdepth'); */ export const { duckattack } = registerControl('duckattack', 'duckatt'); -export const { drive } = registerControl('drive'); - /** * Create byte beats with custom expressions * @@ -700,7 +703,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc'); * The amount the signal is affected by the phaser effect. Defaults to 0.75 * * @name phaserdepth - * @synonyms phd + * @synonyms phd, phasdp * @param {number | Pattern} depth number between 0 and 1 * @example * n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5) @@ -1182,6 +1185,7 @@ export const { dry } = registerControl('dry'); * Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope. * * @name fadeTime + * @synonyms fadeOutTime * @param {number | Pattern} time between 0 and 1 * @example * s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc() From c199b51645f899940fa5e5a63e72b57877086e82 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 2 Sep 2025 16:29:01 -0500 Subject: [PATCH 157/257] Update autocomplete name to be the label and update ref to use main name as a synonym --- packages/codemirror/autocomplete.mjs | 2 +- website/src/repl/components/panel/Reference.jsx | 13 +++++++++---- website/src/repl/components/panel/SettingsTab.jsx | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index bfbf7b333..b56cab826 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -86,7 +86,7 @@ const jsdocCompletions = (() => { seen.add(label); completions.push({ label, - info: () => Autocomplete({ doc }), + info: () => Autocomplete({ doc, label }), type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type }); } diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index 2e2ef4cbc..81826aca2 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -3,22 +3,27 @@ import { useMemo, useState } from 'react'; import jsdocJson from '../../../../../doc.json'; import { Textbox } from '../textbox/Textbox'; -const isValid = ({ name, description }) => - name && !name.startsWith('_') && !!description; +const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description; const availableFunctions = (() => { const seen = new Set(); // avoid repetition const functions = []; for (const doc of jsdocJson.docs) { if (!isValid(doc)) continue; - let docAndSynonyms = [doc.name, ...(doc.synonyms || [])]; - for (const s of docAndSynonyms) { + functions.push(doc); + const synonyms = doc.synonyms || []; + for (const s of synonyms) { if (!s || seen.has(s)) continue; seen.add(s); + // Swap `doc.name` in for `s` in the list of synonyms + const notS = synonyms.filter((x) => x && x !== s); + const synonymsWithDoc = Array.from(new Set([doc.name, ...notS])); functions.push({ ...doc, name: s, // update names for the synonym longname: s, + synonyms: synonymsWithDoc, + synonyms_text: synonymsWithDoc.join(', '), }); } } diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 8f02ca8b8..26c9ae287 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -312,7 +312,7 @@ export function SettingsTab({ started }) { confirmDialog('Sure?').then((r) => { if (r) { const { userPatterns } = settingsMap.get(); // keep current patterns - settingsMap.set({...defaultSettings, userPatterns}); + settingsMap.set({ ...defaultSettings, userPatterns }); } }); }} From 56b1ea3fa50b7a8921f3159f4e1c715aa502bf71 Mon Sep 17 00:00:00 2001 From: Antipathie Date: Wed, 3 Sep 2025 00:15:47 +0200 Subject: [PATCH 158/257] Add soundAlias function --- packages/superdough/superdough.mjs | 13 +++++++++++++ website/src/pages/learn/samples.mdx | 8 ++++++++ 2 files changed, 21 insertions(+) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a518f49b6..6f8717d05 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -113,6 +113,19 @@ export async function aliasBank(...args) { } } +/** + * Register an alias for a sound. + * @param {string} original - The original sound name + * @param {string} alias - The alias to use for the sound + */ +export function soundAlias(original, alias) { + if (getSound(original) == null) { + logger('soundAlias: original sound not found'); + return; + } + soundMap.setKey(alias, getSound(original)); +} + export function getSound(s) { if (typeof s !== 'string') { console.warn(`getSound: expected string got "${s}". fall back to triangle`); diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index a87b8f7dd..eb79cccf7 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -59,6 +59,14 @@ Furthermore, strudel also loads instrument samples from [VCSL](https://github.co To see which sample names are available, open the `sounds` tab in the [REPL](https://strudel.cc/). +You can also create custom aliases for existing sounds using the `soundAlias` function: + + + Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples themselves are not loaded until they are actually played. This behaviour of loading things only when they are needed is also called `lazy loading`. While it saves resources, it can also lead to sounds not being audible the first time they are triggered, because the sound is still loading. From 591c3fe08f26ad8b21c5559b3dcdd3adc9ced236 Mon Sep 17 00:00:00 2001 From: James Walker Date: Wed, 3 Sep 2025 19:16:47 +0100 Subject: [PATCH 159/257] Add documentation for ?n in mini-notation --- website/src/pages/learn/mini-notation.mdx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/src/pages/learn/mini-notation.mdx b/website/src/pages/learn/mini-notation.mdx index c7327bb86..02ead7434 100644 --- a/website/src/pages/learn/mini-notation.mdx +++ b/website/src/pages/learn/mini-notation.mdx @@ -170,10 +170,14 @@ Using "!" we can repeat without speeding up: ## Randomness -Events with a "?" placed after them will have a 50% chance of playing: +Events with a "?" placed after them will have a 50% chance of being removed from the pattern: +Adding a number between 0 and 1 after the "?" will affect the likelihood of the event being removed. For example, events with "?0.1" placed after them will have a 10% chance of being removed: + + + Events separated by a "|" will be chosen from at random: From 46a45b4596902e734cada6dfadf3742cd8b445f9 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 4 Sep 2025 20:51:06 -0500 Subject: [PATCH 160/257] Some missing cleanup --- packages/superdough/superdough.mjs | 11 ++++------- packages/superdough/worklets.mjs | 1 - website/src/repl/components/panel/SettingsTab.jsx | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 8ee3ee4c3..d436a816a 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -686,13 +686,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) let sourceNode; if (source) { sourceNode = source(t, value, hapDuration, cps); - } else { - const soundSource = wt ?? s; - const sound = getSound(soundSource); - if (!sound) { - throw new Error(`sound ${soundSource} not found! Is it loaded?`); - } - const { onTrigger } = sound; + } else if (getSound(s)) { + const { onTrigger } = getSound(s); const onEnded = () => { audioNodes.forEach((n) => n?.disconnect()); activeSoundSources.delete(chainID); @@ -703,6 +698,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) sourceNode = soundHandle.node; activeSoundSources.set(chainID, soundHandle); } + } else { + throw new Error(`sound ${s} not found! Is it loaded?`); } if (!sourceNode) { // if onTrigger does not return anything, we will just silently skip diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index ce0e7ac80..b0c0e8e2d 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -909,7 +909,6 @@ class ByteBeatProcessor extends AudioWorkletProcessor { registerProcessor('byte-beat-processor', ByteBeatProcessor); - export const WarpMode = Object.freeze({ NONE: 0, ASYM: 1, diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 8f02ca8b8..26c9ae287 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -312,7 +312,7 @@ export function SettingsTab({ started }) { confirmDialog('Sure?').then((r) => { if (r) { const { userPatterns } = settingsMap.get(); // keep current patterns - settingsMap.set({...defaultSettings, userPatterns}); + settingsMap.set({ ...defaultSettings, userPatterns }); } }); }} From 0a96fa5896848d83a7fed79a00d98d029cf8e6e5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 6 Sep 2025 00:42:36 +0200 Subject: [PATCH 161/257] hotfix: comment out tauri stuff to fix osc --- packages/osc/superdirtoutput.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/osc/superdirtoutput.js b/packages/osc/superdirtoutput.js index 3f48e66bd..e8fef0301 100644 --- a/packages/osc/superdirtoutput.js +++ b/packages/osc/superdirtoutput.js @@ -1,8 +1,8 @@ -import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs'; -import { isTauri } from '../desktopbridge/utils.mjs'; +/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs'; +import { isTauri } from '../desktopbridge/utils.mjs'; */ import { oscTrigger } from './osc.mjs'; -const trigger = isTauri() ? oscTriggerTauri : oscTrigger; +const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger; export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => { const currentTime = performance.now() / 1000; From 2fbdc9fdd730a147b1e38398e7d3d8a2ba9cb89a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 6 Sep 2025 00:43:03 +0200 Subject: [PATCH 162/257] hotfix: formatting --- website/src/repl/components/panel/SettingsTab.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 8f02ca8b8..26c9ae287 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -312,7 +312,7 @@ export function SettingsTab({ started }) { confirmDialog('Sure?').then((r) => { if (r) { const { userPatterns } = settingsMap.get(); // keep current patterns - settingsMap.set({...defaultSettings, userPatterns}); + settingsMap.set({ ...defaultSettings, userPatterns }); } }); }} From 9c108862ef0edfe1d2c50e51337ff4f646bfbe2f Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 5 Sep 2025 22:09:50 -0500 Subject: [PATCH 163/257] Corrected piping --- packages/superdough/superdough.mjs | 14 +++++++------- website/src/repl/components/panel/SettingsTab.jsx | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 290796956..5e5969fed 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -333,10 +333,10 @@ function getDelay(orbit, delaytime, delayfeedback, t) { let delayNode = orbits[orbit].delayNode; if (delayNode === undefined) { 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; + delayNode = ac.createFeedbackDelay(1, delaytime, delayfeedback); + delayNode.start?.(t); // for some reason, this throws when audion extension is installed.. + connectToOrbit(delayNode, orbit); + orbits[orbit].delayNode = delayNode; } delayNode.delayTime.value !== delaytime && delayNode.delayTime.setValueAtTime(delaytime, t); delayNode.feedback.value !== delayfeedback && delayNode.feedback.setValueAtTime(delayfeedback, t); @@ -462,9 +462,9 @@ function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) { let reverbNode = orbits[orbit].reverbNode; if (reverbNode === undefined) { const ac = getAudioContext(); - const reverb = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin); - connectToOrbit(reverb, orbit); - orbits[orbit].reverbNode = reverb; + reverbNode = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin); + connectToOrbit(reverbNode, orbit); + orbits[orbit].reverbNode = reverbNode; } if ( diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 8f02ca8b8..26c9ae287 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -312,7 +312,7 @@ export function SettingsTab({ started }) { confirmDialog('Sure?').then((r) => { if (r) { const { userPatterns } = settingsMap.get(); // keep current patterns - settingsMap.set({...defaultSettings, userPatterns}); + settingsMap.set({ ...defaultSettings, userPatterns }); } }); }} From 977420e74dfe4a0e2c93b94f6d346a21f765a77e Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Sat, 6 Sep 2025 15:33:08 +0200 Subject: [PATCH 164/257] Fix formatting of REPL footnote --- website/src/pages/technical-manual/repl.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/src/pages/technical-manual/repl.mdx b/website/src/pages/technical-manual/repl.mdx index f53efac41..8c4287af5 100644 --- a/website/src/pages/technical-manual/repl.mdx +++ b/website/src/pages/technical-manual/repl.mdx @@ -9,7 +9,9 @@ import { MiniRepl } from '../../docs/MiniRepl'; {/* The [REPL](https://strudel.cc/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. */} -While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL^[REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel. +While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL[^1], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel. + +[^1]: REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors. Besides a UI for playback control and meta information, the main part of the REPL interface is the code editor powered by CodeMirror. In it, the user can edit and evaluate pattern code live, using one of the available synthesis outputs to create music and/or sound art. The control flow of the REPL follows 3 basic steps: From 798eb22d9a5a5be342b2e78a34f845a3c748d184 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 6 Sep 2025 10:09:44 -0500 Subject: [PATCH 165/257] Use currentTime for exact scheduling --- packages/core/controls.mjs | 8 +-- packages/superdough/superdough.mjs | 21 ++++---- test/__snapshots__/examples.test.mjs.snap | 64 +++++++++++------------ 3 files changed, 47 insertions(+), 46 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 589a0634f..af0c4b1ab 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -590,12 +590,12 @@ export const { duckdepth } = registerControl('duckdepth'); * @param {number | Pattern} time The onset time in seconds * @example * // Clicks - * sound: n(run(8)).scale("c:minor").s("sawtooth").lpf(200).delay(.7).orbit(2) - * duckerWithClick: s("bd*4").duckorbit(2).duckonset(0).postgain(0) + * sound: freq("63.2388").s("sine").orbit(2).gain(4) + * duckerWithClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0).postgain(0) * @example * // No clicks - * sound: n(run(8)).scale("c:minor").s("sawtooth").lpf(200).delay(.7).orbit(2) - * duckerWithoutClick: s("bd*4").duckorbit(2).duckonset(0.003).postgain(0) + * sound: freq("63.2388").s("sine").orbit(2).gain(4) + * duckerWithoutClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0.01).postgain(0) * @example * // Rhythmic * noise: s("pink").distort("2:1").orbit(4) // used rhythmically with 0.3 onset below diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 5e5969fed..59590db6d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -420,7 +420,7 @@ function setOrbit(audioContext, orbit, channels) { } } -function duckOrbit(audioContext, targetOrbit, t, onsettime = 0.003, attacktime = 0.1, duckdepth = 1) { +function duckOrbit(audioContext, targetOrbit, t, onsettime = 0, attacktime = 0.1, duckdepth = 1) { const targetArr = [targetOrbit].flat(); const onsetArr = [onsettime].flat(); const attackArr = [attacktime].flat(); @@ -438,17 +438,18 @@ function duckOrbit(audioContext, targetOrbit, t, onsettime = 0.003, attacktime = webAudioTimeout( audioContext, () => { - gainParam.cancelScheduledValues(t); + const now = audioContext.currentTime; + + // cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime + // on browsers which lack that method const currVal = gainParam.value; + gainParam.cancelScheduledValues(now); + gainParam.setValueAtTime(currVal, now); + + const t0 = Math.max(t, now); // guard against now > t const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal); - - // Guarantees the value is set to currVal at time t. This in conjunction with - // cancelScheduledValues above emulates cancelAndHoldAtTime on browsers which lack - // that method - gainParam.setValueAtTime(currVal, t); - - gainParam.exponentialRampToValueAtTime(duckedVal, t + onset); - gainParam.exponentialRampToValueAtTime(1, t + onset + attack); + gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset); + gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack); }, 0, t - 0.01, diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index ba54328b7..7579794fd 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3170,43 +3170,43 @@ exports[`runs examples > example "duckdepth" example index 1 1`] = ` exports[`runs examples > example "duckonset" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 1/4 → 1/2 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 1/2 → 3/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 3/4 → 1/1 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 1/1 → 5/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 5/4 → 3/2 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 3/2 → 7/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 7/4 → 2/1 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 2/1 → 9/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 9/4 → 5/2 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 5/2 → 11/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 11/4 → 3/1 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 3/1 → 13/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 13/4 → 7/2 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 7/2 → 15/4 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", - "[ 15/4 → 4/1 | s:bd duckorbit:2 duckonset:0 postgain:0 ]", + "[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", ] `; exports[`runs examples > example "duckonset" example index 1 1`] = ` [ - "[ 0/1 → 1/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 1/4 → 1/2 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 1/2 → 3/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 3/4 → 1/1 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 1/1 → 5/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 5/4 → 3/2 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 3/2 → 7/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 7/4 → 2/1 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 2/1 → 9/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 9/4 → 5/2 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 5/2 → 11/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 11/4 → 3/1 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 3/1 → 13/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 13/4 → 7/2 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 7/2 → 15/4 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", - "[ 15/4 → 4/1 | s:bd duckorbit:2 duckonset:0.003 postgain:0 ]", + "[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", ] `; From 2511dcc09e180d9b5fdb3ac1591ddc0059999739 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 7 Sep 2025 15:02:13 -0400 Subject: [PATCH 166/257] working --- packages/superdough/superdough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a518f49b6..792e5a28d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -413,7 +413,7 @@ function connectToOrbit(node, orbit) { function setOrbit(audioContext, orbit, channels) { if (orbits[orbit] == null) { orbits[orbit] = { - gain: new GainNode(audioContext, { gain: 1 }), + gain: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }), }; connectToDestination(orbits[orbit].gain, channels); } From e7e80bfd83b3661e92a93def0c84011e330fb512 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 7 Sep 2025 22:21:03 -0400 Subject: [PATCH 167/257] working --- .../repl/components/button/action-button.jsx | 10 +++++ .../src/repl/components/panel/PatternsTab.jsx | 13 +----- .../src/repl/components/panel/SoundsTab.jsx | 41 ++++++++++++++----- website/src/repl/idbutils.mjs | 8 +++- website/src/settings.mjs | 10 ++++- 5 files changed, 58 insertions(+), 24 deletions(-) create mode 100644 website/src/repl/components/button/action-button.jsx diff --git a/website/src/repl/components/button/action-button.jsx b/website/src/repl/components/button/action-button.jsx new file mode 100644 index 000000000..d589b7bed --- /dev/null +++ b/website/src/repl/components/button/action-button.jsx @@ -0,0 +1,10 @@ +import cx from '@src/cx.mjs'; + +export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) { + return ( + + ); +} diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index 8a43e4a1b..8e2b75b96 100644 --- a/website/src/repl/components/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -12,8 +12,8 @@ import { useMemo } from 'react'; import { getMetadata } from '../../../metadata_parser.js'; import { useExamplePatterns } from '../../useExamplePatterns.jsx'; import { parseJSON, isUdels } from '../../util.mjs'; -import { ButtonGroup } from './Forms.jsx'; -import { settingsMap, useSettings } from '../../../settings.mjs'; +import { useSettings } from '../../../settings.mjs'; +import { ActionButton } from '../button/action-button.jsx'; import { Pagination } from '../pagination/Pagination.jsx'; import { useState } from 'react'; import { useDebounce } from '../usedebounce.jsx'; @@ -75,15 +75,6 @@ function PatternButtons({ patterns, activePattern, onClick, started }) { ); } -function ActionButton({ children, onClick, label, labelIsHidden }) { - return ( - - ); -} - const updateCodeWindow = (context, patternData, reset = false) => { context.handleUpdate(patternData, reset); }; diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index a976eb3d2..7b91f4cf2 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -2,16 +2,21 @@ import useEvent from '@src/useEvent.mjs'; import { useStore } from '@nanostores/react'; import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio'; import { useMemo, useRef, useState } from 'react'; -import { settingsMap, useSettings } from '../../../settings.mjs'; +import { settingsMap, soundFilterType, useSettings } from '../../../settings.mjs'; import { ButtonGroup } from './Forms.jsx'; import ImportSoundsButton from './ImportSoundsButton.jsx'; import { Textbox } from '../textbox/Textbox.jsx'; +import { ActionButton } from '../button/action-button.jsx'; +import { confirmDialog } from '@src/repl/util.mjs'; +import { clearIDB, userSamplesDBConfig } from '@src/repl/idbutils.mjs'; +import { prebake } from '@src/repl/prebake.mjs'; const getSamples = (samples) => Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1; export function SoundsTab() { const sounds = useStore(soundMap); + const { soundsFilter } = useSettings(); const [search, setSearch] = useState(''); const { BASE_URL } = import.meta.env; @@ -27,18 +32,19 @@ export function SoundsTab() { .sort((a, b) => a[0].localeCompare(b[0])) .filter(([name]) => name.toLowerCase().includes(search.toLowerCase())); - if (soundsFilter === 'user') { + if (soundsFilter === soundFilterType.USER) { return filtered.filter(([_, { data }]) => !data.prebake); } - if (soundsFilter === 'drums') { + if (soundsFilter === soundFilterType.DRUMS) { return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines'); } - if (soundsFilter === 'samples') { + if (soundsFilter === soundFilterType.SAMPLES) { return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines'); } - if (soundsFilter === 'synths') { + if (soundsFilter === soundFilterType.SYNTHS) { return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); } + //TODO: tidy this up, it does not need to be saved in settings if (soundsFilter === 'importSounds') { return []; } @@ -57,10 +63,10 @@ export function SoundsTab() { }); }); return ( -
+
setSearch(v)} /> -
+
settingsMap.setKey('soundsFilter', value)} @@ -73,6 +79,23 @@ export function SoundsTab() { }} >
+ { + { + try { + const confirmed = await confirmDialog('Delete all imported user samples?'); + if (confirmed) { + clearIDB(userSamplesDBConfig.dbName); + soundMap.set({}); + await prebake(); + } + } catch (e) { + console.error(e); + } + }} + /> + }
{soundEntries.map(([name, { data, onTrigger }]) => { @@ -151,9 +174,7 @@ export function SoundsTab() { ) : ( '' )} - {!soundEntries.length && soundsFilter !== 'importSounds' - ? 'No custom sounds loaded in this pattern (yet).' - : ''} + {!soundEntries.length && soundsFilter !== 'importSounds' ? 'No custom sounds loaded (yet).' : ''}
); diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index 5fc62c576..f26ee0479 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -12,17 +12,21 @@ export const userSamplesDBConfig = { }; // deletes all of the databases, useful for debugging -function clearIDB() { +function clearAllIDB() { window.indexedDB .databases() .then((r) => { - for (var i = 0; i < r.length; i++) window.indexedDB.deleteDatabase(r[i].name); + for (var i = 0; i < r.length; i++) clearIDB(r[i].name); }) .then(() => { alert('All data cleared.'); }); } +export function clearIDB(dbName) { + return window.indexedDB.deleteDatabase(dbName); +} + // queries the DB, and registers the sounds so they can be played export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => {}) { openDB(config, (objectStore) => { diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 3d99b656c..9c3d78146 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -8,6 +8,14 @@ export const audioEngineTargets = { osc: 'osc', }; +export const soundFilterType = { + USER: 'user', + DRUMS: 'drums', + SAMPLES: 'samples', + SYNTHS: 'synths', + ALL: 'all', +}; + export const defaultSettings = { activeFooter: 'intro', keybindings: 'codemirror', @@ -28,7 +36,7 @@ export const defaultSettings = { fontSize: 18, latestCode: '', isZen: false, - soundsFilter: 'all', + soundsFilter: soundFilterType.ALL, patternFilter: 'community', // panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro panelPosition: 'right', From 5414bbe85ddec4729506bd6891593ba8418f2e7f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 7 Sep 2025 22:27:48 -0400 Subject: [PATCH 168/257] only show button on user samples --- website/src/repl/components/panel/SoundsTab.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index 7b91f4cf2..de22fb3e8 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -79,7 +79,7 @@ export function SoundsTab() { }} >
- { + {soundsFilter === soundFilterType.USER && soundEntries.length > 0 && ( { @@ -95,7 +95,7 @@ export function SoundsTab() { } }} /> - } + )}
{soundEntries.map(([name, { data, onTrigger }]) => { @@ -174,7 +174,7 @@ export function SoundsTab() { ) : ( '' )} - {!soundEntries.length && soundsFilter !== 'importSounds' ? 'No custom sounds loaded (yet).' : ''} + {!soundEntries.length && soundsFilter !== 'importSounds' ? 'No sounds loaded' : ''}
); From a96854545823a3d8a546d3590c07797df539316d Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 7 Sep 2025 22:41:03 -0400 Subject: [PATCH 169/257] change sound background to distinguish action buttons --- website/src/repl/components/panel/SoundsTab.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index de22fb3e8..d1845bfa8 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -97,7 +97,7 @@ export function SoundsTab() { /> )} -
+
{soundEntries.map(([name, { data, onTrigger }]) => { return ( Date: Sun, 7 Sep 2025 22:43:13 -0400 Subject: [PATCH 170/257] fix formatting --- .../src/repl/components/panel/SoundsTab.jsx | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index d1845bfa8..92f30070e 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -79,23 +79,26 @@ export function SoundsTab() { }} >
- {soundsFilter === soundFilterType.USER && soundEntries.length > 0 && ( - { - try { - const confirmed = await confirmDialog('Delete all imported user samples?'); - if (confirmed) { - clearIDB(userSamplesDBConfig.dbName); - soundMap.set({}); - await prebake(); +
+ {soundsFilter === soundFilterType.USER && soundEntries.length > 0 && ( + { + try { + const confirmed = await confirmDialog('Delete all imported user samples?'); + if (confirmed) { + clearIDB(userSamplesDBConfig.dbName); + soundMap.set({}); + await prebake(); + } + } catch (e) { + console.error(e); } - } catch (e) { - console.error(e); - } - }} - /> - )} + }} + /> + )} +
{soundEntries.map(([name, { data, onTrigger }]) => { From 70e776e799e586f1ffb787db6d15fae871f08399 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 7 Sep 2025 22:44:09 -0400 Subject: [PATCH 171/257] cleanup div --- .../src/repl/components/panel/SoundsTab.jsx | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index 92f30070e..433d53712 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -79,26 +79,26 @@ export function SoundsTab() { }} >
-
- {soundsFilter === soundFilterType.USER && soundEntries.length > 0 && ( - { - try { - const confirmed = await confirmDialog('Delete all imported user samples?'); - if (confirmed) { - clearIDB(userSamplesDBConfig.dbName); - soundMap.set({}); - await prebake(); - } - } catch (e) { - console.error(e); + + {soundsFilter === soundFilterType.USER && soundEntries.length > 0 && ( + { + try { + const confirmed = await confirmDialog('Delete all imported user samples?'); + if (confirmed) { + clearIDB(userSamplesDBConfig.dbName); + soundMap.set({}); + await prebake(); } - }} - /> - )} -
+ } catch (e) { + console.error(e); + } + }} + /> + )} +
{soundEntries.map(([name, { data, onTrigger }]) => { From 99bb227cf480e67a59bac1432908e8a77c8be0f6 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 7 Sep 2025 22:48:04 -0400 Subject: [PATCH 172/257] format --- website/src/repl/components/panel/SoundsTab.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index 433d53712..0484da02d 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -99,7 +99,6 @@ export function SoundsTab() { /> )} -
{soundEntries.map(([name, { data, onTrigger }]) => { return ( From 89e7d5230885e2ca372e6df2f4ba4343f08eb648 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 7 Sep 2025 23:28:18 -0400 Subject: [PATCH 173/257] fixed --- packages/osc/superdirtoutput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/osc/superdirtoutput.js b/packages/osc/superdirtoutput.js index e8fef0301..a317af8f1 100644 --- a/packages/osc/superdirtoutput.js +++ b/packages/osc/superdirtoutput.js @@ -6,5 +6,5 @@ const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger; export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => { const currentTime = performance.now() / 1000; - return trigger(null, hap, currentTime, cps, targetTime); + return trigger(hap, currentTime, cps, targetTime); }; From 5e5e7730c1cd2ccc133766e369998e672d0021dd Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 7 Sep 2025 23:29:24 -0400 Subject: [PATCH 174/257] rm errorlogger console --- packages/core/logger.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/logger.mjs b/packages/core/logger.mjs index 488bce5e8..4f2002319 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}`); } From 38ce593c602659a594b81047dd9633790ef7c010 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 9 Sep 2025 20:43:05 -0500 Subject: [PATCH 175/257] Allow penv values to be falsy --- packages/superdough/helpers.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 81bec9399..69e7e8560 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -174,7 +174,7 @@ let curves = ['linear', 'exponential']; export function getPitchEnvelope(param, value, t, holdEnd) { // envelope is active when any of these values is set const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv; - if (!hasEnvelope) { + if (hasEnvelope === undefined) { return; } const penv = nanFallback(value.penv, 1, true); From d0ce82e3cd37e6299329fefec23c5af9e5441d0e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 10 Sep 2025 23:08:16 +0200 Subject: [PATCH 176/257] Publish - @strudel/codemirror@1.2.4 - @strudel/core@1.2.4 - @strudel/csound@1.2.5 - @strudel/draw@1.2.4 - @strudel/gamepad@1.2.4 - @strudel/hydra@1.2.4 - @strudel/midi@1.2.5 - @strudel/mini@1.2.4 - @strudel/mondo@1.1.2 - @strudel/motion@1.2.4 - @strudel/mqtt@1.2.4 - @strudel/osc@1.2.4 - @strudel/repl@1.2.5 - @strudel/sampler@0.2.3 - @strudel/serial@1.2.4 - @strudel/soundfonts@1.2.5 - superdough@1.2.5 - @strudel/tonal@1.2.4 - @strudel/transpiler@1.2.4 - @strudel/web@1.2.5 - @strudel/webaudio@1.2.5 - @strudel/xen@1.2.4 --- packages/codemirror/package.json | 2 +- packages/core/package.json | 2 +- packages/csound/package.json | 2 +- packages/draw/package.json | 2 +- packages/gamepad/package.json | 2 +- packages/hydra/package.json | 2 +- packages/midi/package.json | 2 +- packages/mini/package.json | 2 +- packages/mondough/package.json | 2 +- packages/motion/package.json | 2 +- packages/mqtt/package.json | 2 +- packages/osc/package.json | 2 +- packages/repl/package.json | 2 +- packages/sampler/package.json | 2 +- packages/serial/package.json | 2 +- packages/soundfonts/package.json | 2 +- packages/superdough/package.json | 2 +- packages/tonal/package.json | 2 +- packages/transpiler/package.json | 2 +- packages/web/package.json | 2 +- packages/webaudio/package.json | 2 +- packages/xen/package.json | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 797a4312b..609df6ede 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "1.2.3", + "version": "1.2.4", "description": "Codemirror Extensions for Strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/core/package.json b/packages/core/package.json index 33540c8bf..7cf20cea7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/core", - "version": "1.2.3", + "version": "1.2.4", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", "type": "module", diff --git a/packages/csound/package.json b/packages/csound/package.json index 19b1dd6fd..90130a101 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/csound", - "version": "1.2.4", + "version": "1.2.5", "description": "csound bindings for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/draw/package.json b/packages/draw/package.json index f63367402..6a4c57540 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/draw", - "version": "1.2.3", + "version": "1.2.4", "description": "Helpers for drawing with Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json index 53fad18a6..555eac03f 100644 --- a/packages/gamepad/package.json +++ b/packages/gamepad/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/gamepad", - "version": "1.2.3", + "version": "1.2.4", "description": "Gamepad Inputs for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/hydra/package.json b/packages/hydra/package.json index 1553264d4..7ba79d5d0 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/hydra", - "version": "1.2.3", + "version": "1.2.4", "description": "Hydra integration for strudel", "main": "hydra.mjs", "type": "module", diff --git a/packages/midi/package.json b/packages/midi/package.json index 8dde35984..2342cf7e9 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/midi", - "version": "1.2.4", + "version": "1.2.5", "description": "Midi API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mini/package.json b/packages/mini/package.json index e8fca4ac6..6eeaab0da 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mini", - "version": "1.2.3", + "version": "1.2.4", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mondough/package.json b/packages/mondough/package.json index 99a87f098..f26281490 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mondo", - "version": "1.1.1", + "version": "1.1.2", "description": "mondo notation for strudel", "main": "mondough.mjs", "type": "module", diff --git a/packages/motion/package.json b/packages/motion/package.json index acacc154f..57cac9cc8 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/motion", - "version": "1.2.3", + "version": "1.2.4", "description": "DeviceMotion API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index d5ac6dd71..2e32825fe 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mqtt", - "version": "1.2.3", + "version": "1.2.4", "description": "MQTT API for strudel", "main": "mqtt.mjs", "type": "module", diff --git a/packages/osc/package.json b/packages/osc/package.json index 6e8b57813..d5a272328 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/osc", - "version": "1.2.3", + "version": "1.2.4", "description": "OSC messaging for strudel", "main": "osc.mjs", "type": "module", diff --git a/packages/repl/package.json b/packages/repl/package.json index aabd1b8c7..547ec979f 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "1.2.4", + "version": "1.2.5", "description": "Strudel REPL as a Web Component", "module": "index.mjs", "publishConfig": { diff --git a/packages/sampler/package.json b/packages/sampler/package.json index dd701bcfb..2bf0607b1 100644 --- a/packages/sampler/package.json +++ b/packages/sampler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/sampler", - "version": "0.2.2", + "version": "0.2.3", "description": "", "keywords": [ "tidalcycles", diff --git a/packages/serial/package.json b/packages/serial/package.json index 8abbe7692..9ed89cf2a 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/serial", - "version": "1.2.3", + "version": "1.2.4", "description": "Webserial API for strudel", "main": "serial.mjs", "type": "module", diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index e5c1780ed..07e35674b 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/soundfonts", - "version": "1.2.4", + "version": "1.2.5", "description": "Soundsfont support for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/superdough/package.json b/packages/superdough/package.json index 809da0227..a82117774 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -1,6 +1,6 @@ { "name": "superdough", - "version": "1.2.4", + "version": "1.2.5", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "main": "index.mjs", "type": "module", diff --git a/packages/tonal/package.json b/packages/tonal/package.json index de02e4b1f..1461bdc8e 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/tonal", - "version": "1.2.3", + "version": "1.2.4", "description": "Tonal functions for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index f9ebdfde1..18722bdc2 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/transpiler", - "version": "1.2.3", + "version": "1.2.4", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "main": "index.mjs", "type": "module", diff --git a/packages/web/package.json b/packages/web/package.json index 3f9cc50d1..df21f4055 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/web", - "version": "1.2.4", + "version": "1.2.5", "description": "Easy to setup, opiniated bundle of Strudel for the browser.", "module": "web.mjs", "publishConfig": { diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 78988340a..cbe673a5a 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/webaudio", - "version": "1.2.4", + "version": "1.2.5", "description": "Web Audio helpers for Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/xen/package.json b/packages/xen/package.json index 1cb751157..0a6736d9c 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/xen", - "version": "1.2.3", + "version": "1.2.4", "description": "Xenharmonic API for strudel", "main": "index.mjs", "type": "module", From 5e2e6411e5e67cb2458132f3e643ebe1d265c5cd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 11 Sep 2025 08:26:20 +0200 Subject: [PATCH 177/257] Publish - @strudel/codemirror@1.2.5 - @strudel/repl@1.2.6 --- packages/codemirror/package.json | 2 +- packages/repl/package.json | 2 +- pnpm-lock.yaml | 18 ++---------------- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index be240a338..803b33f3d 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "1.2.4", + "version": "1.2.5", "description": "Codemirror Extensions for Strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/repl/package.json b/packages/repl/package.json index 547ec979f..41a165c34 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "1.2.5", + "version": "1.2.6", "description": "Strudel REPL as a Web Component", "module": "index.mjs", "publishConfig": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 810ffb9d5..f4a24f61c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -215,9 +215,6 @@ importers: '@strudel/transpiler': specifier: workspace:* version: link:../transpiler - codemirror: - specifier: ^6.0.2 - version: 6.0.2 nanostores: specifier: ^0.11.3 version: 0.11.3 @@ -3448,9 +3445,6 @@ packages: resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - codemirror@6.0.2: - resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} - collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -5720,6 +5714,7 @@ packages: node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead node-fetch-native@1.6.6: resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} @@ -6809,6 +6804,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} @@ -11050,16 +11046,6 @@ snapshots: cmd-shim@6.0.3: {} - codemirror@6.0.2: - dependencies: - '@codemirror/autocomplete': 6.18.4 - '@codemirror/commands': 6.8.0 - '@codemirror/language': 6.10.8 - '@codemirror/lint': 6.8.4 - '@codemirror/search': 6.5.8 - '@codemirror/state': 6.5.1 - '@codemirror/view': 6.36.2 - collapse-white-space@2.1.0: {} color-convert@2.0.1: From 27339715b77c57d30a6bc5841ff7f5103a6c876b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 11 Sep 2025 17:22:06 +0200 Subject: [PATCH 178/257] fix: exclude mondough dependencies --- packages/mondough/vite.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mondough/vite.config.js b/packages/mondough/vite.config.js index c46972e96..06c8c4559 100644 --- a/packages/mondough/vite.config.js +++ b/packages/mondough/vite.config.js @@ -1,5 +1,5 @@ import { defineConfig } from 'vite'; -//import { dependencies } from './package.json'; +import { dependencies } from './package.json'; import { resolve } from 'path'; // https://vitejs.dev/config/ @@ -12,7 +12,7 @@ export default defineConfig({ fileName: (ext) => ({ es: 'mondough.mjs' })[ext], }, rollupOptions: { - // external: [...Object.keys(dependencies)], + external: [...Object.keys(dependencies)], }, target: 'esnext', }, From 702e558d1a76813e71e143bef25bec68a81ecc04 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 11 Sep 2025 17:30:38 +0200 Subject: [PATCH 179/257] Publish - @strudel/mondo@1.1.3 --- packages/mondough/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondough/package.json b/packages/mondough/package.json index f26281490..7cec4fd02 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mondo", - "version": "1.1.2", + "version": "1.1.3", "description": "mondo notation for strudel", "main": "mondough.mjs", "type": "module", From c42d00f04866a19f31ded90ed9e582b09f79b813 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 11 Sep 2025 19:11:45 +0200 Subject: [PATCH 180/257] hotfix: export mondo getLocations --- packages/mondough/mondough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 15feb5d86..e409d5a3b 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -108,7 +108,7 @@ export function mondo(code, offset = 0) { return pat.markcss('color: var(--caret,--foreground);text-decoration:underline'); } -let getLocations = (code, offset) => runner.parser.get_locations(code, offset); +export let getLocations = (code, offset) => runner.parser.get_locations(code, offset); export const mondi = (str, offset) => { const code = `[${str}]`; From 637d714ab27e5bf29499c2cce0a15b7cadb58f71 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 11 Sep 2025 19:14:00 +0200 Subject: [PATCH 181/257] Publish - @strudel/mondo@1.1.4 --- packages/mondough/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondough/package.json b/packages/mondough/package.json index 7cec4fd02..c81d76cb6 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mondo", - "version": "1.1.3", + "version": "1.1.4", "description": "mondo notation for strudel", "main": "mondough.mjs", "type": "module", From c0bf86d11e3d795558b5dd01821f700a5e962070 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 11 Sep 2025 23:19:22 +0200 Subject: [PATCH 182/257] Revert "Merge pull request 'supradough poc' (#1362) from supradough into main" This reverts commit 6f06683d707ed5d04464af94d1a978ef21df84bd, reversing changes made to 637d714ab27e5bf29499c2cce0a15b7cadb58f71. --- eslint.config.mjs | 1 - packages/core/controls.mjs | 30 +- packages/superdough/sampler.mjs | 91 +- packages/superdough/superdough.mjs | 10 +- packages/supradough/.gitignore | 1 - packages/supradough/README.md | 3 - packages/supradough/dough-export.mjs | 123 --- packages/supradough/dough-worklet.mjs | 39 - packages/supradough/dough.mjs | 976 ---------------------- packages/supradough/index.mjs | 4 - packages/supradough/package.json | 37 - packages/webaudio/index.mjs | 1 - packages/webaudio/package.json | 3 +- packages/webaudio/supradough.mjs | 130 --- packages/webaudio/webaudio.mjs | 7 +- pnpm-lock.yaml | 20 - test/__snapshots__/examples.test.mjs.snap | 80 +- 17 files changed, 63 insertions(+), 1493 deletions(-) delete mode 100644 packages/supradough/.gitignore delete mode 100644 packages/supradough/README.md delete mode 100644 packages/supradough/dough-export.mjs delete mode 100644 packages/supradough/dough-worklet.mjs delete mode 100644 packages/supradough/dough.mjs delete mode 100644 packages/supradough/index.mjs delete mode 100644 packages/supradough/package.json delete mode 100644 packages/webaudio/supradough.mjs diff --git a/eslint.config.mjs b/eslint.config.mjs index c9ff40ca1..e30b8e8a6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -42,7 +42,6 @@ export default [ '**/hydra.mjs', '**/jsdoc-synonyms.js', 'packages/hs2js/src/hs2js.mjs', - 'packages/supradough/dough-export.mjs', '**/samples', ], }, diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 473ea82d1..58a30652c 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -309,17 +309,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 @@ -1108,27 +1097,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'); diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 9188c17c3..18d1b7797 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -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( diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 0c084e3e9..792e5a28d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -196,7 +196,7 @@ export const resetLoadedSounds = () => soundMap.set({}); let audioContext; export const setDefaultAudioContext = () => { - audioContext = new AudioContext({ latencyHint: 'playback' }); + audioContext = new AudioContext(); return audioContext; }; @@ -212,17 +212,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; diff --git a/packages/supradough/.gitignore b/packages/supradough/.gitignore deleted file mode 100644 index d21cbdf3e..000000000 --- a/packages/supradough/.gitignore +++ /dev/null @@ -1 +0,0 @@ -pattern.wav diff --git a/packages/supradough/README.md b/packages/supradough/README.md deleted file mode 100644 index a8cfa84b3..000000000 --- a/packages/supradough/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# supradough - -platform agnostic synth and sampler intended for live coding. a reimplementation of superdough. \ No newline at end of file diff --git a/packages/supradough/dough-export.mjs b/packages/supradough/dough-export.mjs deleted file mode 100644 index bd4b530b3..000000000 --- a/packages/supradough/dough-export.mjs +++ /dev/null @@ -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,') - .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('*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('*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('') - .voicing() - .s('') - .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)); -}); diff --git a/packages/supradough/dough-worklet.mjs b/packages/supradough/dough-worklet.mjs deleted file mode 100644 index 18c316b2d..000000000 --- a/packages/supradough/dough-worklet.mjs +++ /dev/null @@ -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); diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs deleted file mode 100644 index 711922b63..000000000 --- a/packages/supradough/dough.mjs +++ /dev/null @@ -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++; - } -} diff --git a/packages/supradough/index.mjs b/packages/supradough/index.mjs deleted file mode 100644 index 54a835495..000000000 --- a/packages/supradough/index.mjs +++ /dev/null @@ -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; diff --git a/packages/supradough/package.json b/packages/supradough/package.json deleted file mode 100644 index 7e465c0a9..000000000 --- a/packages/supradough/package.json +++ /dev/null @@ -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 ", - "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": {} -} diff --git a/packages/webaudio/index.mjs b/packages/webaudio/index.mjs index 4933b7a01..362e61c44 100644 --- a/packages/webaudio/index.mjs +++ b/packages/webaudio/index.mjs @@ -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'; diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 49da00f23..cbe673a5a 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -35,8 +35,7 @@ "dependencies": { "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", - "superdough": "workspace:*", - "supradough": "workspace:*" + "superdough": "workspace:*" }, "devDependencies": { "vite": "^6.0.11" diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs deleted file mode 100644 index f97251a07..000000000 --- a/packages/webaudio/supradough.mjs +++ /dev/null @@ -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); - } - }); -} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 383e87f87..429d2a26b 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -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); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c225057f3..f4a24f61c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 @@ -7552,9 +7537,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==} @@ -15976,8 +15958,6 @@ snapshots: walk-up-path@3.0.1: {} - wav-encoder@1.3.0: {} - wav@1.0.2: dependencies: buffer-alloc: 1.2.0 diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index ecbc33eac..41207620f 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -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 ]", From d164b5e7f5694f10bdd40c849878400b6275a3fe Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 12 Sep 2025 10:08:55 +0200 Subject: [PATCH 183/257] Revert "Revert "Merge pull request 'supradough poc' (#1362) from supradough into main"" This reverts commit c0bf86d11e3d795558b5dd01821f700a5e962070. --- eslint.config.mjs | 1 + packages/core/controls.mjs | 30 +- packages/superdough/sampler.mjs | 91 +- packages/superdough/superdough.mjs | 10 +- packages/supradough/.gitignore | 1 + packages/supradough/README.md | 3 + packages/supradough/dough-export.mjs | 123 +++ packages/supradough/dough-worklet.mjs | 39 + packages/supradough/dough.mjs | 976 ++++++++++++++++++++++ packages/supradough/index.mjs | 4 + packages/supradough/package.json | 37 + packages/webaudio/index.mjs | 1 + packages/webaudio/package.json | 3 +- packages/webaudio/supradough.mjs | 130 +++ packages/webaudio/webaudio.mjs | 7 +- pnpm-lock.yaml | 20 + test/__snapshots__/examples.test.mjs.snap | 80 +- 17 files changed, 1493 insertions(+), 63 deletions(-) create mode 100644 packages/supradough/.gitignore create mode 100644 packages/supradough/README.md create mode 100644 packages/supradough/dough-export.mjs create mode 100644 packages/supradough/dough-worklet.mjs create mode 100644 packages/supradough/dough.mjs create mode 100644 packages/supradough/index.mjs create mode 100644 packages/supradough/package.json create mode 100644 packages/webaudio/supradough.mjs diff --git a/eslint.config.mjs b/eslint.config.mjs index e30b8e8a6..c9ff40ca1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -42,6 +42,7 @@ export default [ '**/hydra.mjs', '**/jsdoc-synonyms.js', 'packages/hs2js/src/hs2js.mjs', + 'packages/supradough/dough-export.mjs', '**/samples', ], }, diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 58a30652c..473ea82d1 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -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'); diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 18d1b7797..9188c17c3 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -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( diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 792e5a28d..0c084e3e9 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -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; diff --git a/packages/supradough/.gitignore b/packages/supradough/.gitignore new file mode 100644 index 000000000..d21cbdf3e --- /dev/null +++ b/packages/supradough/.gitignore @@ -0,0 +1 @@ +pattern.wav diff --git a/packages/supradough/README.md b/packages/supradough/README.md new file mode 100644 index 000000000..a8cfa84b3 --- /dev/null +++ b/packages/supradough/README.md @@ -0,0 +1,3 @@ +# supradough + +platform agnostic synth and sampler intended for live coding. a reimplementation of superdough. \ No newline at end of file diff --git a/packages/supradough/dough-export.mjs b/packages/supradough/dough-export.mjs new file mode 100644 index 000000000..bd4b530b3 --- /dev/null +++ b/packages/supradough/dough-export.mjs @@ -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,') + .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('*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('*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('') + .voicing() + .s('') + .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)); +}); diff --git a/packages/supradough/dough-worklet.mjs b/packages/supradough/dough-worklet.mjs new file mode 100644 index 000000000..18c316b2d --- /dev/null +++ b/packages/supradough/dough-worklet.mjs @@ -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); diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs new file mode 100644 index 000000000..711922b63 --- /dev/null +++ b/packages/supradough/dough.mjs @@ -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++; + } +} diff --git a/packages/supradough/index.mjs b/packages/supradough/index.mjs new file mode 100644 index 000000000..54a835495 --- /dev/null +++ b/packages/supradough/index.mjs @@ -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; diff --git a/packages/supradough/package.json b/packages/supradough/package.json new file mode 100644 index 000000000..7e465c0a9 --- /dev/null +++ b/packages/supradough/package.json @@ -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 ", + "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": {} +} diff --git a/packages/webaudio/index.mjs b/packages/webaudio/index.mjs index 362e61c44..4933b7a01 100644 --- a/packages/webaudio/index.mjs +++ b/packages/webaudio/index.mjs @@ -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'; diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index cbe673a5a..49da00f23 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -35,7 +35,8 @@ "dependencies": { "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", - "superdough": "workspace:*" + "superdough": "workspace:*", + "supradough": "workspace:*" }, "devDependencies": { "vite": "^6.0.11" diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs new file mode 100644 index 000000000..f97251a07 --- /dev/null +++ b/packages/webaudio/supradough.mjs @@ -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); + } + }); +} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 429d2a26b..383e87f87 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -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); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4a24f61c..c225057f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 41207620f..ecbc33eac 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -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 ]", From a46617f3f0898ae417b993bbfbee8bfa5daf0765 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 12 Sep 2025 10:16:37 +0200 Subject: [PATCH 184/257] hotfix: use ?audioworklet for supradough worklet import --- packages/supradough/index.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/supradough/index.mjs b/packages/supradough/index.mjs index 54a835495..cb132fb3f 100644 --- a/packages/supradough/index.mjs +++ b/packages/supradough/index.mjs @@ -1,4 +1,5 @@ -import _workletUrl from './dough-worklet.mjs?url'; // todo: change ?url to ?audioworklet before build (?audioworklet doesn't hot reload) +// import _workletUrl from './dough-worklet.mjs?url'; // only for dev (breaks for production build) +import _workletUrl from './dough-worklet.mjs?audioworklet'; // only for prod (breaks in development?!) export * from './dough.mjs'; export const workletUrl = _workletUrl; From 352a3c39d7fd050348825d752239bc9e39f05f5e Mon Sep 17 00:00:00 2001 From: fesmith Date: Sat, 13 Sep 2025 00:00:36 +0200 Subject: [PATCH 185/257] Update website/src/pages/workshop/first-sounds.mdx --- website/src/pages/workshop/first-sounds.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx index 74daf4bab..f120f332a 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -260,16 +260,16 @@ It is quite common that there are many ways to express the same idea. punchcard /> -**selecting sample numbers separately** +**selecting sample numbers separately** -Instead of using ":", we can also use the `n` function to select sample numbers: - - - -This is shorter and more readable than: +Instead of selecting sample numbers one by one: +We can also use the `n` function to make it shorter and more readable: + + + ## Recap Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal. From e18296e76095b18a883b977979b4cf18c8139df8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 13 Sep 2025 19:52:07 +0200 Subject: [PATCH 186/257] hotfix: word --- website/src/pages/learn/code.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/code.mdx b/website/src/pages/learn/code.mdx index 7057c4576..f9c6b3a25 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -44,7 +44,7 @@ xxx("foo").yyy("bar") Generally, `xxx` and `yyy` are called [_functions_](), while `foo` and `bar` are called function [_arguments_ or _parameters_](). So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data. -The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is prepended with a dot (`.`). +The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is preceded with a dot (`.`). Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code. You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects. From ed00686804fecfa05609f7c21c93f2ce3512fded Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Sep 2025 14:29:28 -0500 Subject: [PATCH 187/257] Codeformat --- website/src/pages/learn/xen.mdx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index 32e2c0b43..b8b27ac7f 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -16,10 +16,7 @@ These functions allow the use of scales other than your typical chromatic 12 bas Here's an example of how to configure a basic hexany scale: - + Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3` @@ -67,6 +64,7 @@ Another helpful trick when exploring new tunings is to strum them. Many have a much more enchanting sound that was chosen over many generations of musicians for being strummed. Take the `sanza` tuning: + + This quality is often due to how the tunings were formed with instruments that were played differently than a piano. As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical: From 709b654ed39927f1c5f5ea8ae7bbde54f22e0f11 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Sep 2025 16:21:09 -0500 Subject: [PATCH 188/257] Add synonyms to autocomplete --- packages/codemirror/autocomplete.mjs | 26 ++++++++++++++++--- packages/codemirror/tooltip.mjs | 9 ++++--- packages/core/pattern.mjs | 12 +++++---- website/src/repl/Repl.css | 7 +++++ .../src/repl/components/panel/Reference.jsx | 3 +-- 5 files changed, 42 insertions(+), 15 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index b56cab826..69aa3bc59 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -54,11 +54,12 @@ const buildExamples = (examples) => ` : ''; -export const Autocomplete = ({ doc, label }) => +export const Autocomplete = (doc) => h`
-

${label || getDocLabel(doc)}

+

${getDocLabel(doc)}

+ ${doc.synonyms_text ? `
Synonyms: ${doc.synonyms_text}
` : ''} ${doc.description ? `
${doc.description}
` : ''} ${buildParamsList(doc.params)} ${buildExamples(doc.examples)} @@ -74,19 +75,36 @@ const isValidDoc = (doc) => { const hasExcludedTags = (doc) => ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); +export const getSynonymDoc = (doc, synonym) => { + const synonyms = doc.synonyms || []; + const docLabel = getDocLabel(doc); + // Swap `doc.name` in for `s` in the list of synonyms + const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym); + return { + ...doc, + name: synonym, + longname: synonym, + synonyms: synonymsWithDoc, + synonyms_text: synonymsWithDoc.join(', '), + }; +}; + const jsdocCompletions = (() => { const seen = new Set(); // avoid repetition const completions = []; for (const doc of jsdoc.docs) { if (!isValidDoc(doc) || hasExcludedTags(doc)) continue; - let labels = [getDocLabel(doc), ...(doc.synonyms || [])]; + const docLabel = getDocLabel(doc); + // Remove duplicates + const synonyms = doc.synonyms || []; + let labels = [docLabel, ...synonyms]; for (const label of labels) { // https://codemirror.net/docs/ref/#autocomplete.Completion if (label && !seen.has(label)) { seen.add(label); completions.push({ label, - info: () => Autocomplete({ doc, label }), + info: () => Autocomplete(getSynonymDoc(doc, label)), type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type }); } diff --git a/packages/codemirror/tooltip.mjs b/packages/codemirror/tooltip.mjs index f67e6d14a..d1d0479b2 100644 --- a/packages/codemirror/tooltip.mjs +++ b/packages/codemirror/tooltip.mjs @@ -1,6 +1,6 @@ import { hoverTooltip } from '@codemirror/view'; import jsdoc from '../../doc.json'; -import { Autocomplete } from './autocomplete.mjs'; +import { Autocomplete, getSynonymDoc } from './autocomplete.mjs'; const getDocLabel = (doc) => doc.name || doc.longname; @@ -52,10 +52,11 @@ export const strudelTooltip = hoverTooltip( let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0]; if (!entry) { // Try for synonyms - entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0]; - if (!entry) { + const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0]; + if (!doc) { return null; } + entry = getSynonymDoc(doc, word); } return { @@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip( create(view) { let dom = document.createElement('div'); dom.className = 'strudel-tooltip'; - const ac = Autocomplete({ doc: entry, label: word }); + const ac = Autocomplete(entry); dom.appendChild(ac); return { dom }; }, diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index addc301d5..39790f8a0 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1246,7 +1246,8 @@ export const silence = gap(1); /* Like silence, but with a 'steps' (relative duration) of 0 */ export const nothing = gap(0); -/** A discrete value that repeats once per cycle. +/** + * A discrete value that repeats once per cycle. * * @returns {Pattern} * @example @@ -1299,7 +1300,8 @@ export function sequenceP(pats) { return result; } -/** The given items are played at the same time at the same length. +/** + * The given items are played at the same time at the same length. * * @return {Pattern} * @synonyms polyrhythm, pr @@ -1382,11 +1384,11 @@ export function stackBy(by, ...pats) { .setSteps(steps); } -/** Concatenation: combines a list of patterns, switching between them successively, one per cycle: - * - * synonyms: `cat` +/** + * Concatenation: combines a list of patterns, switching between them successively, one per cycle. * * @return {Pattern} + * @synonyms cat * @example * slowcat("e5", "b4", ["d5", "c5"]) * diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index 498679090..9cf51ff85 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -112,6 +112,13 @@ margin: 0 0 8px 0; } +.autocomplete-info-function-synonyms { + margin: 0 0 12px 0; + color: var(--foreground); + line-height: 1.5; + opacity: 0.8; +} + .autocomplete-info-function-description { margin: 0 0 12px 0; color: var(--foreground); diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index 81826aca2..6007667fa 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -16,8 +16,7 @@ const availableFunctions = (() => { if (!s || seen.has(s)) continue; seen.add(s); // Swap `doc.name` in for `s` in the list of synonyms - const notS = synonyms.filter((x) => x && x !== s); - const synonymsWithDoc = Array.from(new Set([doc.name, ...notS])); + const synonymsWithDoc = [doc.name, ...synonyms].filter((x) => x && x !== s); functions.push({ ...doc, name: s, // update names for the synonym From a9f7d825c8456641b46451e48411d479d4b3e938 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 14 Sep 2025 00:40:54 +0200 Subject: [PATCH 189/257] hotfix: format --- website/src/pages/learn/xen.mdx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index 32e2c0b43..b8b27ac7f 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -16,10 +16,7 @@ These functions allow the use of scales other than your typical chromatic 12 bas Here's an example of how to configure a basic hexany scale: - + Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3` @@ -67,6 +64,7 @@ Another helpful trick when exploring new tunings is to strum them. Many have a much more enchanting sound that was chosen over many generations of musicians for being strummed. Take the `sanza` tuning: + + This quality is often due to how the tunings were formed with instruments that were played differently than a piano. As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical: From 2d2b238da9a3246a5af994584fc585a74890213c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 14 Sep 2025 01:03:14 +0200 Subject: [PATCH 190/257] fix: use template element for string to html --- packages/codemirror/html.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/html.mjs b/packages/codemirror/html.mjs index 527275ef6..f240059d3 100644 --- a/packages/codemirror/html.mjs +++ b/packages/codemirror/html.mjs @@ -1,6 +1,7 @@ -const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null; export let html = (string) => { - return parser?.parseFromString(string, 'text/html').querySelectorAll('*'); + const template = document.createElement('template'); + template.innerHTML = string.trim(); + return template.content.childNodes; }; let parseChunk = (chunk) => { if (Array.isArray(chunk)) return chunk.flat().join(''); From 9782795761648595fc05deacf863a7bf1a0348ab Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 14 Sep 2025 01:04:48 +0200 Subject: [PATCH 191/257] fix: autocomplete container style --- website/src/repl/Repl.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index 9cf51ff85..b5cd34d81 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -80,6 +80,8 @@ min-width: 300px !important; max-height: 400px !important; background-color: var(--lineHighlight) !important; + overflow: auto; + background: var(--background) !important; } /* Main tooltip container */ @@ -91,7 +93,7 @@ font-size: var(--font-size, 13px); line-height: 1.4; max-width: 600px; - max-height: 400px; + height: 100%; min-width: 400px; white-space: normal !important; overflow-y: auto !important; From d6254294b087af1008efc1943a110a36cc2afdb3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 14 Sep 2025 01:09:30 +0200 Subject: [PATCH 192/257] fix: bring back tests on external PRs (push doesn't cut it for forks) --- .forgejo/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index 90cb7e258..6b3759571 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -1,6 +1,6 @@ name: Strudel tests -on: [push] +on: [push, pull_request] jobs: build: @@ -19,7 +19,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: 'pnpm' + cache: "pnpm" - run: pnpm install - run: pnpm run format-check - run: pnpm run lint From 9da027d377e278b5b2bb173139c03dc2de7b72f8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 14 Sep 2025 01:11:34 +0200 Subject: [PATCH 193/257] hotfix: format --- website/src/pages/workshop/first-sounds.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx index f120f332a..1d659972b 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -260,7 +260,7 @@ It is quite common that there are many ways to express the same idea. punchcard /> -**selecting sample numbers separately** +**selecting sample numbers separately** Instead of selecting sample numbers one by one: From f5c4373f996c5e8a699b4c850baa310d16666021 Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Sat, 13 Sep 2025 17:21:19 -0700 Subject: [PATCH 194/257] added plyWith/plyWithClassic functions --- packages/core/pattern.mjs | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 39790f8a0..a88a1eaea 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2399,6 +2399,56 @@ export const stut = register('stut', function (times, feedback, time, pat) { return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i))); }); +export const applyN = register('applyN', function (n, func, p) { + let result = p; + for (let i = 0; i < n; i++) { + result = func(result); + } + return result; +}); + +/** + * The plyWithClassic function repeats each event the given number of times, applying the given function to each event.\n + * Here the function does not take the iteration index as an argument. + * @name plyWithClassic + * @param {number} factor how many times to repeat + * @param {function} func function to apply, given the pattern + * @example + * "<0 [2 4]>" + * .plyWith(4, (p) => p.add(2)) + * .scale("C:minor").note() + */ +export const plyWithClassic = register('plyWithClassic', function (factor, func, pat) { + const result = pat + .fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor)) + .squeezeJoin(); + if (__steps) { + result._steps = Fraction(factor).mulmaybe(pat._steps); + } + return result; +}); + +/** + * The plyWith function repeats each event the given number of times, applying the given function to each event. + * @name plyWith + * @synonyms plywith + * @param {number} factor how many times to repeat + * @param {function} func function to apply, given the pattern and the iteration index + * @example + * "<0 [2 4]>" + * .plyWith(4, (p,n) => p.add(n*2)) + * .scale("C:minor").note() + */ +export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) { + const result = pat + .fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor)) + .squeezeJoin(); + if (__steps) { + result._steps = Fraction(factor).mulmaybe(pat._steps); + } + return result; +}); + /** * Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played. * @name iter From addb6db1c76ed9de1246c8f3f617f6d8220d0892 Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Sun, 14 Sep 2025 02:21:25 -0700 Subject: [PATCH 195/257] updated names + snapshot --- packages/core/pattern.mjs | 19 ++++---- test/__snapshots__/examples.test.mjs.snap | 58 +++++++++++++++++++++++ 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index a88a1eaea..262b92bb3 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2408,9 +2408,9 @@ export const applyN = register('applyN', function (n, func, p) { }); /** - * The plyWithClassic function repeats each event the given number of times, applying the given function to each event.\n - * Here the function does not take the iteration index as an argument. - * @name plyWithClassic + * The plyWith function repeats each event the given number of times, applying the given function to each event.\n + * @name plyWith + * @synonyms plywith * @param {number} factor how many times to repeat * @param {function} func function to apply, given the pattern * @example @@ -2418,7 +2418,7 @@ export const applyN = register('applyN', function (n, func, p) { * .plyWith(4, (p) => p.add(2)) * .scale("C:minor").note() */ -export const plyWithClassic = register('plyWithClassic', function (factor, func, pat) { +export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) { const result = pat .fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor)) .squeezeJoin(); @@ -2429,17 +2429,18 @@ export const plyWithClassic = register('plyWithClassic', function (factor, func, }); /** - * The plyWith function repeats each event the given number of times, applying the given function to each event. - * @name plyWith - * @synonyms plywith + * The plyForEach function repeats each event the given number of times, applying the given function to each event. + * This version of ply uses the iteration index as an argument to the function, similar to echoWith. + * @name plyForEach + * @synonyms plyforeach * @param {number} factor how many times to repeat * @param {function} func function to apply, given the pattern and the iteration index * @example * "<0 [2 4]>" - * .plyWith(4, (p,n) => p.add(n*2)) + * .plyForEach(4, (p,n) => p.add(n*2)) * .scale("C:minor").note() */ -export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) { +export const plyForEach = register(['plyForEach', 'plyforeach'], function (factor, func, pat) { const result = pat .fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor)) .squeezeJoin(); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index ecbc33eac..46359b620 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -7365,6 +7365,64 @@ exports[`runs examples > example "ply" example index 0 1`] = ` ] `; +exports[`runs examples > example "plyForEach" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:C3 ]", + "[ 1/4 → 1/2 | note:Eb3 ]", + "[ 1/2 → 3/4 | note:G3 ]", + "[ 3/4 → 1/1 | note:Bb3 ]", + "[ 1/1 → 9/8 | note:Eb3 ]", + "[ 9/8 → 5/4 | note:G3 ]", + "[ 5/4 → 11/8 | note:Bb3 ]", + "[ 11/8 → 3/2 | note:D4 ]", + "[ 3/2 → 13/8 | note:G3 ]", + "[ 13/8 → 7/4 | note:Bb3 ]", + "[ 7/4 → 15/8 | note:D4 ]", + "[ 15/8 → 2/1 | note:F4 ]", + "[ 2/1 → 9/4 | note:C3 ]", + "[ 9/4 → 5/2 | note:Eb3 ]", + "[ 5/2 → 11/4 | note:G3 ]", + "[ 11/4 → 3/1 | note:Bb3 ]", + "[ 3/1 → 25/8 | note:Eb3 ]", + "[ 25/8 → 13/4 | note:G3 ]", + "[ 13/4 → 27/8 | note:Bb3 ]", + "[ 27/8 → 7/2 | note:D4 ]", + "[ 7/2 → 29/8 | note:G3 ]", + "[ 29/8 → 15/4 | note:Bb3 ]", + "[ 15/4 → 31/8 | note:D4 ]", + "[ 31/8 → 4/1 | note:F4 ]", +] +`; + +exports[`runs examples > example "plyWith" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:C3 ]", + "[ 1/4 → 1/2 | note:Eb3 ]", + "[ 1/2 → 3/4 | note:G3 ]", + "[ 3/4 → 1/1 | note:Bb3 ]", + "[ 1/1 → 9/8 | note:Eb3 ]", + "[ 9/8 → 5/4 | note:G3 ]", + "[ 5/4 → 11/8 | note:Bb3 ]", + "[ 11/8 → 3/2 | note:D4 ]", + "[ 3/2 → 13/8 | note:G3 ]", + "[ 13/8 → 7/4 | note:Bb3 ]", + "[ 7/4 → 15/8 | note:D4 ]", + "[ 15/8 → 2/1 | note:F4 ]", + "[ 2/1 → 9/4 | note:C3 ]", + "[ 9/4 → 5/2 | note:Eb3 ]", + "[ 5/2 → 11/4 | note:G3 ]", + "[ 11/4 → 3/1 | note:Bb3 ]", + "[ 3/1 → 25/8 | note:Eb3 ]", + "[ 25/8 → 13/4 | note:G3 ]", + "[ 13/4 → 27/8 | note:Bb3 ]", + "[ 27/8 → 7/2 | note:D4 ]", + "[ 7/2 → 29/8 | note:G3 ]", + "[ 29/8 → 15/4 | note:Bb3 ]", + "[ 15/4 → 31/8 | note:D4 ]", + "[ 31/8 → 4/1 | note:F4 ]", +] +`; + exports[`runs examples > example "polymeter" example index 0 1`] = ` [ "[ 0/1 → 1/6 | note:c ]", From df1934f87b41e722fb8567ee051902b95e31b2b9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 14 Sep 2025 23:54:51 +0200 Subject: [PATCH 196/257] add some typechecking + fix delay --- packages/supradough/dough.mjs | 190 +++++++++++++++++++++++++++++----- 1 file changed, 165 insertions(+), 25 deletions(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 711922b63..1cb9241ad 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -1,4 +1,6 @@ // this is dough, the superdough without dependencies +// @ts-check +// @ts-ignore ignore next line because sampleRate is unknown const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000; const PI_DIV_SR = Math.PI / SAMPLE_RATE; const ISR = 1 / SAMPLE_RATE; @@ -641,8 +643,8 @@ const note2midi = (note, defaultOctave = 3) => { } 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 octave = Number(oct || defaultOctave); + return (octave + 1) * 12 + chroma + offset; }; const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440; const note2freq = (note) => { @@ -654,9 +656,152 @@ const note2freq = (note) => { }; export class DoughVoice { + /** @type {number} */ + id = 0; + /** @type {number[]} */ out = [0, 0]; + + /** @type {number | undefined} */ + attack; + /** @type {number | undefined} */ + decay; + /** @type {number | undefined} */ + sustain; + /** @type {number} */ + release; + /** @type {number} */ + _begin; + /** @type {number} */ + _duration; + + /** @type {any} */ + _sound; + /** @type {number} */ + _channels = 1; + /** @type {BufferPlayer[] | undefined} */ + _buffers; + /** @type {string | undefined} */ + unit; + + /** @type {ADSR | undefined} */ + _penv; + /** @type {number | undefined} */ + penv; + /** @type {number | undefined} */ + pattack; + /** @type {number | undefined} */ + pdecay; + /** @type {number | undefined} */ + psustain; + /** @type {number | undefined} */ + prelease; + + /** @type {number | undefined} */ + vib; + + _vib; + /** @type {number | undefined} */ + vibmod; + + /** @type {SineOsc | undefined} */ + _fm; + /** @type {number | undefined} */ + fmh; + /** @type {number | undefined} */ + fmi; + + /** @type {ADSR | undefined} */ + _fmenv; + /** @type {number | undefined} */ + fmattack; + /** @type {number | undefined} */ + fmdecay; + /** @type {number | undefined} */ + fmsustain; + /** @type {number | undefined} */ + fmrelease; + + /** @type {ADSR | undefined} */ + _lpenv; + lpenv; + /** @type {number | undefined} */ + lpattack; + /** @type {number | undefined} */ + lpdecay; + /** @type {number | undefined} */ + lpsustain; + /** @type {number | undefined} */ + lprelease; + + /** @type {ADSR | undefined} */ + _hpenv; + /** @type {number | undefined} */ + hpenv; + /** @type {number | undefined} */ + hpattack; + /** @type {number | undefined} */ + hpdecay; + /** @type {number | undefined} */ + hpsustain; + /** @type {number | undefined} */ + hprelease; + + /** @type {ADSR | undefined} */ + _bpenv; + /** @type {number | undefined} */ + bpenv; + /** @type {number | undefined} */ + bpattack; + /** @type {number | undefined} */ + bpdecay; + /** @type {number | undefined} */ + bpsustain; + /** @type {number | undefined} */ + bprelease; + + /** @type {number | undefined} */ + cutoff; + /** @type {number | undefined} */ + hcutoff; + /** @type {number | undefined} */ + bandf; + /** @type {number | undefined} */ + coarse; + /** @type {number | undefined} */ + crush; + /** @type {number | undefined} */ + distort; + + /** @type {number} */ + freq; + /** @type {string | undefined} */ + note; + + /** @type {TwoPoleFilter[] | null | undefined} */ + _lpf; + /** @type {TwoPoleFilter[] | null | undefined} */ + _hpf; + /** @type {TwoPoleFilter[] | null | undefined} */ + _bpf; + /** @type {Chorus[] | null | undefined} */ + _chorus; + /** @type {Coarse[] | null | undefined} */ + _coarse; + /** @type {Crush[] | null | undefined} */ + _crush; + /** @type {Distort[] | null | undefined} */ + _distort; + + /** + * @param {DoughVoice} value + */ constructor(value) { - value.freq ??= note2freq(value.note); + // mandatory controls + this.freq ??= note2freq(value.note); + this._begin = value._begin; + this._duration = value._duration; + this.release = value.release ?? 0; + // the rest.. we use $ for readability let $ = this; Object.assign($, value); $.s = $.s ?? getDefaultValue('s'); @@ -773,7 +918,7 @@ export class DoughVoice { let freq = this.freq * this.speed; // frequency modulation - if (this._fm) { + if (this._fm && this.fmh !== undefined && this.fmi !== undefined) { let fmi = this.fmi; if (this._fmenv) { const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease); @@ -785,37 +930,31 @@ export class DoughVoice { } // vibrato - if (this._vib) { + if (this._vib && this.vibmod !== undefined) { freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12); } // pitch envelope - if (this._penv) { + if (this._penv && this.penv !== undefined) { 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; - } + if (lpf !== undefined && 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; - } + if (hpf !== undefined && this._hpenv && this.hpenv !== undefined) { + 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; - } + if (bpf !== undefined && this._bpenv && this.bpenv !== undefined) { + 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); @@ -891,8 +1030,8 @@ export class Dough { 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(); + this._delayL = new Delay(); + this._delayR = new Delay(); } loadSample(name, channels, sampleRate) { BufferPlayer.samples.set(name, { channels, sampleRate }); @@ -965,8 +1104,9 @@ export class Dough { } } // 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); + const delayL = this._delayL.update(this.delaysend[0], this.delaytime); + const delayR = this._delayR.update(this.delaysend[1], this.delaytime); + this.delaysend[0] = delayL * this.delayfeedback; this.delaysend[1] = delayR * this.delayfeedback; this.out[0] += delayL; From 92b2013cf6efa9f29372060ab6a052081633cb23 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 14 Sep 2025 17:04:39 -0500 Subject: [PATCH 197/257] Correctly handle silences for non-notes --- packages/tonal/test/tonal.test.mjs | 2 +- packages/tonal/tonal.mjs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index cb856fa99..5f61b05a9 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -66,7 +66,7 @@ describe('tonal', () => { n(seq('0b#', '1#b', '2#b#')) .scale('C major') .firstCycleValues.map((h) => h.note), - ).toEqual(['', '', '']); + ).toEqual([]); }); it('snaps notes (upwards) to scale', () => { const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb']; diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 4f189e3d6..ae75ab690 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -189,8 +189,7 @@ function _convertStepToNumberAndOffset(step) { const match = /^(-?\d+)(#+|b+)?$/.exec(step); if (!match) { - logger(`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`, 'error'); - return [silence, 0]; + throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`); } asNumber = Number(match[1]); // These decorations will determine the semitone offset based on the number of @@ -275,8 +274,8 @@ export const scale = register( // legacy.. return pure(step); } - const [number, offset] = _convertStepToNumberAndOffset(step); try { + const [number, offset] = _convertStepToNumberAndOffset(step); let note; if (isObject && value.anchor) { note = stepInNamedScale(number, scale, value.anchor); @@ -287,7 +286,7 @@ export const scale = register( value = pure(isObject ? { ...value, note } : note); } catch (err) { logger(`[tonal] ${err.message}`, 'error'); - value = silence; + return silence; } return value; } From a7e485992e6bba4dd62a30787565e18408152137 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 15 Sep 2025 20:54:39 +0200 Subject: [PATCH 198/257] add tic80 font --- website/public/fonts/tic80/license.txt | 5 +++++ website/public/fonts/tic80/readme.txt | 16 ++++++++++++++++ website/public/fonts/tic80/tic-80-wide-font.otf | Bin 0 -> 10020 bytes .../src/repl/components/panel/SettingsTab.jsx | 1 + website/src/styles/index.css | 5 +++++ 5 files changed, 27 insertions(+) create mode 100644 website/public/fonts/tic80/license.txt create mode 100644 website/public/fonts/tic80/readme.txt create mode 100644 website/public/fonts/tic80/tic-80-wide-font.otf diff --git a/website/public/fonts/tic80/license.txt b/website/public/fonts/tic80/license.txt new file mode 100644 index 000000000..e42ba4eb9 --- /dev/null +++ b/website/public/fonts/tic80/license.txt @@ -0,0 +1,5 @@ +The FontStruction “TIC-80 wide font” +(https://fontstruct.com/fontstructions/show/1388526) by “nesbox” is licensed +under a Creative Commons CC0 Public Domain Dedication license +(http://creativecommons.org/publicdomain/zero/1.0/). +[ancestry] \ No newline at end of file diff --git a/website/public/fonts/tic80/readme.txt b/website/public/fonts/tic80/readme.txt new file mode 100644 index 000000000..b3aaff55c --- /dev/null +++ b/website/public/fonts/tic80/readme.txt @@ -0,0 +1,16 @@ +The font file in this archive was created using Fontstruct the free, online +font-building tool. +This font was created by “nesbox”. +This font has a homepage where this archive and other versions may be found: +https://fontstruct.com/fontstructions/show/1388526 +[ancestry] +Try Fontstruct at https://fontstruct.com +It’s easy and it’s fun. + +Fontstruct is copyright ©2017-2025 Rob Meek + +LEGAL NOTICE: +In using this font you must comply with the licensing terms described in the +file “license.txt” included with this archive. +If you redistribute the font file in this archive, it must be accompanied by all +the other files from this archive, including this one. diff --git a/website/public/fonts/tic80/tic-80-wide-font.otf b/website/public/fonts/tic80/tic-80-wide-font.otf new file mode 100644 index 0000000000000000000000000000000000000000..ba1c1caf4c74de8484950313ad046f5cd7c56ace GIT binary patch literal 10020 zcmeHMZERa-6+YJ=*G}SmHA^U)TP|o~V>EV}u1!0rXqOV&!Dwl_ZYpENN!-M39cQ+) zrjt9S!6qOurU`}wV-iSwYT6$pgeEpV1{>-In#K=4eoS*1(-0E;n1*9#NqNpW@4dcu z+^+ix%u(!f-}|0(o^#G~&b{{CfB5izYNxv?NY3cknDeKn#(zQ-4iWYLb}(-;X}w|FrLs^(X~ILv(Z}zgsuj_n387(ieOH z2i_?V`Mvc3g((z#H3$@lO+Fz{VjZs#zv+wdc(1WThY!*&a-0oyUyfy+|J`}yb1Qrj zBB02AMICwGx>5h#WB1ct=TBbqAG>Srx~p@c&t_fs_(J>r>waJU^;UALz3y78Pj{`A zEqBeo$1l6*FZGjk1K(JiM_#vXL{$R#X98b_cU07hp~Oo#j^dBc-Q~Q~*>E0JL~YkQ z@$=`;mtWfZ(irdkAA9|u#%)mulftsP5APct+&*+VGnqb~n`f|)(=vi1;S9zkl^5?bT8wUOTtE*k4(UESGIwEZdcGq*k(*%aN4?Z>T2h z<;6$`c0d7AWif;u*oPe*)g-U3;JrM+a+&20i!7U_iP}b}M0EWI+Ra@;8KTn|o;+B@z2ipJG zaYe_cI_~OtwBzO26|q!oA@)q{mCntb2Ra|>{9)(0&eyxH>AI=w-mYJERl0|}k99xT zeXb|cb5+ko&qB{{dX{?=y|?t<-}~#{SJ!P^chkCu);-r3=-bq{tM5eLclsXdd%W-I zz8B(={qgV>H${bOOHq5;YSAIk+F!Jt@4(YiP{aiDPH@$ ztxI29{Yrm4@_Zz22kiLLP^1e7{9h?o%3Pdkl2gButS0+WBpjhIdOjZB+W(#nEZ0i4 zQa?(ALZURwiz`V}RxOvF;WA2zxtLN$ub5iiWnGtdmW3Ef56J8X9sz^ZWK+C)wwhp1 zObOn-3KW8vJuWhxu#qi@91m|3Zb?AOA_c7K>|jA-)`ePWfNcvBaJ2%ix}GcLIuluT zA;fFXnr)H^w&1F0&ipwAaqIT0XZ?!Rq$r9J6`Q$piTk2Fz z`n6Y*aOrhptyB-mk5%JG>uHOk`YTDTwiZFsW~#C#=%xz!GrC$rwa~a;oxCRYN`rXV zdX>3{n7o}{y1kv?lVVJ= z*9Y2jc=xQ?pXF?*9W5fuYNxT4D`jnEzyNrpn^P~%1n{6jv$n|`U;#Chz?nF_0VxjP zfhW4#5Otr;ZGF+}f@eiDpw|dDdkoK=F+iYzO%DB0n?WUGqh2u9kmN2enzOX;Gp*rp4*$&TjQ8@EahWvR4a17|Dci&B5BLao#=3ljeE zgUNu|?`Fe4;0)8H(K&IIvhq%$ua-1#8m68%UL=!#ZGSJVBpR^rT=KlL6?blFu~!lv z*O8(X^a*#!wmPT%junc|Wp9F27U2)+0|lY0*qIVq2%)$c7ffi_HmuT}ARf*F3-{_G zdib1aYi4B23sn=Js%OC`^o85ks;9lt%<87zXtLyErUvJQ13O?&y*tznZ_<0O>RH@- zHDYMkFM%_}we71A>ovakR&VZViIHXxsktDd5!5xnYm0&8hW4&)rF}i;)w7&)6aZkF zJRm@L(9#+YIDkFwZ7q-6j-EFjN&>7tD|j?RuU=3?QCX5pC*VQ}s>@~nZ3q-ic@?y~ zM@a)jjokJ{$S&8W)w81gj!UWuof+$93`qTqMd6^PQ7NkpS5+BpqoL#7s3|}6k_8zT zN!7qHoocF~dEKs-42XIkvMRVbWO#E29yrKHI@UU$wK zR$y{yZa7aUt86v(mWhLXYk0)n?w>EP3&iY-tEc^jF~@7L$!q=r=3xOtp<)7;A_^3L z&REYS2RMX`!=T2{TvDEaQc{CnbLh{S`HG|+h7)k^FQLf(ntPzJ7oZg`Lnxc62k4EQ zV{hK2*Bi?^J+KqVkSgUX-s?1%USx6slAh*Pm0`>vn&{BjczWG%+fc7sGQHugaVMK{ zyB=xu1_aj)Jt4U^ceR4jf9KBk{=z3F33Etp+XW#2=%q@O{7!^p+8EU&f~bE@@qh{C z#52mSw^u8ChiLBIu%dNkQ!edb$!tc$C%84u7}xA6|9B=Qe6!yT90l<1;tW1`;H}aL zmjDGDK_Oq#TA9|`aV4_ zYDCzs*feDx1f?m?a&XEe)E?My0?8Yw6VFQ>&iD(7z1|RkS!7FTuv$V2d6erMl;jzj z2sQM(89;fh&RxK*4xgeYe6Qo;29hbhyvR&+c?^^dmlJhYzwEj9MzD_fq&7T=>Jr{yp=3(Qx8WXv zTrTo1oaeDEBvuP}*PZM@6UCERd(s5YdoYS53Pp|;M5i)h^x9yJ0do}*x2y7-0+gVt znS>1IcxUIZ;}+EG91c{#s$ujBL(`oMQ}gK#R;VdqRgFyxNwCw)4QE3y=eWoq43xgP z!I5xy&{}b}#1CwkHgJD4;Kt`V)PI|4{Nm#OL5ICo%3z)O>1C~~Kb`41Wd5OkOW=g} zd;K+@WZ|iddlaB9y1|^m^Dw%_$RQe{c_W8ufPP?Po8t7Okt1}3{$k`PZL)@p+(t3$ zRwK95Q42qx%BzF!vwmgd7|jOu8o85>27YSfE*cN6Gjbmt3>Hx1#54%`_0Zf zP4`h~1aSDRxQJXlV+PkDV9Nv7gv6GC4F?DvSxrd{SwVafYdJ*YP%F&@ z^mCMz3>A^F0$oQ*{JGu=J}>TGUml%!1J)M#b2h;9X&_;Iu+Fx4OQsz2z(&&;{hytGDqesWooMo-EO5OfU@Z^UC&7rimr0+GB;6^@%}Gv~vq|tPLyCDE(iCvq4($|BoD^KV z!dkPy!kx*{oPbT`Ll$ew6z zc}LRuLMAuk;F+Orai2iepA9;hf|GKJ`P5`Oo64Vba>soS4igi(>~yg>TevQn+rT@F*AXurh^Q6klnwjiCo^Z zFtTIo$hAp@Ihs$WikUmo&S);1g%@XZbjUd{H-0KJ;q1v}Q<)iOPkJ&lflav?c7kvx zj1FRDl|(K-m7EooNzq8&na<~uNNjS9EstlWoZIJ86DKn>Q_h{~+37R+f^#B=XR+Bc z$jQ7jo}S!&I=B1yk@@V+yLavc-`}3)Z_jd_f6a6B?OC40d~LjD{8!C#+_mZJx0v^> IUluR_2cfFLr2qf` literal 0 HcmV?d00001 diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 26c9ae287..80daef018 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -74,6 +74,7 @@ const fontFamilyOptions = { FiraCode: 'FiraCode', 'FiraCode-SemiBold': 'FiraCode SemiBold', teletext: 'teletext', + tic80: 'tic80', mode7: 'mode7', BigBlueTerminal: 'BigBlueTerminal', x3270: 'x3270', diff --git a/website/src/styles/index.css b/website/src/styles/index.css index 7fa4b2df8..41b282ff4 100644 --- a/website/src/styles/index.css +++ b/website/src/styles/index.css @@ -50,6 +50,11 @@ src: url('/fonts/teletext/EuropeanTeletext.ttf'); size-adjust: 90%; } +@font-face { + font-family: 'tic80'; + src: url('/fonts/tic80/tic-80-wide-font.otf'); + size-adjust: 60%; +} @font-face { font-family: 'mode7'; src: url('/fonts/mode7/MODE7GX3.TTF'); From 2cba1dcb317fea1046a8a95f673ad888e1960723 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 15 Sep 2025 21:46:37 -0400 Subject: [PATCH 199/257] working --- packages/core/signal.mjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 6bd6fde67..f736ce71f 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -16,7 +16,7 @@ export function steady(value) { } export const signal = (func) => { - const query = (state) => [new Hap(undefined, state.span, func(state.span.begin))]; + const query = (state) => [new Hap(undefined, state.span, func(state.span.begin.valueOf()))]; return new Pattern(query); }; @@ -152,7 +152,10 @@ export const itri2 = fastcat(isaw2, saw2); * * @return {Pattern} */ -export const time = signal(id); +export const time = signal((x) => { + console.info(typeof x) +return x +}); /** * The mouse's x position value ranges from 0 to 1. From 7dfd7dbcdc4500cf69d4a7767ba74f0adc42ffa7 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 15 Sep 2025 21:47:40 -0400 Subject: [PATCH 200/257] rmconsole --- packages/core/signal.mjs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index f736ce71f..2646ccf7c 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -152,10 +152,7 @@ export const itri2 = fastcat(isaw2, saw2); * * @return {Pattern} */ -export const time = signal((x) => { - console.info(typeof x) -return x -}); +export const time = signal(id); /** * The mouse's x position value ranges from 0 to 1. From 74e27ca94f977b2586616ccb023a6a6f96edbedb Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 15 Sep 2025 21:57:56 -0400 Subject: [PATCH 201/257] coerce in correct place --- packages/core/signal.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 2646ccf7c..b246cb9e3 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -16,7 +16,7 @@ export function steady(value) { } export const signal = (func) => { - const query = (state) => [new Hap(undefined, state.span, func(state.span.begin.valueOf()))]; + const query = (state) => [new Hap(undefined, state.span, func(state.span.begin))]; return new Pattern(query); }; @@ -152,7 +152,9 @@ export const itri2 = fastcat(isaw2, saw2); * * @return {Pattern} */ -export const time = signal(id); +export const time = signal(x => { + return x.valueOf() +}); /** * The mouse's x position value ranges from 0 to 1. From 4cc453c640bda94df291b3cb562f5d4d9b75f576 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 15 Sep 2025 22:09:05 -0400 Subject: [PATCH 202/257] fix test --- packages/core/signal.mjs | 4 ++-- packages/core/test/pattern.test.mjs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index b246cb9e3..79e45da7d 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -152,8 +152,8 @@ export const itri2 = fastcat(isaw2, saw2); * * @return {Pattern} */ -export const time = signal(x => { - return x.valueOf() +export const time = signal((x) => { + return x.valueOf(); }); /** diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 1df5c8776..3b6619b56 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -877,7 +877,7 @@ describe('Pattern', () => { .squeezeJoin() .queryArc(3, 4) .map((x) => x.value), - ).toStrictEqual([Fraction(3)]); + ).toStrictEqual([3]); }); }); describe('ply', () => { From 3eb40ee7d3b5c1b1404a0d00e131bca018ca3e2e Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 16 Sep 2025 01:44:01 -0400 Subject: [PATCH 203/257] working in dev --- packages/core/logger.mjs | 5 +++-- packages/superdough/logger.mjs | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/core/logger.mjs b/packages/core/logger.mjs index 4f2002319..6727c2422 100644 --- a/packages/core/logger.mjs +++ b/packages/core/logger.mjs @@ -5,8 +5,9 @@ let debounce = 1000, lastTime; export function errorLogger(e, origin = 'cyclist') { - //TODO: add some kind of debug flag that enables this while in dev mode - // console.error(e); + if (process.env.NODE_ENV === 'development') { + console.error(e); + } logger(`[${origin}] error: ${e.message}`); } diff --git a/packages/superdough/logger.mjs b/packages/superdough/logger.mjs index b3c9c34f3..912db1738 100644 --- a/packages/superdough/logger.mjs +++ b/packages/superdough/logger.mjs @@ -1,8 +1,10 @@ let log = (msg) => console.log(msg); -export function errorLogger(e, origin = 'cyclist') { +export function errorLogger(e, origin = 'superdough') { //TODO: add some kind of debug flag that enables this while in dev mode - // console.error(e); + if (process.env.NODE_ENV === 'development') { + console.error(e); + } logger(`[${origin}] error: ${e.message}`); } From 9a8f8a051c35b9f8ced51b3e2549ff0dcc0ebfb0 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 16 Sep 2025 01:48:37 -0400 Subject: [PATCH 204/257] rm comment --- packages/superdough/logger.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/superdough/logger.mjs b/packages/superdough/logger.mjs index 912db1738..99fd0cf39 100644 --- a/packages/superdough/logger.mjs +++ b/packages/superdough/logger.mjs @@ -1,7 +1,6 @@ let log = (msg) => console.log(msg); export function errorLogger(e, origin = 'superdough') { - //TODO: add some kind of debug flag that enables this while in dev mode if (process.env.NODE_ENV === 'development') { console.error(e); } From 9cdba1a50ee806bd56215d2635ec1cad1338a204 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 16 Sep 2025 11:36:21 -0400 Subject: [PATCH 205/257] Working --- packages/core/pattern.mjs | 4 +++- packages/core/signal.mjs | 4 +--- packages/superdough/superdough.mjs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 262b92bb3..2991cffea 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import TimeSpan from './timespan.mjs'; import Fraction, { lcm } from './fraction.mjs'; +import FractionClass from 'fraction.js'; import Hap from './hap.mjs'; import State from './state.mjs'; import { unionWithObj } from './value.mjs'; @@ -26,6 +27,7 @@ import { } from './util.mjs'; import drawLine from './drawLine.mjs'; import { logger } from './logger.mjs'; +import fraction from './fraction.mjs'; let stringParser; @@ -999,7 +1001,7 @@ addToPrototype('weaveWith', function (t, ...funcs) { // compose matrix functions function _nonArrayObject(x) { - return !Array.isArray(x) && typeof x === 'object'; + return !Array.isArray(x) && typeof x === 'object' && !(x instanceof FractionClass); } function _composeOp(a, b, func) { if (_nonArrayObject(a) || _nonArrayObject(b)) { diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 79e45da7d..6bd6fde67 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -152,9 +152,7 @@ export const itri2 = fastcat(isaw2, saw2); * * @return {Pattern} */ -export const time = signal((x) => { - return x.valueOf(); -}); +export const time = signal(id); /** * The mouse's x position value ranges from 0 to 1. diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index f1f308a7d..b1df93916 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -465,7 +465,7 @@ function duckOrbit(audioContext, targetOrbit, t, onsettime = 0, attacktime = 0.1 gainParam.cancelScheduledValues(now); gainParam.setValueAtTime(currVal, now); - const t0 = Math.max(t, now); // guard against now > t + const t0 = now; // guard against now > t const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal); gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset); gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack); From 89b7eac789e221097a55d5ee20904d5c14b7c3ab Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 16 Sep 2025 11:52:36 -0400 Subject: [PATCH 206/257] fix test --- packages/core/fraction.mjs | 2 ++ packages/core/pattern.mjs | 6 ++---- packages/core/test/pattern.test.mjs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/fraction.mjs b/packages/core/fraction.mjs index 2e3bc68ea..076fbadf6 100644 --- a/packages/core/fraction.mjs +++ b/packages/core/fraction.mjs @@ -126,6 +126,8 @@ export const lcm = (...fractions) => { ); }; +export const isFraction = (x) => x instanceof Fraction; + fraction._original = Fraction; export default fraction; diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 2991cffea..196248eba 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -5,8 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import TimeSpan from './timespan.mjs'; -import Fraction, { lcm } from './fraction.mjs'; -import FractionClass from 'fraction.js'; +import Fraction, {isFraction, lcm } from './fraction.mjs'; import Hap from './hap.mjs'; import State from './state.mjs'; import { unionWithObj } from './value.mjs'; @@ -27,7 +26,6 @@ import { } from './util.mjs'; import drawLine from './drawLine.mjs'; import { logger } from './logger.mjs'; -import fraction from './fraction.mjs'; let stringParser; @@ -1001,7 +999,7 @@ addToPrototype('weaveWith', function (t, ...funcs) { // compose matrix functions function _nonArrayObject(x) { - return !Array.isArray(x) && typeof x === 'object' && !(x instanceof FractionClass); + return !Array.isArray(x) && typeof x === 'object' && !isFraction(x); } function _composeOp(a, b, func) { if (_nonArrayObject(a) || _nonArrayObject(b)) { diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 3b6619b56..1df5c8776 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -877,7 +877,7 @@ describe('Pattern', () => { .squeezeJoin() .queryArc(3, 4) .map((x) => x.value), - ).toStrictEqual([3]); + ).toStrictEqual([Fraction(3)]); }); }); describe('ply', () => { From f2b2f9f9591e5c37667be04b15a35f593aff3cf1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 16 Sep 2025 11:53:19 -0400 Subject: [PATCH 207/257] format --- packages/core/pattern.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 196248eba..d0be3f599 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import TimeSpan from './timespan.mjs'; -import Fraction, {isFraction, lcm } from './fraction.mjs'; +import Fraction, { isFraction, lcm } from './fraction.mjs'; import Hap from './hap.mjs'; import State from './state.mjs'; import { unionWithObj } from './value.mjs'; From a68c7847531ff94faf0a9f0e048643117703d606 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 16 Sep 2025 11:55:31 -0400 Subject: [PATCH 208/257] rm unessecary change --- packages/superdough/superdough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index b1df93916..f1f308a7d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -465,7 +465,7 @@ function duckOrbit(audioContext, targetOrbit, t, onsettime = 0, attacktime = 0.1 gainParam.cancelScheduledValues(now); gainParam.setValueAtTime(currVal, now); - const t0 = now; // guard against now > t + const t0 = Math.max(t, now); // guard against now > t const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal); gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset); gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack); From 5445f0812fe1b7147aeb4866d15114afe56f809c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 17 Sep 2025 12:41:46 +0200 Subject: [PATCH 209/257] configurable port for osc bridge + add bin field --- packages/osc/package.json | 1 + packages/osc/server.js | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/osc/package.json b/packages/osc/package.json index d5a272328..6e8a3394a 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -3,6 +3,7 @@ "version": "1.2.4", "description": "OSC messaging for strudel", "main": "osc.mjs", + "bin": "./server.mjs", "type": "module", "publishConfig": { "main": "dist/index.mjs" diff --git a/packages/osc/server.js b/packages/osc/server.js index 75fc5b1c0..c727e65f7 100644 --- a/packages/osc/server.js +++ b/packages/osc/server.js @@ -6,6 +6,19 @@ This program is free software: you can redistribute it and/or modify it under th import OSC from 'osc-js'; +const args = process.argv.slice(2); +function getArgValue(flag) { + const i = args.indexOf(flag); + if (i !== -1) { + const nextIsFlag = args[i + 1]?.startsWith('--') ?? true; + if (nextIsFlag) return true; + return args[i + 1]; + } +} + +let udpClientPort = Number(getArgValue('--port')) || 57120; +// dirt = 7771 + const config = { receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client udpServer: { @@ -17,7 +30,7 @@ const config = { }, udpClient: { host: 'localhost', // @param {string} Hostname of udp client for messaging - port: 57120, // @param {number} Port of udp client for messaging + port: udpClientPort, // @param {number} Port of udp client for messaging }, wsServer: { host: 'localhost', // @param {string} Hostname of WebSocket server From 820744dc27a80e70a14fab0110c5219ff3c22cc7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 17 Sep 2025 12:42:11 +0200 Subject: [PATCH 210/257] bump osc to 1.2.5 --- packages/osc/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/osc/package.json b/packages/osc/package.json index 6e8a3394a..bf7fb474d 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/osc", - "version": "1.2.4", + "version": "1.2.5", "description": "OSC messaging for strudel", "main": "osc.mjs", "bin": "./server.mjs", From 5d7c6c3e4b29e22ded1ecb07ce52bd03a994920c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 17 Sep 2025 12:47:41 +0200 Subject: [PATCH 211/257] fix: add node shebang + bump osc to 1.2.6 --- packages/osc/package.json | 2 +- packages/osc/server.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/osc/package.json b/packages/osc/package.json index bf7fb474d..ad0d383ff 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/osc", - "version": "1.2.5", + "version": "1.2.6", "description": "OSC messaging for strudel", "main": "osc.mjs", "bin": "./server.mjs", diff --git a/packages/osc/server.js b/packages/osc/server.js index c727e65f7..4d87862ca 100644 --- a/packages/osc/server.js +++ b/packages/osc/server.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + /* server.js - Copyright (C) 2022 Strudel contributors - see From aba594b72ab9c1591167b4f73088c4539f40bfd8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 17 Sep 2025 12:52:42 +0200 Subject: [PATCH 212/257] fix: bin field --- packages/osc/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/osc/package.json b/packages/osc/package.json index ad0d383ff..239acda19 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,9 +1,9 @@ { "name": "@strudel/osc", - "version": "1.2.6", + "version": "1.2.7", "description": "OSC messaging for strudel", "main": "osc.mjs", - "bin": "./server.mjs", + "bin": "./server.js", "type": "module", "publishConfig": { "main": "dist/index.mjs" From 28d7e0e489440beec0fedfbbf1a676d78bd63a73 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 17 Sep 2025 13:39:28 +0200 Subject: [PATCH 213/257] export osc function to be able to do all(osc) --- packages/osc/osc.mjs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index 8262e5569..fe0691522 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import OSC from 'osc-js'; -import { logger, parseNumeral, Pattern, isNote, noteToMidi, ClockCollator } from '@strudel/core'; +import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core'; let connection; // Promise function connect() { @@ -81,6 +81,4 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) { * @memberof Pattern * @returns Pattern */ -Pattern.prototype.osc = function () { - return this.onTrigger(oscTrigger); -}; +export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger)); From cdb623cb66484843915e35fc2a7de2e3e029c71e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 17 Sep 2025 13:39:35 +0200 Subject: [PATCH 214/257] overhaul osc readme --- packages/osc/README.md | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/osc/README.md b/packages/osc/README.md index 3dc65e16e..d5ee60fbc 100644 --- a/packages/osc/README.md +++ b/packages/osc/README.md @@ -4,36 +4,35 @@ OSC output for strudel patterns! Currently only tested with super collider / sup ## Usage -OSC will only work if you run the REPL locally + the OSC server besides it: +Assuming you have [node.js](https://nodejs.org/) installed, you can run the osc bridge server via: -From the project root: - -```js -npm run repl +```sh +npx @strudel/osc ``` -and in a seperate shell: - -```js -npm run osc -``` - -This should give you +You should see something like: ```log osc client running on port 57120 -osc server running on port 57121 +osc server running on port 7771 websocket server running on port 8080 ``` -Now open Supercollider (with the super dirt startup file) +By default it will use port 57120 for the osc client, which is what [superdirt](https://github.com/musikinformatik/SuperDirt) uses. You can change it via the `--port` option: -Now open the REPL and type: - -```js -s(" hh").osc() +```sh +npx @strudel/osc --port 7771 # classic dirt ``` -or just [click here](https://strudel.cc/#cygiPGJkIHNkPiBoaCIpLm9zYygp)... +To test it in strudel, you have can use `all(osc)` to send all events through osc: + +```js +$: s("bd*4") + +all(osc) +``` + + +[open in repl](hhttps://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D) You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api) From 663229ecde6e6cff0a9aa71386e2fa3e6fe10a51 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 17 Sep 2025 13:50:02 +0200 Subject: [PATCH 215/257] bump osc to 1.2.8 --- packages/osc/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/osc/package.json b/packages/osc/package.json index 239acda19..4244c2c61 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/osc", - "version": "1.2.7", + "version": "1.2.8", "description": "OSC messaging for strudel", "main": "osc.mjs", "bin": "./server.js", From 91ad3d729fb60f04e6dcc58f1286236e398cf331 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Sep 2025 20:36:40 +0200 Subject: [PATCH 216/257] feat: osc add --debug flag to log messages + log errors --- packages/osc/server.js | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/osc/server.js b/packages/osc/server.js index 4d87862ca..2571c49d6 100644 --- a/packages/osc/server.js +++ b/packages/osc/server.js @@ -19,7 +19,7 @@ function getArgValue(flag) { } let udpClientPort = Number(getArgValue('--port')) || 57120; -// dirt = 7771 +let debug = Number(getArgValue('--debug')) || 0; const config = { receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client @@ -42,8 +42,34 @@ const config = { const osc = new OSC({ plugin: new OSC.BridgePlugin(config) }); -osc.open(); // start a WebSocket server on port 8080 +if (debug) { + osc.on('*', (message) => { + const { address, args } = message; + let str = ''; + for (let i = 0; i < args.length; i += 2) { + str += `${args[i]}: ${args[i + 1]} `; + } + console.log(`${address} ${str}`); + }); +} + +osc.on('error', (message) => { + if (message.toString().includes('EADDRINUSE')) { + console.log(`------ ERROR ------- +osc server already running! to stop it: +1. run "lsof -ti :57121 | xargs kill -9" (macos / linux) +2. re-run the osc server +`); + } else { + console.log(message); + } +}); + +osc.open(); console.log('osc client running on port', config.udpClient.port); console.log('osc server running on port', config.udpServer.port); console.log('websocket server running on port', config.wsServer.port); +if (debug) { + console.log('debug logs enabled. incoming messages will appear below'); +} From a25f7637968c0036e527254973fec59edc1cb52d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Sep 2025 20:37:19 +0200 Subject: [PATCH 217/257] docs: add --debug flag to readme --- packages/osc/README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/osc/README.md b/packages/osc/README.md index d5ee60fbc..0a7bfedcf 100644 --- a/packages/osc/README.md +++ b/packages/osc/README.md @@ -14,16 +14,28 @@ You should see something like: ```log osc client running on port 57120 -osc server running on port 7771 +osc server running on port 57121 websocket server running on port 8080 ``` +### --port + By default it will use port 57120 for the osc client, which is what [superdirt](https://github.com/musikinformatik/SuperDirt) uses. You can change it via the `--port` option: ```sh npx @strudel/osc --port 7771 # classic dirt ``` +### --debug + +To log all incoming osc messages, add the `--debug` flag: + +```sh +npx @strudel/osc --debug +``` + +## Usage in Strudel + To test it in strudel, you have can use `all(osc)` to send all events through osc: ```js @@ -32,7 +44,6 @@ $: s("bd*4") all(osc) ``` - -[open in repl](hhttps://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D) +[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D) You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api) From c54fa7d2665569006b71aff9019ea17aac9dd725 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Sep 2025 20:39:05 +0200 Subject: [PATCH 218/257] chore: bump osc to 1.2.8 --- packages/osc/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/osc/package.json b/packages/osc/package.json index 4244c2c61..92aa30bf3 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/osc", - "version": "1.2.8", + "version": "1.2.9", "description": "OSC messaging for strudel", "main": "osc.mjs", "bin": "./server.js", From 1c3e07afd3c4fac5367b5afd18dfe4242a72c18c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Sep 2025 20:40:25 +0200 Subject: [PATCH 219/257] fix: rephrase error message + bump to 1.2.10 --- packages/osc/package.json | 2 +- packages/osc/server.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/osc/package.json b/packages/osc/package.json index 92aa30bf3..bc828d798 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/osc", - "version": "1.2.9", + "version": "1.2.10", "description": "OSC messaging for strudel", "main": "osc.mjs", "bin": "./server.js", diff --git a/packages/osc/server.js b/packages/osc/server.js index 2571c49d6..d7ec21c4b 100644 --- a/packages/osc/server.js +++ b/packages/osc/server.js @@ -56,7 +56,7 @@ if (debug) { osc.on('error', (message) => { if (message.toString().includes('EADDRINUSE')) { console.log(`------ ERROR ------- -osc server already running! to stop it: +a server is already running on port 57121! to stop it: 1. run "lsof -ti :57121 | xargs kill -9" (macos / linux) 2. re-run the osc server `); From 8833d623f71f89df51b79a4f8467456f72daa278 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 21 Sep 2025 16:06:28 +0200 Subject: [PATCH 220/257] fix: all function in mondo --- packages/mondough/mondough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index e409d5a3b..669183279 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -85,7 +85,7 @@ function evaluator(node, scope) { let pat; if (type === 'plain' && typeof variable !== 'undefined') { // some function names are not patternable, so we skip reification here - if (['!', 'extend', '@', 'expand', 'square', 'angle'].includes(value)) { + if (['!', 'extend', '@', 'expand', 'square', 'angle', 'all'].includes(value)) { return variable; } pat = reify(variable); From 86248328cc2be957da71566aeb671f69a10ccabe Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 21 Sep 2025 16:09:10 +0200 Subject: [PATCH 221/257] fix: mondo setcps / setcpm --- packages/core/repl.mjs | 17 +++++++++++++++-- packages/mondough/mondough.mjs | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 47231af4d..23c5b25e0 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -85,7 +85,17 @@ export function repl({ const start = () => scheduler.start(); const pause = () => scheduler.pause(); const toggle = () => scheduler.toggle(); - const setCps = (cps) => scheduler.setCps(cps); + const setCps = (cps) => { + scheduler.setCps(unpure(cps)); + return silence; + }; + + function unpure(pat) { + if (pat._Pattern) { + return pat.__pure; + } + return pat; + } /** * Changes the global tempo to the given cycles per minute @@ -97,7 +107,10 @@ export function repl({ * setcpm(140/4) // =140 bpm in 4/4 * $: s("bd*4,[- sd]*2").bank('tr707') */ - const setCpm = (cpm) => scheduler.setCps(cpm / 60); + const setCpm = (cpm) => { + scheduler.setCps(unpure(cpm) / 60); + return silence; + }; // TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`.. diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 669183279..b7ee83787 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -85,7 +85,7 @@ function evaluator(node, scope) { let pat; if (type === 'plain' && typeof variable !== 'undefined') { // some function names are not patternable, so we skip reification here - if (['!', 'extend', '@', 'expand', 'square', 'angle', 'all'].includes(value)) { + if (['!', 'extend', '@', 'expand', 'square', 'angle', 'all', 'setcpm', 'setcps'].includes(value)) { return variable; } pat = reify(variable); From 452827630b2ea1949674e645835a960bb1ae1be1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 21 Sep 2025 16:13:09 +0200 Subject: [PATCH 222/257] move unpure up --- packages/core/repl.mjs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 23c5b25e0..53562f216 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -74,6 +74,14 @@ export function repl({ return silence; }; + // helper to get a patternified pure value out + function unpure(pat) { + if (pat._Pattern) { + return pat.__pure; + } + return pat; + } + const setPattern = async (pattern, autostart = true) => { pattern = editPattern?.(pattern) || pattern; await scheduler.setPattern(pattern, autostart); @@ -90,13 +98,6 @@ export function repl({ return silence; }; - function unpure(pat) { - if (pat._Pattern) { - return pat.__pure; - } - return pat; - } - /** * Changes the global tempo to the given cycles per minute * From c5bd2a7487a8da8ee61bbaedfd0d278e29eba75e Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 21 Sep 2025 23:54:04 -0400 Subject: [PATCH 223/257] working --- packages/core/controls.mjs | 12 ++ packages/superdough/feedbackdelay.mjs | 3 + packages/superdough/helpers.mjs | 7 + packages/superdough/superdough.mjs | 199 +++-------------------- packages/superdough/superdoughoutput.mjs | 192 ++++++++++++++++++++++ 5 files changed, 238 insertions(+), 175 deletions(-) create mode 100644 packages/superdough/superdoughoutput.mjs diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 044cfa96d..f961e4fa4 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -402,6 +402,18 @@ export const { bandq, bpq } = registerControl('bandq', 'bpq'); * */ export const { begin } = registerControl('begin'); +/** + * A pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample. + * + * @memberof Pattern + * @name bufferHold + * @param {number | Pattern} amount between 0 and 1, where 1 is the length of the sample + * @example + * samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/dirt-samples') + * s("rave").begin("<0 .25 .5 .75>").fast(2) + * + */ +export const { bufferHold } = registerControl('bufferHold'); /** * The same as .begin, but cuts off the end off each sample. * diff --git a/packages/superdough/feedbackdelay.mjs b/packages/superdough/feedbackdelay.mjs index c182d6558..13beeb1cf 100644 --- a/packages/superdough/feedbackdelay.mjs +++ b/packages/superdough/feedbackdelay.mjs @@ -23,6 +23,9 @@ if (typeof DelayNode !== 'undefined') { start(t) { this.delayGain.gain.setValueAtTime(this.delayGain.gain.value, t + this.delayTime.value); } + stop(t) { + this.delayGain.gain.setValueAtTime(0, t); + } } AudioContext.prototype.createFeedbackDelay = function (wet, time, feedback) { diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 69e7e8560..5b2fa1a40 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -10,6 +10,13 @@ export function gainNode(value) { return node; } +export function effectSend(input, effect, wet) { + const send = gainNode(wet); + input.connect(send); + send.connect(effect); + return send; +} + const getSlope = (y1, y2, x1, x2) => { const denom = x2 - x1; if (denom === 0) { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index f1f308a7d..0d1845b91 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -7,12 +7,13 @@ 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 { 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, effectSend } from './helpers.mjs'; import { map } from 'nanostores'; -import { logger, errorLogger } from './logger.mjs'; +import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; +import { SuperdoughAudioController } from './superdoughoutput.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; @@ -301,65 +302,16 @@ export async function initAudioOnFirstClick(options) { return audioReady; } -const maxfeedback = 0.98; - -let channelMerger, destinationGain; -//update the output channel configuration to match user's audio device -export function initializeAudioOutput() { - const audioContext = getAudioContext(); - const maxChannelCount = audioContext.destination.maxChannelCount; - audioContext.destination.channelCount = maxChannelCount; - channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount }); - destinationGain = new GainNode(audioContext); - channelMerger.connect(destinationGain); - destinationGain.connect(audioContext.destination); +let controller; +function getSuperdoughAudioController() { + if (controller == null) { + controller = new SuperdoughAudioController(getAudioContext()); + } + return controller; } - -// input: AudioNode, channels: ?Array -export const connectToDestination = (input, channels = [0, 1]) => { - const ctx = getAudioContext(); - if (channelMerger == null) { - initializeAudioOutput(); - } - //This upmix can be removed if correct channel counts are set throughout the app, - // and then strudel could theoretically support surround sound audio files - const stereoMix = new StereoPannerNode(ctx); - input.connect(stereoMix); - - const splitter = new ChannelSplitterNode(ctx, { - numberOfOutputs: stereoMix.channelCount, - }); - stereoMix.connect(splitter); - channels.forEach((ch, i) => { - splitter.connect(channelMerger, i % stereoMix.channelCount, ch % ctx.destination.channelCount); - }); -}; - -export const panic = () => { - if (destinationGain == null) { - return; - } - destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01); - destinationGain = null; - channelMerger == null; -}; - -function getDelay(orbit, delaytime, delayfeedback, t) { - if (delayfeedback > maxfeedback) { - //logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`); - } - delayfeedback = clamp(delayfeedback, 0, 0.98); - let delayNode = orbits[orbit].delayNode; - if (delayNode === undefined) { - const ac = getAudioContext(); - delayNode = ac.createFeedbackDelay(1, delaytime, delayfeedback); - delayNode.start?.(t); // for some reason, this throws when audion extension is installed.. - connectToOrbit(delayNode, orbit); - orbits[orbit].delayNode = delayNode; - } - delayNode.delayTime.value !== delaytime && delayNode.delayTime.setValueAtTime(delaytime, t); - delayNode.feedback.value !== delayfeedback && delayNode.feedback.setValueAtTime(delayfeedback, t); - return delayNode; +export function connectToDestination(input, channels) { + const controller = getSuperdoughAudioController(); + controller.output.connectToDestination(input, channels); } export function getLfo(audioContext, begin, end, properties = {}) { @@ -415,97 +367,6 @@ function getFilterType(ftype) { return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype; } -// type orbit { -// output: GainNode, -// reverbNode: ConvolverNode -// delayNode: FeedbackDelayNode -// } -let orbits = {}; -function connectToOrbit(node, orbit) { - if (orbits[orbit] == null) { - errorLogger(new Error('target orbit does not exist'), 'superdough'); - } - node.connect(orbits[orbit].output); -} - -function setOrbit(audioContext, orbit, channels) { - if (orbits[orbit] == null) { - orbits[orbit] = { - // Setup output node through which all audio filters prior to hitting - // the destination (and thus allows for global volume automation) - output: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }), - }; - connectToDestination(orbits[orbit].output, channels); - } -} - -function duckOrbit(audioContext, targetOrbit, t, onsettime = 0, attacktime = 0.1, duckdepth = 1) { - const targetArr = [targetOrbit].flat(); - const onsetArr = [onsettime].flat(); - const attackArr = [attacktime].flat(); - const depthArr = [duckdepth].flat(); - - targetArr.forEach((target, idx) => { - if (orbits[target] == null) { - errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); - return; - } - const onset = onsetArr[idx] ?? onsetArr[0]; - const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002); - const depth = depthArr[idx] ?? depthArr[0]; - const gainParam = orbits[target].output.gain; - webAudioTimeout( - audioContext, - () => { - const now = audioContext.currentTime; - - // cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime - // on browsers which lack that method - const currVal = gainParam.value; - gainParam.cancelScheduledValues(now); - gainParam.setValueAtTime(currVal, now); - - const t0 = Math.max(t, now); // guard against now > t - const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal); - gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset); - gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack); - }, - 0, - t - 0.01, - ); - }); -} - -let hasChanged = (now, before) => now !== undefined && now !== before; -function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) { - // If no reverb has been created for a given orbit, create one - let reverbNode = orbits[orbit].reverbNode; - if (reverbNode === undefined) { - const ac = getAudioContext(); - reverbNode = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin); - connectToOrbit(reverbNode, orbit); - orbits[orbit].reverbNode = reverbNode; - } - - if ( - hasChanged(duration, reverbNode.duration) || - hasChanged(fade, reverbNode.fade) || - hasChanged(lp, reverbNode.lp) || - hasChanged(dim, reverbNode.dim) || - hasChanged(irspeed, reverbNode.irspeed) || - hasChanged(irbegin, reverbNode.irbegin) || - 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 - reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin); - } - return reverbNode; -} - export let analysers = {}, analysersData = {}; @@ -538,15 +399,8 @@ export function getAnalyzerData(type = 'time', id = 1) { return analysersData[id]; } -function effectSend(input, effect, wet) { - const send = gainNode(wet); - input.connect(send); - send.connect(effect); - return send; -} - export function resetGlobalEffects() { - orbits = {}; + controller.reset(); analysers = {}; analysersData = {}; } @@ -561,6 +415,7 @@ function mapChannelNumbers(channels) { export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); + const audioController = getSuperdoughAudioController(); let { stretch } = value; if (stretch != null) { @@ -679,10 +534,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) ); const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels; - setOrbit(ac, orbit, channels, t, cycle, cps); - + const orbitBus = audioController.getOrbit(orbit, channels); if (duckorbit != null) { - duckOrbit(ac, duckorbit, t, duckonset, duckattack, duckdepth); + orbitBus.duck(duckorbit, t, duckonset, duckattack, duckdepth); } gain = applyGainCurve(nanFallback(gain, 1)); @@ -872,14 +726,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) chain.push(post); // delay - let delaySend; if (delay > 0 && delaytime > 0 && delayfeedback > 0) { - const delayNode = getDelay(orbit, delaytime, delayfeedback, t); - delaySend = effectSend(post, delayNode, delay); - audioNodes.push(delaySend); + orbitBus.getDelay(delaytime, delayfeedback, t); + orbitBus.sendDelay(post, delay); } // reverb - let reverbSend; if (room > 0) { let roomIR; if (ir !== undefined) { @@ -892,25 +743,23 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } roomIR = await loadBuffer(url, ac, ir, 0); } - const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); - reverbSend = effectSend(post, reverbNode, room); - audioNodes.push(reverbSend); + orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); + orbitBus.sendReverb(post, room); } // analyser - let analyserSend; if (analyze) { const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); - analyserSend = effectSend(post, analyserNode, 1); + const 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); + orbitBus.connectToOutput(dryGain); } else { - connectToOrbit(post, orbit); + orbitBus.connectToOutput(post); } // connect chain elements together diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs new file mode 100644 index 000000000..cb391ead7 --- /dev/null +++ b/packages/superdough/superdoughoutput.mjs @@ -0,0 +1,192 @@ +import { effectSend, webAudioTimeout } from './helpers.mjs'; +import { errorLogger } from './logger.mjs'; + +let hasChanged = (now, before) => now !== undefined && now !== before; + +export class Orbit { + reverbNode; + delayNode; + output; + summingNode; + audioContext; + constructor(audioContext) { + this.audioContext = audioContext; + this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); + this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); + this.summingNode.connect(this.output); + } + + disconnect() { + this.output.disconnect(); + this.summingNode.disconnect(); + this.delayNode?.disconnect(); + this.reverbNode?.disconnect(); + } + + getDelay(delaytime = 0, feedback = 0.5, t) { + const maxfeedback = 0.98; + if (feedback > maxfeedback) { + //logger(`feedback was clamped to ${maxfeedback} to save your ears`); + } + feedback = clamp(feedback, 0, 0.98); + if (this.delayNode == null) { + this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback); + this.delayNode.connect(this.summingNode); + this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed.. + } + this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t); + this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t); + return this.delayNode; + } + + getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) { + // If no reverb has been created for a given orbit, create one + if (this.reverbNode == null) { + this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin); + this.reverbNode.connect(this.summingNode); + } + + if ( + hasChanged(duration, this.reverbNode.duration) || + hasChanged(fade, this.reverbNode.fade) || + hasChanged(lp, this.reverbNode.lp) || + hasChanged(dim, this.reverbNode.dim) || + hasChanged(irspeed, this.reverbNode.irspeed) || + hasChanged(irbegin, this.reverbNode.irbegin) || + this.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 + this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin); + } + return this.reverbNode; + } + sendReverb(node, amount) { + effectSend(node, this.reverbNode, amount); + } + + sendDelay(node, amount) { + effectSend(node, this.delayNode, amount); + } + + duck(t, onsettime = 0, attacktime = 0.1, depth = 1) { + const onset = onsettime; + const attack = Math.max(attacktime, 0.002); + const gainParam = this.output.gain; + webAudioTimeout( + this.audioContext, + () => { + const now = this.audioContext.currentTime; + + // cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime + // on browsers which lack that method + const currVal = gainParam.value; + gainParam.cancelScheduledValues(now); + gainParam.setValueAtTime(currVal, now); + + const t0 = Math.max(t, now); // guard against now > t + const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal); + gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset); + gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack); + }, + 0, + t - 0.01, + ); + } + + connectToOutput(node) { + node.connect(this.summingNode); + } +} + +export class SuperdoughOutput { + channelMerger; + destinationGain; + + constructor(audioContext) { + this.audioContext = audioContext; + this.initializeAudio(); + } + + initializeAudio() { + const audioContext = this.audioContext; + const maxChannelCount = audioContext.destination.maxChannelCount; + this.audioContext.destination.channelCount = maxChannelCount; + this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount }); + this.destinationGain = new GainNode(audioContext); + this.channelMerger.connect(this.destinationGain); + this.destinationGain.connect(audioContext.destination); + } + + reset() { + this.channelMerger.disconnect(); + this.destinationGain.disconnect(); + this.destinationGain = null; + this.channelMerger = null; + this.nodes = {}; + this.initializeAudio(); + } + connectToDestination = (input, channels = [0, 1]) => { + //This upmix can be removed if correct channel counts are set throughout the app, + // and then strudel could theoretically support surround sound audio files + const stereoMix = new StereoPannerNode(this.audioContext); + input.connect(stereoMix); + + const splitter = new ChannelSplitterNode(this.audioContext, { + numberOfOutputs: stereoMix.channelCount, + }); + stereoMix.connect(splitter); + channels.forEach((ch, i) => { + splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount); + }); + }; +} + +export class SuperdoughAudioController { + audioContext; + output; + nodes = {}; + + constructor(audioContext) { + this.audioContext = audioContext; + this.output = new SuperdoughOutput(audioContext); + } + + reset() { + Array.from(this.nodes).forEach((node) => { + node.disconnect(); + }); + this.output.reset(); + } + + duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) { + const targetArr = [targetOrbits].flat(); + const onsetArr = [onsettime].flat(); + const attackArr = [attacktime].flat(); + const depthArr = [depth].flat(); + + targetArr.forEach((target, idx) => { + const orbit = this.nodes[target]; + + if (orbit == null) { + errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); + return; + } + const onset = onsetArr[idx] ?? onsetArr[0]; + const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002); + const depth = depthArr[idx] ?? depthArr[0]; + orbit.duck({ t, onsettime: onset, attacktime: attack, depth }); + }); + } + + getOrbit(orbitNum, channels) { + if (this.nodes[orbitNum] == null) { + this.nodes[orbitNum] = new Orbit(this.audioContext); + this.output.connectToDestination(this.nodes[orbitNum].output, channels); + } + return this.nodes[orbitNum]; + } +} From beafd9c7001c5976ae936fc669f3f522b547c435 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 22 Sep 2025 00:07:06 -0400 Subject: [PATCH 224/257] format --- packages/superdough/superdough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 0d1845b91..922fbbe43 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -400,7 +400,7 @@ export function getAnalyzerData(type = 'time', id = 1) { } export function resetGlobalEffects() { - controller.reset(); + controller?.reset(); analysers = {}; analysersData = {}; } From 82893ffc226bb47df9269d270845f206391109ba Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 22 Sep 2025 00:11:02 -0400 Subject: [PATCH 225/257] fix import --- packages/superdough/superdoughoutput.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index cb391ead7..df1a29ab2 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -1,5 +1,6 @@ import { effectSend, webAudioTimeout } from './helpers.mjs'; import { errorLogger } from './logger.mjs'; +import {clamp} from './util.mjs' let hasChanged = (now, before) => now !== undefined && now !== before; From da926805b37dc28a2cce4185b890f77350b7e2ac Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 22 Sep 2025 00:12:48 -0400 Subject: [PATCH 226/257] rm dead code --- packages/core/controls.mjs | 12 ------------ packages/superdough/superdough.mjs | 1 - packages/superdough/superdoughoutput.mjs | 2 +- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index f961e4fa4..044cfa96d 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -402,18 +402,6 @@ export const { bandq, bpq } = registerControl('bandq', 'bpq'); * */ export const { begin } = registerControl('begin'); -/** - * A pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample. - * - * @memberof Pattern - * @name bufferHold - * @param {number | Pattern} amount between 0 and 1, where 1 is the length of the sample - * @example - * samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/dirt-samples') - * s("rave").begin("<0 .25 .5 .75>").fast(2) - * - */ -export const { bufferHold } = registerControl('bufferHold'); /** * The same as .begin, but cuts off the end off each sample. * diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 922fbbe43..72c167adf 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -727,7 +727,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // delay if (delay > 0 && delaytime > 0 && delayfeedback > 0) { - orbitBus.getDelay(delaytime, delayfeedback, t); orbitBus.sendDelay(post, delay); } // reverb diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index df1a29ab2..29113e469 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -1,6 +1,6 @@ import { effectSend, webAudioTimeout } from './helpers.mjs'; import { errorLogger } from './logger.mjs'; -import {clamp} from './util.mjs' +import { clamp } from './util.mjs'; let hasChanged = (now, before) => now !== undefined && now !== before; From fdcdc3aaa0e82ff1c6c69a4604cf7091fc463a10 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 22 Sep 2025 00:13:59 -0400 Subject: [PATCH 227/257] rm deadcode --- packages/superdough/feedbackdelay.mjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/superdough/feedbackdelay.mjs b/packages/superdough/feedbackdelay.mjs index 13beeb1cf..c182d6558 100644 --- a/packages/superdough/feedbackdelay.mjs +++ b/packages/superdough/feedbackdelay.mjs @@ -23,9 +23,6 @@ if (typeof DelayNode !== 'undefined') { start(t) { this.delayGain.gain.setValueAtTime(this.delayGain.gain.value, t + this.delayTime.value); } - stop(t) { - this.delayGain.gain.setValueAtTime(0, t); - } } AudioContext.prototype.createFeedbackDelay = function (wet, time, feedback) { From 1eb9dc73fa6259c29aafbfb4d39972a8fbf72c97 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 22 Sep 2025 00:34:57 -0400 Subject: [PATCH 228/257] fix duck --- packages/superdough/superdough.mjs | 3 ++- packages/superdough/superdoughoutput.mjs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 72c167adf..f22e82d7b 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -536,7 +536,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels; const orbitBus = audioController.getOrbit(orbit, channels); if (duckorbit != null) { - orbitBus.duck(duckorbit, t, duckonset, duckattack, duckdepth); + audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth); } gain = applyGainCurve(nanFallback(gain, 1)); @@ -727,6 +727,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // delay if (delay > 0 && delaytime > 0 && delayfeedback > 0) { + orbitBus.getDelay(delaytime, delayfeedback, t); orbitBus.sendDelay(post, delay); } // reverb diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index 29113e469..57c2e8563 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -179,7 +179,8 @@ export class SuperdoughAudioController { const onset = onsetArr[idx] ?? onsetArr[0]; const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002); const depth = depthArr[idx] ?? depthArr[0]; - orbit.duck({ t, onsettime: onset, attacktime: attack, depth }); + + orbit.duck(t, onset, attack, depth); }); } From 0633954e2498cacb28fb69a6770d0854f589a0a8 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 24 Sep 2025 00:23:05 -0700 Subject: [PATCH 229/257] working --- packages/superdough/superdough.mjs | 5 + packages/superdough/superdoughoutput.mjs | 346 ++++++++++++----------- packages/superdough/worklets.mjs | 75 +++++ 3 files changed, 259 insertions(+), 167 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index f22e82d7b..1d3028e80 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -458,6 +458,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) duckonset, duckattack, duckdepth, + djf, // filters fanchor = getDefaultValue('fanchor'), drive = 0.69, @@ -747,6 +748,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) orbitBus.sendReverb(post, room); } + if (djf != null) { + orbitBus.getDjf(djf, t) + } + // analyser if (analyze) { const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index 57c2e8563..38760d041 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -1,194 +1,206 @@ -import { effectSend, webAudioTimeout } from './helpers.mjs'; +import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs'; import { errorLogger } from './logger.mjs'; import { clamp } from './util.mjs'; let hasChanged = (now, before) => now !== undefined && now !== before; export class Orbit { - reverbNode; - delayNode; - output; - summingNode; - audioContext; - constructor(audioContext) { - this.audioContext = audioContext; - this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); - this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); - this.summingNode.connect(this.output); - } - - disconnect() { - this.output.disconnect(); - this.summingNode.disconnect(); - this.delayNode?.disconnect(); - this.reverbNode?.disconnect(); - } - - getDelay(delaytime = 0, feedback = 0.5, t) { - const maxfeedback = 0.98; - if (feedback > maxfeedback) { - //logger(`feedback was clamped to ${maxfeedback} to save your ears`); - } - feedback = clamp(feedback, 0, 0.98); - if (this.delayNode == null) { - this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback); - this.delayNode.connect(this.summingNode); - this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed.. - } - this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t); - this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t); - return this.delayNode; - } - - getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) { - // If no reverb has been created for a given orbit, create one - if (this.reverbNode == null) { - this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin); - this.reverbNode.connect(this.summingNode); + reverbNode; + delayNode; + output; + summingNode; + djfNode; + audioContext; + constructor(audioContext) { + this.audioContext = audioContext; + this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); + this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); + this.summingNode.connect(this.output); } - if ( - hasChanged(duration, this.reverbNode.duration) || - hasChanged(fade, this.reverbNode.fade) || - hasChanged(lp, this.reverbNode.lp) || - hasChanged(dim, this.reverbNode.dim) || - hasChanged(irspeed, this.reverbNode.irspeed) || - hasChanged(irbegin, this.reverbNode.irbegin) || - this.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 - this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin); + disconnect() { + this.output.disconnect(); + this.summingNode.disconnect(); + this.delayNode?.disconnect(); + this.reverbNode?.disconnect(); } - return this.reverbNode; - } - sendReverb(node, amount) { - effectSend(node, this.reverbNode, amount); - } - sendDelay(node, amount) { - effectSend(node, this.delayNode, amount); - } + getDjf(value, t = 0) { + if (this.djfNode == null){ + this.djfNode = getWorklet(this.audioContext, 'djf-processor', {value}) + this.summingNode.disconnect() + this.summingNode.connect(this.djfNode) + this.djfNode.connect(this.output) + } + const val = this.djfNode.parameters.get('value') + val.setValueAtTime(value, t) + } + + getDelay(delaytime = 0, feedback = 0.5, t) { + const maxfeedback = 0.98; + if (feedback > maxfeedback) { + //logger(`feedback was clamped to ${maxfeedback} to save your ears`); + } + feedback = clamp(feedback, 0, 0.98); + if (this.delayNode == null) { + this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback); + this.delayNode.connect(this.summingNode); + this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed.. + } + this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t); + this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t); + return this.delayNode; + } - duck(t, onsettime = 0, attacktime = 0.1, depth = 1) { - const onset = onsettime; - const attack = Math.max(attacktime, 0.002); - const gainParam = this.output.gain; - webAudioTimeout( - this.audioContext, - () => { - const now = this.audioContext.currentTime; + getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) { + // If no reverb has been created for a given orbit, create one + if (this.reverbNode == null) { + this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin); + this.reverbNode.connect(this.summingNode); + } - // cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime - // on browsers which lack that method - const currVal = gainParam.value; - gainParam.cancelScheduledValues(now); - gainParam.setValueAtTime(currVal, now); + if ( + hasChanged(duration, this.reverbNode.duration) || + hasChanged(fade, this.reverbNode.fade) || + hasChanged(lp, this.reverbNode.lp) || + hasChanged(dim, this.reverbNode.dim) || + hasChanged(irspeed, this.reverbNode.irspeed) || + hasChanged(irbegin, this.reverbNode.irbegin) || + this.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 + this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin); + } + return this.reverbNode; + } + sendReverb(node, amount) { + effectSend(node, this.reverbNode, amount); + } - const t0 = Math.max(t, now); // guard against now > t - const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal); - gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset); - gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack); - }, - 0, - t - 0.01, - ); - } + sendDelay(node, amount) { + effectSend(node, this.delayNode, amount); + } - connectToOutput(node) { - node.connect(this.summingNode); - } + duck(t, onsettime = 0, attacktime = 0.1, depth = 1) { + const onset = onsettime; + const attack = Math.max(attacktime, 0.002); + const gainParam = this.output.gain; + webAudioTimeout( + this.audioContext, + () => { + const now = this.audioContext.currentTime; + + // cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime + // on browsers which lack that method + const currVal = gainParam.value; + gainParam.cancelScheduledValues(now); + gainParam.setValueAtTime(currVal, now); + + const t0 = Math.max(t, now); // guard against now > t + const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal); + gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset); + gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack); + }, + 0, + t - 0.01, + ); + } + + connectToOutput(node) { + node.connect(this.summingNode); + } } export class SuperdoughOutput { - channelMerger; - destinationGain; + channelMerger; + destinationGain; - constructor(audioContext) { - this.audioContext = audioContext; - this.initializeAudio(); - } + constructor(audioContext) { + this.audioContext = audioContext; + this.initializeAudio(); + } - initializeAudio() { - const audioContext = this.audioContext; - const maxChannelCount = audioContext.destination.maxChannelCount; - this.audioContext.destination.channelCount = maxChannelCount; - this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount }); - this.destinationGain = new GainNode(audioContext); - this.channelMerger.connect(this.destinationGain); - this.destinationGain.connect(audioContext.destination); - } + initializeAudio() { + const audioContext = this.audioContext; + const maxChannelCount = audioContext.destination.maxChannelCount; + this.audioContext.destination.channelCount = maxChannelCount; + this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount }); + this.destinationGain = new GainNode(audioContext); + this.channelMerger.connect(this.destinationGain); + this.destinationGain.connect(audioContext.destination); + } - reset() { - this.channelMerger.disconnect(); - this.destinationGain.disconnect(); - this.destinationGain = null; - this.channelMerger = null; - this.nodes = {}; - this.initializeAudio(); - } - connectToDestination = (input, channels = [0, 1]) => { - //This upmix can be removed if correct channel counts are set throughout the app, - // and then strudel could theoretically support surround sound audio files - const stereoMix = new StereoPannerNode(this.audioContext); - input.connect(stereoMix); + reset() { + this.channelMerger.disconnect(); + this.destinationGain.disconnect(); + this.destinationGain = null; + this.channelMerger = null; + this.nodes = {}; + this.initializeAudio(); + } + connectToDestination = (input, channels = [0, 1]) => { + //This upmix can be removed if correct channel counts are set throughout the app, + // and then strudel could theoretically support surround sound audio files + const stereoMix = new StereoPannerNode(this.audioContext); + input.connect(stereoMix); - const splitter = new ChannelSplitterNode(this.audioContext, { - numberOfOutputs: stereoMix.channelCount, - }); - stereoMix.connect(splitter); - channels.forEach((ch, i) => { - splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount); - }); - }; + const splitter = new ChannelSplitterNode(this.audioContext, { + numberOfOutputs: stereoMix.channelCount, + }); + stereoMix.connect(splitter); + channels.forEach((ch, i) => { + splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount); + }); + }; } export class SuperdoughAudioController { - audioContext; - output; - nodes = {}; + audioContext; + output; + nodes = {}; - constructor(audioContext) { - this.audioContext = audioContext; - this.output = new SuperdoughOutput(audioContext); - } - - reset() { - Array.from(this.nodes).forEach((node) => { - node.disconnect(); - }); - this.output.reset(); - } - - duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) { - const targetArr = [targetOrbits].flat(); - const onsetArr = [onsettime].flat(); - const attackArr = [attacktime].flat(); - const depthArr = [depth].flat(); - - targetArr.forEach((target, idx) => { - const orbit = this.nodes[target]; - - if (orbit == null) { - errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); - return; - } - const onset = onsetArr[idx] ?? onsetArr[0]; - const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002); - const depth = depthArr[idx] ?? depthArr[0]; - - orbit.duck(t, onset, attack, depth); - }); - } - - getOrbit(orbitNum, channels) { - if (this.nodes[orbitNum] == null) { - this.nodes[orbitNum] = new Orbit(this.audioContext); - this.output.connectToDestination(this.nodes[orbitNum].output, channels); + constructor(audioContext) { + this.audioContext = audioContext; + this.output = new SuperdoughOutput(audioContext); + } + + reset() { + Array.from(this.nodes).forEach((node) => { + node.disconnect(); + }); + this.output.reset(); + } + + duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) { + const targetArr = [targetOrbits].flat(); + const onsetArr = [onsettime].flat(); + const attackArr = [attacktime].flat(); + const depthArr = [depth].flat(); + + targetArr.forEach((target, idx) => { + const orbit = this.nodes[target]; + + if (orbit == null) { + errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); + return; + } + const onset = onsetArr[idx] ?? onsetArr[0]; + const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002); + const depth = depthArr[idx] ?? depthArr[0]; + + orbit.duck(t, onset, attack, depth); + }); + } + + getOrbit(orbitNum, channels) { + if (this.nodes[orbitNum] == null) { + this.nodes[orbitNum] = new Orbit(this.audioContext); + this.output.connectToDestination(this.nodes[orbitNum].output, channels); + } + return this.nodes[orbitNum]; } - return this.nodes[orbitNum]; - } } diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 2406d56dc..06e81f38c 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -271,6 +271,81 @@ class ShapeProcessor extends AudioWorkletProcessor { } registerProcessor('shape-processor', ShapeProcessor); +class TwoPoleFilter { + s0 = 0; + s1 = 0; + update(s, cutoff, resonance = 0) { + // Out of bound values can produce NaNs + resonance = clamp(resonance, 0, 1) + cutoff = clamp(cutoff, 0, sampleRate / 2 - 1) + const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14); + 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 DJFProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return [ + { name: 'value', defaultValue: 0.5 }, + ]; + } + + constructor() { + super(); + this.filters = [new TwoPoleFilter(), new TwoPoleFilter()] + } + + process(inputs, outputs, parameters) { + const input = inputs[0]; + const output = outputs[0]; + + const hasInput = !(input[0] === undefined); + this.started = hasInput; + + const value = clamp(parameters.value[0], 0, 1); + let filterType = 'none' + let cutoff + let v = 1; + if (value > 0.5) { + filterType = 'hipass' + v = (value - .5) * 2 + } else if (value < 0.5) { + filterType = 'lopass' + v = value * 2 + } + cutoff = Math.pow((v * 11), 4) + + // let cutoff = parameters.frequency[0]; + + for (let i = 0; i < input.length; i++) { + for (let n = 0; n < blockSize; n++) { + if (filterType == 'none') { + output[i][n] = input[i][n] + } else { + this.filters[i].update(input[i][n], cutoff, 0.2) + if (filterType === 'lopass') { + output[i][n] = this.filters[i].s1 + } else if (filterType === 'hipass') { + output[i][n] = input[i][n] - this.filters[i].s1 + } else { + output[i][n] = input[i][n] + } + } + + + + + } + } + return true; + } +} +registerProcessor('djf-processor', DJFProcessor); + function fast_tanh(x) { const x2 = x * x; return (x * (27.0 + x2)) / (27.0 + 9.0 * x2); From 28c94efaa9d036fd361e3cc8708e1465e39a1432 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 24 Sep 2025 00:24:25 -0700 Subject: [PATCH 230/257] format --- packages/superdough/superdough.mjs | 2 +- packages/superdough/superdoughoutput.mjs | 356 +++++++++++------------ packages/superdough/worklets.mjs | 38 +-- 3 files changed, 195 insertions(+), 201 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 1d3028e80..63ed7d30b 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -749,7 +749,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } if (djf != null) { - orbitBus.getDjf(djf, t) + orbitBus.getDjf(djf, t); } // analyser diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index 38760d041..9ef7bcbf3 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -5,202 +5,202 @@ import { clamp } from './util.mjs'; let hasChanged = (now, before) => now !== undefined && now !== before; export class Orbit { - reverbNode; - delayNode; - output; - summingNode; - djfNode; - audioContext; - constructor(audioContext) { - this.audioContext = audioContext; - this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); - this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); - this.summingNode.connect(this.output); + reverbNode; + delayNode; + output; + summingNode; + djfNode; + audioContext; + constructor(audioContext) { + this.audioContext = audioContext; + this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); + this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); + this.summingNode.connect(this.output); + } + + disconnect() { + this.output.disconnect(); + this.summingNode.disconnect(); + this.delayNode?.disconnect(); + this.reverbNode?.disconnect(); + } + + getDjf(value, t = 0) { + if (this.djfNode == null) { + this.djfNode = getWorklet(this.audioContext, 'djf-processor', { value }); + this.summingNode.disconnect(); + this.summingNode.connect(this.djfNode); + this.djfNode.connect(this.output); + } + const val = this.djfNode.parameters.get('value'); + val.setValueAtTime(value, t); + } + + getDelay(delaytime = 0, feedback = 0.5, t) { + const maxfeedback = 0.98; + if (feedback > maxfeedback) { + //logger(`feedback was clamped to ${maxfeedback} to save your ears`); + } + feedback = clamp(feedback, 0, 0.98); + if (this.delayNode == null) { + this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback); + this.delayNode.connect(this.summingNode); + this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed.. + } + this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t); + this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t); + return this.delayNode; + } + + getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) { + // If no reverb has been created for a given orbit, create one + if (this.reverbNode == null) { + this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin); + this.reverbNode.connect(this.summingNode); } - disconnect() { - this.output.disconnect(); - this.summingNode.disconnect(); - this.delayNode?.disconnect(); - this.reverbNode?.disconnect(); + if ( + hasChanged(duration, this.reverbNode.duration) || + hasChanged(fade, this.reverbNode.fade) || + hasChanged(lp, this.reverbNode.lp) || + hasChanged(dim, this.reverbNode.dim) || + hasChanged(irspeed, this.reverbNode.irspeed) || + hasChanged(irbegin, this.reverbNode.irbegin) || + this.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 + this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin); } + return this.reverbNode; + } + sendReverb(node, amount) { + effectSend(node, this.reverbNode, amount); + } - getDjf(value, t = 0) { - if (this.djfNode == null){ - this.djfNode = getWorklet(this.audioContext, 'djf-processor', {value}) - this.summingNode.disconnect() - this.summingNode.connect(this.djfNode) - this.djfNode.connect(this.output) - } - const val = this.djfNode.parameters.get('value') - val.setValueAtTime(value, t) - } - - getDelay(delaytime = 0, feedback = 0.5, t) { - const maxfeedback = 0.98; - if (feedback > maxfeedback) { - //logger(`feedback was clamped to ${maxfeedback} to save your ears`); - } - feedback = clamp(feedback, 0, 0.98); - if (this.delayNode == null) { - this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback); - this.delayNode.connect(this.summingNode); - this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed.. - } - this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t); - this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t); - return this.delayNode; - } + sendDelay(node, amount) { + effectSend(node, this.delayNode, amount); + } - getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) { - // If no reverb has been created for a given orbit, create one - if (this.reverbNode == null) { - this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin); - this.reverbNode.connect(this.summingNode); - } + duck(t, onsettime = 0, attacktime = 0.1, depth = 1) { + const onset = onsettime; + const attack = Math.max(attacktime, 0.002); + const gainParam = this.output.gain; + webAudioTimeout( + this.audioContext, + () => { + const now = this.audioContext.currentTime; - if ( - hasChanged(duration, this.reverbNode.duration) || - hasChanged(fade, this.reverbNode.fade) || - hasChanged(lp, this.reverbNode.lp) || - hasChanged(dim, this.reverbNode.dim) || - hasChanged(irspeed, this.reverbNode.irspeed) || - hasChanged(irbegin, this.reverbNode.irbegin) || - this.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 - this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin); - } - return this.reverbNode; - } - sendReverb(node, amount) { - effectSend(node, this.reverbNode, amount); - } + // cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime + // on browsers which lack that method + const currVal = gainParam.value; + gainParam.cancelScheduledValues(now); + gainParam.setValueAtTime(currVal, now); - sendDelay(node, amount) { - effectSend(node, this.delayNode, amount); - } + const t0 = Math.max(t, now); // guard against now > t + const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal); + gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset); + gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack); + }, + 0, + t - 0.01, + ); + } - duck(t, onsettime = 0, attacktime = 0.1, depth = 1) { - const onset = onsettime; - const attack = Math.max(attacktime, 0.002); - const gainParam = this.output.gain; - webAudioTimeout( - this.audioContext, - () => { - const now = this.audioContext.currentTime; - - // cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime - // on browsers which lack that method - const currVal = gainParam.value; - gainParam.cancelScheduledValues(now); - gainParam.setValueAtTime(currVal, now); - - const t0 = Math.max(t, now); // guard against now > t - const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal); - gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset); - gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack); - }, - 0, - t - 0.01, - ); - } - - connectToOutput(node) { - node.connect(this.summingNode); - } + connectToOutput(node) { + node.connect(this.summingNode); + } } export class SuperdoughOutput { - channelMerger; - destinationGain; + channelMerger; + destinationGain; - constructor(audioContext) { - this.audioContext = audioContext; - this.initializeAudio(); - } + constructor(audioContext) { + this.audioContext = audioContext; + this.initializeAudio(); + } - initializeAudio() { - const audioContext = this.audioContext; - const maxChannelCount = audioContext.destination.maxChannelCount; - this.audioContext.destination.channelCount = maxChannelCount; - this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount }); - this.destinationGain = new GainNode(audioContext); - this.channelMerger.connect(this.destinationGain); - this.destinationGain.connect(audioContext.destination); - } + initializeAudio() { + const audioContext = this.audioContext; + const maxChannelCount = audioContext.destination.maxChannelCount; + this.audioContext.destination.channelCount = maxChannelCount; + this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount }); + this.destinationGain = new GainNode(audioContext); + this.channelMerger.connect(this.destinationGain); + this.destinationGain.connect(audioContext.destination); + } - reset() { - this.channelMerger.disconnect(); - this.destinationGain.disconnect(); - this.destinationGain = null; - this.channelMerger = null; - this.nodes = {}; - this.initializeAudio(); - } - connectToDestination = (input, channels = [0, 1]) => { - //This upmix can be removed if correct channel counts are set throughout the app, - // and then strudel could theoretically support surround sound audio files - const stereoMix = new StereoPannerNode(this.audioContext); - input.connect(stereoMix); + reset() { + this.channelMerger.disconnect(); + this.destinationGain.disconnect(); + this.destinationGain = null; + this.channelMerger = null; + this.nodes = {}; + this.initializeAudio(); + } + connectToDestination = (input, channels = [0, 1]) => { + //This upmix can be removed if correct channel counts are set throughout the app, + // and then strudel could theoretically support surround sound audio files + const stereoMix = new StereoPannerNode(this.audioContext); + input.connect(stereoMix); - const splitter = new ChannelSplitterNode(this.audioContext, { - numberOfOutputs: stereoMix.channelCount, - }); - stereoMix.connect(splitter); - channels.forEach((ch, i) => { - splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount); - }); - }; + const splitter = new ChannelSplitterNode(this.audioContext, { + numberOfOutputs: stereoMix.channelCount, + }); + stereoMix.connect(splitter); + channels.forEach((ch, i) => { + splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount); + }); + }; } export class SuperdoughAudioController { - audioContext; - output; - nodes = {}; + audioContext; + output; + nodes = {}; - constructor(audioContext) { - this.audioContext = audioContext; - this.output = new SuperdoughOutput(audioContext); - } - - reset() { - Array.from(this.nodes).forEach((node) => { - node.disconnect(); - }); - this.output.reset(); - } - - duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) { - const targetArr = [targetOrbits].flat(); - const onsetArr = [onsettime].flat(); - const attackArr = [attacktime].flat(); - const depthArr = [depth].flat(); - - targetArr.forEach((target, idx) => { - const orbit = this.nodes[target]; - - if (orbit == null) { - errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); - return; - } - const onset = onsetArr[idx] ?? onsetArr[0]; - const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002); - const depth = depthArr[idx] ?? depthArr[0]; - - orbit.duck(t, onset, attack, depth); - }); - } - - getOrbit(orbitNum, channels) { - if (this.nodes[orbitNum] == null) { - this.nodes[orbitNum] = new Orbit(this.audioContext); - this.output.connectToDestination(this.nodes[orbitNum].output, channels); - } - return this.nodes[orbitNum]; + constructor(audioContext) { + this.audioContext = audioContext; + this.output = new SuperdoughOutput(audioContext); + } + + reset() { + Array.from(this.nodes).forEach((node) => { + node.disconnect(); + }); + this.output.reset(); + } + + duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) { + const targetArr = [targetOrbits].flat(); + const onsetArr = [onsettime].flat(); + const attackArr = [attacktime].flat(); + const depthArr = [depth].flat(); + + targetArr.forEach((target, idx) => { + const orbit = this.nodes[target]; + + if (orbit == null) { + errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); + return; + } + const onset = onsetArr[idx] ?? onsetArr[0]; + const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002); + const depth = depthArr[idx] ?? depthArr[0]; + + orbit.duck(t, onset, attack, depth); + }); + } + + getOrbit(orbitNum, channels) { + if (this.nodes[orbitNum] == null) { + this.nodes[orbitNum] = new Orbit(this.audioContext); + this.output.connectToDestination(this.nodes[orbitNum].output, channels); } + return this.nodes[orbitNum]; + } } diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 06e81f38c..4d25617fb 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -276,8 +276,8 @@ class TwoPoleFilter { s1 = 0; update(s, cutoff, resonance = 0) { // Out of bound values can produce NaNs - resonance = clamp(resonance, 0, 1) - cutoff = clamp(cutoff, 0, sampleRate / 2 - 1) + resonance = clamp(resonance, 0, 1); + cutoff = clamp(cutoff, 0, sampleRate / 2 - 1); const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14); const r = Math.pow(0.5, (resonance + 0.125) / 0.125); const mrc = 1 - r * c; @@ -289,14 +289,12 @@ class TwoPoleFilter { class DJFProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { - return [ - { name: 'value', defaultValue: 0.5 }, - ]; + return [{ name: 'value', defaultValue: 0.5 }]; } constructor() { super(); - this.filters = [new TwoPoleFilter(), new TwoPoleFilter()] + this.filters = [new TwoPoleFilter(), new TwoPoleFilter()]; } process(inputs, outputs, parameters) { @@ -307,38 +305,34 @@ class DJFProcessor extends AudioWorkletProcessor { this.started = hasInput; const value = clamp(parameters.value[0], 0, 1); - let filterType = 'none' - let cutoff + let filterType = 'none'; + let cutoff; let v = 1; if (value > 0.5) { - filterType = 'hipass' - v = (value - .5) * 2 + filterType = 'hipass'; + v = (value - 0.5) * 2; } else if (value < 0.5) { - filterType = 'lopass' - v = value * 2 + filterType = 'lopass'; + v = value * 2; } - cutoff = Math.pow((v * 11), 4) + cutoff = Math.pow(v * 11, 4); // let cutoff = parameters.frequency[0]; for (let i = 0; i < input.length; i++) { for (let n = 0; n < blockSize; n++) { if (filterType == 'none') { - output[i][n] = input[i][n] + output[i][n] = input[i][n]; } else { - this.filters[i].update(input[i][n], cutoff, 0.2) + this.filters[i].update(input[i][n], cutoff, 0.2); if (filterType === 'lopass') { - output[i][n] = this.filters[i].s1 + output[i][n] = this.filters[i].s1; } else if (filterType === 'hipass') { - output[i][n] = input[i][n] - this.filters[i].s1 + output[i][n] = input[i][n] - this.filters[i].s1; } else { - output[i][n] = input[i][n] + output[i][n] = input[i][n]; } } - - - - } } return true; From da81ba00fb056f77c23ceccab29e92ad78892fd6 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 24 Sep 2025 20:32:45 -0700 Subject: [PATCH 231/257] adjust control range --- packages/superdough/worklets.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 4d25617fb..fa0fa5345 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -308,10 +308,10 @@ class DJFProcessor extends AudioWorkletProcessor { let filterType = 'none'; let cutoff; let v = 1; - if (value > 0.5) { + if (value > 0.52) { filterType = 'hipass'; v = (value - 0.5) * 2; - } else if (value < 0.5) { + } else if (value < 0.48) { filterType = 'lopass'; v = value * 2; } From c474b8c92e882ca739fa95d7004aaacf8816e435 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 24 Sep 2025 20:33:51 -0700 Subject: [PATCH 232/257] rm comment --- packages/superdough/worklets.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index fa0fa5345..2a675bba1 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -317,8 +317,6 @@ class DJFProcessor extends AudioWorkletProcessor { } cutoff = Math.pow(v * 11, 4); - // let cutoff = parameters.frequency[0]; - for (let i = 0; i < input.length; i++) { for (let n = 0; n < blockSize; n++) { if (filterType == 'none') { From d3d9f23c3c2a0ae5ab2d4f1e3c479a83f11f40de Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 24 Sep 2025 22:06:53 -0700 Subject: [PATCH 233/257] Remove unnecessary defaulting --- packages/superdough/wavetable.mjs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 9b9e325f7..ccf319201 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -186,13 +186,11 @@ async function onTriggerSynth(t, value, onended, bank, frameLen) { const ac = getAudioContext(); let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); let sourceDesc, holdEnd, envEnd; - let { unison = 5, spread = 0.6, detune, wtPos, wtWarp, wtWarpMode } = value; + let { unison, spread, detune, wtPos, wtWarp, wtWarpMode } = value; if (typeof wtWarpMode === 'string') { wtWarpMode = WarpMode[wtWarpMode.toUpperCase()] ?? WarpMode.NONE; } - detune = detune ?? 0.18; const frequency = getFrequencyFromValue(value); - const voices = clamp(unison, 1, 100); let { tableUrl, label } = getTableInfo(value, bank); const payload = await loadWavetableFrames(tableUrl, label, frameLen); holdEnd = t + duration; @@ -208,7 +206,7 @@ async function onTriggerSynth(t, value, onended, bank, frameLen) { position: wtPos, warp: wtWarp, warpMode: wtWarpMode, - voices, + voices: unison, spread, }, { outputChannelCount: [2] }, From e3741ae8faad7f1b9c160a6949c71a0ff3b03ee2 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 25 Sep 2025 00:05:50 -0700 Subject: [PATCH 234/257] Add control over phase randomization and fix a bug with supersaw --- packages/core/controls.mjs | 10 ++++++++++ packages/superdough/superdough.mjs | 1 - packages/superdough/wavetable.mjs | 13 +++++++------ packages/superdough/worklets.mjs | 8 +++++--- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 7bf554577..89583b475 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -120,6 +120,16 @@ export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWar */ export const { wtWarpMode, wavetableWarpMode } = registerControl('wtWarpMode', 'wavetableWarpMode'); +/** + * Amount of randomness of the initial phase of the wavetable oscillator. + * + * @name wtPhaseRand + * @param {number | Pattern} mode Warp mode: an integer + * @synonyms wavetableWarpMode + * + */ +export const { wtPhaseRand, wavetablePhaseRand } = registerControl('wtPhaseRand', 'wavetablePhaseRand'); + /** * Define a custom webaudio node to use as a sound source. * diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index b2b5c8d67..f1f308a7d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -594,7 +594,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) tremolophase = 0, tremoloshape, s = getDefaultValue('s'), - wt, bank, source, gain = getDefaultValue('gain'), diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index ccf319201..044b651e7 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -186,7 +186,7 @@ async function onTriggerSynth(t, value, onended, bank, frameLen) { const ac = getAudioContext(); let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); let sourceDesc, holdEnd, envEnd; - let { unison, spread, detune, wtPos, wtWarp, wtWarpMode } = value; + let { wtWarpMode } = value; if (typeof wtWarpMode === 'string') { wtWarpMode = WarpMode[wtWarpMode.toUpperCase()] ?? WarpMode.NONE; } @@ -202,12 +202,13 @@ async function onTriggerSynth(t, value, onended, bank, frameLen) { begin: t, end: envEnd, frequency, - detune, - position: wtPos, - warp: wtWarp, + detune: value.detune, + position: value.wtPos, + warp: value.wtWarp, warpMode: wtWarpMode, - voices: unison, - spread, + voices: value.unison, + spread: value.spread, + phaserand: value.wtPhaseRand, }, { outputChannelCount: [2] }, ); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index b0c0e8e2d..44b82f624 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -474,10 +474,10 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { gainR = gain1; } // Individual voice detuning - freq = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n)); + const voiceFreq = 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); + const dt = mod(voiceFreq / sampleRate, 1); this.phase[n] = this.phase[n] ?? Math.random(); const v = waveshapes.sawblep(this.phase[n], dt); @@ -988,6 +988,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { { name: 'warpMode', defaultValue: 0 }, { name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 }, { name: 'spread', defaultValue: 0, minValue: 0, maxValue: 1 }, + { name: 'phaserand', defaultValue: 1, minValue: 0, maxValue: 1 }, ]; } @@ -1188,6 +1189,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const warpAmount = pv(parameters.warp, i); const warpMode = pv(parameters.warpMode, i); const voices = pv(parameters.voices, i); + const phaseRand = pv(parameters.phaserand, i); const gain1 = Math.sqrt(1 - spread); const gain2 = Math.sqrt(spread); let f = pv(parameters.frequency, i); @@ -1207,7 +1209,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const bank = this.tables[level]; // warp phase then sample - this.phase[n] = this.phase[n] ?? Math.random(); + this.phase[n] = this.phase[n] ?? Math.random() * phaseRand; let ph = this._warpPhase(this.phase[n], warpAmount, warpMode); const s0 = this._sampleFrame(bank[fIdx], ph); const s1 = this._sampleFrame(bank[Math.min(this.numFrames - 1, fIdx + 1)], ph); From bd8d207a3d7271fde88f578f36f1901e8bef0008 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 25 Sep 2025 00:17:13 -0700 Subject: [PATCH 235/257] Typo --- packages/core/controls.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 89583b475..989097d69 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -114,7 +114,7 @@ export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWar * spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip * * @name wtWarpMode - * @param {number | Pattern} mode Warp mode: an integer + * @param {number | Pattern} mode Warp mode * @synonyms wavetableWarpMode * */ @@ -124,8 +124,8 @@ export const { wtWarpMode, wavetableWarpMode } = registerControl('wtWarpMode', ' * Amount of randomness of the initial phase of the wavetable oscillator. * * @name wtPhaseRand - * @param {number | Pattern} mode Warp mode: an integer - * @synonyms wavetableWarpMode + * @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random) + * @synonyms wavetablePhaseRand * */ export const { wtPhaseRand, wavetablePhaseRand } = registerControl('wtPhaseRand', 'wavetablePhaseRand'); From 96981c3c1d8ddc1e837c48ee8e4c3c883e6066e7 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 25 Sep 2025 00:36:28 -0700 Subject: [PATCH 236/257] A bit more cleanup --- packages/core/controls.mjs | 2 +- packages/superdough/wavetable.mjs | 31 ++++++++++--------------------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 989097d69..9b5519704 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -114,7 +114,7 @@ export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWar * spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip * * @name wtWarpMode - * @param {number | Pattern} mode Warp mode + * @param {number | string | Pattern} mode Warp mode * @synonyms wavetableWarpMode * */ diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 044b651e7..feb5098b0 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -182,20 +182,19 @@ export const tables = async (url, frameLen, json) => { }; async function onTriggerSynth(t, value, onended, bank, frameLen) { - let { s, n = 0, duration } = value; + const { s, n = 0, duration } = value; const ac = getAudioContext(); - let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); - let sourceDesc, holdEnd, envEnd; + const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); let { wtWarpMode } = value; if (typeof wtWarpMode === 'string') { wtWarpMode = WarpMode[wtWarpMode.toUpperCase()] ?? WarpMode.NONE; } const frequency = getFrequencyFromValue(value); - let { tableUrl, label } = getTableInfo(value, bank); + const { tableUrl, label } = getTableInfo(value, bank); const payload = await loadWavetableFrames(tableUrl, label, frameLen); - holdEnd = t + duration; - envEnd = holdEnd + release + 0.01; - const worklet = getWorklet( + const holdEnd = t + duration; + const envEnd = holdEnd + release; + const source = getWorklet( ac, 'wavetable-oscillator-processor', { @@ -212,34 +211,24 @@ async function onTriggerSynth(t, value, onended, bank, frameLen) { }, { outputChannelCount: [2] }, ); - worklet.port.postMessage({ type: 'tables', payload }); - sourceDesc = { source: worklet }; - const { source } = sourceDesc; + source.port.postMessage({ type: 'tables', payload }); if (ac.currentTime > t) { logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight'); return; } - if (!source) { - logger(`[wavetable] could not load "${s}:${n}"`, 'error'); - return; - } - let vibratoOscillator = getVibratoOscillator(source.detune, value, t); + const vibratoOscillator = getVibratoOscillator(source.detune, value, t); const envGain = ac.createGain(); const node = source.connect(envGain); getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); getPitchEnvelope(source.detune, value, t, holdEnd); - - const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... - node.connect(out); - let handle = { node: out, bufferSource: source, oscillator: worklet }; - let timeoutNode = webAudioTimeout( + const handle = { node, source }; + const timeoutNode = webAudioTimeout( ac, () => { source.disconnect(); destroyAudioWorkletNode(source); vibratoOscillator?.stop(); node.disconnect(); - out.disconnect(); onended(); }, t, From f8f42565ae36839b63b37e85d2a7be5a1d7b322c Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 25 Sep 2025 00:40:25 -0700 Subject: [PATCH 237/257] Typo - add back slight delay in cleanup --- packages/superdough/wavetable.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index feb5098b0..5805685d7 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -193,7 +193,7 @@ async function onTriggerSynth(t, value, onended, bank, frameLen) { const { tableUrl, label } = getTableInfo(value, bank); const payload = await loadWavetableFrames(tableUrl, label, frameLen); const holdEnd = t + duration; - const envEnd = holdEnd + release; + const envEnd = holdEnd + release + 0.01; const source = getWorklet( ac, 'wavetable-oscillator-processor', From 8b2c35b7a3217a57fdc954ba3eca5d4592720520 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 25 Sep 2025 01:10:41 -0700 Subject: [PATCH 238/257] Cleanup --- packages/superdough/wavetable.mjs | 22 ++++++++++------------ packages/superdough/worklets.mjs | 8 ++++---- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 5805685d7..c628549e7 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -1,5 +1,5 @@ import { getAudioContext, registerSound } from './index.mjs'; -import { clamp, getSoundIndex, valueToMidi } from './util.mjs'; +import { getSoundIndex, valueToMidi } from './util.mjs'; import { destroyAudioWorkletNode, getADSRValues, @@ -86,28 +86,27 @@ function humanFileSize(bytes, si) { return bytes.toFixed(1) + ' ' + units[u]; } -export function getTableInfo(hapValue, bank) { - const { wt, n = 0 } = hapValue; +export function getTableInfo(hapValue, tableUrls) { + const { s, n = 0 } = hapValue; let midi = valueToMidi(hapValue, 36); let transpose = midi - 36; // C3 is middle C; - const index = getSoundIndex(n, bank.length); - const tableUrl = bank[index]; - const label = `${wt}:${index}`; + const index = getSoundIndex(n, tableUrls.length); + const tableUrl = tableUrls[index]; + const label = `${s}:${index}`; return { transpose, tableUrl, index, midi, label }; } -const loadBuffer = (url, ac, wt, n = 0) => { - const label = wt ? `table "${wt}:${n}"` : 'table'; +const loadBuffer = (url, ac, label) => { url = url.replace('#', '%23'); if (!loadCache[url]) { - logger(`[wavetable] load ${label}..`, 'load-table', { url }); + logger(`[wavetable] load table ${label}..`, 'load-table', { url }); const timestamp = Date.now(); loadCache[url] = fetch(url) .then((res) => res.arrayBuffer()) .then(async (res) => { const took = Date.now() - timestamp; const size = humanFileSize(res.byteLength); - logger(`[wavetable] load ${label}... done! loaded ${size} in ${took}ms`, 'loaded-table', { url }); + logger(`[wavetable] load table ${label}... done! loaded ${size} in ${took}ms`, 'loaded-table', { url }); const decoded = await ac.decodeAudioData(res); return decoded; }); @@ -151,7 +150,7 @@ const _processTables = (json, baseUrl, frameLen) => { }; /** - * Loads a collection of wavetables to use with `wt` + * Loads a collection of wavetables to use with `s` * * @name tables */ @@ -167,7 +166,6 @@ export const tables = async (url, frameLen, json) => { // not a browser return; } - const base = url.split('/').slice(0, -1).join('/'); if (typeof fetch === 'undefined') { // skip fetch when in node / testing return; diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 44b82f624..daf19e537 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -474,10 +474,10 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { gainR = gain1; } // Individual voice detuning - const voiceFreq = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n)); + const freqVoice = 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(voiceFreq / sampleRate, 1); + const dt = mod(freqVoice / sampleRate, 1); this.phase[n] = this.phase[n] ?? Math.random(); const v = waveshapes.sawblep(this.phase[n], dt); @@ -1203,14 +1203,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { gainL = gain2; gainR = gain1; } - let fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune + const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune const dPhase = fVoice / sampleRate; const level = this._chooseMip(dPhase); const bank = this.tables[level]; // warp phase then sample this.phase[n] = this.phase[n] ?? Math.random() * phaseRand; - let ph = this._warpPhase(this.phase[n], warpAmount, warpMode); + const ph = this._warpPhase(this.phase[n], warpAmount, warpMode); const s0 = this._sampleFrame(bank[fIdx], ph); const s1 = this._sampleFrame(bank[Math.min(this.numFrames - 1, fIdx + 1)], ph); let s = s0 + (s1 - s0) * frac; From dfc4a0818c551b8978fa537da05557a4edfc4f43 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 25 Sep 2025 23:39:08 -0700 Subject: [PATCH 239/257] res adjust --- packages/superdough/worklets.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 9592dd7df..af8a4a633 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -322,10 +322,10 @@ class DJFProcessor extends AudioWorkletProcessor { let filterType = 'none'; let cutoff; let v = 1; - if (value > 0.52) { + if (value > 0.51) { filterType = 'hipass'; v = (value - 0.5) * 2; - } else if (value < 0.48) { + } else if (value < 0.49) { filterType = 'lopass'; v = value * 2; } @@ -336,7 +336,7 @@ class DJFProcessor extends AudioWorkletProcessor { if (filterType == 'none') { output[i][n] = input[i][n]; } else { - this.filters[i].update(input[i][n], cutoff, 0.2); + this.filters[i].update(input[i][n], cutoff, 0.1); if (filterType === 'lopass') { output[i][n] = this.filters[i].s1; } else if (filterType === 'hipass') { From 266cb614a87a6fbe7b7d8e32c211adcf6923339c Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 25 Sep 2025 23:44:32 -0700 Subject: [PATCH 240/257] add better example --- packages/core/controls.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 52 ++++++++++++++--------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 9b5519704..2565b73bb 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1171,7 +1171,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq'); * @name djf * @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter * @example - * n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc() + * n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>") * */ export const { djf } = registerControl('djf'); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index f91503ca2..a05975580 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2831,26 +2831,38 @@ exports[`runs examples > example "distort" example index 1 1`] = ` exports[`runs examples > example "djf" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | n:0 s:superzow octave:3 djf:0.5 ]", - "[ 1/4 → 1/2 | n:3 s:superzow octave:3 djf:0.5 ]", - "[ 1/2 → 3/4 | n:7 s:superzow octave:3 djf:0.5 ]", - "[ 3/4 → 1/1 | n:10 s:superzow octave:3 djf:0.5 ]", - "[ 3/4 → 1/1 | n:24 s:superzow octave:3 djf:0.5 ]", - "[ 1/1 → 5/4 | n:0 s:superzow octave:3 djf:0.25 ]", - "[ 5/4 → 3/2 | n:3 s:superzow octave:3 djf:0.25 ]", - "[ 3/2 → 7/4 | n:7 s:superzow octave:3 djf:0.25 ]", - "[ 7/4 → 2/1 | n:10 s:superzow octave:3 djf:0.25 ]", - "[ 7/4 → 2/1 | n:24 s:superzow octave:3 djf:0.25 ]", - "[ 2/1 → 9/4 | n:0 s:superzow octave:3 djf:0.5 ]", - "[ 9/4 → 5/2 | n:3 s:superzow octave:3 djf:0.5 ]", - "[ 5/2 → 11/4 | n:7 s:superzow octave:3 djf:0.5 ]", - "[ 11/4 → 3/1 | n:10 s:superzow octave:3 djf:0.5 ]", - "[ 11/4 → 3/1 | n:24 s:superzow octave:3 djf:0.5 ]", - "[ 3/1 → 13/4 | n:0 s:superzow octave:3 djf:0.75 ]", - "[ 13/4 → 7/2 | n:3 s:superzow octave:3 djf:0.75 ]", - "[ 7/2 → 15/4 | n:7 s:superzow octave:3 djf:0.75 ]", - "[ 15/4 → 4/1 | n:10 s:superzow octave:3 djf:0.75 ]", - "[ 15/4 → 4/1 | n:24 s:superzow octave:3 djf:0.75 ]", + "[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]", + "[ 1/8 → 1/4 | note:G4 s:supersaw djf:0.5 ]", + "[ 1/4 → 3/8 | note:Bb3 s:supersaw djf:0.5 ]", + "[ 3/8 → 1/2 | note:C4 s:supersaw djf:0.5 ]", + "[ 1/2 → 5/8 | note:A3 s:supersaw djf:0.5 ]", + "[ 5/8 → 3/4 | note:F3 s:supersaw djf:0.5 ]", + "[ 3/4 → 7/8 | note:G3 s:supersaw djf:0.5 ]", + "[ 7/8 → 1/1 | note:C4 s:supersaw djf:0.5 ]", + "[ 1/1 → 9/8 | note:Eb4 s:supersaw djf:0.3 ]", + "[ 9/8 → 5/4 | note:G4 s:supersaw djf:0.3 ]", + "[ 5/4 → 11/8 | note:A4 s:supersaw djf:0.3 ]", + "[ 11/8 → 3/2 | note:F3 s:supersaw djf:0.3 ]", + "[ 3/2 → 13/8 | note:F4 s:supersaw djf:0.3 ]", + "[ 13/8 → 7/4 | note:D4 s:supersaw djf:0.3 ]", + "[ 7/4 → 15/8 | note:G3 s:supersaw djf:0.3 ]", + "[ 15/8 → 2/1 | note:F4 s:supersaw djf:0.3 ]", + "[ 2/1 → 17/8 | note:Eb5 s:supersaw djf:0.2 ]", + "[ 17/8 → 9/4 | note:D5 s:supersaw djf:0.2 ]", + "[ 9/4 → 19/8 | note:Bb3 s:supersaw djf:0.2 ]", + "[ 19/8 → 5/2 | note:C5 s:supersaw djf:0.2 ]", + "[ 5/2 → 21/8 | note:D4 s:supersaw djf:0.2 ]", + "[ 21/8 → 11/4 | note:F3 s:supersaw djf:0.2 ]", + "[ 11/4 → 23/8 | note:G4 s:supersaw djf:0.2 ]", + "[ 23/8 → 3/1 | note:D3 s:supersaw djf:0.2 ]", + "[ 3/1 → 25/8 | note:G3 s:supersaw djf:0.75 ]", + "[ 25/8 → 13/4 | note:Bb3 s:supersaw djf:0.75 ]", + "[ 13/4 → 27/8 | note:Eb5 s:supersaw djf:0.75 ]", + "[ 27/8 → 7/2 | note:C4 s:supersaw djf:0.75 ]", + "[ 7/2 → 29/8 | note:C4 s:supersaw djf:0.75 ]", + "[ 29/8 → 15/4 | note:Eb5 s:supersaw djf:0.75 ]", + "[ 15/4 → 31/8 | note:Bb4 s:supersaw djf:0.75 ]", + "[ 31/8 → 4/1 | note:A4 s:supersaw djf:0.75 ]", ] `; From 2b3b6389431f9b38f0d26da37f0eac202c38f39f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 26 Sep 2025 21:47:45 -0500 Subject: [PATCH 241/257] sample source for WT --- packages/superdough/sampler.mjs | 38 ++++++++++++++++++++++--------- packages/superdough/util.mjs | 1 + packages/superdough/wavetable.mjs | 17 ++++++++------ packages/superdough/worklets.mjs | 5 ++-- website/src/repl/idbutils.mjs | 11 +++------ 5 files changed, 44 insertions(+), 28 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 9188c17c3..0e9633d4d 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,5 +1,5 @@ import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs'; -import { getAudioContext, registerSound } from './index.mjs'; +import { getAudioContext, registerSound, registerWaveTable } from './index.mjs'; import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -79,14 +79,14 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => { bufferSource.buffer = buffer; bufferSource.playbackRate.value = playbackRate; - const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue; + const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue; // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate, // rather than the current playback rate, so even if the sound is playing at twice its normal speed, // the midway point through a 10-second audio buffer is still 5." const offset = begin * bufferSource.buffer.duration; - const loop = s.startsWith('wt_') ? 1 : hapValue.loop; + const loop = hapValue.loop; if (loop) { bufferSource.loop = true; bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset; @@ -267,16 +267,13 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option return samples(json, baseUrl || base, options); } const { prebake, tag } = options; + + processSampleMap( sampleMap, - (key, bank) => - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { - type: 'sample', - samples: bank, - baseUrl, - prebake, - tag, - }), + (key, bank) => { + registerSample(key, bank, { baseUrl, prebake, tag }) + }, baseUrl, ); }; @@ -366,3 +363,22 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { return handle; } + + +function registerSample(key, bank, params) { + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { + type: 'sample', + samples: bank, + ...params + }) +} + +export function registerSampleSource(key, bank, params) { + const isWavetable = key.startsWith('wt_'); + if (isWavetable) { + registerWaveTable(key,bank, params) + } else { + registerSample(key, bank, params) + } + +} \ No newline at end of file diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 764ebb43e..80dd31a9d 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -76,3 +76,4 @@ export function cycleToSeconds(cycle, cps) { export function secondsToCycle(t, cps) { return t * cps; } + diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index c628549e7..067656045 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -140,15 +140,18 @@ const _processTables = (json, baseUrl, frameLen) => { baseUrl = githubPath(baseUrl, ''); } value = value.map((v) => baseUrl + v); - registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, value, frameLen), { - type: 'wavetable', - tables: value, - baseUrl, - frameLen, - }); + registerWaveTable(key,value, {baseUrl, frameLen}) }); }; +export function registerWaveTable(key, bank, params) { + registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, bank, params?.frameLen ?? 2048), { + type: 'wavetable', + tables: bank, + ...params + }); +} + /** * Loads a collection of wavetables to use with `s` * @@ -179,7 +182,7 @@ export const tables = async (url, frameLen, json) => { }); }; -async function onTriggerSynth(t, value, onended, bank, frameLen) { +export async function onTriggerSynth(t, value, onended, bank, frameLen) { const { s, n = 0, duration } = value; const ac = getAudioContext(); const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index af8a4a633..14093d098 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1239,6 +1239,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } const outL = outputs[0][0]; const outR = outputs[0][1] || outputs[0][0]; + const gainAdjustment = .15; if (!this.tables) { outL.fill(0); @@ -1284,8 +1285,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { s = -s; } - outL[i] += (s * gainL) / Math.sqrt(voices); - outR[i] += (s * gainR) / Math.sqrt(voices); + outL[i] += ((s * gainL) / Math.sqrt(voices)) * gainAdjustment; + outR[i] += ((s * gainR) / Math.sqrt(voices)) * gainAdjustment; this.phase[n] = wrapPhase(this.phase[n] + dPhase); } } diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index f26ee0479..5ac604559 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -1,4 +1,4 @@ -import { registerSound, onTriggerSample } from '@strudel/webaudio'; +import { registerSampleSource } from '@strudel/webaudio'; import { isAudioFile } from './files.mjs'; import { logger } from '@strudel/core'; @@ -76,13 +76,8 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = }) .map((title) => titlePathMap.get(title)); - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { - type: 'sample', - samples: value, - baseUrl: undefined, - prebake: false, - tag: undefined, - }); + registerSampleSource(key,value, {prebake: false}) + }); logger('imported sounds registered!', 'success'); From 97beaec25ae69d5ec146df1dfa77eaddd0d4fbed Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 26 Sep 2025 20:46:10 -0700 Subject: [PATCH 242/257] Added examples, fixed samplerate issue on import, added to settings tab, fixed spread, added default wavetables, change default phaserand to 0 --- packages/core/controls.mjs | 10 +- packages/superdough/wavetable.mjs | 90 +++++++--- packages/superdough/worklets.mjs | 23 +-- test/__snapshots__/examples.test.mjs.snap | 159 ++++++++++++++++++ website/public/uzu-wavetables.json | 54 ++++++ .../src/repl/components/panel/SoundsTab.jsx | 6 +- website/src/repl/prebake.mjs | 6 +- website/src/settings.mjs | 1 + 8 files changed, 311 insertions(+), 38 deletions(-) create mode 100644 website/public/uzu-wavetables.json diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 9b5519704..3e5e9e63c 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -93,7 +93,8 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound'); * @name wtPos * @param {number | Pattern} position Position in the wavetable from 0 to 1 * @synonyms wavetablePosition - * + * @example + * s("squelch").seg(8).note("F1").wtPos("0 0.25 0.5 0.75 1") */ export const { wtPos, wavetablePosition } = registerControl('wtPos', 'wavetablePosition'); @@ -103,7 +104,9 @@ export const { wtPos, wavetablePosition } = registerControl('wtPos', 'wavetableP * @name wtWarp * @param {number | Pattern} amount Warp of the wavetable from 0 to 1 * @synonyms wavetableWarp - * + * @example + * s("basique").seg(8).note("F1").wtWarp("0 0.25 0.5 0.75 1") + * .wtWarpMode("spin") */ export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWarp'); @@ -116,6 +119,9 @@ export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWar * @name wtWarpMode * @param {number | string | Pattern} mode Warp mode * @synonyms wavetableWarpMode + * @example + * s("morgana").seg(8).note("F1").wtWarp("0 0.25 0.5 0.75 1") + * .wtWarpMode("*2") * */ export const { wtWarpMode, wavetableWarpMode } = registerControl('wtWarpMode', 'wavetableWarpMode'); diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index c628549e7..14706f925 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -39,8 +39,7 @@ export const WarpMode = Object.freeze({ }); async function loadWavetableFrames(url, label, frameLen = 256) { - const ac = getAudioContext(); - const buf = await loadBuffer(url, ac, label); + const buf = await loadBuffer(url, label); const ch0 = buf.getChannelData(0); const total = ch0.length; const numFrames = Math.floor(total / frameLen); @@ -96,7 +95,37 @@ export function getTableInfo(hapValue, tableUrls) { return { transpose, tableUrl, index, midi, label }; } -const loadBuffer = (url, ac, label) => { +// Extract the sample rate of a .wav file +function parseWavSampleRate(arrBuf) { + const dv = new DataView(arrBuf); + // Header is "RIFFWAVE", so 12 bytes + let p = 12; + // Look through chunks for the format header + // (they will always have an 8 byte header (id and size) followed by a payload) + while (p + 8 <= dv.byteLength) { + // Parse id + const id = String.fromCharCode(dv.getUint8(p), dv.getUint8(p + 1), dv.getUint8(p + 2), dv.getUint8(p + 3)); + // Parse chunk size + const size = dv.getUint32(p + 4, true); + if (id === 'fmt ') { + // The format chunk contains the sample rate after + // 8 bytes of header, 2 bytes of format tag, 2 bytes of num channels + // (for a total of 12) + return dv.getUint32(p + 12, true); + } + // Advance to next chunk + p += 8 + size + (size & 1); + } + return null; +} + +async function decodeAtNativeRate(arr) { + const sr = parseWavSampleRate(arr) || 44100; + const tempAC = new OfflineAudioContext(1, 1, sr); + return await tempAC.decodeAudioData(arr); +} + +const loadBuffer = (url, label) => { url = url.replace('#', '%23'); if (!loadCache[url]) { logger(`[wavetable] load table ${label}..`, 'load-table', { url }); @@ -107,7 +136,7 @@ const loadBuffer = (url, ac, label) => { const took = Date.now() - timestamp; const size = humanFileSize(res.byteLength); logger(`[wavetable] load table ${label}... done! loaded ${size} in ${took}ms`, 'loaded-table', { url }); - const decoded = await ac.decodeAudioData(res); + const decoded = await decodeAtNativeRate(res); return decoded; }); } @@ -127,25 +156,40 @@ function githubPath(base, subpath = '') { return `https://raw.githubusercontent.com/${path}/${subpath}`; } -const _processTables = (json, baseUrl, frameLen) => { - return Object.entries(json).forEach(([key, value]) => { - if (typeof value === 'string') { - value = [value]; +const _processTables = (json, baseUrl, frameLen, options = {}) => { + baseUrl = json._base || baseUrl; + return Object.entries(json).forEach(([key, tables]) => { + if (key === '_base') return false; + if (typeof tables === 'string') { + tables = [tables]; } - if (typeof value !== 'object') { + if (typeof tables !== 'object') { throw new Error('wrong json format for ' + key); } - baseUrl = value._base || baseUrl; - if (baseUrl.startsWith('github:')) { - baseUrl = githubPath(baseUrl, ''); + let resolvedUrl = baseUrl; + if (resolvedUrl.startsWith('github:')) { + resolvedUrl = githubPath(resolvedUrl, ''); + } + tables = tables + .map((t) => resolvedUrl + t) + .filter((t) => { + if (!t.toLowerCase().endsWith('.wav')) { + logger(`[wavetable] skipping ${t} -- wavetables must be ".wav" format`); + return false; + } + return true; + }); + if (tables.length) { + const { prebake, tag } = options; + registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, tables, frameLen), { + type: 'wavetable', + tables, + baseUrl, + frameLen, + prebake, + tag, + }); } - value = value.map((v) => baseUrl + v); - registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, value, frameLen), { - type: 'wavetable', - tables: value, - baseUrl, - frameLen, - }); }); }; @@ -154,7 +198,7 @@ const _processTables = (json, baseUrl, frameLen) => { * * @name tables */ -export const tables = async (url, frameLen, json) => { +export const tables = async (url, frameLen, json, options = {}) => { if (json !== undefined) return _processTables(json, url, frameLen); if (url.startsWith('github:')) { url = githubPath(url, 'strudel.json'); @@ -172,14 +216,14 @@ export const tables = async (url, frameLen, json) => { } return fetch(url) .then((res) => res.json()) - .then((json) => _processTables(json, url, frameLen)) + .then((json) => _processTables(json, url, frameLen, options)) .catch((error) => { console.error(error); throw new Error(`error loading "${url}"`); }); }; -async function onTriggerSynth(t, value, onended, bank, frameLen) { +async function onTriggerSynth(t, value, onended, tables, frameLen) { const { s, n = 0, duration } = value; const ac = getAudioContext(); const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); @@ -188,7 +232,7 @@ async function onTriggerSynth(t, value, onended, bank, frameLen) { wtWarpMode = WarpMode[wtWarpMode.toUpperCase()] ?? WarpMode.NONE; } const frequency = getFrequencyFromValue(value); - const { tableUrl, label } = getTableInfo(value, bank); + const { tableUrl, label } = getTableInfo(value, tables); const payload = await loadWavetableFrames(tableUrl, label, frameLen); const holdEnd = t + duration; const envEnd = holdEnd + release + 0.01; diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index daf19e537..dbfb78f81 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -988,7 +988,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { { name: 'warpMode', defaultValue: 0 }, { name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 }, { name: 'spread', defaultValue: 0, minValue: 0, maxValue: 1 }, - { name: 'phaserand', defaultValue: 1, minValue: 0, maxValue: 1 }, + { name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 }, ]; } @@ -1155,10 +1155,10 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } _sampleFrame(frame, phase) { - const pos = phase * (frame.length - 1); + const pos = phase * frame.length; const i = pos | 0; const frac = pos - i; - const a = frame[i]; + const a = frame[i % frame.length]; const b = frame[(i + 1) % frame.length]; return a + (b - a) * frac; } @@ -1181,7 +1181,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { for (let i = 0; i < outL.length; i++) { const detune = pv(parameters.detune, i); - const spread = pv(parameters.spread, i) * 0.5 + 0.5; const tablePos = pv(parameters.position, i); const idx = tablePos * (this.numFrames - 1); const fIdx = idx | 0; @@ -1189,11 +1188,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const warpAmount = pv(parameters.warp, i); const warpMode = pv(parameters.warpMode, i); const voices = pv(parameters.voices, i); + const spread = voices > 1 ? pv(parameters.spread, i) : 0; const phaseRand = pv(parameters.phaserand, i); - const gain1 = Math.sqrt(1 - spread); - const gain2 = Math.sqrt(spread); + const gain1 = Math.sqrt(0.5 - 0.5 * spread); + const gain2 = Math.sqrt(0.5 + 0.5 * spread); let f = pv(parameters.frequency, i); f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune + const normalizer = 0.3 / Math.sqrt(voices); for (let n = 0; n < voices; n++) { const isOdd = (n & 1) == 1; let gainL = gain1; @@ -1206,19 +1207,19 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune const dPhase = fVoice / sampleRate; const level = this._chooseMip(dPhase); - const bank = this.tables[level]; + const table = this.tables[level]; // warp phase then sample this.phase[n] = this.phase[n] ?? Math.random() * phaseRand; const ph = this._warpPhase(this.phase[n], warpAmount, warpMode); - const s0 = this._sampleFrame(bank[fIdx], ph); - const s1 = this._sampleFrame(bank[Math.min(this.numFrames - 1, fIdx + 1)], ph); + const s0 = this._sampleFrame(table[fIdx], ph); + const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph); let s = s0 + (s1 - s0) * frac; if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { s = -s; } - outL[i] += (s * gainL) / Math.sqrt(voices); - outR[i] += (s * gainR) / Math.sqrt(voices); + outL[i] += s * gainL * normalizer; + outR[i] += s * gainR * normalizer; this.phase[n] = wrapPhase(this.phase[n] + dPhase); } } diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index f91503ca2..3305def11 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -11981,6 +11981,165 @@ exports[`runs examples > example "withValue" example index 0 1`] = ` ] `; +exports[`runs examples > example "wtPos" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:squelch note:F1 wtPos:0 ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:squelch note:F1 wtPos:0 ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:squelch note:F1 wtPos:0.25 ]", + "[ 1/4 → 3/8 | s:squelch note:F1 wtPos:0.25 ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:squelch note:F1 wtPos:0.25 ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:squelch note:F1 wtPos:0.5 ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:squelch note:F1 wtPos:0.5 ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:squelch note:F1 wtPos:0.75 ]", + "[ 5/8 → 3/4 | s:squelch note:F1 wtPos:0.75 ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:squelch note:F1 wtPos:0.75 ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:squelch note:F1 wtPos:1 ]", + "[ 7/8 → 1/1 | s:squelch note:F1 wtPos:1 ]", + "[ 1/1 → 9/8 | s:squelch note:F1 wtPos:0 ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:squelch note:F1 wtPos:0 ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:squelch note:F1 wtPos:0.25 ]", + "[ 5/4 → 11/8 | s:squelch note:F1 wtPos:0.25 ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:squelch note:F1 wtPos:0.25 ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:squelch note:F1 wtPos:0.5 ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:squelch note:F1 wtPos:0.5 ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:squelch note:F1 wtPos:0.75 ]", + "[ 13/8 → 7/4 | s:squelch note:F1 wtPos:0.75 ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:squelch note:F1 wtPos:0.75 ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:squelch note:F1 wtPos:1 ]", + "[ 15/8 → 2/1 | s:squelch note:F1 wtPos:1 ]", + "[ 2/1 → 17/8 | s:squelch note:F1 wtPos:0 ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:squelch note:F1 wtPos:0 ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:squelch note:F1 wtPos:0.25 ]", + "[ 9/4 → 19/8 | s:squelch note:F1 wtPos:0.25 ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:squelch note:F1 wtPos:0.25 ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:squelch note:F1 wtPos:0.5 ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:squelch note:F1 wtPos:0.5 ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:squelch note:F1 wtPos:0.75 ]", + "[ 21/8 → 11/4 | s:squelch note:F1 wtPos:0.75 ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:squelch note:F1 wtPos:0.75 ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:squelch note:F1 wtPos:1 ]", + "[ 23/8 → 3/1 | s:squelch note:F1 wtPos:1 ]", + "[ 3/1 → 25/8 | s:squelch note:F1 wtPos:0 ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:squelch note:F1 wtPos:0 ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:squelch note:F1 wtPos:0.25 ]", + "[ 13/4 → 27/8 | s:squelch note:F1 wtPos:0.25 ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:squelch note:F1 wtPos:0.25 ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:squelch note:F1 wtPos:0.5 ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:squelch note:F1 wtPos:0.5 ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:squelch note:F1 wtPos:0.75 ]", + "[ 29/8 → 15/4 | s:squelch note:F1 wtPos:0.75 ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:squelch note:F1 wtPos:0.75 ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:squelch note:F1 wtPos:1 ]", + "[ 31/8 → 4/1 | s:squelch note:F1 wtPos:1 ]", +] +`; + +exports[`runs examples > example "wtWarp" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 1/4 → 3/8 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 5/8 → 3/4 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", + "[ 7/8 → 1/1 | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", + "[ 1/1 → 9/8 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 5/4 → 11/8 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 13/8 → 7/4 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", + "[ 15/8 → 2/1 | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", + "[ 2/1 → 17/8 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 9/4 → 19/8 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 21/8 → 11/4 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", + "[ 23/8 → 3/1 | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", + "[ 3/1 → 25/8 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 13/4 → 27/8 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 29/8 → 15/4 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", + "[ 31/8 → 4/1 | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", +] +`; + +exports[`runs examples > example "wtWarpMode" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:morgana note:F1 wtWarp:0 wtWarpMode:asym ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:morgana note:F1 wtWarp:0 wtWarpMode:asym ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:morgana note:F1 wtWarp:0.25 wtWarpMode:asym ]", + "[ 1/4 → 3/8 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:asym ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:asym ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:morgana note:F1 wtWarp:0.5 wtWarpMode:asym ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:morgana note:F1 wtWarp:0.5 wtWarpMode:bendp ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:morgana note:F1 wtWarp:0.75 wtWarpMode:bendp ]", + "[ 5/8 → 3/4 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:bendp ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:bendp ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:morgana note:F1 wtWarp:1 wtWarpMode:bendp ]", + "[ 7/8 → 1/1 | s:morgana note:F1 wtWarp:1 wtWarpMode:bendp ]", + "[ 1/1 → 9/8 | s:morgana note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:morgana note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:morgana note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 5/4 → 11/8 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:morgana note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:morgana note:F1 wtWarp:0.5 wtWarpMode:logistic ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:morgana note:F1 wtWarp:0.75 wtWarpMode:logistic ]", + "[ 13/8 → 7/4 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:logistic ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:logistic ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:morgana note:F1 wtWarp:1 wtWarpMode:logistic ]", + "[ 15/8 → 2/1 | s:morgana note:F1 wtWarp:1 wtWarpMode:logistic ]", + "[ 2/1 → 17/8 | s:morgana note:F1 wtWarp:0 wtWarpMode:sync ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:morgana note:F1 wtWarp:0 wtWarpMode:sync ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:morgana note:F1 wtWarp:0.25 wtWarpMode:sync ]", + "[ 9/4 → 19/8 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:sync ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:sync ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:morgana note:F1 wtWarp:0.5 wtWarpMode:sync ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:morgana note:F1 wtWarp:0.5 wtWarpMode:wormhole ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:morgana note:F1 wtWarp:0.75 wtWarpMode:wormhole ]", + "[ 21/8 → 11/4 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:wormhole ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:wormhole ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:morgana note:F1 wtWarp:1 wtWarpMode:wormhole ]", + "[ 23/8 → 3/1 | s:morgana note:F1 wtWarp:1 wtWarpMode:wormhole ]", + "[ 3/1 → 25/8 | s:morgana note:F1 wtWarp:0 wtWarpMode:brownian ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:morgana note:F1 wtWarp:0 wtWarpMode:brownian ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:morgana note:F1 wtWarp:0.25 wtWarpMode:brownian ]", + "[ 13/4 → 27/8 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:brownian ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:brownian ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:morgana note:F1 wtWarp:0.5 wtWarpMode:brownian ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:morgana note:F1 wtWarp:0.5 wtWarpMode:asym ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:morgana note:F1 wtWarp:0.75 wtWarpMode:asym ]", + "[ 29/8 → 15/4 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:asym ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:asym ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:morgana note:F1 wtWarp:1 wtWarpMode:asym ]", + "[ 31/8 → 4/1 | s:morgana note:F1 wtWarp:1 wtWarpMode:asym ]", +] +`; + exports[`runs examples > example "xfade" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh gain:0 ]", diff --git a/website/public/uzu-wavetables.json b/website/public/uzu-wavetables.json new file mode 100644 index 000000000..4497791f0 --- /dev/null +++ b/website/public/uzu-wavetables.json @@ -0,0 +1,54 @@ +{ + "_base": "http://localhost:5432", + "Bad Day": [ + "/Bad Day.wav" + ], + "Basique": [ + "/Basique.wav" + ], + "Crickets": [ + "/Crickets.wav" + ], + "Curses": [ + "/Curses.wav" + ], + "Earl Grey": [ + "/Earl Grey.wav" + ], + "Echoes": [ + "/Echoes.wav" + ], + "Glimmer": [ + "/Glimmer.wav" + ], + "Majick": [ + "/Majick.wav" + ], + "Meditation": [ + "/Meditation.wav" + ], + "Morgana": [ + "/Morgana.wav" + ], + "Red Alert": [ + "/Red Alert.wav" + ], + "Sad Piano": [ + "/Sad Piano.wav" + ], + "Shook": [ + "/Shook.wav" + ], + "Sludge": [ + "/Sludge.wav" + ], + "Squelch": [ + "/Squelch.wav" + ], + "Summers Day": [ + "/Summers Day.wav" + ], + "Wasp": [ + "/Wasp.wav" + ] +} \ No newline at end of file diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index 0484da02d..ff64fc18e 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -44,6 +44,9 @@ export function SoundsTab() { if (soundsFilter === soundFilterType.SYNTHS) { return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); } + if (soundsFilter === soundFilterType.WAVETABLES) { + return filtered.filter(([_, { data }]) => data.type === 'wavetable'); + } //TODO: tidy this up, it does not need to be saved in settings if (soundsFilter === 'importSounds') { return []; @@ -74,6 +77,7 @@ export function SoundsTab() { samples: 'samples', drums: 'drum-machines', synths: 'Synths', + wavetables: 'Wavetables', user: 'User', importSounds: 'import-sounds', }} @@ -125,7 +129,7 @@ export function SoundsTab() { > {' '} {name} - {data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''} + {data?.type === 'sample' || data?.type === 'wavetable' ? `(${getSamples(data.samples)})` : ''} {data?.type === 'soundfont' ? `(${data.fonts.length})` : ''} ); diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index fac6f5bb6..855798fdd 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -1,5 +1,5 @@ import { Pattern, noteToMidi, valueToMidi } from '@strudel/core'; -import { aliasBank, registerSynthSounds, registerZZFXSounds, samples } from '@strudel/webaudio'; +import { aliasBank, registerSynthSounds, registerZZFXSounds, samples, tables } from '@strudel/webaudio'; import { registerSamplesFromDB } from './idbutils.mjs'; import './piano.mjs'; import './files.mjs'; @@ -32,6 +32,10 @@ export async function prebake() { prebake: true, tag: 'drum-machines', }), + tables(`${baseNoTrailing}/uzu-wavetables.json`, 2048, undefined, { + prebake: true, + tag: 'wavetables', + }), samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples( { diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 9c3d78146..9365a6db5 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -13,6 +13,7 @@ export const soundFilterType = { DRUMS: 'drums', SAMPLES: 'samples', SYNTHS: 'synths', + WAVETABLES: 'wavetables', ALL: 'all', }; From 4bd103e61200fe888196807b7c43a9ff206cb38c Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 27 Sep 2025 11:31:43 -0400 Subject: [PATCH 243/257] gain adj --- packages/superdough/worklets.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 14093d098..826e96104 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1049,7 +1049,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { { name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, { name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, { name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 }, - { name: 'detune', defaultValue: 0 }, + { name: 'detune', defaultValue: .18 }, { name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'warpMode', defaultValue: 0 }, From a9957b45e5ce101fe3142d9dbeb6112cacc188a8 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 27 Sep 2025 13:22:21 -0400 Subject: [PATCH 244/257] suppoert existing wt_ api --- packages/superdough/sampler.mjs | 2 +- website/public/uzu-wavetables.json | 22 ++++++++++++++++++++++ website/src/repl/prebake.mjs | 3 +++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 website/public/uzu-wavetables.json diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 0e9633d4d..ef4bdc542 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -272,7 +272,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option processSampleMap( sampleMap, (key, bank) => { - registerSample(key, bank, { baseUrl, prebake, tag }) + registerSampleSource(key, bank, { baseUrl, prebake, tag }) }, baseUrl, ); diff --git a/website/public/uzu-wavetables.json b/website/public/uzu-wavetables.json new file mode 100644 index 000000000..be8a4e16e --- /dev/null +++ b/website/public/uzu-wavetables.json @@ -0,0 +1,22 @@ +{ + "_base": "https://raw.githubusercontent.com/tidalcycles/uzu-wavetables/main/", + "wt_digital": [ + "wt_digital/wt_bad_day.wav", + "wt_digital/wt_basique.wav", + "wt_digital/wt_crickets.wav", + "wt_digital/wt_curses.wav", + "wt_digital/wt_earl_grey.wav", + "wt_digital/wt_echoes.wav", + "wt_digital/wt_glimmer.wav", + "wt_digital/wt_majick.wav", + "wt_digital/wt_meditation.wav", + "wt_digital/wt_morgana.wav", + "wt_digital/wt_red_alert.wav", + "wt_digital/wt_sad_piano.wav", + "wt_digital/wt_shook.wav", + "wt_digital/wt_sludge.wav", + "wt_digital/wt_squelch.wav", + "wt_digital/wt_summer.wav", + "wt_digital/wt_wasp.wav" + ] +} \ No newline at end of file diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index fac6f5bb6..0b552b871 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -32,6 +32,9 @@ export async function prebake() { prebake: true, tag: 'drum-machines', }), + samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, { + prebake: true, + }), samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples( { From a8918d55fb35cb66279c64f1721c7b8a5d0d693b Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 27 Sep 2025 10:45:30 -0700 Subject: [PATCH 245/257] Actually update normalizer --- packages/superdough/worklets.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 8c40c2763..dc0cf8077 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1261,7 +1261,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const gain2 = Math.sqrt(0.5 + 0.5 * spread); let f = pv(parameters.frequency, i); f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune - const normalizer = 0.3 / Math.sqrt(voices); + const normalizer = 0.3 / voices; for (let n = 0; n < voices; n++) { const isOdd = (n & 1) == 1; let gainL = gain1; From c5c05ebadc749824de9d77a9ad5640f519ac161f Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 27 Sep 2025 11:02:57 -0700 Subject: [PATCH 246/257] Clean up to align with other PR --- packages/core/controls.mjs | 8 +- packages/superdough/worklets.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 159 ---------------------- website/public/uzu-wavetables.json | 54 -------- website/src/repl/prebake.mjs | 4 - 5 files changed, 6 insertions(+), 221 deletions(-) delete mode 100644 website/public/uzu-wavetables.json diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 73e44473e..fef0e45e6 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -94,7 +94,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound'); * @param {number | Pattern} position Position in the wavetable from 0 to 1 * @synonyms wavetablePosition * @example - * s("squelch").seg(8).note("F1").wtPos("0 0.25 0.5 0.75 1") + * s("squelch").bank("wt_digital").seg(8).note("F1").wtPos("0 0.25 0.5 0.75 1") */ export const { wtPos, wavetablePosition } = registerControl('wtPos', 'wavetablePosition'); @@ -105,7 +105,7 @@ export const { wtPos, wavetablePosition } = registerControl('wtPos', 'wavetableP * @param {number | Pattern} amount Warp of the wavetable from 0 to 1 * @synonyms wavetableWarp * @example - * s("basique").seg(8).note("F1").wtWarp("0 0.25 0.5 0.75 1") + * s("basique").bank("wt_digital").seg(8).note("F1").wtWarp("0 0.25 0.5 0.75 1") * .wtWarpMode("spin") */ export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWarp'); @@ -120,7 +120,7 @@ export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWar * @param {number | string | Pattern} mode Warp mode * @synonyms wavetableWarpMode * @example - * s("morgana").seg(8).note("F1").wtWarp("0 0.25 0.5 0.75 1") + * s("morgana").bank("wt_digital").seg(8).note("F1").wtWarp("0 0.25 0.5 0.75 1") * .wtWarpMode("*2") * */ @@ -132,6 +132,8 @@ export const { wtWarpMode, wavetableWarpMode } = registerControl('wtWarpMode', ' * @name wtPhaseRand * @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random) * @synonyms wavetablePhaseRand + * @example + * s("basique").bank("wt_digital").seg(16).wtPhaseRand("<0 1>") * */ export const { wtPhaseRand, wavetablePhaseRand } = registerControl('wtPhaseRand', 'wavetablePhaseRand'); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index dc0cf8077..a83ad9e4f 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1054,7 +1054,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'warpMode', defaultValue: 0 }, { name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 }, - { name: 'spread', defaultValue: 0, minValue: 0, maxValue: 1 }, + { name: 'spread', defaultValue: 0.18, minValue: 0, maxValue: 1 }, { name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 }, ]; } diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index c2135e108..a05975580 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -11993,165 +11993,6 @@ exports[`runs examples > example "withValue" example index 0 1`] = ` ] `; -exports[`runs examples > example "wtPos" example index 0 1`] = ` -[ - "[ 0/1 → 1/8 | s:squelch note:F1 wtPos:0 ]", - "[ (1/8 → 1/5) ⇝ 1/4 | s:squelch note:F1 wtPos:0 ]", - "[ 1/8 ⇜ (1/5 → 1/4) | s:squelch note:F1 wtPos:0.25 ]", - "[ 1/4 → 3/8 | s:squelch note:F1 wtPos:0.25 ]", - "[ (3/8 → 2/5) ⇝ 1/2 | s:squelch note:F1 wtPos:0.25 ]", - "[ 3/8 ⇜ (2/5 → 1/2) | s:squelch note:F1 wtPos:0.5 ]", - "[ (1/2 → 3/5) ⇝ 5/8 | s:squelch note:F1 wtPos:0.5 ]", - "[ 1/2 ⇜ (3/5 → 5/8) | s:squelch note:F1 wtPos:0.75 ]", - "[ 5/8 → 3/4 | s:squelch note:F1 wtPos:0.75 ]", - "[ (3/4 → 4/5) ⇝ 7/8 | s:squelch note:F1 wtPos:0.75 ]", - "[ 3/4 ⇜ (4/5 → 7/8) | s:squelch note:F1 wtPos:1 ]", - "[ 7/8 → 1/1 | s:squelch note:F1 wtPos:1 ]", - "[ 1/1 → 9/8 | s:squelch note:F1 wtPos:0 ]", - "[ (9/8 → 6/5) ⇝ 5/4 | s:squelch note:F1 wtPos:0 ]", - "[ 9/8 ⇜ (6/5 → 5/4) | s:squelch note:F1 wtPos:0.25 ]", - "[ 5/4 → 11/8 | s:squelch note:F1 wtPos:0.25 ]", - "[ (11/8 → 7/5) ⇝ 3/2 | s:squelch note:F1 wtPos:0.25 ]", - "[ 11/8 ⇜ (7/5 → 3/2) | s:squelch note:F1 wtPos:0.5 ]", - "[ (3/2 → 8/5) ⇝ 13/8 | s:squelch note:F1 wtPos:0.5 ]", - "[ 3/2 ⇜ (8/5 → 13/8) | s:squelch note:F1 wtPos:0.75 ]", - "[ 13/8 → 7/4 | s:squelch note:F1 wtPos:0.75 ]", - "[ (7/4 → 9/5) ⇝ 15/8 | s:squelch note:F1 wtPos:0.75 ]", - "[ 7/4 ⇜ (9/5 → 15/8) | s:squelch note:F1 wtPos:1 ]", - "[ 15/8 → 2/1 | s:squelch note:F1 wtPos:1 ]", - "[ 2/1 → 17/8 | s:squelch note:F1 wtPos:0 ]", - "[ (17/8 → 11/5) ⇝ 9/4 | s:squelch note:F1 wtPos:0 ]", - "[ 17/8 ⇜ (11/5 → 9/4) | s:squelch note:F1 wtPos:0.25 ]", - "[ 9/4 → 19/8 | s:squelch note:F1 wtPos:0.25 ]", - "[ (19/8 → 12/5) ⇝ 5/2 | s:squelch note:F1 wtPos:0.25 ]", - "[ 19/8 ⇜ (12/5 → 5/2) | s:squelch note:F1 wtPos:0.5 ]", - "[ (5/2 → 13/5) ⇝ 21/8 | s:squelch note:F1 wtPos:0.5 ]", - "[ 5/2 ⇜ (13/5 → 21/8) | s:squelch note:F1 wtPos:0.75 ]", - "[ 21/8 → 11/4 | s:squelch note:F1 wtPos:0.75 ]", - "[ (11/4 → 14/5) ⇝ 23/8 | s:squelch note:F1 wtPos:0.75 ]", - "[ 11/4 ⇜ (14/5 → 23/8) | s:squelch note:F1 wtPos:1 ]", - "[ 23/8 → 3/1 | s:squelch note:F1 wtPos:1 ]", - "[ 3/1 → 25/8 | s:squelch note:F1 wtPos:0 ]", - "[ (25/8 → 16/5) ⇝ 13/4 | s:squelch note:F1 wtPos:0 ]", - "[ 25/8 ⇜ (16/5 → 13/4) | s:squelch note:F1 wtPos:0.25 ]", - "[ 13/4 → 27/8 | s:squelch note:F1 wtPos:0.25 ]", - "[ (27/8 → 17/5) ⇝ 7/2 | s:squelch note:F1 wtPos:0.25 ]", - "[ 27/8 ⇜ (17/5 → 7/2) | s:squelch note:F1 wtPos:0.5 ]", - "[ (7/2 → 18/5) ⇝ 29/8 | s:squelch note:F1 wtPos:0.5 ]", - "[ 7/2 ⇜ (18/5 → 29/8) | s:squelch note:F1 wtPos:0.75 ]", - "[ 29/8 → 15/4 | s:squelch note:F1 wtPos:0.75 ]", - "[ (15/4 → 19/5) ⇝ 31/8 | s:squelch note:F1 wtPos:0.75 ]", - "[ 15/4 ⇜ (19/5 → 31/8) | s:squelch note:F1 wtPos:1 ]", - "[ 31/8 → 4/1 | s:squelch note:F1 wtPos:1 ]", -] -`; - -exports[`runs examples > example "wtWarp" example index 0 1`] = ` -[ - "[ 0/1 → 1/8 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ (1/8 → 1/5) ⇝ 1/4 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ 1/8 ⇜ (1/5 → 1/4) | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 1/4 → 3/8 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ (3/8 → 2/5) ⇝ 1/2 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 3/8 ⇜ (2/5 → 1/2) | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ (1/2 → 3/5) ⇝ 5/8 | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ 1/2 ⇜ (3/5 → 5/8) | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 5/8 → 3/4 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ (3/4 → 4/5) ⇝ 7/8 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 3/4 ⇜ (4/5 → 7/8) | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", - "[ 7/8 → 1/1 | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", - "[ 1/1 → 9/8 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ (9/8 → 6/5) ⇝ 5/4 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ 9/8 ⇜ (6/5 → 5/4) | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 5/4 → 11/8 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ (11/8 → 7/5) ⇝ 3/2 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 11/8 ⇜ (7/5 → 3/2) | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ (3/2 → 8/5) ⇝ 13/8 | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ 3/2 ⇜ (8/5 → 13/8) | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 13/8 → 7/4 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ (7/4 → 9/5) ⇝ 15/8 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 7/4 ⇜ (9/5 → 15/8) | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", - "[ 15/8 → 2/1 | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", - "[ 2/1 → 17/8 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ (17/8 → 11/5) ⇝ 9/4 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ 17/8 ⇜ (11/5 → 9/4) | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 9/4 → 19/8 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ (19/8 → 12/5) ⇝ 5/2 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 19/8 ⇜ (12/5 → 5/2) | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ (5/2 → 13/5) ⇝ 21/8 | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ 5/2 ⇜ (13/5 → 21/8) | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 21/8 → 11/4 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ (11/4 → 14/5) ⇝ 23/8 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 11/4 ⇜ (14/5 → 23/8) | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", - "[ 23/8 → 3/1 | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", - "[ 3/1 → 25/8 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ (25/8 → 16/5) ⇝ 13/4 | s:basique note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ 25/8 ⇜ (16/5 → 13/4) | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 13/4 → 27/8 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ (27/8 → 17/5) ⇝ 7/2 | s:basique note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 27/8 ⇜ (17/5 → 7/2) | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ (7/2 → 18/5) ⇝ 29/8 | s:basique note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ 7/2 ⇜ (18/5 → 29/8) | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 29/8 → 15/4 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ (15/4 → 19/5) ⇝ 31/8 | s:basique note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 15/4 ⇜ (19/5 → 31/8) | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", - "[ 31/8 → 4/1 | s:basique note:F1 wtWarp:1 wtWarpMode:spin ]", -] -`; - -exports[`runs examples > example "wtWarpMode" example index 0 1`] = ` -[ - "[ 0/1 → 1/8 | s:morgana note:F1 wtWarp:0 wtWarpMode:asym ]", - "[ (1/8 → 1/5) ⇝ 1/4 | s:morgana note:F1 wtWarp:0 wtWarpMode:asym ]", - "[ 1/8 ⇜ (1/5 → 1/4) | s:morgana note:F1 wtWarp:0.25 wtWarpMode:asym ]", - "[ 1/4 → 3/8 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:asym ]", - "[ (3/8 → 2/5) ⇝ 1/2 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:asym ]", - "[ 3/8 ⇜ (2/5 → 1/2) | s:morgana note:F1 wtWarp:0.5 wtWarpMode:asym ]", - "[ (1/2 → 3/5) ⇝ 5/8 | s:morgana note:F1 wtWarp:0.5 wtWarpMode:bendp ]", - "[ 1/2 ⇜ (3/5 → 5/8) | s:morgana note:F1 wtWarp:0.75 wtWarpMode:bendp ]", - "[ 5/8 → 3/4 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:bendp ]", - "[ (3/4 → 4/5) ⇝ 7/8 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:bendp ]", - "[ 3/4 ⇜ (4/5 → 7/8) | s:morgana note:F1 wtWarp:1 wtWarpMode:bendp ]", - "[ 7/8 → 1/1 | s:morgana note:F1 wtWarp:1 wtWarpMode:bendp ]", - "[ 1/1 → 9/8 | s:morgana note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ (9/8 → 6/5) ⇝ 5/4 | s:morgana note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ 9/8 ⇜ (6/5 → 5/4) | s:morgana note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 5/4 → 11/8 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ (11/8 → 7/5) ⇝ 3/2 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 11/8 ⇜ (7/5 → 3/2) | s:morgana note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ (3/2 → 8/5) ⇝ 13/8 | s:morgana note:F1 wtWarp:0.5 wtWarpMode:logistic ]", - "[ 3/2 ⇜ (8/5 → 13/8) | s:morgana note:F1 wtWarp:0.75 wtWarpMode:logistic ]", - "[ 13/8 → 7/4 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:logistic ]", - "[ (7/4 → 9/5) ⇝ 15/8 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:logistic ]", - "[ 7/4 ⇜ (9/5 → 15/8) | s:morgana note:F1 wtWarp:1 wtWarpMode:logistic ]", - "[ 15/8 → 2/1 | s:morgana note:F1 wtWarp:1 wtWarpMode:logistic ]", - "[ 2/1 → 17/8 | s:morgana note:F1 wtWarp:0 wtWarpMode:sync ]", - "[ (17/8 → 11/5) ⇝ 9/4 | s:morgana note:F1 wtWarp:0 wtWarpMode:sync ]", - "[ 17/8 ⇜ (11/5 → 9/4) | s:morgana note:F1 wtWarp:0.25 wtWarpMode:sync ]", - "[ 9/4 → 19/8 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:sync ]", - "[ (19/8 → 12/5) ⇝ 5/2 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:sync ]", - "[ 19/8 ⇜ (12/5 → 5/2) | s:morgana note:F1 wtWarp:0.5 wtWarpMode:sync ]", - "[ (5/2 → 13/5) ⇝ 21/8 | s:morgana note:F1 wtWarp:0.5 wtWarpMode:wormhole ]", - "[ 5/2 ⇜ (13/5 → 21/8) | s:morgana note:F1 wtWarp:0.75 wtWarpMode:wormhole ]", - "[ 21/8 → 11/4 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:wormhole ]", - "[ (11/4 → 14/5) ⇝ 23/8 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:wormhole ]", - "[ 11/4 ⇜ (14/5 → 23/8) | s:morgana note:F1 wtWarp:1 wtWarpMode:wormhole ]", - "[ 23/8 → 3/1 | s:morgana note:F1 wtWarp:1 wtWarpMode:wormhole ]", - "[ 3/1 → 25/8 | s:morgana note:F1 wtWarp:0 wtWarpMode:brownian ]", - "[ (25/8 → 16/5) ⇝ 13/4 | s:morgana note:F1 wtWarp:0 wtWarpMode:brownian ]", - "[ 25/8 ⇜ (16/5 → 13/4) | s:morgana note:F1 wtWarp:0.25 wtWarpMode:brownian ]", - "[ 13/4 → 27/8 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:brownian ]", - "[ (27/8 → 17/5) ⇝ 7/2 | s:morgana note:F1 wtWarp:0.25 wtWarpMode:brownian ]", - "[ 27/8 ⇜ (17/5 → 7/2) | s:morgana note:F1 wtWarp:0.5 wtWarpMode:brownian ]", - "[ (7/2 → 18/5) ⇝ 29/8 | s:morgana note:F1 wtWarp:0.5 wtWarpMode:asym ]", - "[ 7/2 ⇜ (18/5 → 29/8) | s:morgana note:F1 wtWarp:0.75 wtWarpMode:asym ]", - "[ 29/8 → 15/4 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:asym ]", - "[ (15/4 → 19/5) ⇝ 31/8 | s:morgana note:F1 wtWarp:0.75 wtWarpMode:asym ]", - "[ 15/4 ⇜ (19/5 → 31/8) | s:morgana note:F1 wtWarp:1 wtWarpMode:asym ]", - "[ 31/8 → 4/1 | s:morgana note:F1 wtWarp:1 wtWarpMode:asym ]", -] -`; - exports[`runs examples > example "xfade" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh gain:0 ]", diff --git a/website/public/uzu-wavetables.json b/website/public/uzu-wavetables.json deleted file mode 100644 index 4497791f0..000000000 --- a/website/public/uzu-wavetables.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "_base": "http://localhost:5432", - "Bad Day": [ - "/Bad Day.wav" - ], - "Basique": [ - "/Basique.wav" - ], - "Crickets": [ - "/Crickets.wav" - ], - "Curses": [ - "/Curses.wav" - ], - "Earl Grey": [ - "/Earl Grey.wav" - ], - "Echoes": [ - "/Echoes.wav" - ], - "Glimmer": [ - "/Glimmer.wav" - ], - "Majick": [ - "/Majick.wav" - ], - "Meditation": [ - "/Meditation.wav" - ], - "Morgana": [ - "/Morgana.wav" - ], - "Red Alert": [ - "/Red Alert.wav" - ], - "Sad Piano": [ - "/Sad Piano.wav" - ], - "Shook": [ - "/Shook.wav" - ], - "Sludge": [ - "/Sludge.wav" - ], - "Squelch": [ - "/Squelch.wav" - ], - "Summers Day": [ - "/Summers Day.wav" - ], - "Wasp": [ - "/Wasp.wav" - ] -} \ No newline at end of file diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 855798fdd..79f30bfe0 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -32,10 +32,6 @@ export async function prebake() { prebake: true, tag: 'drum-machines', }), - tables(`${baseNoTrailing}/uzu-wavetables.json`, 2048, undefined, { - prebake: true, - tag: 'wavetables', - }), samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples( { From 8c3ee6db6bb042a2cf234aeea5cd3501fc2eb7e5 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 27 Sep 2025 11:07:04 -0700 Subject: [PATCH 247/257] Missed deletion --- website/src/repl/prebake.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 79f30bfe0..fac6f5bb6 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -1,5 +1,5 @@ import { Pattern, noteToMidi, valueToMidi } from '@strudel/core'; -import { aliasBank, registerSynthSounds, registerZZFXSounds, samples, tables } from '@strudel/webaudio'; +import { aliasBank, registerSynthSounds, registerZZFXSounds, samples } from '@strudel/webaudio'; import { registerSamplesFromDB } from './idbutils.mjs'; import './piano.mjs'; import './files.mjs'; From 626a99ba5bbaa7032bf5ef62fe2c2d9849ed911a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 27 Sep 2025 15:22:54 -0400 Subject: [PATCH 248/257] supports old documented way of loading wavetables: --- packages/superdough/sampler.mjs | 33 +++++-------------------------- packages/superdough/util.mjs | 29 ++++++++++++++++++++++++++- packages/superdough/wavetable.mjs | 20 +++++++------------ 3 files changed, 40 insertions(+), 42 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index ef4bdc542..02e1fb334 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,4 +1,4 @@ -import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs'; +import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs'; import { getAudioContext, registerSound, registerWaveTable } from './index.mjs'; import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -22,39 +22,16 @@ function humanFileSize(bytes, si) { return bytes.toFixed(1) + ' ' + units[u]; } -// deduces relevant info for sample loading from hap.value and sample definition -// it encapsulates the core sampler logic into a pure and synchronous function -// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format) export function getSampleInfo(hapValue, bank) { - const { s, n = 0, speed = 1.0 } = hapValue; - let midi = valueToMidi(hapValue, 36); - let transpose = midi - 36; // C3 is middle C; - let sampleUrl; - let index = 0; - if (Array.isArray(bank)) { - index = getSoundIndex(n, bank.length); - sampleUrl = bank[index]; - } else { - const midiDiff = (noteA) => noteToMidi(noteA) - midi; - // object format will expect keys as notes - const closest = Object.keys(bank) - .filter((k) => !k.startsWith('_')) - .reduce( - (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), - null, - ); - transpose = -midiDiff(closest); // semitones to repitch - index = getSoundIndex(n, bank[closest].length); - sampleUrl = bank[closest][index]; - } - const label = `${s}:${index}`; + const { speed = 1.0 } = hapValue; + const {transpose, url, index, midi, label} = getCommonSampleInfo(hapValue, bank) let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12); - return { transpose, sampleUrl, index, midi, label, playbackRate }; + return { transpose, url, index, midi, label, playbackRate }; } // takes hapValue and returns buffer + playbackRate. export const getSampleBuffer = async (hapValue, bank, resolveUrl) => { - let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank); + let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank); if (resolveUrl) { sampleUrl = await resolveUrl(sampleUrl); } diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 80dd31a9d..1886c55e5 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -76,4 +76,31 @@ export function cycleToSeconds(cycle, cps) { export function secondsToCycle(t, cps) { return t * cps; } - +// deduces relevant info for sample loading from hap.value and sample definition +// it encapsulates the core sampler logic into a pure and synchronous function +// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format) +export function getCommonSampleInfo(hapValue, bank) { + const { s, n = 0 } = hapValue; + let midi = valueToMidi(hapValue, 36); + let transpose = midi - 36; // C3 is middle C; + let url; + let index = 0; + if (Array.isArray(bank)) { + index = getSoundIndex(n, bank.length); + url = bank[index]; + } else { + const midiDiff = (noteA) => noteToMidi(noteA) - midi; + // object format will expect keys as notes + const closest = Object.keys(bank) + .filter((k) => !k.startsWith('_')) + .reduce( + (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), + null, + ); + transpose = -midiDiff(closest); // semitones to repitch + index = getSoundIndex(n, bank[closest].length); + url = bank[closest][index]; + } + const label = `${s}:${index}`; + return { transpose, url, index, midi, label }; +} \ No newline at end of file diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 067656045..a7e708043 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -1,5 +1,5 @@ import { getAudioContext, registerSound } from './index.mjs'; -import { getSoundIndex, valueToMidi } from './util.mjs'; +import { getCommonSampleInfo, getSoundIndex, valueToMidi } from './util.mjs'; import { destroyAudioWorkletNode, getADSRValues, @@ -38,12 +38,12 @@ export const WarpMode = Object.freeze({ FLIP: 21, }); -async function loadWavetableFrames(url, label, frameLen = 256) { +async function loadWavetableFrames(url, label, frameLen = 2048) { const ac = getAudioContext(); const buf = await loadBuffer(url, ac, label); const ch0 = buf.getChannelData(0); const total = ch0.length; - const numFrames = Math.floor(total / frameLen); + const numFrames = Math.max(1,Math.floor(total / frameLen)); const frames = new Array(numFrames); for (let i = 0; i < numFrames; i++) { const start = i * frameLen; @@ -86,14 +86,8 @@ function humanFileSize(bytes, si) { return bytes.toFixed(1) + ' ' + units[u]; } -export function getTableInfo(hapValue, tableUrls) { - const { s, n = 0 } = hapValue; - let midi = valueToMidi(hapValue, 36); - let transpose = midi - 36; // C3 is middle C; - const index = getSoundIndex(n, tableUrls.length); - const tableUrl = tableUrls[index]; - const label = `${s}:${index}`; - return { transpose, tableUrl, index, midi, label }; +export function getTableInfo(hapValue, urls) { + return getCommonSampleInfo(hapValue,urls) } const loadBuffer = (url, ac, label) => { @@ -191,8 +185,8 @@ export async function onTriggerSynth(t, value, onended, bank, frameLen) { wtWarpMode = WarpMode[wtWarpMode.toUpperCase()] ?? WarpMode.NONE; } const frequency = getFrequencyFromValue(value); - const { tableUrl, label } = getTableInfo(value, bank); - const payload = await loadWavetableFrames(tableUrl, label, frameLen); + const { url, label } = getTableInfo(value, bank); + const payload = await loadWavetableFrames(url, label, frameLen); const holdEnd = t + duration; const envEnd = holdEnd + release + 0.01; const source = getWorklet( From 774372a339d929d8826187efc90afe5ae7e5e337 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 27 Sep 2025 15:29:55 -0400 Subject: [PATCH 249/257] format --- packages/superdough/sampler.mjs | 19 ++++++++----------- packages/superdough/util.mjs | 4 ++-- packages/superdough/wavetable.mjs | 10 +++++----- packages/superdough/worklets.mjs | 4 ++-- website/src/repl/idbutils.mjs | 3 +-- website/src/repl/prebake.mjs | 2 +- 6 files changed, 19 insertions(+), 23 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 02e1fb334..b195ba79c 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -23,8 +23,8 @@ function humanFileSize(bytes, si) { } export function getSampleInfo(hapValue, bank) { - const { speed = 1.0 } = hapValue; - const {transpose, url, index, midi, label} = getCommonSampleInfo(hapValue, bank) + const { speed = 1.0 } = hapValue; + const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank); let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12); return { transpose, url, index, midi, label, playbackRate }; } @@ -245,11 +245,10 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option } const { prebake, tag } = options; - processSampleMap( sampleMap, (key, bank) => { - registerSampleSource(key, bank, { baseUrl, prebake, tag }) + registerSampleSource(key, bank, { baseUrl, prebake, tag }); }, baseUrl, ); @@ -341,21 +340,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { return handle; } - function registerSample(key, bank, params) { registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { type: 'sample', samples: bank, - ...params - }) + ...params, + }); } export function registerSampleSource(key, bank, params) { const isWavetable = key.startsWith('wt_'); if (isWavetable) { - registerWaveTable(key,bank, params) + registerWaveTable(key, bank, params); } else { - registerSample(key, bank, params) + registerSample(key, bank, params); } - -} \ No newline at end of file +} diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 1886c55e5..0ba095175 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -100,7 +100,7 @@ export function getCommonSampleInfo(hapValue, bank) { transpose = -midiDiff(closest); // semitones to repitch index = getSoundIndex(n, bank[closest].length); url = bank[closest][index]; - } + } const label = `${s}:${index}`; return { transpose, url, index, midi, label }; -} \ No newline at end of file +} diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index a7e708043..c54f3a888 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -43,7 +43,7 @@ async function loadWavetableFrames(url, label, frameLen = 2048) { const buf = await loadBuffer(url, ac, label); const ch0 = buf.getChannelData(0); const total = ch0.length; - const numFrames = Math.max(1,Math.floor(total / frameLen)); + const numFrames = Math.max(1, Math.floor(total / frameLen)); const frames = new Array(numFrames); for (let i = 0; i < numFrames; i++) { const start = i * frameLen; @@ -87,7 +87,7 @@ function humanFileSize(bytes, si) { } export function getTableInfo(hapValue, urls) { - return getCommonSampleInfo(hapValue,urls) + return getCommonSampleInfo(hapValue, urls); } const loadBuffer = (url, ac, label) => { @@ -134,15 +134,15 @@ const _processTables = (json, baseUrl, frameLen) => { baseUrl = githubPath(baseUrl, ''); } value = value.map((v) => baseUrl + v); - registerWaveTable(key,value, {baseUrl, frameLen}) + registerWaveTable(key, value, { baseUrl, frameLen }); }); }; -export function registerWaveTable(key, bank, params) { +export function registerWaveTable(key, bank, params) { registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, bank, params?.frameLen ?? 2048), { type: 'wavetable', tables: bank, - ...params + ...params, }); } diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 826e96104..91eb68634 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1049,7 +1049,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { { name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, { name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, { name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 }, - { name: 'detune', defaultValue: .18 }, + { name: 'detune', defaultValue: 0.18 }, { name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'warpMode', defaultValue: 0 }, @@ -1239,7 +1239,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } const outL = outputs[0][0]; const outR = outputs[0][1] || outputs[0][0]; - const gainAdjustment = .15; + const gainAdjustment = 0.3; if (!this.tables) { outL.fill(0); diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index 5ac604559..d87d649c2 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -76,8 +76,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = }) .map((title) => titlePathMap.get(title)); - registerSampleSource(key,value, {prebake: false}) - + registerSampleSource(key, value, { prebake: false }); }); logger('imported sounds registered!', 'success'); diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 0b552b871..1fbc84021 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -33,7 +33,7 @@ export async function prebake() { tag: 'drum-machines', }), samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, { - prebake: true, + prebake: true, }), samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples( From 917d01c47d9ab245fb09ce121af657a184c52c1f Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 27 Sep 2025 12:48:37 -0700 Subject: [PATCH 250/257] Add LFOs and envelopes --- packages/core/controls.mjs | 172 ++++++++++++++++++++++++++++- packages/superdough/helpers.mjs | 30 +++++ packages/superdough/superdough.mjs | 39 ++----- packages/superdough/synth.mjs | 3 +- packages/superdough/wavetable.mjs | 37 ++++++- 5 files changed, 247 insertions(+), 34 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index fef0e45e6..23a1dfc3b 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -98,6 +98,90 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound'); */ export const { wtPos, wavetablePosition } = registerControl('wtPos', 'wavetablePosition'); +/** + * Attack time of the wavetable oscillator's position envelope + * + * @name wtPosAttack + * @synonyms wtPosAtt + * @param {number | Pattern} time attack time in seconds + */ +export const { wtPosAttack, wtPosAtt } = registerControl('wtPosAttack', 'wtPosAtt'); + +/** + * Decay time of the wavetable oscillator's position envelope + * + * @name wtPosDecay + * @synonyms wtPosDec + * @param {number | Pattern} time decay time in seconds + */ +export const { wtPosDecay, wtPosDec } = registerControl('wtPosDecay', 'wtPosDec'); + +/** + * Sustain time of the wavetable oscillator's position envelope + * + * @name wtPosAttack + * @synonyms wtPosSus + * @param {number | Pattern} gain sustain level (0 to 1) + */ +export const { wtPosSustain, wtPosSus } = registerControl('wtPosSustain', 'wtPosSus'); + +/** + * Release time of the wavetable oscillator's position envelope + * + * @name wtPosRelease + * @synonyms wtPosRel + * @param {number | Pattern} time release time in seconds + */ +export const { wtPosRelease, wtPosRel } = registerControl('wtPosRelease', 'wtPosRel'); + +/** + * Rate of the LFO for the wavetable oscillator's position + * + * @name wtPosRate + * @param {number | Pattern} rate rate in hertz + */ +export const { wtPosRate } = registerControl('wtPosRate'); + +/** + * Depth of the LFO for the wavetable oscillator's position + * + * @name wtPosDepth + * @param {number | Pattern} depth depth of modulation + */ +export const { wtPosDepth } = registerControl('wtPosDepth'); + +/** + * Whether to sync the LFO for the wavetable oscillator's position + * + * @name wtPosSynced + * @param {number | Pattern} synced Whether to sync the lfo to CPM. > 0.5 will be synced + */ +export const { wtPosSynced } = registerControl('wtPosSynced'); + +/** + * Shape of the LFO for the wavetable oscillator's position + * + * @name wtPosShape + * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) + */ +export const { wtPosShape } = registerControl('wtPosShape'); + +/** + * DC offset of the LFO for the wavetable oscillator's position + * + * @name wtPosDCOffset + * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar + */ +export const { wtPosDCOffset } = registerControl('wtPosDCOffset'); + +/** + * Skew of the LFO for the wavetable oscillator's position + * + * @name wtPosSkew + * @param {number | Pattern} skew How much to bend the LFO shape + */ +export const { wtPosSkew } = registerControl('wtPosSkew'); + /** * Amount of warp (alteration of the waveform) to apply to the wavetable oscillator * @@ -108,10 +192,94 @@ export const { wtPos, wavetablePosition } = registerControl('wtPos', 'wavetableP * s("basique").bank("wt_digital").seg(8).note("F1").wtWarp("0 0.25 0.5 0.75 1") * .wtWarpMode("spin") */ -export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWarp'); +export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWarp') /** - * Amount of warp (alteration of the waveform) to apply to the wavetable oscillator. + * Attack time of the wavetable oscillator's warp envelope + * + * @name wtWarpAttack + * @synonyms wtWarpAtt + * @param {number | Pattern} time attack time in seconds + */ +export const { wtWarpAttack, wtWarpAtt } = registerControl('wtWarpAttack', 'wtWarpAtt'); + +/** + * Decay time of the wavetable oscillator's warp envelope + * + * @name wtWarpDecay + * @synonyms wtWarpDec + * @param {number | Pattern} time decay time in seconds + */ +export const { wtWarpDecay, wtWarpDec } = registerControl('wtWarpDecay', 'wtWarpDec'); + +/** + * Sustain time of the wavetable oscillator's warp envelope + * + * @name wtWarpAttack + * @synonyms wtWarpSus + * @param {number | Pattern} gain sustain level (0 to 1) + */ +export const { wtWarpSustain, wtWarpSus } = registerControl('wtWarpSustain', 'wtWarpSus'); + +/** + * Release time of the wavetable oscillator's warp envelope + * + * @name wtWarpRelease + * @synonyms wtWarpRel + * @param {number | Pattern} time release time in seconds + */ +export const { wtWarpRelease, wtWarpRel } = registerControl('wtWarpRelease', 'wtWarpRel'); + +/** + * Rate of the LFO for the wavetable oscillator's warp + * + * @name wtWarpRate + * @param {number | Pattern} rate rate in hertz + */ +export const { wtWarpRate } = registerControl('wtWarpRate'); + +/** + * Depth of the LFO for the wavetable oscillator's warp + * + * @name wtWarpDepth + * @param {number | Pattern} depth depth of modulation + */ +export const { wtWarpDepth } = registerControl('wtWarpDepth'); + +/** + * Whether to sync the LFO for the wavetable oscillator's warp + * + * @name wtWarpSynced + * @param {number | Pattern} synced Whether to sync the lfo to CPM. > 0.5 will be synced + */ +export const { wtWarpSynced } = registerControl('wtWarpSynced'); + +/** + * Shape of the LFO for the wavetable oscillator's warp + * + * @name wtWarpShape + * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) + */ +export const { wtWarpShape } = registerControl('wtWarpShape'); + +/** + * DC offset of the LFO for the wavetable oscillator's warp + * + * @name wtWarpDCOffset + * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar + */ +export const { wtWarpDCOffset } = registerControl('wtWarpDCOffset'); + +/** + * Skew of the LFO for the wavetable oscillator's warp + * + * @name wtWarpSkew + * @param {number | Pattern} skew How much to bend the LFO shape + */ +export const { wtWarpSkew } = registerControl('wtWarpSkew'); + +/** + * Type of warp (alteration of the waveform) to apply to the wavetable oscillator. * * The current options are: none, asym, bendp, bendm, bendmp, sync, quant, fold, pwm, orbit, * spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 4921d873e..d3b6ca6c7 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -97,6 +97,36 @@ export const getParamADSR = ( param[ramp](min, end + release); }; +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 function getLfo(audioContext, begin, end, properties = {}) { + const { shape = 0, ...props } = properties; + const { dcoffset = -0.5, depth = 1 } = properties; + debugger; + const lfoprops = { + frequency: 1, + depth, + skew: 0.5, + phaseoffset: 0, + time: begin, + begin, + end, + shape: getModulationShapeInput(shape), + dcoffset, + min: dcoffset * depth, + max: dcoffset * depth + depth, + curve: 1, + ...props, + }; + + return getWorklet(audioContext, 'lfo-processor', lfoprops); +} + export function getCompressor(ac, threshold, ratio, knee, attack, release) { const options = { threshold: threshold ?? -3, diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 63ed7d30b..48264ec05 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -9,7 +9,7 @@ import './reverb.mjs'; import './vowel.mjs'; import { nanFallback, _mod, cycleToSeconds } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; -import { createFilter, gainNode, getCompressor, getWorklet, effectSend } from './helpers.mjs'; +import { createFilter, gainNode, getCompressor, getLfo, getWorklet, effectSend } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; @@ -29,13 +29,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 = {}) { @@ -314,28 +307,6 @@ export function connectToDestination(input, channels) { controller.output.connectToDestination(input, channels); } -export function getLfo(audioContext, begin, end, properties = {}) { - const { shape = 0, ...props } = properties; - const { dcoffset = -0.5, depth = 1 } = properties; - const lfoprops = { - frequency: 1, - depth, - skew: 0.5, - phaseoffset: 0, - time: begin, - begin, - end, - shape: getModulationShapeInput(shape), - dcoffset, - min: dcoffset * depth, - max: dcoffset * depth + depth, - curve: 1, - ...props, - }; - - return getWorklet(audioContext, 'lfo-processor', lfoprops); -} - 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 }); @@ -682,6 +653,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) tremolo = cps * tremolosync; } + if (value.wtPosSynced != null) { + value.wtPosRate /= cps; + } + + if (value.wtWarpSynced != null) { + value.wtWarpRate /= cps; + } + 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 diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 5b1b4edf1..5dc5dc08a 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,11 +1,12 @@ import { clamp } from './util.mjs'; -import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs'; +import { registerSound, getAudioContext, soundMap } from './superdough.mjs'; import { applyFM, destroyAudioWorkletNode, gainNode, getADSRValues, getFrequencyFromValue, + getLfo, getParamADSR, getPitchEnvelope, getVibratoOscillator, diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 14706f925..dede7a2e5 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -4,6 +4,7 @@ import { destroyAudioWorkletNode, getADSRValues, getFrequencyFromValue, + getLfo, getParamADSR, getPitchEnvelope, getVibratoOscillator, @@ -235,7 +236,8 @@ async function onTriggerSynth(t, value, onended, tables, frameLen) { const { tableUrl, label } = getTableInfo(value, tables); const payload = await loadWavetableFrames(tableUrl, label, frameLen); const holdEnd = t + duration; - const envEnd = holdEnd + release + 0.01; + const endWithRelease = holdEnd + release; + const envEnd = endWithRelease + 0.01; const source = getWorklet( ac, 'wavetable-oscillator-processor', @@ -258,6 +260,37 @@ async function onTriggerSynth(t, value, onended, tables, frameLen) { logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight'); return; } + const posADSRParams = [value.wtPosAttack, value.wtPosDecay, value.wtPosSustain, value.wtPosRelease]; + const warpADSRParams = [value.wtPosAttack, value.wtPosDecay, value.wtPosSustain, value.wtPosRelease]; + const wtParams = source.parameters; + const positionParam = wtParams.get('position'); + const warpParam = wtParams.get('warp'); + if (posADSRParams.some((p) => p !== undefined)) { + const [pAttack, pDecay, pSustain, pRelease] = getADSRValues(posADSRParams); + getParamADSR(positionParam, pAttack, pDecay, pSustain, pRelease, 0, 1, t, holdEnd, 'linear'); + } else { + const posLFO = getLfo(ac, t, endWithRelease, { + frequency: value.wtPosRate, + depth: value.wtPosDepth, + shape: value.wtPosShape, + skew: value.wtPosSkew, + dcoffset: value.wtPosDCOffset ?? 0, + }); + posLFO.connect(positionParam); + } + if (posADSRParams.some((p) => p !== undefined)) { + const [wAttack, wDecay, wSustain, wRelease] = getADSRValues(warpADSRParams); + getParamADSR(warpParam, wAttack, wDecay, wSustain, wRelease, 0, 1, t, holdEnd, 'linear'); + } else { + const warpLFO = getLfo(ac, t, endWithRelease, { + frequency: value.wtWarpRate, + depth: value.wtWarpDepth, + shape: value.wtWarpShape, + skew: value.wtWarpSkew, + dcoffset: value.wtWarpDCOffset ?? 0, + }); + warpLFO.connect(warpParam); + } const vibratoOscillator = getVibratoOscillator(source.detune, value, t); const envGain = ac.createGain(); const node = source.connect(envGain); @@ -271,6 +304,8 @@ async function onTriggerSynth(t, value, onended, tables, frameLen) { destroyAudioWorkletNode(source); vibratoOscillator?.stop(); node.disconnect(); + warpLFO.disconnect(); + posLFO.disconnect(); onended(); }, t, From 23d4bfa9d9ec9b2f3bc7dd6469da113dfc794629 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 28 Sep 2025 09:19:39 -0400 Subject: [PATCH 251/257] rename controls --- packages/core/controls.mjs | 162 ++++++++++++++++------------- packages/superdough/superdough.mjs | 2 +- packages/superdough/wavetable.mjs | 88 ++++++++++------ 3 files changed, 144 insertions(+), 108 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index b58465965..7e3c2a8e2 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -90,193 +90,191 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound'); /** * Position in the wavetable of the wavetable oscillator * - * @name wtPos + * @name wt * @param {number | Pattern} position Position in the wavetable from 0 to 1 * @synonyms wavetablePosition * @example - * s("squelch").bank("wt_digital").seg(8).note("F1").wtPos("0 0.25 0.5 0.75 1") + * s("squelch").bank("wt_digital").seg(8).note("F1").wt("0 0.25 0.5 0.75 1") */ -export const { wtPos, wavetablePosition } = registerControl('wtPos', 'wavetablePosition'); +export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePosition'); +/** + * Amount of envelope applied wavetable oscillator's position envelope + * + * @name wtenv + * @param {number | Pattern} amount between 0 and 1 + */ +export const { wtenv } = registerControl('wtenv'); /** * Attack time of the wavetable oscillator's position envelope * - * @name wtPosAttack - * @synonyms wtPosAtt + * @name wtattack + * @synonyms wtatt * @param {number | Pattern} time attack time in seconds */ -export const { wtPosAttack, wtPosAtt } = registerControl('wtPosAttack', 'wtPosAtt'); +export const { wtattack, wtatt } = registerControl('wtattack', 'wtatt'); /** * Decay time of the wavetable oscillator's position envelope * - * @name wtPosDecay - * @synonyms wtPosDec + * @name wtdecay + * @synonyms wtdec * @param {number | Pattern} time decay time in seconds */ -export const { wtPosDecay, wtPosDec } = registerControl('wtPosDecay', 'wtPosDec'); +export const { wtdecay, wtdec } = registerControl('wtdecay', 'wtdec'); /** * Sustain time of the wavetable oscillator's position envelope * - * @name wtPosAttack - * @synonyms wtPosSus + * @name wtsustain + * @synonyms wtsus * @param {number | Pattern} gain sustain level (0 to 1) */ -export const { wtPosSustain, wtPosSus } = registerControl('wtPosSustain', 'wtPosSus'); +export const { wtsustain, wtsus } = registerControl('wtsustain', 'wtsus'); /** * Release time of the wavetable oscillator's position envelope * - * @name wtPosRelease - * @synonyms wtPosRel + * @name wtrelease + * @synonyms wtrel * @param {number | Pattern} time release time in seconds */ -export const { wtPosRelease, wtPosRel } = registerControl('wtPosRelease', 'wtPosRel'); +export const { wtrelease, wtrel } = registerControl('wtrelease', 'wtrel'); /** * Rate of the LFO for the wavetable oscillator's position * - * @name wtPosRate + * @name wtrate * @param {number | Pattern} rate rate in hertz */ -export const { wtPosRate } = registerControl('wtPosRate'); +export const { wtrate } = registerControl('wtrate'); +/** + * cycle synced rate of the LFO for the wavetable oscillator's position + * + * @name wtsync + * @param {number | Pattern} rate rate in cycles + */ +export const { wtsync } = registerControl('wtsync'); /** * Depth of the LFO for the wavetable oscillator's position * - * @name wtPosDepth + * @name wtdepth * @param {number | Pattern} depth depth of modulation */ -export const { wtPosDepth } = registerControl('wtPosDepth'); - -/** - * Whether to sync the LFO for the wavetable oscillator's position - * - * @name wtPosSynced - * @param {number | Pattern} synced Whether to sync the lfo to CPM. > 0.5 will be synced - */ -export const { wtPosSynced } = registerControl('wtPosSynced'); +export const { wtdepth } = registerControl('wtdepth'); /** * Shape of the LFO for the wavetable oscillator's position * - * @name wtPosShape + * @name wtshape * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) */ -export const { wtPosShape } = registerControl('wtPosShape'); +export const { wtshape } = registerControl('wtshape'); /** * DC offset of the LFO for the wavetable oscillator's position * - * @name wtPosDCOffset + * @name wtdc * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar */ -export const { wtPosDCOffset } = registerControl('wtPosDCOffset'); +export const { wtdc } = registerControl('wtdc'); /** * Skew of the LFO for the wavetable oscillator's position * - * @name wtPosSkew + * @name wtskew * @param {number | Pattern} skew How much to bend the LFO shape */ -export const { wtPosSkew } = registerControl('wtPosSkew'); +export const { wtskew } = registerControl('wtskew'); /** * Amount of warp (alteration of the waveform) to apply to the wavetable oscillator * - * @name wtWarp + * @name warp * @param {number | Pattern} amount Warp of the wavetable from 0 to 1 * @synonyms wavetableWarp * @example - * s("basique").bank("wt_digital").seg(8).note("F1").wtWarp("0 0.25 0.5 0.75 1") - * .wtWarpMode("spin") + * s("basique").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1") + * .warpmode("spin") */ -export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWarp'); +export const { warp, wavetableWarp } = registerControl('warp', 'wavetableWarp'); /** * Attack time of the wavetable oscillator's warp envelope * - * @name wtWarpAttack - * @synonyms wtWarpAtt + * @name warpattack + * @synonyms warpatt * @param {number | Pattern} time attack time in seconds */ -export const { wtWarpAttack, wtWarpAtt } = registerControl('wtWarpAttack', 'wtWarpAtt'); +export const { warpattack, warpatt } = registerControl('warpattack', 'warpatt'); /** * Decay time of the wavetable oscillator's warp envelope * - * @name wtWarpDecay - * @synonyms wtWarpDec + * @name warpdecay + * @synonyms warpdec * @param {number | Pattern} time decay time in seconds */ -export const { wtWarpDecay, wtWarpDec } = registerControl('wtWarpDecay', 'wtWarpDec'); +export const { warpdecay, warpdec } = registerControl('warpdecay', 'warpdec'); /** * Sustain time of the wavetable oscillator's warp envelope * - * @name wtWarpAttack - * @synonyms wtWarpSus + * @name warpsustain + * @synonyms warpsus * @param {number | Pattern} gain sustain level (0 to 1) */ -export const { wtWarpSustain, wtWarpSus } = registerControl('wtWarpSustain', 'wtWarpSus'); +export const { warpsustain, warpsus } = registerControl('warpsustain', 'warpsus'); /** * Release time of the wavetable oscillator's warp envelope * - * @name wtWarpRelease - * @synonyms wtWarpRel + * @name warprelease + * @synonyms warprel * @param {number | Pattern} time release time in seconds */ -export const { wtWarpRelease, wtWarpRel } = registerControl('wtWarpRelease', 'wtWarpRel'); +export const { warprelease, warprel } = registerControl('warprelease', 'warprel'); /** * Rate of the LFO for the wavetable oscillator's warp * - * @name wtWarpRate + * @name warprate * @param {number | Pattern} rate rate in hertz */ -export const { wtWarpRate } = registerControl('wtWarpRate'); +export const { warprate } = registerControl('warprate'); /** * Depth of the LFO for the wavetable oscillator's warp * - * @name wtWarpDepth + * @name warpdepth * @param {number | Pattern} depth depth of modulation */ -export const { wtWarpDepth } = registerControl('wtWarpDepth'); - -/** - * Whether to sync the LFO for the wavetable oscillator's warp - * - * @name wtWarpSynced - * @param {number | Pattern} synced Whether to sync the lfo to CPM. > 0.5 will be synced - */ -export const { wtWarpSynced } = registerControl('wtWarpSynced'); +export const { warpdepth } = registerControl('warpdepth'); /** * Shape of the LFO for the wavetable oscillator's warp * - * @name wtWarpShape + * @name warpshape * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) */ -export const { wtWarpShape } = registerControl('wtWarpShape'); +export const { warpshape } = registerControl('warpshape'); /** * DC offset of the LFO for the wavetable oscillator's warp * - * @name wtWarpDCOffset + * @name warpdc * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar */ -export const { wtWarpDCOffset } = registerControl('wtWarpDCOffset'); +export const { warpdc } = registerControl('warpdc'); /** * Skew of the LFO for the wavetable oscillator's warp * - * @name wtWarpSkew + * @name warpskew * @param {number | Pattern} skew How much to bend the LFO shape */ -export const { wtWarpSkew } = registerControl('wtWarpSkew'); +export const { warpskew } = registerControl('warpskew'); /** * Type of warp (alteration of the waveform) to apply to the wavetable oscillator. @@ -284,27 +282,43 @@ export const { wtWarpSkew } = registerControl('wtWarpSkew'); * The current options are: none, asym, bendp, bendm, bendmp, sync, quant, fold, pwm, orbit, * spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip * - * @name wtWarpMode + * @name warpmode * @param {number | string | Pattern} mode Warp mode * @synonyms wavetableWarpMode * @example - * s("morgana").bank("wt_digital").seg(8).note("F1").wtWarp("0 0.25 0.5 0.75 1") - * .wtWarpMode("*2") + * s("morgana").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1") + * .warpmode("*2") * */ -export const { wtWarpMode, wavetableWarpMode } = registerControl('wtWarpMode', 'wavetableWarpMode'); +export const { warpmode, wavetableWarpMode } = registerControl('warpmode', 'wavetableWarpMode'); /** * Amount of randomness of the initial phase of the wavetable oscillator. * - * @name wtPhaseRand + * @name wtphaserand * @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random) * @synonyms wavetablePhaseRand * @example - * s("basique").bank("wt_digital").seg(16).wtPhaseRand("<0 1>") + * s("basique").bank("wt_digital").seg(16).wtphaserand("<0 1>") * */ -export const { wtPhaseRand, wavetablePhaseRand } = registerControl('wtPhaseRand', 'wavetablePhaseRand'); +export const { wtphaserand, wavetablePhaseRand } = registerControl('wtphaserand', 'wavetablePhaseRand'); + +/** + * Amount of envelope applied wavetable oscillator's position envelope + * + * @name warpenv + * @param {number | Pattern} amount between 0 and 1 + */ +export const { warpenv } = registerControl('warpenv'); + +/** + * cycle synced rate of the LFO for the wavetable warp position + * + * @name warpsync + * @param {number | Pattern} rate rate in cycles + */ +export const { warpsync } = registerControl('warpsync'); /** * Define a custom webaudio node to use as a sound source. diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 48264ec05..52ed9bff8 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -555,7 +555,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) audioNodes.forEach((n) => n?.disconnect()); activeSoundSources.delete(chainID); }; - const soundHandle = await onTrigger(t, value, onEnded); + const soundHandle = await onTrigger(t, value, onEnded, cps); if (soundHandle) { sourceNode = soundHandle.node; diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 095bd079e..eddc2e3e8 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -14,7 +14,7 @@ import { import { logger } from './logger.mjs'; const WT_MAX_MIP_LEVELS = 6; -export const WarpMode = Object.freeze({ +export const Warpmode = Object.freeze({ NONE: 0, ASYM: 1, MIRROR: 2, @@ -177,11 +177,17 @@ const _processTables = (json, baseUrl, frameLen, options = {}) => { }; export function registerWaveTable(key, tables, params) { - registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, tables, params?.frameLen ?? 2048), { - type: 'wavetable', - tables, - ...params, - }); + registerSound( + key, + (t, hapValue, onended, cps) => { + return onTriggerSynth(t, hapValue, onended, tables, cps, params?.frameLen ?? 2048); + }, + { + type: 'wavetable', + tables, + ...params, + }, + ); } /** @@ -214,13 +220,13 @@ export const tables = async (url, frameLen, json, options = {}) => { }); }; -export async function onTriggerSynth(t, value, onended, tables, frameLen) { - const { s, n = 0, duration } = value; +export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { + const { s, n = 0, duration, wtenv } = value; const ac = getAudioContext(); const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); - let { wtWarpMode } = value; - if (typeof wtWarpMode === 'string') { - wtWarpMode = WarpMode[wtWarpMode.toUpperCase()] ?? WarpMode.NONE; + let { warpmode } = value; + if (typeof warpmode === 'string') { + warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE; } const frequency = getFrequencyFromValue(value); const { url, label } = getCommonSampleInfo(value, tables); @@ -236,12 +242,12 @@ export async function onTriggerSynth(t, value, onended, tables, frameLen) { end: envEnd, frequency, detune: value.detune, - position: value.wtPos, - warp: value.wtWarp, - warpMode: wtWarpMode, + position: value.wt, + warp: value.warp, + warpMode: warpmode, voices: value.unison, spread: value.spread, - phaserand: value.wtPhaseRand, + phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0, }, { outputChannelCount: [2] }, ); @@ -250,36 +256,52 @@ export async function onTriggerSynth(t, value, onended, tables, frameLen) { logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight'); return; } - const posADSRParams = [value.wtPosAttack, value.wtPosDecay, value.wtPosSustain, value.wtPosRelease]; - const warpADSRParams = [value.wtPosAttack, value.wtPosDecay, value.wtPosSustain, value.wtPosRelease]; + const posADSRParams = [value.wtattack, value.wtdecay, value.wtsustain, value.wtrelease]; + const warpADSRParams = [value.warpattack, value.warpdecay, value.warpsustain, value.warprelease]; const wtParams = source.parameters; const positionParam = wtParams.get('position'); const warpParam = wtParams.get('warp'); let posLFO; - if (posADSRParams.some((p) => p !== undefined)) { - const [pAttack, pDecay, pSustain, pRelease] = getADSRValues(posADSRParams); - getParamADSR(positionParam, pAttack, pDecay, pSustain, pRelease, 0, 1, t, holdEnd, 'linear'); - } else { + if ([wtenv, ...posADSRParams].some((p) => p !== undefined)) { + const [pAttack, pDecay, pSustain, pRelease] = getADSRValues(posADSRParams, 'linear', [0, 0.5, 0, 0.1]); + const min = value.wt ?? 0; + const max = (wtenv ?? 0.5) + min; + getParamADSR(positionParam, pAttack, pDecay, pSustain, pRelease, min, max, t, holdEnd, 'linear'); + } + let wtrate = value.wtrate; + if (value.wtsync != null) { + wtrate = wtrate = cps * value.wtsync; + } + if ([wtrate, value.wtdepth, value.wtshape, value.wtskew, value.wtdc].some((p) => p != null)) { const posLFO = getLfo(ac, t, endWithRelease, { - frequency: value.wtPosRate, - depth: value.wtPosDepth, - shape: value.wtPosShape, - skew: value.wtPosSkew, - dcoffset: value.wtPosDCOffset ?? 0, + frequency: wtrate, + depth: value.wtdepth ?? 0.5, + shape: value.wtshape, + skew: value.wtskew, + dcoffset: value.wtdc ?? 0, }); posLFO.connect(positionParam); } let warpLFO; + if (posADSRParams.some((p) => p !== undefined)) { const [wAttack, wDecay, wSustain, wRelease] = getADSRValues(warpADSRParams); - getParamADSR(warpParam, wAttack, wDecay, wSustain, wRelease, 0, 1, t, holdEnd, 'linear'); - } else { + const min = value.warp ?? 0; + const max = (value.warpenv ?? 0.5) + min; + getParamADSR(warpParam, wAttack, wDecay, wSustain, wRelease, min, max, t, holdEnd, 'linear', [0, 0.5, 0, 0.1]); + } + let warprate = value.warprate; + if (value.warpsync != null) { + console.info(value.warpsync); + warprate = warprate = cps * value.warpsync; + } + if ([warprate, value.warpdepth, value.warpshape, value.warpskew, value.warpdc].some((p) => p != null)) { const warpLFO = getLfo(ac, t, endWithRelease, { - frequency: value.wtWarpRate, - depth: value.wtWarpDepth, - shape: value.wtWarpShape, - skew: value.wtWarpSkew, - dcoffset: value.wtWarpDCOffset ?? 0, + frequency: warprate, + depth: value.warpdepth ?? 0.5, + shape: value.warpshape, + skew: value.warpskew, + dcoffset: value.warpdc ?? 0, }); warpLFO.connect(warpParam); } From c689874bb473a58e1b7031a385227af675c4af08 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 28 Sep 2025 14:57:12 -0400 Subject: [PATCH 252/257] simplify modulators --- packages/superdough/helpers.mjs | 35 +++++++++++++ packages/superdough/wavetable.mjs | 84 ++++++++++++++++--------------- 2 files changed, 78 insertions(+), 41 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 859329b20..c6ebd1b89 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -153,6 +153,41 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => { return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)]; }; +// helper utility for applying standard modulators to a parameter +export function applyParameterModulators(audioContext, param, start, end, envelopeValues, lfoValues) { + let { amount,offset,defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues; + + if (amount == null) { + const hasADSRParams = values.some(p => p != null); + amount = hasADSRParams ? defaultAmount : 0; + } + + const min = offset ?? 0; + const max = amount + min + const diff = Math.abs(max - min) + if (diff) { + const [attack, decay, sustain, release] = getADSRValues(values, curve, defaultValues); + getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve); + } + let lfo + let {defaultDepth = 1,depth, dcoffset, ...getLfoInputs} = lfoValues + + if (depth == null) { + const hasLFOParams = Object.values(getLfoInputs).some(v => v != null) + depth = hasLFOParams ? defaultDepth : 0; + } + if (depth) { + lfo = getLfo(audioContext, start, end, { + depth, + dcoffset, + ...getLfoInputs + }); + lfo.connect(param); + } + + return { lfo, disconnect: () => lfo?.disconnect() } +} + export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) { const curve = 'exponential'; const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]); diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index eddc2e3e8..bcf4ea461 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -1,6 +1,7 @@ import { getAudioContext, registerSound } from './index.mjs'; import { getCommonSampleInfo } from './util.mjs'; import { + applyParameterModulators, destroyAudioWorkletNode, getADSRValues, getFrequencyFromValue, @@ -220,8 +221,10 @@ export const tables = async (url, frameLen, json, options = {}) => { }); }; + + export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { - const { s, n = 0, duration, wtenv } = value; + const { s, n = 0, duration } = value; const ac = getAudioContext(); const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); let { warpmode } = value; @@ -245,7 +248,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { position: value.wt, warp: value.warp, warpMode: warpmode, - voices: value.unison, + voices: Math.max(value.unison ?? 1, 1), spread: value.spread, phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0, }, @@ -261,55 +264,54 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const wtParams = source.parameters; const positionParam = wtParams.get('position'); const warpParam = wtParams.get('warp'); - let posLFO; - if ([wtenv, ...posADSRParams].some((p) => p !== undefined)) { - const [pAttack, pDecay, pSustain, pRelease] = getADSRValues(posADSRParams, 'linear', [0, 0.5, 0, 0.1]); - const min = value.wt ?? 0; - const max = (wtenv ?? 0.5) + min; - getParamADSR(positionParam, pAttack, pDecay, pSustain, pRelease, min, max, t, holdEnd, 'linear'); - } + let wtrate = value.wtrate; if (value.wtsync != null) { - wtrate = wtrate = cps * value.wtsync; + wtrate = cps * value.wtsync; } - if ([wtrate, value.wtdepth, value.wtshape, value.wtskew, value.wtdc].some((p) => p != null)) { - const posLFO = getLfo(ac, t, endWithRelease, { - frequency: wtrate, - depth: value.wtdepth ?? 0.5, - shape: value.wtshape, - skew: value.wtskew, - dcoffset: value.wtdc ?? 0, - }); - posLFO.connect(positionParam); - } - let warpLFO; - if (posADSRParams.some((p) => p !== undefined)) { - const [wAttack, wDecay, wSustain, wRelease] = getADSRValues(warpADSRParams); - const min = value.warp ?? 0; - const max = (value.warpenv ?? 0.5) + min; - getParamADSR(warpParam, wAttack, wDecay, wSustain, wRelease, min, max, t, holdEnd, 'linear', [0, 0.5, 0, 0.1]); - } + const wtPosModulators = applyParameterModulators(ac, positionParam, t, endWithRelease, { + offset: value.wt, + amount: value.wtenv, + defaultAmount: 0.5, + shape: 'linear', + values: posADSRParams, + holdEnd, + defaultValues: [0, 0.5, 0, 0.1], + }, { + frequency: wtrate, + depth: value.wtdepth, + defaultDepth: 0.5, + shape: value.wtshape, + skew: value.wtskew, + dcoffset: value.wtdc ?? 0, + }); + let warprate = value.warprate; if (value.warpsync != null) { - console.info(value.warpsync); warprate = warprate = cps * value.warpsync; } - if ([warprate, value.warpdepth, value.warpshape, value.warpskew, value.warpdc].some((p) => p != null)) { - const warpLFO = getLfo(ac, t, endWithRelease, { - frequency: warprate, - depth: value.warpdepth ?? 0.5, - shape: value.warpshape, - skew: value.warpskew, - dcoffset: value.warpdc ?? 0, - }); - warpLFO.connect(warpParam); - } + const wtWarpModulators = applyParameterModulators(ac, warpParam, t, endWithRelease, { + offset: value.warp, + amount: value.warpenv, + defaultAmount: 0.5, + shape: 'linear', + values: warpADSRParams, + holdEnd, + defaultValues: [0, 0.5, 0, 0.1], + }, { + frequency: warprate, + depth: value.warpdepth, + defaultDepth: 0.5, + shape: value.warpshape, + skew: value.warpskew, + dcoffset: value.warpdc ?? 0, + }); const vibratoOscillator = getVibratoOscillator(source.detune, value, t); const envGain = ac.createGain(); const node = source.connect(envGain); getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); - getPitchEnvelope(source.detune, value, t, holdEnd); + getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd); const handle = { node, source }; const timeoutNode = webAudioTimeout( ac, @@ -318,8 +320,8 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { destroyAudioWorkletNode(source); vibratoOscillator?.stop(); node.disconnect(); - posLFO?.disconnect(); - warpLFO?.disconnect(); + wtPosModulators?.disconnect(); + wtWarpModulators?.disconnect(); onended(); }, t, From 54ebc97ddc3531c4d814e800887e22afc6308d09 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 28 Sep 2025 15:07:35 -0400 Subject: [PATCH 253/257] fix tests --- packages/superdough/helpers.mjs | 22 +- packages/superdough/wavetable.mjs | 80 ++-- packages/superdough/worklets.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 440 +++++++++++----------- 4 files changed, 278 insertions(+), 266 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index c6ebd1b89..47bca330d 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -155,37 +155,37 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => { // helper utility for applying standard modulators to a parameter export function applyParameterModulators(audioContext, param, start, end, envelopeValues, lfoValues) { - let { amount,offset,defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues; - + let { amount, offset, defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues; + if (amount == null) { - const hasADSRParams = values.some(p => p != null); + const hasADSRParams = values.some((p) => p != null); amount = hasADSRParams ? defaultAmount : 0; } const min = offset ?? 0; - const max = amount + min - const diff = Math.abs(max - min) + const max = amount + min; + const diff = Math.abs(max - min); if (diff) { const [attack, decay, sustain, release] = getADSRValues(values, curve, defaultValues); getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve); } - let lfo - let {defaultDepth = 1,depth, dcoffset, ...getLfoInputs} = lfoValues - + let lfo; + let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues; + if (depth == null) { - const hasLFOParams = Object.values(getLfoInputs).some(v => v != null) + const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null); depth = hasLFOParams ? defaultDepth : 0; } if (depth) { lfo = getLfo(audioContext, start, end, { depth, dcoffset, - ...getLfoInputs + ...getLfoInputs, }); lfo.connect(param); } - return { lfo, disconnect: () => lfo?.disconnect() } + return { lfo, disconnect: () => lfo?.disconnect() }; } export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) { diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index bcf4ea461..4a21cabe4 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -221,8 +221,6 @@ export const tables = async (url, frameLen, json, options = {}) => { }); }; - - export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const { s, n = 0, duration } = value; const ac = getAudioContext(); @@ -270,43 +268,57 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { wtrate = cps * value.wtsync; } - const wtPosModulators = applyParameterModulators(ac, positionParam, t, endWithRelease, { - offset: value.wt, - amount: value.wtenv, - defaultAmount: 0.5, - shape: 'linear', - values: posADSRParams, - holdEnd, - defaultValues: [0, 0.5, 0, 0.1], - }, { - frequency: wtrate, - depth: value.wtdepth, - defaultDepth: 0.5, - shape: value.wtshape, - skew: value.wtskew, - dcoffset: value.wtdc ?? 0, - }); + const wtPosModulators = applyParameterModulators( + ac, + positionParam, + t, + endWithRelease, + { + offset: value.wt, + amount: value.wtenv, + defaultAmount: 0.5, + shape: 'linear', + values: posADSRParams, + holdEnd, + defaultValues: [0, 0.5, 0, 0.1], + }, + { + frequency: wtrate, + depth: value.wtdepth, + defaultDepth: 0.5, + shape: value.wtshape, + skew: value.wtskew, + dcoffset: value.wtdc ?? 0, + }, + ); let warprate = value.warprate; if (value.warpsync != null) { warprate = warprate = cps * value.warpsync; } - const wtWarpModulators = applyParameterModulators(ac, warpParam, t, endWithRelease, { - offset: value.warp, - amount: value.warpenv, - defaultAmount: 0.5, - shape: 'linear', - values: warpADSRParams, - holdEnd, - defaultValues: [0, 0.5, 0, 0.1], - }, { - frequency: warprate, - depth: value.warpdepth, - defaultDepth: 0.5, - shape: value.warpshape, - skew: value.warpskew, - dcoffset: value.warpdc ?? 0, - }); + const wtWarpModulators = applyParameterModulators( + ac, + warpParam, + t, + endWithRelease, + { + offset: value.warp, + amount: value.warpenv, + defaultAmount: 0.5, + shape: 'linear', + values: warpADSRParams, + holdEnd, + defaultValues: [0, 0.5, 0, 0.1], + }, + { + frequency: warprate, + depth: value.warpdepth, + defaultDepth: 0.5, + shape: value.warpshape, + skew: value.warpskew, + dcoffset: value.warpdc ?? 0, + }, + ); const vibratoOscillator = getVibratoOscillator(source.detune, value, t); const envGain = ac.createGain(); const node = source.connect(envGain); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 0a2d40d91..cea5a1ea5 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1054,7 +1054,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'warpMode', defaultValue: 0 }, { name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 }, - { name: 'spread', defaultValue: 0.18, minValue: 0, maxValue: 1 }, + { name: 'spread', defaultValue: 0.4, minValue: 0, maxValue: 1 }, { name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 }, ]; } diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index bfa4ae40e..73278947d 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -11789,6 +11789,112 @@ exports[`runs examples > example "vowel" example index 1 1`] = ` ] `; +exports[`runs examples > example "warp" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 1/4 → 3/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 5/8 → 3/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 7/8 → 1/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 1/1 → 9/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 5/4 → 11/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 13/8 → 7/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 15/8 → 2/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 2/1 → 17/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 9/4 → 19/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 21/8 → 11/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 23/8 → 3/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 3/1 → 25/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 13/4 → 27/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 29/8 → 15/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 31/8 → 4/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", +] +`; + +exports[`runs examples > example "warpmode" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:asym ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:asym ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]", + "[ 1/4 → 3/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:asym ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:bendp ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]", + "[ 5/8 → 3/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:bendp ]", + "[ 7/8 → 1/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:bendp ]", + "[ 1/1 → 9/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 5/4 → 11/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:logistic ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]", + "[ 13/8 → 7/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:logistic ]", + "[ 15/8 → 2/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:logistic ]", + "[ 2/1 → 17/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:sync ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:sync ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]", + "[ 9/4 → 19/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:sync ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:wormhole ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]", + "[ 21/8 → 11/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:wormhole ]", + "[ 23/8 → 3/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:wormhole ]", + "[ 3/1 → 25/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:brownian ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:brownian ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]", + "[ 13/4 → 27/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:brownian ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:asym ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]", + "[ 29/8 → 15/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:asym ]", + "[ 31/8 → 4/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:asym ]", +] +`; + exports[`runs examples > example "wchoose" example index 0 1`] = ` [ "[ 0/1 → 1/5 | note:c2 s:sine ]", @@ -11993,231 +12099,125 @@ exports[`runs examples > example "withValue" example index 0 1`] = ` ] `; -exports[`runs examples > example "wtPhaseRand" example index 0 1`] = ` +exports[`runs examples > example "wt" example index 0 1`] = ` [ - "[ 0/1 → 1/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 1/16 → 1/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 1/8 → 3/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 3/16 → 1/4 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 1/4 → 5/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 5/16 → 3/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 3/8 → 7/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 7/16 → 1/2 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 1/2 → 9/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 9/16 → 5/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 5/8 → 11/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 11/16 → 3/4 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 3/4 → 13/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 13/16 → 7/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 7/8 → 15/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 15/16 → 1/1 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 1/1 → 17/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 17/16 → 9/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 9/8 → 19/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 19/16 → 5/4 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 5/4 → 21/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 21/16 → 11/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 11/8 → 23/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 23/16 → 3/2 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 3/2 → 25/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 25/16 → 13/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 13/8 → 27/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 27/16 → 7/4 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 7/4 → 29/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 29/16 → 15/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 15/8 → 31/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 31/16 → 2/1 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 2/1 → 33/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 33/16 → 17/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 17/8 → 35/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 35/16 → 9/4 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 9/4 → 37/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 37/16 → 19/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 19/8 → 39/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 39/16 → 5/2 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 5/2 → 41/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 41/16 → 21/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 21/8 → 43/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 43/16 → 11/4 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 11/4 → 45/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 45/16 → 23/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 23/8 → 47/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 47/16 → 3/1 | s:basique bank:wt_digital wtPhaseRand:0 ]", - "[ 3/1 → 49/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 49/16 → 25/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 25/8 → 51/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 51/16 → 13/4 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 13/4 → 53/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 53/16 → 27/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 27/8 → 55/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 55/16 → 7/2 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 7/2 → 57/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 57/16 → 29/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 29/8 → 59/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 59/16 → 15/4 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 15/4 → 61/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 61/16 → 31/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 31/8 → 63/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", - "[ 63/16 → 4/1 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 0/1 → 1/8 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 1/4 → 3/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 5/8 → 3/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 7/8 → 1/1 | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 1/1 → 9/8 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 5/4 → 11/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 13/8 → 7/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 15/8 → 2/1 | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 2/1 → 17/8 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 9/4 → 19/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 21/8 → 11/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 23/8 → 3/1 | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 3/1 → 25/8 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 13/4 → 27/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 29/8 → 15/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 31/8 → 4/1 | s:squelch bank:wt_digital note:F1 wt:1 ]", ] `; -exports[`runs examples > example "wtPos" example index 0 1`] = ` +exports[`runs examples > example "wtphaserand" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", - "[ (1/8 → 1/5) ⇝ 1/4 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", - "[ 1/8 ⇜ (1/5 → 1/4) | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", - "[ 1/4 → 3/8 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", - "[ (3/8 → 2/5) ⇝ 1/2 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", - "[ 3/8 ⇜ (2/5 → 1/2) | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", - "[ (1/2 → 3/5) ⇝ 5/8 | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", - "[ 1/2 ⇜ (3/5 → 5/8) | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", - "[ 5/8 → 3/4 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", - "[ (3/4 → 4/5) ⇝ 7/8 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", - "[ 3/4 ⇜ (4/5 → 7/8) | s:squelch bank:wt_digital note:F1 wtPos:1 ]", - "[ 7/8 → 1/1 | s:squelch bank:wt_digital note:F1 wtPos:1 ]", - "[ 1/1 → 9/8 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", - "[ (9/8 → 6/5) ⇝ 5/4 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", - "[ 9/8 ⇜ (6/5 → 5/4) | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", - "[ 5/4 → 11/8 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", - "[ (11/8 → 7/5) ⇝ 3/2 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", - "[ 11/8 ⇜ (7/5 → 3/2) | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", - "[ (3/2 → 8/5) ⇝ 13/8 | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", - "[ 3/2 ⇜ (8/5 → 13/8) | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", - "[ 13/8 → 7/4 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", - "[ (7/4 → 9/5) ⇝ 15/8 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", - "[ 7/4 ⇜ (9/5 → 15/8) | s:squelch bank:wt_digital note:F1 wtPos:1 ]", - "[ 15/8 → 2/1 | s:squelch bank:wt_digital note:F1 wtPos:1 ]", - "[ 2/1 → 17/8 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", - "[ (17/8 → 11/5) ⇝ 9/4 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", - "[ 17/8 ⇜ (11/5 → 9/4) | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", - "[ 9/4 → 19/8 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", - "[ (19/8 → 12/5) ⇝ 5/2 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", - "[ 19/8 ⇜ (12/5 → 5/2) | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", - "[ (5/2 → 13/5) ⇝ 21/8 | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", - "[ 5/2 ⇜ (13/5 → 21/8) | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", - "[ 21/8 → 11/4 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", - "[ (11/4 → 14/5) ⇝ 23/8 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", - "[ 11/4 ⇜ (14/5 → 23/8) | s:squelch bank:wt_digital note:F1 wtPos:1 ]", - "[ 23/8 → 3/1 | s:squelch bank:wt_digital note:F1 wtPos:1 ]", - "[ 3/1 → 25/8 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", - "[ (25/8 → 16/5) ⇝ 13/4 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", - "[ 25/8 ⇜ (16/5 → 13/4) | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", - "[ 13/4 → 27/8 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", - "[ (27/8 → 17/5) ⇝ 7/2 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", - "[ 27/8 ⇜ (17/5 → 7/2) | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", - "[ (7/2 → 18/5) ⇝ 29/8 | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", - "[ 7/2 ⇜ (18/5 → 29/8) | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", - "[ 29/8 → 15/4 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", - "[ (15/4 → 19/5) ⇝ 31/8 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", - "[ 15/4 ⇜ (19/5 → 31/8) | s:squelch bank:wt_digital note:F1 wtPos:1 ]", - "[ 31/8 → 4/1 | s:squelch bank:wt_digital note:F1 wtPos:1 ]", -] -`; - -exports[`runs examples > example "wtWarp" example index 0 1`] = ` -[ - "[ 0/1 → 1/8 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ (1/8 → 1/5) ⇝ 1/4 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ 1/8 ⇜ (1/5 → 1/4) | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 1/4 → 3/8 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ (3/8 → 2/5) ⇝ 1/2 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 3/8 ⇜ (2/5 → 1/2) | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ (1/2 → 3/5) ⇝ 5/8 | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ 1/2 ⇜ (3/5 → 5/8) | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 5/8 → 3/4 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ (3/4 → 4/5) ⇝ 7/8 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 3/4 ⇜ (4/5 → 7/8) | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", - "[ 7/8 → 1/1 | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", - "[ 1/1 → 9/8 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ (9/8 → 6/5) ⇝ 5/4 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ 9/8 ⇜ (6/5 → 5/4) | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 5/4 → 11/8 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ (11/8 → 7/5) ⇝ 3/2 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 11/8 ⇜ (7/5 → 3/2) | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ (3/2 → 8/5) ⇝ 13/8 | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ 3/2 ⇜ (8/5 → 13/8) | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 13/8 → 7/4 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ (7/4 → 9/5) ⇝ 15/8 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 7/4 ⇜ (9/5 → 15/8) | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", - "[ 15/8 → 2/1 | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", - "[ 2/1 → 17/8 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ (17/8 → 11/5) ⇝ 9/4 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ 17/8 ⇜ (11/5 → 9/4) | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 9/4 → 19/8 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ (19/8 → 12/5) ⇝ 5/2 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 19/8 ⇜ (12/5 → 5/2) | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ (5/2 → 13/5) ⇝ 21/8 | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ 5/2 ⇜ (13/5 → 21/8) | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 21/8 → 11/4 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ (11/4 → 14/5) ⇝ 23/8 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 11/4 ⇜ (14/5 → 23/8) | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", - "[ 23/8 → 3/1 | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", - "[ 3/1 → 25/8 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ (25/8 → 16/5) ⇝ 13/4 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ 25/8 ⇜ (16/5 → 13/4) | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 13/4 → 27/8 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ (27/8 → 17/5) ⇝ 7/2 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 27/8 ⇜ (17/5 → 7/2) | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ (7/2 → 18/5) ⇝ 29/8 | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ 7/2 ⇜ (18/5 → 29/8) | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 29/8 → 15/4 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ (15/4 → 19/5) ⇝ 31/8 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", - "[ 15/4 ⇜ (19/5 → 31/8) | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", - "[ 31/8 → 4/1 | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", -] -`; - -exports[`runs examples > example "wtWarpMode" example index 0 1`] = ` -[ - "[ 0/1 → 1/8 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:asym ]", - "[ (1/8 → 1/5) ⇝ 1/4 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:asym ]", - "[ 1/8 ⇜ (1/5 → 1/4) | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:asym ]", - "[ 1/4 → 3/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:asym ]", - "[ (3/8 → 2/5) ⇝ 1/2 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:asym ]", - "[ 3/8 ⇜ (2/5 → 1/2) | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:asym ]", - "[ (1/2 → 3/5) ⇝ 5/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:bendp ]", - "[ 1/2 ⇜ (3/5 → 5/8) | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:bendp ]", - "[ 5/8 → 3/4 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:bendp ]", - "[ (3/4 → 4/5) ⇝ 7/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:bendp ]", - "[ 3/4 ⇜ (4/5 → 7/8) | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:bendp ]", - "[ 7/8 → 1/1 | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:bendp ]", - "[ 1/1 → 9/8 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ (9/8 → 6/5) ⇝ 5/4 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", - "[ 9/8 ⇜ (6/5 → 5/4) | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 5/4 → 11/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ (11/8 → 7/5) ⇝ 3/2 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", - "[ 11/8 ⇜ (7/5 → 3/2) | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", - "[ (3/2 → 8/5) ⇝ 13/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:logistic ]", - "[ 3/2 ⇜ (8/5 → 13/8) | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:logistic ]", - "[ 13/8 → 7/4 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:logistic ]", - "[ (7/4 → 9/5) ⇝ 15/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:logistic ]", - "[ 7/4 ⇜ (9/5 → 15/8) | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:logistic ]", - "[ 15/8 → 2/1 | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:logistic ]", - "[ 2/1 → 17/8 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:sync ]", - "[ (17/8 → 11/5) ⇝ 9/4 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:sync ]", - "[ 17/8 ⇜ (11/5 → 9/4) | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:sync ]", - "[ 9/4 → 19/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:sync ]", - "[ (19/8 → 12/5) ⇝ 5/2 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:sync ]", - "[ 19/8 ⇜ (12/5 → 5/2) | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:sync ]", - "[ (5/2 → 13/5) ⇝ 21/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:wormhole ]", - "[ 5/2 ⇜ (13/5 → 21/8) | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:wormhole ]", - "[ 21/8 → 11/4 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:wormhole ]", - "[ (11/4 → 14/5) ⇝ 23/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:wormhole ]", - "[ 11/4 ⇜ (14/5 → 23/8) | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:wormhole ]", - "[ 23/8 → 3/1 | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:wormhole ]", - "[ 3/1 → 25/8 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:brownian ]", - "[ (25/8 → 16/5) ⇝ 13/4 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:brownian ]", - "[ 25/8 ⇜ (16/5 → 13/4) | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:brownian ]", - "[ 13/4 → 27/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:brownian ]", - "[ (27/8 → 17/5) ⇝ 7/2 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:brownian ]", - "[ 27/8 ⇜ (17/5 → 7/2) | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:brownian ]", - "[ (7/2 → 18/5) ⇝ 29/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:asym ]", - "[ 7/2 ⇜ (18/5 → 29/8) | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:asym ]", - "[ 29/8 → 15/4 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:asym ]", - "[ (15/4 → 19/5) ⇝ 31/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:asym ]", - "[ 15/4 ⇜ (19/5 → 31/8) | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:asym ]", - "[ 31/8 → 4/1 | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:asym ]", + "[ 0/1 → 1/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/16 → 1/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/8 → 3/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 3/16 → 1/4 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/4 → 5/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 5/16 → 3/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 3/8 → 7/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 7/16 → 1/2 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/2 → 9/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 9/16 → 5/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 5/8 → 11/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 11/16 → 3/4 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 3/4 → 13/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 13/16 → 7/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 7/8 → 15/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 15/16 → 1/1 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/1 → 17/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 17/16 → 9/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 9/8 → 19/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 19/16 → 5/4 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 5/4 → 21/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 21/16 → 11/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 11/8 → 23/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 23/16 → 3/2 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 3/2 → 25/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 25/16 → 13/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 13/8 → 27/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 27/16 → 7/4 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 7/4 → 29/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 29/16 → 15/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 15/8 → 31/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 31/16 → 2/1 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 2/1 → 33/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 33/16 → 17/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 17/8 → 35/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 35/16 → 9/4 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 9/4 → 37/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 37/16 → 19/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 19/8 → 39/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 39/16 → 5/2 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 5/2 → 41/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 41/16 → 21/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 21/8 → 43/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 43/16 → 11/4 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 11/4 → 45/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 45/16 → 23/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 23/8 → 47/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 47/16 → 3/1 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 3/1 → 49/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 49/16 → 25/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 25/8 → 51/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 51/16 → 13/4 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 13/4 → 53/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 53/16 → 27/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 27/8 → 55/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 55/16 → 7/2 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 7/2 → 57/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 57/16 → 29/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 29/8 → 59/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 59/16 → 15/4 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 15/4 → 61/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 61/16 → 31/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 31/8 → 63/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 63/16 → 4/1 | s:basique bank:wt_digital wtphaserand:1 ]", ] `; From bf3fe605d9a71543e9922f7983f03e2f89129be9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 28 Sep 2025 15:21:06 -0400 Subject: [PATCH 254/257] fix unison gain --- packages/superdough/worklets.mjs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index cea5a1ea5..7f5cbcf23 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1054,7 +1054,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'warpMode', defaultValue: 0 }, { name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 }, - { name: 'spread', defaultValue: 0.4, minValue: 0, maxValue: 1 }, + { name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 }, { name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 }, ]; } @@ -1239,7 +1239,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } const outL = outputs[0][0]; const outR = outputs[0][1] || outputs[0][0]; - const gainAdjustment = 0.3; if (!this.tables) { outL.fill(0); @@ -1256,13 +1255,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const warpAmount = pv(parameters.warp, i); const warpMode = pv(parameters.warpMode, i); const voices = pv(parameters.voices, i); - const spread = voices > 1 ? pv(parameters.spread, i) : 0; const phaseRand = pv(parameters.phaserand, i); + const spread = voices > 1 ? pv(parameters.spread, i) : 0; const gain1 = Math.sqrt(0.5 - 0.5 * spread); const gain2 = Math.sqrt(0.5 + 0.5 * spread); let f = pv(parameters.frequency, i); f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune - const normalizer = 0.3 / voices; + const normalizer = 0.3 / Math.sqrt(voices); for (let n = 0; n < voices; n++) { const isOdd = (n & 1) == 1; let gainL = gain1; From d94f145649b3bf3ea77d49fe26246fc075b353e0 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 28 Sep 2025 15:23:58 -0400 Subject: [PATCH 255/257] fix vib --- packages/superdough/wavetable.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 4a21cabe4..48b7e661c 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -319,7 +319,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { dcoffset: value.warpdc ?? 0, }, ); - const vibratoOscillator = getVibratoOscillator(source.detune, value, t); + const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t); const envGain = ac.createGain(); const node = source.connect(envGain); getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); From 8d45016ccf4ca0d63e5d903a63c21e54f85e8785 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 28 Sep 2025 19:11:13 -0400 Subject: [PATCH 256/257] working --- website/public/uzu-wavetables.json | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/website/public/uzu-wavetables.json b/website/public/uzu-wavetables.json index d9e0eeb90..b2d662d5d 100644 --- a/website/public/uzu-wavetables.json +++ b/website/public/uzu-wavetables.json @@ -5,35 +5,11 @@ "wt_digital/wt_basique.wav", "wt_digital/wt_crickets.wav", "wt_digital/wt_curses.wav", - "wt_digital/wt_earl_grey.wav", - "wt_digital/wt_echoes.wav", - "wt_digital/wt_glimmer.wav", - "wt_digital/wt_majick.wav", - "wt_digital/wt_meditation.wav", - "wt_digital/wt_morgana.wav", - "wt_digital/wt_red_alert.wav", - "wt_digital/wt_sad_piano.wav", - "wt_digital/wt_shook.wav", - "wt_digital/wt_sludge.wav", - "wt_digital/wt_squelch.wav", - "wt_digital/wt_summer.wav", - "wt_digital/wt_wasp.wav" + "wt_digital/wt_echoes.wav" ], "wt_digital_bad_day": ["wt_digital/wt_bad_day.wav"], "wt_digital_basique": ["wt_digital/wt_basique.wav"], "wt_digital_crickets": ["wt_digital/wt_crickets.wav"], "wt_digital_curses": ["wt_digital/wt_curses.wav"], - "wt_digital_earl_grey": ["wt_digital/wt_earl_grey.wav"], - "wt_digital_echoes": ["wt_digital/wt_echoes.wav"], - "wt_digital_glimmer": ["wt_digital/wt_glimmer.wav"], - "wt_digital_majick": ["wt_digital/wt_majick.wav"], - "wt_digital_meditation": ["wt_digital/wt_meditation.wav"], - "wt_digital_morgana": ["wt_digital/wt_morgana.wav"], - "wt_digital_red_alert": ["wt_digital/wt_red_alert.wav"], - "wt_digital_sad_piano": ["wt_digital/wt_sad_piano.wav"], - "wt_digital_shook": ["wt_digital/wt_shook.wav"], - "wt_digital_sludge": ["wt_digital/wt_sludge.wav"], - "wt_digital_squelch": ["wt_digital/wt_squelch.wav"], - "wt_digital_summer": ["wt_digital/wt_summer.wav"], - "wt_digital_wasp": ["wt_digital/wt_wasp.wav"] + "wt_digital_echoes": ["wt_digital/wt_echoes.wav"] } \ No newline at end of file From 48718daea8c6e60a5430a639a09159b795cf2e9b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 28 Sep 2025 19:57:19 -0400 Subject: [PATCH 257/257] add_vgame --- website/public/uzu-wavetables.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/website/public/uzu-wavetables.json b/website/public/uzu-wavetables.json index b2d662d5d..375b1fad3 100644 --- a/website/public/uzu-wavetables.json +++ b/website/public/uzu-wavetables.json @@ -11,5 +11,18 @@ "wt_digital_basique": ["wt_digital/wt_basique.wav"], "wt_digital_crickets": ["wt_digital/wt_crickets.wav"], "wt_digital_curses": ["wt_digital/wt_curses.wav"], - "wt_digital_echoes": ["wt_digital/wt_echoes.wav"] + "wt_digital_echoes": ["wt_digital/wt_echoes.wav"], + "wt_vgame": [ + "wt_vgame/wt_vgame10.wav", + "wt_vgame/wt_vgame11.wav", + "wt_vgame/wt_vgame12.wav", + "wt_vgame/wt_vgame13.wav", + "wt_vgame/wt_vgame14.wav", + "wt_vgame/wt_vgame15.wav", + "wt_vgame/wt_vgame16.wav", + "wt_vgame/wt_vgame17.wav", + "wt_vgame/wt_vgame18.wav", + "wt_vgame/wt_vgame19.wav", + "wt_vgame/wt_vgame20.wav" + ] } \ No newline at end of file