From 496d60dc1cd83cbb7901996632879f058cfdae8a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 3 May 2024 14:56:16 -0400 Subject: [PATCH 001/538] testing --- packages/core/controls.mjs | 11 +++++ packages/core/cyclist.mjs | 2 +- packages/core/neocyclist.mjs | 11 ++++- packages/core/repl.mjs | 6 +-- packages/superdough/superdough.mjs | 5 ++- packages/superdough/worklets.mjs | 65 ++++++++++++++++++++++++++++++ packages/webaudio/webaudio.mjs | 7 ++-- 7 files changed, 98 insertions(+), 9 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 166f0ac23..2364cd994 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -430,6 +430,17 @@ export const { crush } = registerControl('crush'); */ export const { coarse } = registerControl('coarse'); +/** + * modulate the output gain of a sound with a continuous wave + * + * @name gainmod + * @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on. + * @example + * s("triangle").gainmod("2:1:0") + * + */ +export const { gainmod } = registerControl('gainmod'); + /** * Allows you to set the output channels on the interface * diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 08b893752..d61267475 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -56,7 +56,7 @@ export class Cyclist { // the following line is dumb and only here for backwards compatibility // see https://github.com/tidalcycles/strudel/pull/1004 const deadline = targetTime - phase; - onTrigger?.(hap, deadline, duration, this.cps, targetTime); + onTrigger?.(hap, deadline, duration, this.cps, targetTime, end); } }); } catch (e) { diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 4600d0ef9..6bc0a8404 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -4,6 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ +import Fraction from 'fraction.js'; import { logger } from './logger.mjs'; export class NeoCyclist { @@ -86,7 +87,15 @@ export class NeoCyclist { this.latency + this.worker_time_dif; const duration = hap.duration / this.cps; - onTrigger?.(hap, 0, duration, this.cps, targetTime); + onTrigger?.( + hap, + 0, + duration, + this.cps, + targetTime, + this.cycle, + // + Fraction(this.latency).div(this.cps) + ); } }); }; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 5b7bcd71b..af5935787 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -178,15 +178,15 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { + async (hap, deadline, duration, cps, t, cycle = 0) => { // TODO: get rid of deadline after https://github.com/tidalcycles/strudel/pull/1004 try { if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); + await defaultOutput(hap, deadline, duration, cps, t, cycle); } if (hap.context.onTrigger) { // call signature of output / onTrigger is different... - await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t); + await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t, cycle); } } catch (err) { logger(`[cyclist] error: ${err.message}`, 'error'); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c0ba96e06..138a3a38d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -260,7 +260,8 @@ export function resetGlobalEffects() { analysersData = {}; } -export const superdough = async (value, t, hapDuration) => { +export const superdough = async (value, t, hapDuration, cps, cycle) => { + console.log({ cps, cycle }); const ac = getAudioContext(); if (typeof value !== 'object') { throw new Error( @@ -322,6 +323,7 @@ export const superdough = async (value, t, hapDuration) => { phasercenter, // coarse, + gainmod, crush, shape, shapevol = 1, @@ -470,6 +472,7 @@ export const superdough = async (value, t, hapDuration) => { crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); + gainmod !== undefined && chain.push(getWorklet(ac, 'gainmod-processor', { speed: gainmod, cps, cycle })); compressorThreshold !== undefined && chain.push( diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index bab28987e..f29d1028f 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1,6 +1,36 @@ // coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js // LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE +const linearEnvelope = (startVal, EndVal, startTime, endTime, currentTime, hold = 0) => { + let min = 0.001; + currentTime = currentTime - hold; + if (startTime > currentTime) { + return 1; + } + if (currentTime > endTime) { + return min; + } + // change relative start time to 0 to prevent numeric overflow + currentTime = currentTime - startTime; + endTime = endTime - startTime; + startTime = 0; + + let x1 = startTime; + let y1 = startVal; + let x2 = endTime; + let y2 = EndVal; + + // Calculate the growth or decay rate (b) + let b = y1 / y2 / (x1 - x2); + + // Calculate the initial value (a) + let a = y1 / (b * x1); + + let x = currentTime; + // calculate y for any x + return a * (b * x); +}; + class CoarseProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [{ name: 'coarse', defaultValue: 1 }]; @@ -62,6 +92,41 @@ class CrushProcessor extends AudioWorkletProcessor { } registerProcessor('crush-processor', CrushProcessor); +class GainModProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return [ + { name: 'cps', defaultValue: 0.5 }, + { name: 'speed', defaultValue: 0.5 }, + { name: 'cycle', defaultValue: 0 }, + ]; + } + + constructor() { + super(); + } + + process(inputs, outputs, parameters) { + const input = inputs[0]; + const output = outputs[0]; + const blockSize = 128; + + let crush = parameters.crush[0] ?? 8; + crush = Math.max(1, crush); + + if (input[0] == null || output[0] == null) { + return false; + } + for (let n = 0; n < blockSize; n++) { + for (let i = 0; i < input.length; i++) { + const x = Math.pow(2, crush - 1); + output[i][n] = Math.round(input[i][n] * x) / x; + } + } + return true; + } +} +registerProcessor('gainmod-processor', GainModProcessor); + class ShapeProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index f82a436a9..36d11b096 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -17,9 +17,9 @@ const hap2value = (hap) => { export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps); // uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 -export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => - superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration); - +export const webaudioOutput = (hap, deadline, hapDuration, cps, t, cycle) => { + return superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration, cps, cycle); +}; Pattern.prototype.webaudio = function () { return this.onTrigger(webaudioOutputTrigger); }; @@ -30,6 +30,7 @@ export function webaudioScheduler(options = {}) { defaultOutput: webaudioOutput, ...options, }; + const { defaultOutput, getTime } = options; return new strudel.Cyclist({ ...options, From 66a27a609ca3bc26c9f6bd962fad9429d2e686b5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 3 May 2024 14:58:20 -0400 Subject: [PATCH 002/538] implementing --- packages/superdough/worklets.mjs | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index f29d1028f..9800a8ed1 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -106,23 +106,21 @@ class GainModProcessor extends AudioWorkletProcessor { } process(inputs, outputs, parameters) { - const input = inputs[0]; - const output = outputs[0]; - const blockSize = 128; - - let crush = parameters.crush[0] ?? 8; - crush = Math.max(1, crush); - - if (input[0] == null || output[0] == null) { - return false; - } - for (let n = 0; n < blockSize; n++) { - for (let i = 0; i < input.length; i++) { - const x = Math.pow(2, crush - 1); - output[i][n] = Math.round(input[i][n] * x) / x; - } - } - return true; + // const input = inputs[0]; + // const output = outputs[0]; + // const blockSize = 128; + // let crush = parameters.crush[0] ?? 8; + // crush = Math.max(1, crush); + // if (input[0] == null || output[0] == null) { + // return false; + // } + // for (let n = 0; n < blockSize; n++) { + // for (let i = 0; i < input.length; i++) { + // const x = Math.pow(2, crush - 1); + // output[i][n] = Math.round(input[i][n] * x) / x; + // } + // } + // return true; } } registerProcessor('gainmod-processor', GainModProcessor); From 0d900c01342d0d6d860b67b2ea8a40867de21fb3 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 5 May 2024 12:02:51 -0400 Subject: [PATCH 003/538] sine test --- packages/superdough/worklets.mjs | 47 ++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 9800a8ed1..be4d92c41 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -91,6 +91,14 @@ class CrushProcessor extends AudioWorkletProcessor { } } registerProcessor('crush-processor', CrushProcessor); +const sine = (phase, dt) => { + return Math.sin(Math.PI * 2 * phase); + + // return Math.sin(phase); +}; +const getModulator = (frequency, values, ct) => { + Math.sin(); +}; class GainModProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { @@ -103,23 +111,44 @@ class GainModProcessor extends AudioWorkletProcessor { constructor() { super(); + this.phase; + this.inc = 0; + } + + incrementPhase(dt) { + this.phase += dt; + if (this.phase > 1.0) { + this.phase = this.phase - 1; + } } process(inputs, outputs, parameters) { - // const input = inputs[0]; - // const output = outputs[0]; - // const blockSize = 128; + const speed = parameters['speed'][0]; + const cps = parameters['cps'][0]; + const cycle = parameters['cycle'][0]; + const frequency = speed * cps; + if (this.phase == null) { + this.phase = (cycle * cps * frequency) % 1; + } + + const input = inputs[0]; + const output = outputs[0]; + const blockSize = 128; // let crush = parameters.crush[0] ?? 8; // crush = Math.max(1, crush); // if (input[0] == null || output[0] == null) { // return false; // } - // for (let n = 0; n < blockSize; n++) { - // for (let i = 0; i < input.length; i++) { - // const x = Math.pow(2, crush - 1); - // output[i][n] = Math.round(input[i][n] * x) / x; - // } - // } + const dt = frequency / sampleRate; + for (let n = 0; n < blockSize; n++) { + for (let i = 0; i < input.length; i++) { + const modval = sine(this.phase, dt); + + output[i][n] = input[i][n] * modval; + } + this.incrementPhase(dt); + this.inc += 1; + } // return true; } } From 3159f665604089b4233b140441e1630e7a872b12 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 5 May 2024 22:52:18 -0400 Subject: [PATCH 004/538] working --- packages/superdough/superdough.mjs | 15 +++- packages/superdough/worklets.mjs | 140 ++++++++++++++--------------- 2 files changed, 82 insertions(+), 73 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 138a3a38d..b6e06e635 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -261,7 +261,6 @@ export function resetGlobalEffects() { } export const superdough = async (value, t, hapDuration, cps, cycle) => { - console.log({ cps, cycle }); const ac = getAudioContext(); if (typeof value !== 'object') { throw new Error( @@ -282,6 +281,8 @@ export const superdough = async (value, t, hapDuration, cps, cycle) => { } // destructure let { + amdepth = 1, + amshape = 'tri', s = 'triangle', bank, source, @@ -326,6 +327,7 @@ export const superdough = async (value, t, hapDuration, cps, cycle) => { gainmod, crush, shape, + shapevol = 1, distort, distortvol = 1, @@ -472,7 +474,16 @@ export const superdough = async (value, t, hapDuration, cps, cycle) => { crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); - gainmod !== undefined && chain.push(getWorklet(ac, 'gainmod-processor', { speed: gainmod, cps, cycle })); + gainmod !== undefined && + chain.push( + getWorklet(ac, 'gainmod-processor', { + speed: gainmod, + // depth: amdepth, shape: amshape, + + cps, + cycle, + }), + ); compressorThreshold !== undefined && chain.push( diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index be4d92c41..2f1375f01 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1,34 +1,70 @@ // coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js // LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE -const linearEnvelope = (startVal, EndVal, startTime, endTime, currentTime, hold = 0) => { - let min = 0.001; - currentTime = currentTime - hold; - if (startTime > currentTime) { - return 1; +const clamp = (num, min, max) => Math.min(Math.max(num, min), max); +// adjust waveshape to remove frequencies above nyquist to prevent aliasing +// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517 +function polyBlep(phase, dt) { + // 0 <= phase < 1 + if (phase < dt) { + phase /= dt; + // 2 * (phase - phase^2/2 - 0.5) + return phase + phase - phase * phase - 1; } - if (currentTime > endTime) { - return min; + + // -1 < phase < 0 + else if (phase > 1 - dt) { + phase = (phase - 1) / dt; + // 2 * (phase^2/2 + phase + 0.5) + return phase * phase + phase + phase + 1; } - // change relative start time to 0 to prevent numeric overflow - currentTime = currentTime - startTime; - endTime = endTime - startTime; - startTime = 0; - let x1 = startTime; - let y1 = startVal; - let x2 = endTime; - let y2 = EndVal; + // 0 otherwise + else { + return 0; + } +} - // Calculate the growth or decay rate (b) - let b = y1 / y2 / (x1 - x2); +const waveshapes = { + sine(phase) { + return Math.sin(Math.PI * 2 * phase); + }, + custom(phase, values = [0, 1]) { + const numParts = values.length - 1; + const currPart = Math.floor(phase * numParts); - // Calculate the initial value (a) - let a = y1 / (b * x1); - - let x = currentTime; - // calculate y for any x - return a * (b * x); + const partLength = 1 / numParts; + const startVal = clamp(values[currPart], 0, 1); + const endVal = clamp(values[currPart + 1], 0, 1); + const y2 = endVal; + const y1 = startVal; + const x1 = 0; + const x2 = partLength; + const slope = (y2 - y1) / (x2 - x1); + return slope * (phase - partLength * currPart) + startVal; + }, + sawblep(phase, dt) { + const v = 2 * phase - 1; + return v - polyBlep(phase, dt); + }, + ramp(phase) { + return phase; + }, + saw(phase) { + return 1 - phase; + }, + tri(phase, skew = 0.5) { + if (phase >= skew) { + return 1 - ((phase / (1 - skew)) % 1); + } + return (phase / skew) % 1; + }, + square(phase, skew = 0.5) { + if (phase >= skew) { + return 1; + } + return 0; + }, }; class CoarseProcessor extends AudioWorkletProcessor { @@ -91,14 +127,6 @@ class CrushProcessor extends AudioWorkletProcessor { } } registerProcessor('crush-processor', CrushProcessor); -const sine = (phase, dt) => { - return Math.sin(Math.PI * 2 * phase); - - // return Math.sin(phase); -}; -const getModulator = (frequency, values, ct) => { - Math.sin(); -}; class GainModProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { @@ -106,13 +134,14 @@ class GainModProcessor extends AudioWorkletProcessor { { name: 'cps', defaultValue: 0.5 }, { name: 'speed', defaultValue: 0.5 }, { name: 'cycle', defaultValue: 0 }, + // { name: 'shape', defaultValue: 'tri' }, + // { name: 'depth', defaultValue: 1 }, ]; } constructor() { super(); this.phase; - this.inc = 0; } incrementPhase(dt) { @@ -126,30 +155,27 @@ class GainModProcessor extends AudioWorkletProcessor { const speed = parameters['speed'][0]; const cps = parameters['cps'][0]; const cycle = parameters['cycle'][0]; + + const blockSize = 128; const frequency = speed * cps; if (this.phase == null) { - this.phase = (cycle * cps * frequency) % 1; + const secondsPassed = cycle / cps; + this.phase = Math.max(0, (secondsPassed * frequency) % 1); } const input = inputs[0]; const output = outputs[0]; - const blockSize = 128; - // let crush = parameters.crush[0] ?? 8; - // crush = Math.max(1, crush); - // if (input[0] == null || output[0] == null) { - // return false; - // } + const dt = frequency / sampleRate; for (let n = 0; n < blockSize; n++) { for (let i = 0; i < input.length; i++) { - const modval = sine(this.phase, dt); + const modval = waveshapes.tri(this.phase, 0.99); output[i][n] = input[i][n] * modval; } this.incrementPhase(dt); - this.inc += 1; } - // return true; + return true; } } registerProcessor('gainmod-processor', GainModProcessor); @@ -222,34 +248,6 @@ class DistortProcessor extends AudioWorkletProcessor { } registerProcessor('distort-processor', DistortProcessor); -// adjust waveshape to remove frequencies above nyquist to prevent aliasing -// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517 -const polyBlep = (phase, dt) => { - // 0 <= phase < 1 - if (phase < dt) { - phase /= dt; - // 2 * (phase - phase^2/2 - 0.5) - return phase + phase - phase * phase - 1; - } - - // -1 < phase < 0 - else if (phase > 1 - dt) { - phase = (phase - 1) / dt; - // 2 * (phase^2/2 + phase + 0.5) - return phase * phase + phase + phase + 1; - } - - // 0 otherwise - else { - return 0; - } -}; - -const saw = (phase, dt) => { - const v = 2 * phase - 1; - return v - polyBlep(phase, dt); -}; - function lerp(a, b, n) { return n * (b - a) + a; } @@ -349,7 +347,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { for (let i = 0; i < output[0].length; i++) { this.phase[n] = this.phase[n] ?? Math.random(); - const v = saw(this.phase[n], dt); + const v = waveshapes.sawblep(this.phase[n], dt); output[0][i] = output[0][i] + v * gainL; output[1][i] = output[1][i] + v * gainR; From 7df663e87dac1c28220fc2cac21ab68ab976b5f9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 5 May 2024 23:54:30 -0400 Subject: [PATCH 005/538] memory leaking --- packages/core/controls.mjs | 31 +++++++++++++++++++++++++----- packages/superdough/superdough.mjs | 11 ++++++----- packages/superdough/worklets.mjs | 9 +++++---- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 2364cd994..b99f83564 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -431,16 +431,37 @@ export const { crush } = registerControl('crush'); export const { coarse } = registerControl('coarse'); /** - * modulate the output gain of a sound with a continuous wave + * modulate the amplitude of a sound with a continuous waveform * - * @name gainmod - * @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on. + * @name am + * @param {number | Pattern} speed modulation speed in cycles * @example - * s("triangle").gainmod("2:1:0") + * s("triangle").am("2").amshape("").amdepth(.5) * */ -export const { gainmod } = registerControl('gainmod'); +export const { am } = registerControl(['am', 'amdepth', 'amshape']); +/** + * depth of amplitude modulation + * + * @name amdepth + * @param {number | Pattern} depth + * @example + * s("triangle").am(1).amdepth("1") + * + */ +export const { amdepth } = registerControl('amdepth'); + +/** + * shape of amplitude modulation + * + * @name amshape + * @param {number | Pattern} shape tri | square | sine | saw | ramp + * @example + * note("{f g c d}%16").am(4).amshape("ramp").s("sawtooth") + * + */ +export const { amshape } = registerControl('amshape'); /** * Allows you to set the output channels on the interface * diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index b6e06e635..13c138dcc 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -324,7 +324,7 @@ export const superdough = async (value, t, hapDuration, cps, cycle) => { phasercenter, // coarse, - gainmod, + am, crush, shape, @@ -474,11 +474,12 @@ export const superdough = async (value, t, hapDuration, cps, cycle) => { crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); - gainmod !== undefined && + am !== undefined && chain.push( - getWorklet(ac, 'gainmod-processor', { - speed: gainmod, - // depth: amdepth, shape: amshape, + getWorklet(ac, 'am-processor', { + speed: am, + depth: amdepth, + // shape: amshape, cps, cycle, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 2f1375f01..b3e32576a 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -128,14 +128,14 @@ class CrushProcessor extends AudioWorkletProcessor { } registerProcessor('crush-processor', CrushProcessor); -class GainModProcessor extends AudioWorkletProcessor { +class AMProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ { name: 'cps', defaultValue: 0.5 }, { name: 'speed', defaultValue: 0.5 }, { name: 'cycle', defaultValue: 0 }, // { name: 'shape', defaultValue: 'tri' }, - // { name: 'depth', defaultValue: 1 }, + { name: 'depth', defaultValue: 1 }, ]; } @@ -155,6 +155,7 @@ class GainModProcessor extends AudioWorkletProcessor { const speed = parameters['speed'][0]; const cps = parameters['cps'][0]; const cycle = parameters['cycle'][0]; + const depth = clamp(parameters['depth'][0], 0, 1); const blockSize = 128; const frequency = speed * cps; @@ -169,7 +170,7 @@ class GainModProcessor extends AudioWorkletProcessor { const dt = frequency / sampleRate; for (let n = 0; n < blockSize; n++) { for (let i = 0; i < input.length; i++) { - const modval = waveshapes.tri(this.phase, 0.99); + const modval = waveshapes.tri(this.phase, 0.99) * depth + (1 - depth); output[i][n] = input[i][n] * modval; } @@ -178,7 +179,7 @@ class GainModProcessor extends AudioWorkletProcessor { return true; } } -registerProcessor('gainmod-processor', GainModProcessor); +registerProcessor('am-processor', AMProcessor); class ShapeProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { From a5b602250614afce28e71d0f5ea93fe258f554a7 Mon Sep 17 00:00:00 2001 From: Kaspars Date: Tue, 7 May 2024 23:56:39 +0200 Subject: [PATCH 006/538] - fix scale() to allow both n() and note() at the same time - fix scale() to allow scale names without tonic and then default it to C --- packages/tonal/test/tonal.test.mjs | 15 +++++++++++++++ packages/tonal/tonal.mjs | 7 ++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 8dd0e1861..968534c80 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -30,6 +30,14 @@ describe('tonal', () => { .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); + it('scale with n and note values', () => { + expect( + n(0, 1, 2) + .note(3, 4, 5) + .scale('C major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['F3', 'G3', 'A3']); + }); it('scale with colon', () => { expect( n(0, 1, 2) @@ -37,6 +45,13 @@ describe('tonal', () => { .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); + it('scale without tonic', () => { + expect( + n(0, 1, 2) + .scale('major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); it('scale with mininotation colon', () => { expect( n(0, 1, 2) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 4c25d23ea..012a0fc20 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -14,7 +14,7 @@ function scaleStep(step, scale) { scale = scale.replaceAll(':', ' '); step = Math.ceil(step); let { intervals, tonic, empty } = Scale.get(scale); - if ((empty && isNote(scale)) || (!empty && !tonic)) { + if ((empty && isNote(scale)) || (empty && !tonic)) { throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`); } else if (empty) { throw new Error(`invalid scale "${scale}"`); @@ -199,10 +199,7 @@ export const scale = register( pat .fmap((value) => { const isObject = typeof value === 'object'; - let step = isObject ? value.n : value; - if (isObject) { - delete value.n; // remove n so it won't cause trouble - } + let step = isObject ? (value.note ?? value.n) : value; if (isNote(step)) { // legacy.. return pure(step); From 4e6d39666a7474f44e28c8e56b9c4f719f88b198 Mon Sep 17 00:00:00 2001 From: Kaspars Date: Wed, 8 May 2024 00:00:58 +0200 Subject: [PATCH 007/538] codeformat --- packages/tonal/test/tonal.test.mjs | 14 +++++++------- packages/tonal/tonal.mjs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 968534c80..647fed807 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -32,10 +32,10 @@ describe('tonal', () => { }); it('scale with n and note values', () => { expect( - n(0, 1, 2) - .note(3, 4, 5) - .scale('C major') - .firstCycleValues.map((h) => h.note), + n(0, 1, 2) + .note(3, 4, 5) + .scale('C major') + .firstCycleValues.map((h) => h.note), ).toEqual(['F3', 'G3', 'A3']); }); it('scale with colon', () => { @@ -47,9 +47,9 @@ describe('tonal', () => { }); it('scale without tonic', () => { expect( - n(0, 1, 2) - .scale('major') - .firstCycleValues.map((h) => h.note), + n(0, 1, 2) + .scale('major') + .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); it('scale with mininotation colon', () => { diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 012a0fc20..29691d192 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -199,7 +199,7 @@ export const scale = register( pat .fmap((value) => { const isObject = typeof value === 'object'; - let step = isObject ? (value.note ?? value.n) : value; + let step = isObject ? value.note ?? value.n : value; if (isNote(step)) { // legacy.. return pure(step); From 33a73c37bc5caf29c7e1f04dbe26501b5727d5f9 Mon Sep 17 00:00:00 2001 From: Kaspars Date: Wed, 8 May 2024 00:46:27 +0200 Subject: [PATCH 008/538] fix tests --- packages/tonal/tonal.mjs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 29691d192..acf35a596 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -199,7 +199,15 @@ export const scale = register( pat .fmap((value) => { const isObject = typeof value === 'object'; - let step = isObject ? value.note ?? value.n : value; + let step = value; + if (isObject) { + if (value.note) { + step = value.note; + } else { + step = value.n; + delete value.n; // remove n so it won't cause trouble + } + } if (isNote(step)) { // legacy.. return pure(step); From 16472b59ddef33bc7fdb332cbcf23ecc6e09be48 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 8 May 2024 00:05:05 -0400 Subject: [PATCH 009/538] fix cyclist clock cycle --- packages/core/cyclist.mjs | 4 +- packages/core/neocyclist.mjs | 11 +-- packages/superdough/worklets.mjs | 158 +++++++++++++++++-------------- 3 files changed, 92 insertions(+), 81 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index d61267475..b33e9cc36 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -34,6 +34,8 @@ export class Cyclist { try { const begin = this.lastEnd; + const cycle = this.now(); + this.lastBegin = begin; const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; this.lastEnd = end; @@ -56,7 +58,7 @@ export class Cyclist { // the following line is dumb and only here for backwards compatibility // see https://github.com/tidalcycles/strudel/pull/1004 const deadline = targetTime - phase; - onTrigger?.(hap, deadline, duration, this.cps, targetTime, end); + onTrigger?.(hap, deadline, duration, this.cps, targetTime, cycle); } }); } catch (e) { diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 6bc0a8404..8f9b269e0 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -4,7 +4,6 @@ Copyright (C) 2022 Strudel contributors - see . */ -import Fraction from 'fraction.js'; import { logger } from './logger.mjs'; export class NeoCyclist { @@ -87,15 +86,7 @@ export class NeoCyclist { this.latency + this.worker_time_dif; const duration = hap.duration / this.cps; - onTrigger?.( - hap, - 0, - duration, - this.cps, - targetTime, - this.cycle, - // + Fraction(this.latency).div(this.cps) - ); + onTrigger?.(hap, 0, duration, this.cps, targetTime, this.cycle); } }); }; diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index b3e32576a..107e9bf7f 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -2,6 +2,7 @@ // LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE const clamp = (num, min, max) => Math.min(Math.max(num, min), max); +const blockSize = 128; // adjust waveshape to remove frequencies above nyquist to prevent aliasing // referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517 function polyBlep(phase, dt) { @@ -67,6 +68,64 @@ const waveshapes = { }, }; +class AMProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return [ + { name: 'cps', defaultValue: 0.5 }, + { name: 'speed', defaultValue: 0.5 }, + { name: 'cycle', defaultValue: 0 }, + // { name: 'shape', defaultValue: 'tri' }, + { name: 'depth', defaultValue: 1 }, + ]; + } + + constructor() { + super(); + this.phase; + this.started = false; + } + + incrementPhase(dt) { + this.phase += dt; + if (this.phase > 1.0) { + this.phase = this.phase - 1; + } + } + + process(inputs, outputs, parameters) { + const input = inputs[0]; + const output = outputs[0]; + const hasInput = !(input[0] === undefined); + if (this.started && !hasInput) { + return false; + } + this.started = hasInput; + + const speed = parameters['speed'][0]; + const cps = parameters['cps'][0]; + const cycle = parameters['cycle'][0]; + const depth = clamp(parameters['depth'][0], 0, 1); + const blockSize = 128; + const frequency = speed * cps; + if (this.phase == null) { + const secondsPassed = cycle / cps; + this.phase = Math.max(0, (secondsPassed * frequency) % 1); + } + + const dt = frequency / sampleRate; + for (let n = 0; n < blockSize; n++) { + for (let i = 0; i < input.length; i++) { + const modval = waveshapes.tri(this.phase, 0.99) * depth + (1 - depth); + + output[i][n] = input[i][n] * modval; + } + this.incrementPhase(dt); + } + return true; + } +} +registerProcessor('am-processor', AMProcessor); + class CoarseProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [{ name: 'coarse', defaultValue: 1 }]; @@ -74,19 +133,21 @@ class CoarseProcessor extends AudioWorkletProcessor { constructor() { super(); + this.started = false; } process(inputs, outputs, parameters) { const input = inputs[0]; const output = outputs[0]; - const blockSize = 128; + + const hasInput = !(input[0] === undefined); + if (this.started && !hasInput) { + return false; + } + this.started = hasInput; let coarse = parameters.coarse[0] ?? 0; coarse = Math.max(1, coarse); - - if (input[0] == null || output[0] == null) { - return false; - } for (let n = 0; n < blockSize; n++) { for (let i = 0; i < input.length; i++) { output[i][n] = n % coarse === 0 ? input[i][n] : output[i][n - 1]; @@ -104,19 +165,22 @@ class CrushProcessor extends AudioWorkletProcessor { constructor() { super(); + this.started = false; } process(inputs, outputs, parameters) { const input = inputs[0]; const output = outputs[0]; - const blockSize = 128; + + const hasInput = !(input[0] === undefined); + if (this.started && !hasInput) { + return false; + } + this.started = hasInput; let crush = parameters.crush[0] ?? 8; crush = Math.max(1, crush); - if (input[0] == null || output[0] == null) { - return false; - } for (let n = 0; n < blockSize; n++) { for (let i = 0; i < input.length; i++) { const x = Math.pow(2, crush - 1); @@ -128,59 +192,6 @@ class CrushProcessor extends AudioWorkletProcessor { } registerProcessor('crush-processor', CrushProcessor); -class AMProcessor extends AudioWorkletProcessor { - static get parameterDescriptors() { - return [ - { name: 'cps', defaultValue: 0.5 }, - { name: 'speed', defaultValue: 0.5 }, - { name: 'cycle', defaultValue: 0 }, - // { name: 'shape', defaultValue: 'tri' }, - { name: 'depth', defaultValue: 1 }, - ]; - } - - constructor() { - super(); - this.phase; - } - - incrementPhase(dt) { - this.phase += dt; - if (this.phase > 1.0) { - this.phase = this.phase - 1; - } - } - - process(inputs, outputs, parameters) { - const speed = parameters['speed'][0]; - const cps = parameters['cps'][0]; - const cycle = parameters['cycle'][0]; - const depth = clamp(parameters['depth'][0], 0, 1); - - const blockSize = 128; - const frequency = speed * cps; - if (this.phase == null) { - const secondsPassed = cycle / cps; - this.phase = Math.max(0, (secondsPassed * frequency) % 1); - } - - const input = inputs[0]; - const output = outputs[0]; - - const dt = frequency / sampleRate; - for (let n = 0; n < blockSize; n++) { - for (let i = 0; i < input.length; i++) { - const modval = waveshapes.tri(this.phase, 0.99) * depth + (1 - depth); - - output[i][n] = input[i][n] * modval; - } - this.incrementPhase(dt); - } - return true; - } -} -registerProcessor('am-processor', AMProcessor); - class ShapeProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ @@ -191,21 +202,24 @@ class ShapeProcessor extends AudioWorkletProcessor { constructor() { super(); + this.started = false; } process(inputs, outputs, parameters) { const input = inputs[0]; const output = outputs[0]; - const blockSize = 128; + + const hasInput = !(input[0] === undefined); + if (this.started && !hasInput) { + return false; + } + this.started = hasInput; let shape = parameters.shape[0]; shape = shape < 1 ? shape : 1.0 - 4e-10; shape = (2.0 * shape) / (1.0 - shape); const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0])); - if (input[0] == null || output[0] == null) { - return false; - } for (let n = 0; n < blockSize; n++) { for (let i = 0; i < input.length; i++) { output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain; @@ -226,19 +240,22 @@ class DistortProcessor extends AudioWorkletProcessor { constructor() { super(); + this.started = false; } process(inputs, outputs, parameters) { const input = inputs[0]; const output = outputs[0]; - const blockSize = 128; + + const hasInput = !(input[0] === undefined); + if (this.started && !hasInput) { + return false; + } + this.started = hasInput; const shape = Math.expm1(parameters.distort[0]); const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0])); - if (input[0] == null || output[0] == null) { - return false; - } for (let n = 0; n < blockSize; n++) { for (let i = 0; i < input.length; i++) { output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain; @@ -249,6 +266,7 @@ class DistortProcessor extends AudioWorkletProcessor { } registerProcessor('distort-processor', DistortProcessor); +// SUPERSAW function lerp(a, b, n) { return n * (b - a) + a; } From a4f10d533947f0e6b735e3cfaa40e87439d7e4ee Mon Sep 17 00:00:00 2001 From: Kaspars Date: Wed, 8 May 2024 10:38:06 +0200 Subject: [PATCH 010/538] bug fix --- packages/tonal/test/tonal.test.mjs | 6 +++--- packages/tonal/tonal.mjs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 647fed807..73c4de151 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -33,10 +33,10 @@ describe('tonal', () => { it('scale with n and note values', () => { expect( n(0, 1, 2) - .note(3, 4, 5) + .note(3, 4, 0) .scale('C major') - .firstCycleValues.map((h) => h.note), - ).toEqual(['F3', 'G3', 'A3']); + .firstCycleValues.map((h) => [h.n, h.note]), + ).toEqual([[0, 'F3'], [1, 'G3'], [2, 'C3']]); }); it('scale with colon', () => { expect( diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index acf35a596..dd723c2da 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -201,7 +201,7 @@ export const scale = register( const isObject = typeof value === 'object'; let step = value; if (isObject) { - if (value.note) { + if (typeof value.note !== 'undefined') { step = value.note; } else { step = value.n; From 8d8b8ab99ed1a18c7b43c6368c1aefcb65988f0e Mon Sep 17 00:00:00 2001 From: Kaspars Date: Wed, 8 May 2024 10:42:06 +0200 Subject: [PATCH 011/538] codeformat --- packages/tonal/test/tonal.test.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 73c4de151..755b16324 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -36,7 +36,11 @@ describe('tonal', () => { .note(3, 4, 0) .scale('C major') .firstCycleValues.map((h) => [h.n, h.note]), - ).toEqual([[0, 'F3'], [1, 'G3'], [2, 'C3']]); + ).toEqual([ + [0, 'F3'], + [1, 'G3'], + [2, 'C3'], + ]); }); it('scale with colon', () => { expect( From 6c05a4eb7becfb47e322eff6b6a00d351d5dc888 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 9 May 2024 00:21:21 -0400 Subject: [PATCH 012/538] fixed tri shape --- packages/core/controls.mjs | 23 ++++++++++++++++++++++- packages/core/cyclist.mjs | 5 ++++- packages/superdough/superdough.mjs | 8 ++++++-- packages/superdough/worklets.mjs | 24 ++++++++++++++---------- 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index b99f83564..46523588a 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -439,7 +439,7 @@ export const { coarse } = registerControl('coarse'); * s("triangle").am("2").amshape("").amdepth(.5) * */ -export const { am } = registerControl(['am', 'amdepth', 'amshape']); +export const { am } = registerControl(['am', 'amdepth', 'amskew', 'amphase']); /** * depth of amplitude modulation @@ -451,6 +451,27 @@ export const { am } = registerControl(['am', 'amdepth', 'amshape']); * */ export const { amdepth } = registerControl('amdepth'); +/** + * alter the shape of the modulation waveform + * + * @name amskew + * @param {number | Pattern} amount between 0 & 1, the shape of the waveform + * @example + * note("{f a c e}%16").am(4).amskew("<.5 0 1>") + * + */ +export const { amskew } = registerControl('amskew'); + +/** + * alter the phase of the modulation waveform + * + * @name amphase + * @param {number | Pattern} offset the offset in cycles of the modulation + * @example + * note("{f a c e}%16").am(4).amphase("<0 .25 .66>") + * + */ +export const { amphase } = registerControl('amskew'); /** * shape of amplitude modulation diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index b33e9cc36..41e31ce5c 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -34,13 +34,16 @@ export class Cyclist { try { const begin = this.lastEnd; - const cycle = this.now(); this.lastBegin = begin; const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; + this.lastEnd = end; + this.lastTick = phase; + const cycle = this.now(); + if (phase < t) { // avoid querying haps that are in the past anyway console.log(`skip query: too late`); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 13c138dcc..25859278d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -281,8 +281,10 @@ export const superdough = async (value, t, hapDuration, cps, cycle) => { } // destructure let { + am, amdepth = 1, - amshape = 'tri', + amskew = 0.5, + amphase = 0, s = 'triangle', bank, source, @@ -324,7 +326,7 @@ export const superdough = async (value, t, hapDuration, cps, cycle) => { phasercenter, // coarse, - am, + crush, shape, @@ -479,6 +481,8 @@ export const superdough = async (value, t, hapDuration, cps, cycle) => { getWorklet(ac, 'am-processor', { speed: am, depth: amdepth, + skew: amskew, + phaseoffset: amphase, // shape: amshape, cps, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 107e9bf7f..a3a5eaf5c 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1,7 +1,7 @@ // coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js // LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE - -const clamp = (num, min, max) => Math.min(Math.max(num, min), max); +import { clamp, _mod } from './util.mjs'; +// const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const blockSize = 128; // adjust waveshape to remove frequencies above nyquist to prevent aliasing // referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517 @@ -55,16 +55,17 @@ const waveshapes = { return 1 - phase; }, tri(phase, skew = 0.5) { + const x = 1 - skew; if (phase >= skew) { - return 1 - ((phase / (1 - skew)) % 1); + return 1 / x - phase / x; } - return (phase / skew) % 1; + return phase / skew; }, square(phase, skew = 0.5) { if (phase >= skew) { - return 1; + return 0; } - return 0; + return 1; }, }; @@ -74,8 +75,9 @@ class AMProcessor extends AudioWorkletProcessor { { name: 'cps', defaultValue: 0.5 }, { name: 'speed', defaultValue: 0.5 }, { name: 'cycle', defaultValue: 0 }, - // { name: 'shape', defaultValue: 'tri' }, + { name: 'skew', defaultValue: 0.5 }, { name: 'depth', defaultValue: 1 }, + { name: 'phaseoffset', defaultValue: 0 }, ]; } @@ -104,18 +106,20 @@ class AMProcessor extends AudioWorkletProcessor { const speed = parameters['speed'][0]; const cps = parameters['cps'][0]; const cycle = parameters['cycle'][0]; - const depth = clamp(parameters['depth'][0], 0, 1); + const depth = parameters['depth'][0]; + const skew = parameters['skew'][0]; + const phaseoffset = parameters['phaseoffset'][0]; const blockSize = 128; const frequency = speed * cps; if (this.phase == null) { const secondsPassed = cycle / cps; - this.phase = Math.max(0, (secondsPassed * frequency) % 1); + this.phase = _mod(secondsPassed * frequency + phaseoffset, 1); } const dt = frequency / sampleRate; for (let n = 0; n < blockSize; n++) { for (let i = 0; i < input.length; i++) { - const modval = waveshapes.tri(this.phase, 0.99) * depth + (1 - depth); + const modval = clamp(waveshapes.tri(this.phase, skew) * depth + (1 - depth), 0, 1); output[i][n] = input[i][n] * modval; } From 6a33032da75daac665357ad460f3066e7a1b92a9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 9 May 2024 23:26:04 -0400 Subject: [PATCH 013/538] formatting --- packages/core/controls.mjs | 2 +- packages/superdough/worklets.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 46523588a..b8cdb6ed7 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -471,7 +471,7 @@ export const { amskew } = registerControl('amskew'); * note("{f a c e}%16").am(4).amphase("<0 .25 .66>") * */ -export const { amphase } = registerControl('amskew'); +export const { amphase } = registerControl('amphase'); /** * shape of amplitude modulation diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index a3a5eaf5c..3fa89ca63 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -109,7 +109,7 @@ class AMProcessor extends AudioWorkletProcessor { const depth = parameters['depth'][0]; const skew = parameters['skew'][0]; const phaseoffset = parameters['phaseoffset'][0]; - const blockSize = 128; + const frequency = speed * cps; if (this.phase == null) { const secondsPassed = cycle / cps; From cba96049a3de9eb1baa9982bf5873831870b4595 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 10 May 2024 00:03:52 -0400 Subject: [PATCH 014/538] fixed duplicate variable --- packages/superdough/worklets.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 99eca92b5..3fa89ca63 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -130,7 +130,6 @@ class AMProcessor extends AudioWorkletProcessor { } registerProcessor('am-processor', AMProcessor); -const blockSize = 128; class CoarseProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [{ name: 'coarse', defaultValue: 1 }]; From 19df19375d146f037107358d5f5222f0217d6747 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 13 May 2024 10:33:18 -0400 Subject: [PATCH 015/538] kinda fixed it, need to find cause of incorrect cps calculation --- packages/core/cyclist.mjs | 17 +++++++++-------- packages/core/neocyclist.mjs | 3 --- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 41e31ce5c..046b6ff7d 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -8,7 +8,7 @@ import createClock from './zyklus.mjs'; import { logger } from './logger.mjs'; export class Cyclist { - constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) { + constructor({ interval = 0.05, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) { this.started = false; this.cps = 0.5; this.num_ticks_since_cps_change = 0; @@ -35,14 +35,11 @@ export class Cyclist { try { const begin = this.lastEnd; - this.lastBegin = begin; const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; - - this.lastEnd = end; - - this.lastTick = phase; - - const cycle = this.now(); + let cycle = this.now(); + //magic number that fixes cycle calcuation, cycle is probably not being calculated correctly + const modifier = this.cps * -interval; + cycle = cycle + modifier; if (phase < t) { // avoid querying haps that are in the past anyway @@ -60,10 +57,14 @@ export class Cyclist { const duration = hap.duration / this.cps; // the following line is dumb and only here for backwards compatibility // see https://github.com/tidalcycles/strudel/pull/1004 + const deadline = targetTime - phase; onTrigger?.(hap, deadline, duration, this.cps, targetTime, cycle); } }); + this.lastBegin = begin; + this.lastEnd = end; + this.lastTick = phase; } catch (e) { logger(`[cyclist] error: ${e.message}`); onError?.(e); diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 8f9b269e0..6a92050c5 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -10,11 +10,8 @@ export class NeoCyclist { constructor({ onTrigger, onToggle, getTime }) { this.started = false; this.cps = 0.5; - this.lastTick = 0; // absolute time when last tick (clock callback) happened this.getTime = getTime; // get absolute time this.time_at_last_tick_message = 0; - - this.num_cycles_at_cps_change = 0; this.onToggle = onToggle; this.latency = 0.1; // fixed trigger time offset this.cycle = 0; From e024ae2142797ea8a31aa4dea229003514640b5b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 13 May 2024 20:42:59 -0400 Subject: [PATCH 016/538] almost figured it out --- packages/core/cyclist.mjs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 046b6ff7d..75f92090c 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -36,11 +36,13 @@ export class Cyclist { const begin = this.lastEnd; const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; - let cycle = this.now(); + let cycle2 = this.now(); //magic number that fixes cycle calcuation, cycle is probably not being calculated correctly const modifier = this.cps * -interval; - cycle = cycle + modifier; - + cycle2 = cycle2 + modifier; + // let cycle = begin + (phase - t) * this.cps; + // cycle = begin t + // console.log({ phase, t, cycle2 }); if (phase < t) { // avoid querying haps that are in the past anyway console.log(`skip query: too late`); @@ -49,7 +51,9 @@ export class Cyclist { // query the pattern for events const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); - + let mod2 = t - phase - this.latency; + mod2 = mod2 * this.cps; + console.log(this.clock.duration); haps.forEach((hap) => { if (hap.hasOnset()) { const targetTime = @@ -59,9 +63,14 @@ export class Cyclist { // see https://github.com/tidalcycles/strudel/pull/1004 const deadline = targetTime - phase; + // console.log(); + let cycle = hap.whole.begin.valueOf() + mod2; + + // console.log({ cycle, cycle2, mod2 }); onTrigger?.(hap, deadline, duration, this.cps, targetTime, cycle); } }); + this.lastBegin = begin; this.lastEnd = end; this.lastTick = phase; From c6c5a3035c90f4a19e5cbd747bae5ccbff77806c Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 13 May 2024 21:06:24 -0400 Subject: [PATCH 017/538] fixed non synced clock --- packages/core/controls.mjs | 4 ++-- packages/core/cyclist.mjs | 23 +++++------------------ 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index b8cdb6ed7..06e808be4 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -434,12 +434,13 @@ export const { coarse } = registerControl('coarse'); * modulate the amplitude of a sound with a continuous waveform * * @name am + * @synonyms tremelo * @param {number | Pattern} speed modulation speed in cycles * @example * s("triangle").am("2").amshape("").amdepth(.5) * */ -export const { am } = registerControl(['am', 'amdepth', 'amskew', 'amphase']); +export const { am, tremolo } = registerControl(['am', 'amdepth', 'amskew', 'amphase'], 'tremolo'); /** * depth of amplitude modulation @@ -1582,7 +1583,6 @@ export const { zmod } = registerControl('zmod'); // like crush but scaled differently export const { zcrush } = registerControl('zcrush'); export const { zdelay } = registerControl('zdelay'); -export const { tremolo } = registerControl('tremolo'); export const { zzfx } = registerControl('zzfx'); /** diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 75f92090c..5e2ee378a 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -34,15 +34,7 @@ export class Cyclist { try { const begin = this.lastEnd; - const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; - let cycle2 = this.now(); - //magic number that fixes cycle calcuation, cycle is probably not being calculated correctly - const modifier = this.cps * -interval; - cycle2 = cycle2 + modifier; - // let cycle = begin + (phase - t) * this.cps; - // cycle = begin t - // console.log({ phase, t, cycle2 }); if (phase < t) { // avoid querying haps that are in the past anyway console.log(`skip query: too late`); @@ -51,23 +43,18 @@ export class Cyclist { // query the pattern for events const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); - let mod2 = t - phase - this.latency; - mod2 = mod2 * this.cps; - console.log(this.clock.duration); + const cycle_gap = (t - phase - this.latency) * this.cps; + haps.forEach((hap) => { if (hap.hasOnset()) { const targetTime = (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency; const duration = hap.duration / this.cps; + const cycle = hap.whole.begin.valueOf() + cycle_gap; // the following line is dumb and only here for backwards compatibility // see https://github.com/tidalcycles/strudel/pull/1004 - - const deadline = targetTime - phase; - // console.log(); - let cycle = hap.whole.begin.valueOf() + mod2; - - // console.log({ cycle, cycle2, mod2 }); - onTrigger?.(hap, deadline, duration, this.cps, targetTime, cycle); + const _deadline = targetTime - phase; + onTrigger?.(hap, _deadline, duration, this.cps, targetTime, cycle); } }); From 4ba9d47bfffe26246c2836e816bfaf6006f9060a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 13 May 2024 21:10:43 -0400 Subject: [PATCH 018/538] fix test --- packages/superdough/worklets.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 3fa89ca63..81a9c9086 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -115,12 +115,11 @@ class AMProcessor extends AudioWorkletProcessor { const secondsPassed = cycle / cps; this.phase = _mod(secondsPassed * 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 < input.length; i++) { const modval = clamp(waveshapes.tri(this.phase, skew) * depth + (1 - depth), 0, 1); - output[i][n] = input[i][n] * modval; } this.incrementPhase(dt); From d5ab6b7211201ac0437ec1d9ace20a48de9d3015 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 13 May 2024 22:38:20 -0400 Subject: [PATCH 019/538] fix tests --- test/__snapshots__/examples.test.mjs.snap | 375 +++++++++++++--------- 1 file changed, 225 insertions(+), 150 deletions(-) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 30db05a20..4c98231e5 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -779,6 +779,24 @@ exports[`runs examples > example "always" example index 0 1`] = ` ] `; +exports[`runs examples > example "am" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:triangle am:2 amshape:tri amdepth:0.5 ]", + "[ 1/1 → 2/1 | s:triangle am:2 amshape:saw amdepth:0.5 ]", + "[ 2/1 → 3/1 | s:triangle am:2 amshape:ramp amdepth:0.5 ]", + "[ 3/1 → 4/1 | s:triangle am:2 amshape:square amdepth:0.5 ]", +] +`; + +exports[`runs examples > example "amdepth" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:triangle am:1 amdepth:1 ]", + "[ 1/1 → 2/1 | s:triangle am:1 amdepth:1 ]", + "[ 2/1 → 3/1 | s:triangle am:1 amdepth:1 ]", + "[ 3/1 → 4/1 | s:triangle am:1 amdepth:1 ]", +] +`; + exports[`runs examples > example "amp" example index 0 1`] = ` [ "[ (0/1 → 1/12) ⇝ 1/8 | s:bd amp:0.1 ]", @@ -840,6 +858,213 @@ exports[`runs examples > example "amp" example index 0 1`] = ` ] `; +exports[`runs examples > example "amphase" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:f am:4 amphase:0 ]", + "[ 1/16 → 1/8 | note:a am:4 amphase:0 ]", + "[ 1/8 → 3/16 | note:c am:4 amphase:0 ]", + "[ 3/16 → 1/4 | note:e am:4 amphase:0 ]", + "[ 1/4 → 5/16 | note:f am:4 amphase:0 ]", + "[ 5/16 → 3/8 | note:a am:4 amphase:0 ]", + "[ 3/8 → 7/16 | note:c am:4 amphase:0 ]", + "[ 7/16 → 1/2 | note:e am:4 amphase:0 ]", + "[ 1/2 → 9/16 | note:f am:4 amphase:0 ]", + "[ 9/16 → 5/8 | note:a am:4 amphase:0 ]", + "[ 5/8 → 11/16 | note:c am:4 amphase:0 ]", + "[ 11/16 → 3/4 | note:e am:4 amphase:0 ]", + "[ 3/4 → 13/16 | note:f am:4 amphase:0 ]", + "[ 13/16 → 7/8 | note:a am:4 amphase:0 ]", + "[ 7/8 → 15/16 | note:c am:4 amphase:0 ]", + "[ 15/16 → 1/1 | note:e am:4 amphase:0 ]", + "[ 1/1 → 17/16 | note:f am:4 amphase:0.25 ]", + "[ 17/16 → 9/8 | note:a am:4 amphase:0.25 ]", + "[ 9/8 → 19/16 | note:c am:4 amphase:0.25 ]", + "[ 19/16 → 5/4 | note:e am:4 amphase:0.25 ]", + "[ 5/4 → 21/16 | note:f am:4 amphase:0.25 ]", + "[ 21/16 → 11/8 | note:a am:4 amphase:0.25 ]", + "[ 11/8 → 23/16 | note:c am:4 amphase:0.25 ]", + "[ 23/16 → 3/2 | note:e am:4 amphase:0.25 ]", + "[ 3/2 → 25/16 | note:f am:4 amphase:0.25 ]", + "[ 25/16 → 13/8 | note:a am:4 amphase:0.25 ]", + "[ 13/8 → 27/16 | note:c am:4 amphase:0.25 ]", + "[ 27/16 → 7/4 | note:e am:4 amphase:0.25 ]", + "[ 7/4 → 29/16 | note:f am:4 amphase:0.25 ]", + "[ 29/16 → 15/8 | note:a am:4 amphase:0.25 ]", + "[ 15/8 → 31/16 | note:c am:4 amphase:0.25 ]", + "[ 31/16 → 2/1 | note:e am:4 amphase:0.25 ]", + "[ 2/1 → 33/16 | note:f am:4 amphase:0.66 ]", + "[ 33/16 → 17/8 | note:a am:4 amphase:0.66 ]", + "[ 17/8 → 35/16 | note:c am:4 amphase:0.66 ]", + "[ 35/16 → 9/4 | note:e am:4 amphase:0.66 ]", + "[ 9/4 → 37/16 | note:f am:4 amphase:0.66 ]", + "[ 37/16 → 19/8 | note:a am:4 amphase:0.66 ]", + "[ 19/8 → 39/16 | note:c am:4 amphase:0.66 ]", + "[ 39/16 → 5/2 | note:e am:4 amphase:0.66 ]", + "[ 5/2 → 41/16 | note:f am:4 amphase:0.66 ]", + "[ 41/16 → 21/8 | note:a am:4 amphase:0.66 ]", + "[ 21/8 → 43/16 | note:c am:4 amphase:0.66 ]", + "[ 43/16 → 11/4 | note:e am:4 amphase:0.66 ]", + "[ 11/4 → 45/16 | note:f am:4 amphase:0.66 ]", + "[ 45/16 → 23/8 | note:a am:4 amphase:0.66 ]", + "[ 23/8 → 47/16 | note:c am:4 amphase:0.66 ]", + "[ 47/16 → 3/1 | note:e am:4 amphase:0.66 ]", + "[ 3/1 → 49/16 | note:f am:4 amphase:0 ]", + "[ 49/16 → 25/8 | note:a am:4 amphase:0 ]", + "[ 25/8 → 51/16 | note:c am:4 amphase:0 ]", + "[ 51/16 → 13/4 | note:e am:4 amphase:0 ]", + "[ 13/4 → 53/16 | note:f am:4 amphase:0 ]", + "[ 53/16 → 27/8 | note:a am:4 amphase:0 ]", + "[ 27/8 → 55/16 | note:c am:4 amphase:0 ]", + "[ 55/16 → 7/2 | note:e am:4 amphase:0 ]", + "[ 7/2 → 57/16 | note:f am:4 amphase:0 ]", + "[ 57/16 → 29/8 | note:a am:4 amphase:0 ]", + "[ 29/8 → 59/16 | note:c am:4 amphase:0 ]", + "[ 59/16 → 15/4 | note:e am:4 amphase:0 ]", + "[ 15/4 → 61/16 | note:f am:4 amphase:0 ]", + "[ 61/16 → 31/8 | note:a am:4 amphase:0 ]", + "[ 31/8 → 63/16 | note:c am:4 amphase:0 ]", + "[ 63/16 → 4/1 | note:e am:4 amphase:0 ]", +] +`; + +exports[`runs examples > example "amshape" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 1/16 → 1/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 1/8 → 3/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 3/16 → 1/4 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 1/4 → 5/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 5/16 → 3/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 3/8 → 7/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 7/16 → 1/2 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 1/2 → 9/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 9/16 → 5/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 5/8 → 11/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 11/16 → 3/4 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 3/4 → 13/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 13/16 → 7/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 7/8 → 15/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 15/16 → 1/1 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 1/1 → 17/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 17/16 → 9/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 9/8 → 19/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 19/16 → 5/4 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 5/4 → 21/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 21/16 → 11/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 11/8 → 23/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 23/16 → 3/2 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 3/2 → 25/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 25/16 → 13/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 13/8 → 27/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 27/16 → 7/4 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 7/4 → 29/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 29/16 → 15/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 15/8 → 31/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 31/16 → 2/1 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 2/1 → 33/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 33/16 → 17/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 17/8 → 35/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 35/16 → 9/4 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 9/4 → 37/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 37/16 → 19/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 19/8 → 39/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 39/16 → 5/2 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 5/2 → 41/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 41/16 → 21/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 21/8 → 43/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 43/16 → 11/4 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 11/4 → 45/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 45/16 → 23/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 23/8 → 47/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 47/16 → 3/1 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 3/1 → 49/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 49/16 → 25/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 25/8 → 51/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 51/16 → 13/4 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 13/4 → 53/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 53/16 → 27/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 27/8 → 55/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 55/16 → 7/2 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 7/2 → 57/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 57/16 → 29/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 29/8 → 59/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 59/16 → 15/4 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 15/4 → 61/16 | note:f am:4 amshape:ramp s:sawtooth ]", + "[ 61/16 → 31/8 | note:g am:4 amshape:ramp s:sawtooth ]", + "[ 31/8 → 63/16 | note:c am:4 amshape:ramp s:sawtooth ]", + "[ 63/16 → 4/1 | note:d am:4 amshape:ramp s:sawtooth ]", +] +`; + +exports[`runs examples > example "amskew" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:f am:4 amskew:0.5 ]", + "[ 1/16 → 1/8 | note:a am:4 amskew:0.5 ]", + "[ 1/8 → 3/16 | note:c am:4 amskew:0.5 ]", + "[ 3/16 → 1/4 | note:e am:4 amskew:0.5 ]", + "[ 1/4 → 5/16 | note:f am:4 amskew:0.5 ]", + "[ 5/16 → 3/8 | note:a am:4 amskew:0.5 ]", + "[ 3/8 → 7/16 | note:c am:4 amskew:0.5 ]", + "[ 7/16 → 1/2 | note:e am:4 amskew:0.5 ]", + "[ 1/2 → 9/16 | note:f am:4 amskew:0.5 ]", + "[ 9/16 → 5/8 | note:a am:4 amskew:0.5 ]", + "[ 5/8 → 11/16 | note:c am:4 amskew:0.5 ]", + "[ 11/16 → 3/4 | note:e am:4 amskew:0.5 ]", + "[ 3/4 → 13/16 | note:f am:4 amskew:0.5 ]", + "[ 13/16 → 7/8 | note:a am:4 amskew:0.5 ]", + "[ 7/8 → 15/16 | note:c am:4 amskew:0.5 ]", + "[ 15/16 → 1/1 | note:e am:4 amskew:0.5 ]", + "[ 1/1 → 17/16 | note:f am:4 amskew:0 ]", + "[ 17/16 → 9/8 | note:a am:4 amskew:0 ]", + "[ 9/8 → 19/16 | note:c am:4 amskew:0 ]", + "[ 19/16 → 5/4 | note:e am:4 amskew:0 ]", + "[ 5/4 → 21/16 | note:f am:4 amskew:0 ]", + "[ 21/16 → 11/8 | note:a am:4 amskew:0 ]", + "[ 11/8 → 23/16 | note:c am:4 amskew:0 ]", + "[ 23/16 → 3/2 | note:e am:4 amskew:0 ]", + "[ 3/2 → 25/16 | note:f am:4 amskew:0 ]", + "[ 25/16 → 13/8 | note:a am:4 amskew:0 ]", + "[ 13/8 → 27/16 | note:c am:4 amskew:0 ]", + "[ 27/16 → 7/4 | note:e am:4 amskew:0 ]", + "[ 7/4 → 29/16 | note:f am:4 amskew:0 ]", + "[ 29/16 → 15/8 | note:a am:4 amskew:0 ]", + "[ 15/8 → 31/16 | note:c am:4 amskew:0 ]", + "[ 31/16 → 2/1 | note:e am:4 amskew:0 ]", + "[ 2/1 → 33/16 | note:f am:4 amskew:1 ]", + "[ 33/16 → 17/8 | note:a am:4 amskew:1 ]", + "[ 17/8 → 35/16 | note:c am:4 amskew:1 ]", + "[ 35/16 → 9/4 | note:e am:4 amskew:1 ]", + "[ 9/4 → 37/16 | note:f am:4 amskew:1 ]", + "[ 37/16 → 19/8 | note:a am:4 amskew:1 ]", + "[ 19/8 → 39/16 | note:c am:4 amskew:1 ]", + "[ 39/16 → 5/2 | note:e am:4 amskew:1 ]", + "[ 5/2 → 41/16 | note:f am:4 amskew:1 ]", + "[ 41/16 → 21/8 | note:a am:4 amskew:1 ]", + "[ 21/8 → 43/16 | note:c am:4 amskew:1 ]", + "[ 43/16 → 11/4 | note:e am:4 amskew:1 ]", + "[ 11/4 → 45/16 | note:f am:4 amskew:1 ]", + "[ 45/16 → 23/8 | note:a am:4 amskew:1 ]", + "[ 23/8 → 47/16 | note:c am:4 amskew:1 ]", + "[ 47/16 → 3/1 | note:e am:4 amskew:1 ]", + "[ 3/1 → 49/16 | note:f am:4 amskew:0.5 ]", + "[ 49/16 → 25/8 | note:a am:4 amskew:0.5 ]", + "[ 25/8 → 51/16 | note:c am:4 amskew:0.5 ]", + "[ 51/16 → 13/4 | note:e am:4 amskew:0.5 ]", + "[ 13/4 → 53/16 | note:f am:4 amskew:0.5 ]", + "[ 53/16 → 27/8 | note:a am:4 amskew:0.5 ]", + "[ 27/8 → 55/16 | note:c am:4 amskew:0.5 ]", + "[ 55/16 → 7/2 | note:e am:4 amskew:0.5 ]", + "[ 7/2 → 57/16 | note:f am:4 amskew:0.5 ]", + "[ 57/16 → 29/8 | note:a am:4 amskew:0.5 ]", + "[ 29/8 → 59/16 | note:c am:4 amskew:0.5 ]", + "[ 59/16 → 15/4 | note:e am:4 amskew:0.5 ]", + "[ 15/4 → 61/16 | note:f am:4 amskew:0.5 ]", + "[ 61/16 → 31/8 | note:a am:4 amskew:0.5 ]", + "[ 31/8 → 63/16 | note:c am:4 amskew:0.5 ]", + "[ 63/16 → 4/1 | note:e am:4 amskew:0.5 ]", +] +`; + exports[`runs examples > example "apply" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:C3 ]", @@ -5035,72 +5260,6 @@ exports[`runs examples > example "ply" example index 0 1`] = ` ] `; -exports[`runs examples > example "polymeter" example index 0 1`] = ` -[ - "[ 0/1 → 1/3 | c ]", - "[ 0/1 → 1/3 | c2 ]", - "[ 1/3 → 2/3 | eb ]", - "[ 1/3 → 2/3 | g2 ]", - "[ 2/3 → 1/1 | g ]", - "[ 2/3 → 1/1 | c2 ]", - "[ 1/1 → 4/3 | c ]", - "[ 1/1 → 4/3 | g2 ]", - "[ 4/3 → 5/3 | eb ]", - "[ 4/3 → 5/3 | c2 ]", - "[ 5/3 → 2/1 | g ]", - "[ 5/3 → 2/1 | g2 ]", - "[ 2/1 → 7/3 | c ]", - "[ 2/1 → 7/3 | c2 ]", - "[ 7/3 → 8/3 | eb ]", - "[ 7/3 → 8/3 | g2 ]", - "[ 8/3 → 3/1 | g ]", - "[ 8/3 → 3/1 | c2 ]", - "[ 3/1 → 10/3 | c ]", - "[ 3/1 → 10/3 | g2 ]", - "[ 10/3 → 11/3 | eb ]", - "[ 10/3 → 11/3 | c2 ]", - "[ 11/3 → 4/1 | g ]", - "[ 11/3 → 4/1 | g2 ]", -] -`; - -exports[`runs examples > example "polymeterSteps" example index 0 1`] = ` -[ - "[ 0/1 → 1/4 | c ]", - "[ 0/1 → 1/4 | e ]", - "[ 1/4 → 1/2 | d ]", - "[ 1/4 → 1/2 | f ]", - "[ 1/2 → 3/4 | c ]", - "[ 1/2 → 3/4 | g ]", - "[ 3/4 → 1/1 | d ]", - "[ 3/4 → 1/1 | e ]", - "[ 1/1 → 5/4 | c ]", - "[ 1/1 → 5/4 | f ]", - "[ 5/4 → 3/2 | d ]", - "[ 5/4 → 3/2 | g ]", - "[ 3/2 → 7/4 | c ]", - "[ 3/2 → 7/4 | e ]", - "[ 7/4 → 2/1 | d ]", - "[ 7/4 → 2/1 | f ]", - "[ 2/1 → 9/4 | c ]", - "[ 2/1 → 9/4 | g ]", - "[ 9/4 → 5/2 | d ]", - "[ 9/4 → 5/2 | e ]", - "[ 5/2 → 11/4 | c ]", - "[ 5/2 → 11/4 | f ]", - "[ 11/4 → 3/1 | d ]", - "[ 11/4 → 3/1 | g ]", - "[ 3/1 → 13/4 | c ]", - "[ 3/1 → 13/4 | e ]", - "[ 13/4 → 7/2 | d ]", - "[ 13/4 → 7/2 | f ]", - "[ 7/2 → 15/4 | c ]", - "[ 7/2 → 15/4 | g ]", - "[ 15/4 → 4/1 | d ]", - "[ 15/4 → 4/1 | e ]", -] -`; - exports[`runs examples > example "postgain" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", @@ -7216,31 +7375,6 @@ exports[`runs examples > example "stack" example index 0 2`] = ` ] `; -exports[`runs examples > example "stepcat" example index 0 1`] = ` -[ - "[ 0/1 → 1/5 | s:bd ]", - "[ 1/5 → 2/5 | s:cp ]", - "[ 2/5 → 3/5 | s:bd ]", - "[ 3/5 → 4/5 | s:mt ]", - "[ 4/5 → 1/1 | s:bd ]", - "[ 1/1 → 6/5 | s:bd ]", - "[ 6/5 → 7/5 | s:cp ]", - "[ 7/5 → 8/5 | s:bd ]", - "[ 8/5 → 9/5 | s:mt ]", - "[ 9/5 → 2/1 | s:bd ]", - "[ 2/1 → 11/5 | s:bd ]", - "[ 11/5 → 12/5 | s:cp ]", - "[ 12/5 → 13/5 | s:bd ]", - "[ 13/5 → 14/5 | s:mt ]", - "[ 14/5 → 3/1 | s:bd ]", - "[ 3/1 → 16/5 | s:bd ]", - "[ 16/5 → 17/5 | s:cp ]", - "[ 17/5 → 18/5 | s:bd ]", - "[ 18/5 → 19/5 | s:mt ]", - "[ 19/5 → 4/1 | s:bd ]", -] -`; - exports[`runs examples > example "steps" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd ]", @@ -7554,65 +7688,6 @@ exports[`runs examples > example "swingBy" example index 0 1`] = ` ] `; -exports[`runs examples > example "timecat" example index 0 1`] = ` -[ - "[ 0/1 → 3/4 | note:e3 ]", - "[ 3/4 → 1/1 | note:g3 ]", - "[ 1/1 → 7/4 | note:e3 ]", - "[ 7/4 → 2/1 | note:g3 ]", - "[ 2/1 → 11/4 | note:e3 ]", - "[ 11/4 → 3/1 | note:g3 ]", - "[ 3/1 → 15/4 | note:e3 ]", - "[ 15/4 → 4/1 | note:g3 ]", -] -`; - -exports[`runs examples > example "timecat" example index 1 1`] = ` -[ - "[ 0/1 → 1/5 | s:bd ]", - "[ 1/5 → 2/5 | s:sd ]", - "[ 2/5 → 3/5 | s:cp ]", - "[ 3/5 → 4/5 | s:hh ]", - "[ 4/5 → 1/1 | s:hh ]", - "[ 1/1 → 6/5 | s:bd ]", - "[ 6/5 → 7/5 | s:sd ]", - "[ 7/5 → 8/5 | s:cp ]", - "[ 8/5 → 9/5 | s:hh ]", - "[ 9/5 → 2/1 | s:hh ]", - "[ 2/1 → 11/5 | s:bd ]", - "[ 11/5 → 12/5 | s:sd ]", - "[ 12/5 → 13/5 | s:cp ]", - "[ 13/5 → 14/5 | s:hh ]", - "[ 14/5 → 3/1 | s:hh ]", - "[ 3/1 → 16/5 | s:bd ]", - "[ 16/5 → 17/5 | s:sd ]", - "[ 17/5 → 18/5 | s:cp ]", - "[ 18/5 → 19/5 | s:hh ]", - "[ 19/5 → 4/1 | s:hh ]", -] -`; - -exports[`runs examples > example "toTactus" example index 0 1`] = ` -[ - "[ 0/1 → 1/4 | s:bd ]", - "[ 1/4 → 1/2 | s:sd ]", - "[ 1/2 → 3/4 | s:cp ]", - "[ 3/4 → 1/1 | s:bd ]", - "[ 1/1 → 5/4 | s:sd ]", - "[ 5/4 → 3/2 | s:cp ]", - "[ 3/2 → 7/4 | s:bd ]", - "[ 7/4 → 2/1 | s:sd ]", - "[ 2/1 → 9/4 | s:cp ]", - "[ 9/4 → 5/2 | s:bd ]", - "[ 5/2 → 11/4 | s:sd ]", - "[ 11/4 → 3/1 | s:cp ]", - "[ 3/1 → 13/4 | s:bd ]", - "[ 13/4 → 7/2 | s:sd ]", - "[ 7/2 → 15/4 | s:cp ]", - "[ 15/4 → 4/1 | s:bd ]", -] -`; - exports[`runs examples > example "transpose" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C2 ]", From 4a181d5b335611833b1b38dbe039ae4f520224d1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 13 May 2024 23:02:36 -0400 Subject: [PATCH 020/538] updated non synced clock to work more like synced clock --- packages/core/cyclist.mjs | 93 ++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 5e2ee378a..893a7619e 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -11,60 +11,53 @@ export class Cyclist { constructor({ interval = 0.05, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) { this.started = false; this.cps = 0.5; - this.num_ticks_since_cps_change = 0; - this.lastTick = 0; // absolute time when last tick (clock callback) happened - this.lastBegin = 0; // query begin of last tick - this.lastEnd = 0; // query end of last tick + this.time_at_last_tick_message = 0; + this.cycle = 0; this.getTime = getTime; // get absolute time this.num_cycles_at_cps_change = 0; this.seconds_at_cps_change; // clock phase when cps was changed + this.num_ticks_since_cps_change = 0; this.onToggle = onToggle; this.latency = latency; // fixed trigger time offset + + this.interval = interval; + this.clock = createClock( getTime, // called slightly before each cycle - (phase, duration, _, t) => { - if (this.num_ticks_since_cps_change === 0) { - this.num_cycles_at_cps_change = this.lastEnd; - this.seconds_at_cps_change = phase; - } - this.num_ticks_since_cps_change++; - const seconds_since_cps_change = this.num_ticks_since_cps_change * duration; - const num_cycles_since_cps_change = seconds_since_cps_change * this.cps; + (phase, duration, _, time) => { + const num_seconds_since_cps_change = this.num_ticks_since_cps_change * duration; + const tickdeadline = phase - time; + const lastTick = time + tickdeadline; + const num_cycles_since_cps_change = num_seconds_since_cps_change * this.cps; + const begin = this.num_cycles_at_cps_change + num_cycles_since_cps_change; + const secondsSinceLastTick = time - lastTick - duration; + const eventLength = duration * this.cps; + const end = begin + eventLength; + this.cycle = begin + secondsSinceLastTick * this.cps; + const num_seconds_at_cps_change = this.num_cycles_at_cps_change / this.cps; + const time_dif = time - (num_seconds_at_cps_change + num_seconds_since_cps_change) + tickdeadline; - try { - const begin = this.lastEnd; - const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; - if (phase < t) { - // avoid querying haps that are in the past anyway - console.log(`skip query: too late`); - return; + if (this.started === false) { + return; + } + + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + + //account for latency and tick duration when using cycle calculations for audio downstream + const cycle_gap = (this.latency - duration) * this.cps; + + haps.forEach((hap) => { + if (hap.hasOnset()) { + let targetTime = (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps; + targetTime = targetTime + num_seconds_at_cps_change + this.latency + time_dif; + const duration = hap.duration / this.cps; + onTrigger?.(hap, tickdeadline, duration, this.cps, targetTime, this.cycle - cycle_gap); } + }); - // query the pattern for events - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); - const cycle_gap = (t - phase - this.latency) * this.cps; - - haps.forEach((hap) => { - if (hap.hasOnset()) { - const targetTime = - (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency; - const duration = hap.duration / this.cps; - const cycle = hap.whole.begin.valueOf() + cycle_gap; - // the following line is dumb and only here for backwards compatibility - // see https://github.com/tidalcycles/strudel/pull/1004 - const _deadline = targetTime - phase; - onTrigger?.(hap, _deadline, duration, this.cps, targetTime, cycle); - } - }); - - this.lastBegin = begin; - this.lastEnd = end; - this.lastTick = phase; - } catch (e) { - logger(`[cyclist] error: ${e.message}`); - onError?.(e); - } + this.time_at_last_tick_message = this.getTime(); + this.num_ticks_since_cps_change++; }, interval, // duration of each cycle 0.1, @@ -77,16 +70,24 @@ export class Cyclist { if (!this.started) { return 0; } - const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; - return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; + const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps; + return this.cycle + gap; } + + setCycle = (cycle) => { + this.num_ticks_since_cps_change = 0; + this.num_cycles_at_cps_change = cycle; + }; setStarted(v) { this.started = v; + + this.setCycle(0); this.onToggle?.(v); } start() { this.num_ticks_since_cps_change = 0; this.num_cycles_at_cps_change = 0; + if (!this.pattern) { throw new Error('Scheduler: no pattern set! call .setPattern first.'); } @@ -115,6 +116,8 @@ export class Cyclist { if (this.cps === cps) { return; } + const num_seconds_since_cps_change = this.num_ticks_since_cps_change * this.interval; + this.num_cycles_at_cps_change = this.num_cycles_at_cps_change + num_seconds_since_cps_change * cps; this.cps = cps; this.num_ticks_since_cps_change = 0; } From 8ad48e1633ff5c26e1d29b7257c96f855f4764f0 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 13 May 2024 23:02:48 -0400 Subject: [PATCH 021/538] remove unesseccary time call --- packages/core/cyclist.mjs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 893a7619e..55dcb35b6 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -26,6 +26,9 @@ export class Cyclist { getTime, // called slightly before each cycle (phase, duration, _, time) => { + if (this.started === false) { + return; + } const num_seconds_since_cps_change = this.num_ticks_since_cps_change * duration; const tickdeadline = phase - time; const lastTick = time + tickdeadline; @@ -38,14 +41,9 @@ export class Cyclist { const num_seconds_at_cps_change = this.num_cycles_at_cps_change / this.cps; const time_dif = time - (num_seconds_at_cps_change + num_seconds_since_cps_change) + tickdeadline; - if (this.started === false) { - return; - } - - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); - //account for latency and tick duration when using cycle calculations for audio downstream const cycle_gap = (this.latency - duration) * this.cps; + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); haps.forEach((hap) => { if (hap.hasOnset()) { @@ -55,8 +53,7 @@ export class Cyclist { onTrigger?.(hap, tickdeadline, duration, this.cps, targetTime, this.cycle - cycle_gap); } }); - - this.time_at_last_tick_message = this.getTime(); + this.time_at_last_tick_message = time; this.num_ticks_since_cps_change++; }, interval, // duration of each cycle From bee8c812548e795b8ce04324b3929ec1afe52b89 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 13 May 2024 23:14:30 -0400 Subject: [PATCH 022/538] simplify targettime --- packages/core/cyclist.mjs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 55dcb35b6..a170ca3a5 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -38,17 +38,15 @@ export class Cyclist { const eventLength = duration * this.cps; const end = begin + eventLength; this.cycle = begin + secondsSinceLastTick * this.cps; - const num_seconds_at_cps_change = this.num_cycles_at_cps_change / this.cps; - const time_dif = time - (num_seconds_at_cps_change + num_seconds_since_cps_change) + tickdeadline; //account for latency and tick duration when using cycle calculations for audio downstream const cycle_gap = (this.latency - duration) * this.cps; - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); haps.forEach((hap) => { if (hap.hasOnset()) { let targetTime = (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps; - targetTime = targetTime + num_seconds_at_cps_change + this.latency + time_dif; + targetTime = targetTime + this.latency + tickdeadline + time - num_seconds_since_cps_change; const duration = hap.duration / this.cps; onTrigger?.(hap, tickdeadline, duration, this.cps, targetTime, this.cycle - cycle_gap); } From 62705da3deef4f9ceb7aaaa031ccc13d7bb3ad20 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 15 Mar 2025 10:29:23 +0100 Subject: [PATCH 023/538] begin uzu package --- packages/uzu/README.md | 29 ++++ packages/uzu/package.json | 37 +++++ packages/uzu/test/uzu.test.mjs | 72 ++++++++ packages/uzu/uzu.mjs | 294 +++++++++++++++++++++++++++++++++ packages/uzu/vite.config.js | 19 +++ pnpm-lock.yaml | 14 +- 6 files changed, 462 insertions(+), 3 deletions(-) create mode 100644 packages/uzu/README.md create mode 100644 packages/uzu/package.json create mode 100644 packages/uzu/test/uzu.test.mjs create mode 100644 packages/uzu/uzu.mjs create mode 100644 packages/uzu/vite.config.js diff --git a/packages/uzu/README.md b/packages/uzu/README.md new file mode 100644 index 000000000..53eef51f8 --- /dev/null +++ b/packages/uzu/README.md @@ -0,0 +1,29 @@ +# uzu + +an experimental parser for an *uzulang*, a custom dsl for patterns that can stand on its own feet. more info: + +- [uzulang I](https://garten.salat.dev/uzu/uzulang1.html) +- [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) + +```js +import { UzuRunner } from 'uzu' + +const runner = UzuRunner({ seq, cat, s, crush, speed, '*': fast }); +const pat = runner.run('s [bd hh*2 cp.(crush 4) ] . speed .8') +``` + +the above code will create the following call structure: + +```lisp +(speed + (s + (seq bd + (* hh 2) + (crush cp 4) + (cat mt ht lt) + ) + ) .8 +) +``` + +you can pass all available functions to *UzuRunner* as an object. diff --git a/packages/uzu/package.json b/packages/uzu/package.json new file mode 100644 index 000000000..9b59078d5 --- /dev/null +++ b/packages/uzu/package.json @@ -0,0 +1,37 @@ +{ + "name": "uzu", + "version": "1.1.0", + "description": "an uzu notation", + "main": "uzu.mjs", + "type": "module", + "publishConfig": { + "main": "dist/uzu.mjs" + }, + "scripts": { + "test": "vitest run", + "bench": "vitest bench", + "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/blob/main/packages/uzu/README.md", + "devDependencies": { + "vite": "^6.0.11", + "vitest": "^3.0.4" + } +} diff --git a/packages/uzu/test/uzu.test.mjs b/packages/uzu/test/uzu.test.mjs new file mode 100644 index 000000000..c58eb1a86 --- /dev/null +++ b/packages/uzu/test/uzu.test.mjs @@ -0,0 +1,72 @@ +/* +uzu.test.mjs - +Copyright (C) 2022 Strudel contributors - see +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 { describe, expect, it } from 'vitest'; +import { UzuParser, UzuRunner, printAst } from '../uzu.mjs'; + +const parser = new UzuParser(); +const p = (code) => parser.parse(code); + +describe('uzu s-expressions parser', () => { + it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] })); + it('should parse a single item', () => + expect(p('a')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] })); + it('should parse an empty list', () => expect(p('()')).toEqual({ type: 'list', children: [] })); + it('should parse a list with 1 item', () => + expect(p('(a)')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] })); + it('should parse a list with 2 items', () => + expect(p('(a b)')).toEqual({ + type: 'list', + children: [ + { type: 'plain', value: 'a' }, + { type: 'plain', value: 'b' }, + ], + })); + it('should parse a list with 2 items', () => + expect(p('(a (b c))')).toEqual({ + type: 'list', + children: [ + { type: 'plain', value: 'a' }, + { + type: 'list', + children: [ + { type: 'plain', value: 'b' }, + { type: 'plain', value: 'c' }, + ], + }, + ], + })); + it('should parse numbers', () => + expect(p('(1 .2 1.2 10 22.3)')).toEqual({ + type: 'list', + children: [ + { type: 'number', value: '1' }, + { type: 'number', value: '.2' }, + { type: 'number', value: '1.2' }, + { type: 'number', value: '10' }, + { type: 'number', value: '22.3' }, + ], + })); +}); + +let desguar = (a) => { + return printAst(parser.parse(a), true); +}; + +describe('uzu sugar', () => { + it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(seq a b c)')); + it('should desugar [] nested', () => expect(desguar('[a [b c]]')).toEqual('(seq a (seq b c))')); + it('should desugar <>', () => expect(desguar('')).toEqual('(cat a b c)')); + it('should desugar <> nested', () => expect(desguar('>')).toEqual('(cat a (cat b c))')); + it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(seq a (cat b c))')); + it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(cat a (seq b c))')); + it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast (s jazz) 2)')); + it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow (fast (s jazz) 2) 2)')); + it('should desugar README example', () => + expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( + '(speed (s (seq bd (* hh 2) (crush cp 4) (cat mt ht lt))) .8)', + )); +}); diff --git a/packages/uzu/uzu.mjs b/packages/uzu/uzu.mjs new file mode 100644 index 000000000..ead760be0 --- /dev/null +++ b/packages/uzu/uzu.mjs @@ -0,0 +1,294 @@ +/* +uzu.mjs - +Copyright (C) 2022 Strudel contributors - see +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 . +*/ + +// evolved from https://garten.salat.dev/lisp/parser.html +export class UzuParser { + // these are the tokens we expect + token_types = { + string: /^\"(.*?)\"/, + open_list: /^\(/, + close_list: /^\)/, + open_cat: /^\/, + open_seq: /^\[/, + close_seq: /^\]/, + number: /^[0-9]*\.?[0-9]+/, // before pipe! + pipe: /^\./, + stack: /^\,/, + op: /^[\*\/]/, + plain: /^[a-zA-Z0-9\-]+/, + }; + // matches next token + next_token(code) { + for (let type in this.token_types) { + const match = code.match(this.token_types[type]); + if (match) { + return { type, value: match[0] }; + } + } + throw new Error(`zilp: could not match '${code}'`); + } + // takes code string, returns list of matched tokens (if valid) + tokenize(code) { + let tokens = []; + while (code.length > 0) { + code = code.trim(); + const token = this.next_token(code); + code = code.slice(token.value.length); + tokens.push(token); + } + return tokens; + } + // take code, return abstract syntax tree + parse(code) { + this.tokens = this.tokenize(code); + const expressions = []; + while (this.tokens.length) { + expressions.push(this.parse_expr()); + } + if (expressions.length === 0) { + // empty case + return { type: 'list', children: [] }; + } + // do we have multiple top level expressions or a single non list? + if (expressions.length > 1 || expressions[0].type !== 'list') { + return { + type: 'list', + children: this.desugar_children(expressions), + }; + } + // we have a single list + return expressions[0]; + } + // parses any valid expression + parse_expr() { + if (!this.tokens[0]) { + throw new Error(`unexpected end of file`); + } + let next = this.tokens[0]?.type; + if (next === 'open_list') { + return this.parse_list(); + } + if (next === 'open_cat') { + return this.parse_cat(); + } + if (next === 'open_seq') { + return this.parse_seq(); + } + return this.consume(next); + } + desugar_children(children) { + children = this.resolve_ops(children); + children = this.resolve_pipes(children); + return children; + } + // Token[] => Token[][] . returns empty list if type not found + split_children(children, type) { + const chunks = []; + while (true) { + let commaIndex = children.findIndex((child) => child.type === type); + if (commaIndex === -1) break; + const chunk = children.slice(0, commaIndex); + chunks.push(chunk); + children = children.slice(commaIndex + 1); + } + if (!chunks.length) { + return []; + } + chunks.push(children); + return chunks; + } + desugar_stack(children) { + let [type, ...rest] = children; + // children is expected to contain seq or cat as first item + const chunks = this.split_children(rest, 'stack'); + if (!chunks.length) { + // no stack + return children; + } + // collect args of stack function + const args = chunks.map((chunk) => { + if (chunk.length === 1) { + // chunks of one element can be added to the stack as is + return chunk[0]; + } else { + // chunks of multiple args are added to a subsequence of type + return { type: 'list', children: [type, ...chunk] }; + } + }); + return [{ type: 'plain', value: 'stack' }, ...args]; + } + resolve_ops(children) { + while (true) { + let opIndex = children.findIndex((child) => child.type === 'op'); + if (opIndex === -1) break; + const op = { type: 'plain', value: children[opIndex].value }; + if (opIndex === children.length - 1) { + throw new Error(`cannot use operator as last child.`); + } + if (opIndex === 0) { + // regular function call (assuming each operator exists as function) + children[opIndex] = op; + continue; + } + const left = children[opIndex - 1]; + const right = children[opIndex + 1]; + if (left.type === 'pipe') { + // "x !* 2" => (* 2 x) + children[opIndex] = op; + continue; + } + // convert infix to prefix notation + const call = { type: 'list', children: [op, left, right] }; + // insert call while keeping other siblings + children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; + // unwrap double list.. e.g. (s jazz) * 2 + if (children.length === 1) { + // there might be a cleaner solution + children = children[0].children; + } + } + return children; + } + resolve_pipes(children) { + while (true) { + let pipeIndex = children.findIndex((child) => child.type === 'pipe'); + // no pipe => we're done + if (pipeIndex === -1) break; + // pipe up front => lambda + if (pipeIndex === 0) { + // . as lambda: (.fast 2) = x=>x.fast(2) + // TODO: this doesn't work for (.fast 2 .speed 2) + // probably needs proper ast representation of lambda + children[pipeIndex] = { type: 'plain', value: '.' }; + continue; + } + const rightSide = children.slice(pipeIndex + 2); + const right = children[pipeIndex + 1]; + if (right.type === 'list') { + // apply function only to left sibling (high precedence) + // s jazz.(fast 2) => s (fast jazz 2) + const [callee, ...rest] = right.children; + const leftSide = children.slice(0, pipeIndex - 1); + const left = children[pipeIndex - 1]; + const args = [callee, left, ...rest]; + const call = { type: 'list', children: args }; + children = [...leftSide, call, ...rightSide]; + } else { + // apply function to all left siblings (low precedence) + // s jazz . fast 2 => fast (s jazz) 2 + let leftSide = children.slice(0, pipeIndex); + if (leftSide.length === 1) { + leftSide = leftSide[0]; + } else { + // wrap in (..) if multiple items on the left side + leftSide = { type: 'list', children: leftSide }; + } + children = [right, leftSide, ...rightSide]; + } + } + return children; + } + parse_pair(open_type, close_type) { + this.consume(open_type); + const children = []; + while (this.tokens[0]?.type !== close_type) { + children.push(this.parse_expr()); + } + this.consume(close_type); + return children; + } + parse_list() { + let children = this.parse_pair('open_list', 'close_list'); + children = this.desugar_children(children); + return { type: 'list', children }; + } + parse_cat() { + let children = this.parse_pair('open_cat', 'close_cat'); + children = [{ type: 'plain', value: 'cat' }, ...children]; + children = this.desugar_children(children); + children = this.desugar_stack(children, 'cat'); + return { type: 'list', children }; + } + parse_seq() { + let children = this.parse_pair('open_seq', 'close_seq'); + children = [{ type: 'plain', value: 'seq' }, ...children]; + children = this.desugar_children(children); + children = this.desugar_stack(children, 'seq'); + return { type: 'list', children }; + } + consume(type) { + // shift removes first element and returns it + const token = this.tokens.shift(); + if (token.type !== type) { + throw new Error(`expected token type ${type}, got ${token.type}`); + } + return token; + } +} + +export function printAst(ast, compact = false, lvl = 0) { + const br = compact ? '' : '\n'; + const spaces = compact ? '' : Array(lvl).fill(' ').join(''); + if (ast.type === 'list') { + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ + ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; + } + return `${ast.value}`; +} + +// lisp runner +export class UzuRunner { + constructor(lib) { + this.parser = new UzuParser(); + this.lib = lib; + } + // a helper to check conditions and throw if they are not met + assert(condition, error) { + if (!condition) { + throw new Error(error); + } + } + run(code) { + const ast = this.parser.parse(code); + return this.call(ast); + } + call(ast) { + // for a node to be callable, it needs to be a list + this.assert(ast.type === 'list', `function call: expected list, got ${ast.type}`); + // the first element is expected to be the function name + this.assert(ast.children[0]?.type === 'plain', `function call: expected first child to be plain, got ${ast.type}`); + + // process args + const args = ast.children.slice(1).map((arg) => { + if (arg.type === 'string') { + return this.lib.string(arg.value.slice(1, -1)); + } + if (arg.type === 'plain') { + return this.lib.plain(arg.value); + } + if (arg.type === 'number') { + return this.lib.number(Number(arg.value)); + } + return this.call(arg); + }); + + const name = ast.children[0].value; + if (name === '.') { + // lambda : (.fast 2) = x=>fast(2, x) + const callee = ast.children[1].value; + const innerFn = this.lib[callee]; + this.assert(innerFn, `function call: unknown function name "${callee}"`); + return (pat) => innerFn(pat, args.slice(1)); + } + + // look up function in lib + const fn = this.lib[name]; + this.assert(fn, `function call: unknown function name "${name}"`); + return fn(...args); + } +} diff --git a/packages/uzu/vite.config.js b/packages/uzu/vite.config.js new file mode 100644 index 000000000..e61a69e40 --- /dev/null +++ b/packages/uzu/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +//import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'uzu.mjs'), + formats: ['es'], + fileName: (ext) => ({ es: 'uzu.mjs' })[ext], + }, + rollupOptions: { + // external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32fd711e2..bbe64f07a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,7 +41,7 @@ importers: version: 2.2.7 '@vitest/coverage-v8': specifier: 3.0.4 - version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)) + version: 3.0.4(vitest@3.0.4) '@vitest/ui': specifier: ^3.0.4 version: 3.0.4(vitest@3.0.4) @@ -540,6 +540,15 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/uzu: + 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) + vitest: + specifier: ^3.0.4 + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/web: dependencies: '@strudel/core': @@ -7561,7 +7570,6 @@ 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==} @@ -10248,7 +10256,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))': + '@vitest/coverage-v8@3.0.4(vitest@3.0.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 From dbe3915368e0404e56523adddaa18038ca0cac49 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 15 Mar 2025 10:35:22 +0100 Subject: [PATCH 024/538] fix: lint errors --- packages/uzu/uzu.mjs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/uzu/uzu.mjs b/packages/uzu/uzu.mjs index ead760be0..6a5157811 100644 --- a/packages/uzu/uzu.mjs +++ b/packages/uzu/uzu.mjs @@ -8,18 +8,18 @@ This program is free software: you can redistribute it and/or modify it under th export class UzuParser { // these are the tokens we expect token_types = { - string: /^\"(.*?)\"/, + string: /^"(.*?)"/, open_list: /^\(/, close_list: /^\)/, - open_cat: /^\/, + open_cat: /^/, open_seq: /^\[/, close_seq: /^\]/, number: /^[0-9]*\.?[0-9]+/, // before pipe! pipe: /^\./, - stack: /^\,/, - op: /^[\*\/]/, - plain: /^[a-zA-Z0-9\-]+/, + stack: /^,/, + op: /^[*/]/, + plain: /^[a-zA-Z0-9-]+/, }; // matches next token next_token(code) { From b71b1354c3c8dcdfe6b8c79d1a170c9d1ae3759e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 00:51:59 +0100 Subject: [PATCH 025/538] uzu: stacks now work within () + write more tests --- packages/uzu/test/uzu.test.mjs | 23 +++++++++++++++++++++-- packages/uzu/uzu.mjs | 31 ++++++++++++++++++++----------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/packages/uzu/test/uzu.test.mjs b/packages/uzu/test/uzu.test.mjs index c58eb1a86..33451f0cf 100644 --- a/packages/uzu/test/uzu.test.mjs +++ b/packages/uzu/test/uzu.test.mjs @@ -58,13 +58,32 @@ let desguar = (a) => { describe('uzu sugar', () => { it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(seq a b c)')); - it('should desugar [] nested', () => expect(desguar('[a [b c]]')).toEqual('(seq a (seq b c))')); + it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(seq a (seq b c) d)')); it('should desugar <>', () => expect(desguar('')).toEqual('(cat a b c)')); - it('should desugar <> nested', () => expect(desguar('>')).toEqual('(cat a (cat b c))')); + it('should desugar <> nested', () => expect(desguar(' d>')).toEqual('(cat a (cat b c) d)')); it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(seq a (cat b c))')); it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(cat a (seq b c))')); + it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast (s jazz) 2)')); + it('should desugar . seq', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (seq bd cp) 2)')); it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow (fast (s jazz) 2) 2)')); + it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast (s cp) 2)')); + it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (seq bd cp) 2)')); + it('should desugar . within , within []', () => + expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast (seq bd cp) 2) x)')); + + it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(seq jazz (fast hh 2))')); + + it('should desugar , seq', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)')); + it('should desugar , seq 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (seq hh oh))')); + it('should desugar , seq 3', () => expect(desguar('[bd cp, hh oh]')).toEqual('(stack (seq bd cp) (seq hh oh))')); + it('should desugar , cat', () => expect(desguar('')).toEqual('(stack bd hh)')); + it('should desugar , cat 2', () => expect(desguar('')).toEqual('(stack bd (cat hh oh))')); + it('should desugar , cat 3', () => expect(desguar('')).toEqual('(stack (cat bd cp) (cat hh oh))')); + it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); + it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(seq a (* b 2) c (/ d 3) e)')); + it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(seq a (* (seq b c) 3))')); + it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( '(speed (s (seq bd (* hh 2) (crush cp 4) (cat mt ht lt))) .8)', diff --git a/packages/uzu/uzu.mjs b/packages/uzu/uzu.mjs index 6a5157811..e79488a59 100644 --- a/packages/uzu/uzu.mjs +++ b/packages/uzu/uzu.mjs @@ -86,10 +86,14 @@ export class UzuParser { return children; } // Token[] => Token[][] . returns empty list if type not found - split_children(children, type) { + split_children(children, split_type, sequence_type) { + if (sequence_type) { + // if given, the first child is ignored + children = children.slice(1); + } const chunks = []; while (true) { - let commaIndex = children.findIndex((child) => child.type === type); + let commaIndex = children.findIndex((child) => child.type === split_type); if (commaIndex === -1) break; const chunk = children.slice(0, commaIndex); chunks.push(chunk); @@ -101,13 +105,11 @@ export class UzuParser { chunks.push(children); return chunks; } - desugar_stack(children) { - let [type, ...rest] = children; + desugar_stack(children, sequence_type) { // children is expected to contain seq or cat as first item - const chunks = this.split_children(rest, 'stack'); + const chunks = this.split_children(children, 'stack', sequence_type); if (!chunks.length) { - // no stack - return children; + return this.desugar_children(children); } // collect args of stack function const args = chunks.map((chunk) => { @@ -115,8 +117,14 @@ export class UzuParser { // chunks of one element can be added to the stack as is return chunk[0]; } else { - // chunks of multiple args are added to a subsequence of type - return { type: 'list', children: [type, ...chunk] }; + // chunks of multiple args + if (sequence_type) { + // if given, each chunk needs to be prefixed + // [a b, c d] => (stack (seq a b) (seq c d)) + chunk = [{ type: 'plain', value: sequence_type }, ...chunk]; + } + chunk = this.desugar_children(chunk); + return { type: 'list', children: chunk }; } }); return [{ type: 'plain', value: 'stack' }, ...args]; @@ -203,21 +211,22 @@ export class UzuParser { } parse_list() { let children = this.parse_pair('open_list', 'close_list'); + children = this.desugar_stack(children); children = this.desugar_children(children); return { type: 'list', children }; } parse_cat() { let children = this.parse_pair('open_cat', 'close_cat'); children = [{ type: 'plain', value: 'cat' }, ...children]; - children = this.desugar_children(children); children = this.desugar_stack(children, 'cat'); + children = this.desugar_children(children); return { type: 'list', children }; } parse_seq() { let children = this.parse_pair('open_seq', 'close_seq'); children = [{ type: 'plain', value: 'seq' }, ...children]; - children = this.desugar_children(children); children = this.desugar_stack(children, 'seq'); + children = this.desugar_children(children); return { type: 'list', children }; } consume(type) { From e3df504423b51e29d4b7ca11199cac6641d9f7a0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 02:01:16 +0100 Subject: [PATCH 026/538] rename uzu to mondo --- packages/{uzu => mondo}/README.md | 8 ++++---- packages/{uzu/uzu.mjs => mondo/mondo.mjs} | 8 ++++---- packages/{uzu => mondo}/package.json | 10 +++++----- .../uzu.test.mjs => mondo/test/mondo.test.mjs} | 10 +++++----- packages/{uzu => mondo}/vite.config.js | 4 ++-- pnpm-lock.yaml | 18 +++++++++--------- 6 files changed, 29 insertions(+), 29 deletions(-) rename packages/{uzu => mondo}/README.md (72%) rename packages/{uzu/uzu.mjs => mondo/mondo.mjs} (98%) rename packages/{uzu => mondo}/package.json (79%) rename packages/{uzu/test/uzu.test.mjs => mondo/test/mondo.test.mjs} (95%) rename packages/{uzu => mondo}/vite.config.js (78%) diff --git a/packages/uzu/README.md b/packages/mondo/README.md similarity index 72% rename from packages/uzu/README.md rename to packages/mondo/README.md index 53eef51f8..d7ee20ac4 100644 --- a/packages/uzu/README.md +++ b/packages/mondo/README.md @@ -1,4 +1,4 @@ -# uzu +# mondo an experimental parser for an *uzulang*, a custom dsl for patterns that can stand on its own feet. more info: @@ -6,9 +6,9 @@ an experimental parser for an *uzulang*, a custom dsl for patterns that can stan - [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) ```js -import { UzuRunner } from 'uzu' +import { MondoRunner } from 'uzu' -const runner = UzuRunner({ seq, cat, s, crush, speed, '*': fast }); +const runner = MondoRunner({ seq, cat, s, crush, speed, '*': fast }); const pat = runner.run('s [bd hh*2 cp.(crush 4) ] . speed .8') ``` @@ -26,4 +26,4 @@ the above code will create the following call structure: ) ``` -you can pass all available functions to *UzuRunner* as an object. +you can pass all available functions to *MondoRunner* as an object. diff --git a/packages/uzu/uzu.mjs b/packages/mondo/mondo.mjs similarity index 98% rename from packages/uzu/uzu.mjs rename to packages/mondo/mondo.mjs index e79488a59..2037cce7c 100644 --- a/packages/uzu/uzu.mjs +++ b/packages/mondo/mondo.mjs @@ -1,11 +1,11 @@ /* -uzu.mjs - +mondo.mjs - Copyright (C) 2022 Strudel contributors - see 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 . */ // evolved from https://garten.salat.dev/lisp/parser.html -export class UzuParser { +export class MondoParser { // these are the tokens we expect token_types = { string: /^"(.*?)"/, @@ -251,9 +251,9 @@ export function printAst(ast, compact = false, lvl = 0) { } // lisp runner -export class UzuRunner { +export class MondoRunner { constructor(lib) { - this.parser = new UzuParser(); + this.parser = new MondoParser(); this.lib = lib; } // a helper to check conditions and throw if they are not met diff --git a/packages/uzu/package.json b/packages/mondo/package.json similarity index 79% rename from packages/uzu/package.json rename to packages/mondo/package.json index 9b59078d5..d8a4a0bf2 100644 --- a/packages/uzu/package.json +++ b/packages/mondo/package.json @@ -1,11 +1,11 @@ { - "name": "uzu", + "name": "mondo", "version": "1.1.0", - "description": "an uzu notation", - "main": "uzu.mjs", + "description": "a language for functional composition that translates to js", + "main": "mondo.mjs", "type": "module", "publishConfig": { - "main": "dist/uzu.mjs" + "main": "dist/mondo.mjs" }, "scripts": { "test": "vitest run", @@ -29,7 +29,7 @@ "bugs": { "url": "https://github.com/tidalcycles/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel/blob/main/packages/uzu/README.md", + "homepage": "https://github.com/tidalcycles/strudel/blob/main/packages/mondo/README.md", "devDependencies": { "vite": "^6.0.11", "vitest": "^3.0.4" diff --git a/packages/uzu/test/uzu.test.mjs b/packages/mondo/test/mondo.test.mjs similarity index 95% rename from packages/uzu/test/uzu.test.mjs rename to packages/mondo/test/mondo.test.mjs index 33451f0cf..6ed2df176 100644 --- a/packages/uzu/test/uzu.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -1,16 +1,16 @@ /* -uzu.test.mjs - +mondo.test.mjs - Copyright (C) 2022 Strudel contributors - see 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 { describe, expect, it } from 'vitest'; -import { UzuParser, UzuRunner, printAst } from '../uzu.mjs'; +import { MondoParser, MondoRunner, printAst } from '../mondo.mjs'; -const parser = new UzuParser(); +const parser = new MondoParser(); const p = (code) => parser.parse(code); -describe('uzu s-expressions parser', () => { +describe('mondo s-expressions parser', () => { it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] })); it('should parse a single item', () => expect(p('a')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] })); @@ -56,7 +56,7 @@ let desguar = (a) => { return printAst(parser.parse(a), true); }; -describe('uzu sugar', () => { +describe('mondo sugar', () => { it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(seq a b c)')); it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(seq a (seq b c) d)')); it('should desugar <>', () => expect(desguar('')).toEqual('(cat a b c)')); diff --git a/packages/uzu/vite.config.js b/packages/mondo/vite.config.js similarity index 78% rename from packages/uzu/vite.config.js rename to packages/mondo/vite.config.js index e61a69e40..19e53d7f3 100644 --- a/packages/uzu/vite.config.js +++ b/packages/mondo/vite.config.js @@ -7,9 +7,9 @@ export default defineConfig({ plugins: [], build: { lib: { - entry: resolve(__dirname, 'uzu.mjs'), + entry: resolve(__dirname, 'mondo.mjs'), formats: ['es'], - fileName: (ext) => ({ es: 'uzu.mjs' })[ext], + fileName: (ext) => ({ es: 'mondo.mjs' })[ext], }, rollupOptions: { // external: [...Object.keys(dependencies)], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbe64f07a..264b7440b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -344,6 +344,15 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/mondo: + 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) + vitest: + specifier: ^3.0.4 + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/motion: dependencies: '@strudel/core': @@ -540,15 +549,6 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) - packages/uzu: - 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) - vitest: - specifier: ^3.0.4 - version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) - packages/web: dependencies: '@strudel/core': From e782dc09dd3c31fb497d1f66a50bfa59924af8bb Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 10:47:32 +0100 Subject: [PATCH 027/538] export strudelScope for mondo to use --- packages/core/evaluate.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/evaluate.mjs b/packages/core/evaluate.mjs index e3e73d596..7d57e435e 100644 --- a/packages/core/evaluate.mjs +++ b/packages/core/evaluate.mjs @@ -4,6 +4,8 @@ Copyright (C) 2022 Strudel contributors - see . */ +export const strudelScope = {}; + export const evalScope = async (...args) => { const results = await Promise.allSettled(args); const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value); @@ -18,6 +20,7 @@ export const evalScope = async (...args) => { modules.forEach((module) => { Object.entries(module).forEach(([name, value]) => { globalThis[name] = value; + strudelScope[name] = value; }); }); return modules; From 11196fb1e6601241039f2dfd4dca6c4434ad7a1a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 10:48:38 +0100 Subject: [PATCH 028/538] breaking: refactor controls - multiple args wont be interpreted as sequence anymore - you can now pass value, pat to set value to pat --- packages/core/controls.mjs | 26 ++++++++++++++++------- packages/core/signal.mjs | 8 +++---- packages/core/test/pattern.test.mjs | 2 +- packages/tonal/test/tonal.test.mjs | 14 ++++++------ test/__snapshots__/examples.test.mjs.snap | 16 ++++---------- website/src/repl/tunes.mjs | 12 +++++------ 6 files changed, 40 insertions(+), 38 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 1f2d3e703..dbf1255e8 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -4,13 +4,14 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, register, sequence } from './pattern.mjs'; +import { Pattern, register, reify } from './pattern.mjs'; export function createParam(names) { let isMulti = Array.isArray(names); names = !isMulti ? [names] : names; const name = names[0]; + // todo: make this less confusing const withVal = (xs) => { let bag; // check if we have an object with an unnamed control (.value) @@ -35,25 +36,34 @@ export function createParam(names) { } }; - const func = (...pats) => sequence(...pats).withValue(withVal); - - const setter = function (...pats) { - if (!pats.length) { - return this.fmap(withVal); + // todo: make this less confusing + const func = function (value, pat) { + if (!pat) { + return reify(value).withValue(withVal); } - return this.set(func(...pats)); + if (typeof value === 'undefined') { + return pat.fmap(withVal); + } + return pat.set(reify(value).withValue(withVal)); + }; + Pattern.prototype[name] = function (value) { + return func(value, this); }; - Pattern.prototype[name] = setter; return func; } // maps control alias names to the "main" control name const controlAlias = new Map(); +export function isControlName(name) { + return controlAlias.has(name); +} + export function registerControl(names, ...aliases) { const name = Array.isArray(names) ? names[0] : names; let bag = {}; bag[name] = createParam(names); + controlAlias.set(name, name); aliases.forEach((alias) => { bag[alias] = bag[name]; controlAlias.set(alias, name); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 5348173d3..aa8dd9a6b 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -279,26 +279,26 @@ const _rearrangeWith = (ipat, n, pat) => { }; /** - * @name shuffle * Slices a pattern into the given number of parts, then plays those parts in random order. * Each part will be played exactly once per cycle. + * @name shuffle * @example * note("c d e f").sound("piano").shuffle(4) * @example - * note("c d e f".shuffle(4), "g").sound("piano") + * seq("c d e f".shuffle(4), "g").note().sound("piano") */ export const shuffle = register('shuffle', (n, pat) => { return _rearrangeWith(randrun(n), n, pat); }); /** - * @name scramble * Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`, * but parts might be played more than once, or not at all, per cycle. + * @name scramble * @example * note("c d e f").sound("piano").scramble(4) * @example - * note("c d e f".scramble(4), "g").sound("piano") + * seq("c d e f".scramble(4), "g").note().sound("piano") */ export const scramble = register('scramble', (n, pat) => { return _rearrangeWith(_irand(n)._segment(n), n, pat); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 08611032c..ee6ce197f 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1001,7 +1001,7 @@ describe('Pattern', () => { }); describe('hurry', () => { it('Can speed up patterns and sounds', () => { - sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2)); + sameFirst(s(sequence('a', 'b')).hurry(2), s(sequence('a', 'b')).fast(2).speed(2)); }); }); /*describe('composable functions', () => { diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 8dd0e1861..3a5d920e5 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -25,28 +25,28 @@ describe('tonal', () => { }); it('scale with n values', () => { expect( - n(0, 1, 2) + n(seq(0, 1, 2)) .scale('C major') .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); it('scale with colon', () => { expect( - n(0, 1, 2) + n(seq(0, 1, 2)) .scale('C:major') .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); it('scale with mininotation colon', () => { expect( - n(0, 1, 2) + 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(40, 40, 40) + note(seq(40, 40, 40)) .transpose(0, 1, 2) .firstCycleValues.map((h) => h.note), ).toEqual([40, 41, 42]); @@ -54,7 +54,7 @@ describe('tonal', () => { }); it('transposes note numbers with interval strings', () => { expect( - note(40, 40, 40) + note(seq(40, 40, 40)) .transpose('1P', '2M', '3m') .firstCycleValues.map((h) => h.note), ).toEqual([40, 42, 43]); @@ -62,7 +62,7 @@ describe('tonal', () => { }); it('transposes note strings with interval numbers', () => { expect( - note('c', 'c', 'c') + note(seq('c', 'c', 'c')) .transpose(0, 1, 2) .firstCycleValues.map((h) => h.note), ).toEqual(['C', 'Db', 'D']); @@ -70,7 +70,7 @@ describe('tonal', () => { }); it('transposes note strings with interval strings', () => { expect( - note('c', 'c', 'c') + note(seq('c', 'c', 'c')) .transpose('1P', '2M', '3m') .firstCycleValues.map((h) => h.note), ).toEqual(['C', 'D', 'Eb']); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 96a11d49b..93ca7ff68 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -7401,9 +7401,7 @@ exports[`runs examples > example "scope" example index 0 1`] = ` ] `; -exports[`runs examples > example "scramble -Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`, -but parts might be played more than once, or not at all, per cycle." example index 0 1`] = ` +exports[`runs examples > example "scramble" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano ]", "[ 1/4 → 1/2 | note:d s:piano ]", @@ -7424,9 +7422,7 @@ but parts might be played more than once, or not at all, per cycle." example ind ] `; -exports[`runs examples > example "scramble -Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`, -but parts might be played more than once, or not at all, per cycle." example index 1 1`] = ` +exports[`runs examples > example "scramble" example index 1 1`] = ` [ "[ 0/1 → 1/8 | note:c s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]", @@ -7835,9 +7831,7 @@ exports[`runs examples > example "shrink" example index 3 1`] = ` ] `; -exports[`runs examples > example "shuffle -Slices a pattern into the given number of parts, then plays those parts in random order. -Each part will be played exactly once per cycle." example index 0 1`] = ` +exports[`runs examples > example "shuffle" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano ]", "[ 1/4 → 1/2 | note:d s:piano ]", @@ -7858,9 +7852,7 @@ Each part will be played exactly once per cycle." example index 0 1`] = ` ] `; -exports[`runs examples > example "shuffle -Slices a pattern into the given number of parts, then plays those parts in random order. -Each part will be played exactly once per cycle." example index 1 1`] = ` +exports[`runs examples > example "shuffle" example index 1 1`] = ` [ "[ 0/1 → 1/8 | note:c s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]", diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index cffe0c0eb..4e2a7617f 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -69,32 +69,32 @@ stack( export const giantSteps = `// John Coltrane - Giant Steps -let melody = note( +let melody = seq( "[F#5 D5] [B4 G4] Bb4 [B4 A4]", "[D5 Bb4] [G4 Eb4] F#4 [G4 F4]", "Bb4 [B4 A4] D5 [D#5 C#5]", "F#5 [G5 F5] Bb5 [F#5 F#5]", -) +).note() stack( // melody melody.color('#F8E71C'), // chords - chord( + seq( "[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]", "[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]", "Eb^7 [Am7 D7] G^7 [C#m7 F#7]", "B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]" - ).dict('lefthand') + ).chord().dict('lefthand') .anchor(melody).mode('duck') .voicing().color('#7ED321'), // bass - note( + seq( "[B2 D2] [G2 Bb2] [Eb2 Bb3] [A2 D2]", "[G2 Bb2] [Eb2 F#2] [B2 F#2] [F2 Bb2]", "[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]", "[B2 F#2] [F2 Bb2] [Eb2 Bb3] [C#2 F#2]" - ).color('#00B8D4') + ).note().color('#00B8D4') ).slow(20) .pianoroll({fold:1})`; From fae59a6bcd1624f951737b1aa2f145879069f2c4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 10:49:18 +0100 Subject: [PATCH 029/538] feat: add mondough package to run strudel via mondo --- packages/mondo/mondo.mjs | 22 +++++-- packages/mondough/README.md | 3 + packages/mondough/mondough.mjs | 35 +++++++++++ packages/mondough/package.json | 42 +++++++++++++ packages/mondough/vite.config.js | 19 ++++++ pnpm-lock.yaml | 103 +++++++++++++++++++++++++++++++ website/package.json | 1 + website/src/repl/util.mjs | 1 + 8 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 packages/mondough/README.md create mode 100644 packages/mondough/mondough.mjs create mode 100644 packages/mondough/package.json create mode 100644 packages/mondough/vite.config.js diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 2037cce7c..907951db2 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,8 +19,11 @@ export class MondoParser { pipe: /^\./, stack: /^,/, op: /^[*/]/, - plain: /^[a-zA-Z0-9-]+/, + plain: /^[a-zA-Z0-9-_]+/, }; + constructor(config) { + this.config = config; + } // matches next token next_token(code) { for (let type in this.token_types) { @@ -29,7 +32,7 @@ export class MondoParser { return { type, value: match[0] }; } } - throw new Error(`zilp: could not match '${code}'`); + throw new Error(`mondo: could not match '${code}'`); } // takes code string, returns list of matched tokens (if valid) tokenize(code) { @@ -182,7 +185,7 @@ export class MondoParser { const [callee, ...rest] = right.children; const leftSide = children.slice(0, pipeIndex - 1); const left = children[pipeIndex - 1]; - const args = [callee, left, ...rest]; + let args = [callee, left, ...rest]; const call = { type: 'list', children: args }; children = [...leftSide, call, ...rightSide]; } else { @@ -200,6 +203,10 @@ export class MondoParser { } return children; } + flip_call(children) { + let [name, first, ...rest] = children; + return [name, ...rest, first]; + } parse_pair(open_type, close_type) { this.consume(open_type); const children = []; @@ -252,9 +259,10 @@ export function printAst(ast, compact = false, lvl = 0) { // lisp runner export class MondoRunner { - constructor(lib) { - this.parser = new MondoParser(); + constructor(lib, config = {}) { + this.parser = new MondoParser(config); this.lib = lib; + this.config = config; } // a helper to check conditions and throw if they are not met assert(condition, error) { @@ -264,6 +272,7 @@ export class MondoRunner { } run(code) { const ast = this.parser.parse(code); + console.log(printAst(ast)); return this.call(ast); } call(ast) { @@ -298,6 +307,9 @@ export class MondoRunner { // look up function in lib const fn = this.lib[name]; this.assert(fn, `function call: unknown function name "${name}"`); + if (this.lib.call) { + return this.lib.call(fn, args, name); + } return fn(...args); } } diff --git a/packages/mondough/README.md b/packages/mondough/README.md new file mode 100644 index 000000000..3ae758912 --- /dev/null +++ b/packages/mondough/README.md @@ -0,0 +1,3 @@ +# @strudel/mondough + +connects mondo to strudel. diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs new file mode 100644 index 000000000..1d29749f9 --- /dev/null +++ b/packages/mondough/mondough.mjs @@ -0,0 +1,35 @@ +import { strudelScope, reify, fast, slow, isControlName } from '@strudel/core'; +import { MondoRunner } from '../mondo/mondo.mjs'; + +let runner = new MondoRunner(strudelScope, { pipepost: true }); + +//strudelScope.plain = reify; +strudelScope.plain = (v) => { + // console.log('plain', v); + return reify(v); + // return v; +}; +// strudelScope.number = (n) => n; +strudelScope.number = reify; + +strudelScope.call = (fn, args, name) => { + const [pat, ...rest] = args; + if (!['seq', 'cat', 'stack'].includes(name)) { + args = [...rest, pat]; + } + + // console.log('call', name, ...flipped); + + return fn(...args); +}; + +strudelScope['*'] = fast; +strudelScope['/'] = slow; + +export function mondo(code, offset = 0) { + if (Array.isArray(code)) { + code = code.join(''); + } + const pat = runner.run(code, { pipepost: true }); + return pat; +} diff --git a/packages/mondough/package.json b/packages/mondough/package.json new file mode 100644 index 000000000..0754a697f --- /dev/null +++ b/packages/mondough/package.json @@ -0,0 +1,42 @@ +{ + "name": "@strudel/mondough", + "version": "1.1.0", + "description": "mondo notation for strudel", + "main": "mondough.mjs", + "type": "module", + "publishConfig": { + "main": "dist/mondough.mjs" + }, + "scripts": { + "test": "vitest run", + "bench": "vitest bench", + "build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs", + "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", + "dependencies": { + "@strudel/core": "workspace:*" + }, + "devDependencies": { + "mondo": "*", + "vite": "^6.0.11", + "vitest": "^3.0.4" + } +} diff --git a/packages/mondough/vite.config.js b/packages/mondough/vite.config.js new file mode 100644 index 000000000..c46972e96 --- /dev/null +++ b/packages/mondough/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +//import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'mondough.mjs'), + formats: ['es'], + fileName: (ext) => ({ es: 'mondough.mjs' })[ext], + }, + rollupOptions: { + // external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 264b7440b..f5b63c56f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -353,6 +353,22 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/mondough: + dependencies: + '@strudel/core': + specifier: workspace:* + version: link:../core + devDependencies: + mondo: + specifier: '*' + version: 0.4.4 + 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) + vitest: + specifier: ^3.0.4 + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/motion: dependencies: '@strudel/core': @@ -674,6 +690,9 @@ importers: '@strudel/mini': specifier: workspace:* version: link:../packages/mini + '@strudel/mondough': + specifier: workspace:* + version: link:../packages/mondough '@strudel/motion': specifier: workspace:* version: link:../packages/motion @@ -2963,6 +2982,10 @@ packages: resolution: {integrity: sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ==} engines: {node: '>= 14.0.0'} + amdefine@1.0.1: + resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} + engines: {node: '>=0.4.2'} + ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -3091,6 +3114,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async@0.2.10: + resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -3325,6 +3351,9 @@ packages: claviature@0.1.0: resolution: {integrity: sha512-Ai12axNwQ7x/F9QAj64RYKsgvi5Y33+X3GUSKAC/9s/adEws8TSSc0efeiqhKNGKBo6rT/c+CSCwSXzXxwxZzQ==} + cldr-plurals@1.0.0: + resolution: {integrity: sha512-xGkehDsjj/ng1LtYiAzdiqDgqTQ/qmWafDlB6R8CfXbpXe4Ge2Tl4l7gxiA+yaD1WCTP3EVfwerdmVwfco7vxw==} + clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -4366,6 +4395,9 @@ packages: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + globalize@0.1.1: + resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==} + globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -4427,6 +4459,11 @@ packages: h3@1.14.0: resolution: {integrity: sha512-ao22eiONdgelqcnknw0iD645qW0s9NnrJHr5OBz4WOMdBdycfSas1EQf1wXRsm+PcB2Yoj43pjBPwqIpJQTeWg==} + handlebars@1.1.2: + resolution: {integrity: sha512-6lekK3aSHE7XnEqEs+JWQJRSRdCqJYHEVjEfWWZv9pjLUYZNeP26EtlgOrpDCSX+k5N1m74urTbztgBOCko1Kg==} + engines: {node: '>=0.4.7'} + hasBin: true + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -5574,6 +5611,10 @@ packages: engines: {node: '>=18'} hasBin: true + mondo@0.4.4: + resolution: {integrity: sha512-3ot6iKwC9KZvwmyZ9dgPCnlt0O1GuBnK8QZyhnoJdtkIiRL9X/cmQuu88CbFSznQx6bq19xQdwJHpv8hggH62Q==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -5815,6 +5856,9 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + optimist@0.3.7: + resolution: {integrity: sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -6713,6 +6757,10 @@ packages: source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.1.43: + resolution: {integrity: sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==} + engines: {node: '>=0.8.0'} + source-map@0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} @@ -7143,6 +7191,11 @@ packages: ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + uglify-js@2.3.6: + resolution: {integrity: sha512-T2LWWydxf5+Btpb0S/Gg/yKFmYjnX9jtQ4mdN9YRq73BhN21EhU0Dvw3wYDLqd3TooGUJlCKf3Gfyjjy/RTcWA==} + engines: {node: '>=0.4.0'} + hasBin: true + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -7161,6 +7214,9 @@ packages: underscore@1.13.7: resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} + underscore@1.5.2: + resolution: {integrity: sha512-yejOFsRnTJs0N9CK5Apzf6maDO2djxGoLLrlZlvGs2o9ZQuhIhDL18rtFyy4FBIbOkzA6+4hDgXbgz5EvDQCXQ==} + undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} @@ -7543,6 +7599,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@0.0.3: + resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==} + engines: {node: '>=0.4.0'} + wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -10426,6 +10486,9 @@ snapshots: '@algolia/requester-fetch': 5.20.0 '@algolia/requester-node-http': 5.20.0 + amdefine@1.0.1: + optional: true + ansi-align@3.0.1: dependencies: string-width: 4.2.3 @@ -10635,6 +10698,9 @@ snapshots: async-function@1.0.0: {} + async@0.2.10: + optional: true + async@3.2.6: {} asynckit@0.4.0: {} @@ -10892,6 +10958,8 @@ snapshots: claviature@0.1.0: {} + cldr-plurals@1.0.0: {} + clean-stack@2.2.0: {} cli-boxes@3.0.0: {} @@ -12034,6 +12102,8 @@ snapshots: minipass: 4.2.8 path-scurry: 1.11.1 + globalize@0.1.1: {} + globals@11.12.0: {} globals@14.0.0: {} @@ -12100,6 +12170,12 @@ snapshots: uncrypto: 0.1.3 unenv: 1.10.0 + handlebars@1.1.2: + dependencies: + optimist: 0.3.7 + optionalDependencies: + uglify-js: 2.3.6 + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -13683,6 +13759,13 @@ snapshots: requirejs: 2.3.7 requirejs-config-file: 4.0.0 + mondo@0.4.4: + dependencies: + cldr-plurals: 1.0.0 + globalize: 0.1.1 + handlebars: 1.1.2 + underscore: 1.5.2 + mrmime@2.0.0: {} ms@2.0.0: {} @@ -13975,6 +14058,10 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + optimist@0.3.7: + dependencies: + wordwrap: 0.0.3 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -15082,6 +15169,11 @@ snapshots: buffer-from: 1.1.2 source-map: 0.6.1 + source-map@0.1.43: + dependencies: + amdefine: 1.0.1 + optional: true + source-map@0.5.7: {} source-map@0.6.1: {} @@ -15549,6 +15641,13 @@ snapshots: ufo@1.5.4: {} + uglify-js@2.3.6: + dependencies: + async: 0.2.10 + optimist: 0.3.7 + source-map: 0.1.43 + optional: true + uglify-js@3.19.3: optional: true @@ -15565,6 +15664,8 @@ snapshots: underscore@1.13.7: {} + underscore@1.5.2: {} + undici-types@6.20.0: {} unenv@1.10.0: @@ -15945,6 +16046,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@0.0.3: {} + wordwrap@1.0.0: {} workbox-background-sync@7.0.0: diff --git a/website/package.json b/website/package.json index 257ff6430..e42493520 100644 --- a/website/package.json +++ b/website/package.json @@ -42,6 +42,7 @@ "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", "@strudel/webaudio": "workspace:*", + "@strudel/mondough": "workspace:*", "@strudel/xen": "workspace:*", "@supabase/supabase-js": "^2.48.1", "@tailwindcss/forms": "^0.5.10", diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index a8d184285..cb5e6f36e 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -84,6 +84,7 @@ export function loadModules() { import('@strudel/gamepad'), import('@strudel/motion'), import('@strudel/mqtt'), + import('@strudel/mondough'), ]; if (isTauri()) { modules = modules.concat([ From b0da353115fe1dcb96142e74710947ca008706e7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 21:04:05 +0100 Subject: [PATCH 030/538] mondo highlighting --- packages/mondo/mondo.mjs | 64 ++++++++++++++++++++---------- packages/mondo/test/mondo.test.mjs | 18 ++++++++- packages/mondough/mondough.mjs | 29 ++++++++------ packages/mondough/package.json | 3 +- packages/transpiler/transpiler.mjs | 62 +++++++++++++++++++++++++++-- 5 files changed, 138 insertions(+), 38 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 907951db2..be5e819fe 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,35 +19,46 @@ export class MondoParser { pipe: /^\./, stack: /^,/, op: /^[*/]/, - plain: /^[a-zA-Z0-9-_]+/, + plain: /^[a-zA-Z0-9-_\^]+/, }; - constructor(config) { - this.config = config; - } // matches next token - next_token(code) { + next_token(code, offset = 0) { for (let type in this.token_types) { const match = code.match(this.token_types[type]); if (match) { - return { type, value: match[0] }; + let token = { type, value: match[0] }; + if (offset !== -1) { + // add location + token.loc = [offset, offset + match[0].length]; + } + return token; } } throw new Error(`mondo: could not match '${code}'`); } // takes code string, returns list of matched tokens (if valid) - tokenize(code) { + tokenize(code, offset = 0) { let tokens = []; + let locEnabled = offset !== -1; + let trim = () => { + // trim whitespace at start, update offset + offset += code.length - code.trimStart().length; + // trim start and end to not confuse parser + return code.trim(); + }; + code = trim(); while (code.length > 0) { - code = code.trim(); - const token = this.next_token(code); + code = trim(); + const token = this.next_token(code, locEnabled ? offset : -1); code = code.slice(token.value.length); + offset += token.value.length; tokens.push(token); } return tokens; } // take code, return abstract syntax tree - parse(code) { - this.tokens = this.tokenize(code); + parse(code, offset) { + this.tokens = this.tokenize(code, offset); const expressions = []; while (this.tokens.length) { expressions.push(this.parse_expr()); @@ -244,6 +255,20 @@ export class MondoParser { } return token; } + get_locations(code, offset = 0) { + let walk = (ast, locations = []) => { + if (ast.type === 'list') { + return ast.children.slice(1).forEach((child) => walk(child, locations)); + } + if (ast.loc) { + locations.push(ast.loc); + } + }; + const ast = this.parse(code, offset); + let locations = []; + walk(ast, locations); + return locations; + } } export function printAst(ast, compact = false, lvl = 0) { @@ -259,10 +284,9 @@ export function printAst(ast, compact = false, lvl = 0) { // lisp runner export class MondoRunner { - constructor(lib, config = {}) { - this.parser = new MondoParser(config); + constructor(lib) { + this.parser = new MondoParser(); this.lib = lib; - this.config = config; } // a helper to check conditions and throw if they are not met assert(condition, error) { @@ -270,9 +294,9 @@ export class MondoRunner { throw new Error(error); } } - run(code) { - const ast = this.parser.parse(code); - console.log(printAst(ast)); + run(code, offset = 0) { + const ast = this.parser.parse(code, offset); + // console.log(printAst(ast)); return this.call(ast); } call(ast) { @@ -284,13 +308,13 @@ export class MondoRunner { // process args const args = ast.children.slice(1).map((arg) => { if (arg.type === 'string') { - return this.lib.string(arg.value.slice(1, -1)); + return this.lib.string(arg.value.slice(1, -1), arg); } if (arg.type === 'plain') { - return this.lib.plain(arg.value); + return this.lib.plain(arg.value, arg); } if (arg.type === 'number') { - return this.lib.number(Number(arg.value)); + return this.lib.number(Number(arg.value), arg); } return this.call(arg); }); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 6ed2df176..40595004d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -8,8 +8,24 @@ import { describe, expect, it } from 'vitest'; import { MondoParser, MondoRunner, printAst } from '../mondo.mjs'; const parser = new MondoParser(); -const p = (code) => parser.parse(code); +const p = (code) => parser.parse(code, -1); +describe('mondo tokenizer', () => { + const parser = new MondoParser(); + it('should tokenize with locations', () => + expect( + parser + .tokenize('(one two three)') + .map((t) => t.value + '=' + t.loc.join('-')) + .join(' '), + ).toEqual('(=0-1 one=1-4 two=5-8 three=9-14 )=14-15')); + // it('should parse with locations', () => expect(parser.parse('(one two three)')).toEqual()); + it('should get locations', () => + expect(parser.get_locations('s bd rim')).toEqual([ + [2, 4], + [5, 8], + ])); +}); describe('mondo s-expressions parser', () => { it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] })); it('should parse a single item', () => diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 1d29749f9..03437be64 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,25 +1,22 @@ -import { strudelScope, reify, fast, slow, isControlName } from '@strudel/core'; +import { strudelScope, reify, fast, slow } from '@strudel/core'; +import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; -let runner = new MondoRunner(strudelScope, { pipepost: true }); +let runner = new MondoRunner(strudelScope, { pipepost: true, loc: true }); -//strudelScope.plain = reify; -strudelScope.plain = (v) => { - // console.log('plain', v); - return reify(v); - // return v; +let getLeaf = (value, token) => { + const [from, to] = token.loc; + return reify(value).withLoc(from, to); }; -// strudelScope.number = (n) => n; -strudelScope.number = reify; + +strudelScope.plain = getLeaf; +strudelScope.number = getLeaf; strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; if (!['seq', 'cat', 'stack'].includes(name)) { args = [...rest, pat]; } - - // console.log('call', name, ...flipped); - return fn(...args); }; @@ -30,6 +27,12 @@ export function mondo(code, offset = 0) { if (Array.isArray(code)) { code = code.join(''); } - const pat = runner.run(code, { pipepost: true }); + const pat = runner.run(code, offset); return pat; } + +// tell transpiler how to get locations for mondo`` calls +registerLanguage('mondo', { + getLocations: (code, offset) => runner.parser.get_locations(code, offset), +}); + diff --git a/packages/mondough/package.json b/packages/mondough/package.json index 0754a697f..f444c5560 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -32,7 +32,8 @@ }, "homepage": "https://github.com/tidalcycles/strudel#readme", "dependencies": { - "@strudel/core": "workspace:*" + "@strudel/core": "workspace:*", + "@strudel/transpiler": "workspace:*" }, "devDependencies": { "mondo": "*", diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 2e566305f..5eeecd62b 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -8,6 +8,16 @@ export function registerWidgetType(type) { widgetMethods.push(type); } +let languages = new Map(); +// config = { getLocations: (code: string, offset?: number) => number[][] } +// see mondough.mjs for example use +// the language will kick in when the code contains a template literal of type +// example: mondo`...` will use language of type "mondo" +// TODO: refactor tidal.mjs to use this +export function registerLanguage(type, config) { + languages.set(type, config); +} + export function transpiler(input, options = {}) { const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; @@ -26,7 +36,19 @@ export function transpiler(input, options = {}) { walk(ast, { enter(node, parent /* , prop, index */) { - if (isTidalTeplateLiteral(node)) { + if (isLanguageLiteral(node)) { + const { name } = node.tag; + const language = languages.get(name); + const code = node.quasi.quasis[0].value.raw; + const offset = node.quasi.start + 1; + if (emitMiniLocations) { + const locs = language.getLocations(code, offset); + miniLocations = miniLocations.concat(locs); + } + this.skip(); + return this.replace(languageWithLocation(name, code, offset)); + } + if (isTemplateLiteral(node, 'tidal')) { const raw = node.quasi.quasis[0].value.raw; const offset = node.quasi.start + 1; if (emitMiniLocations) { @@ -219,12 +241,16 @@ function labelToP(node) { }; } +function isLanguageLiteral(node) { + return node.type === 'TaggedTemplateExpression' && languages.has(node.tag.name); +} + // tidal highlighting // this feels kind of stupid, when we also know the location inside the string op (tidal.mjs) // but maybe it's the only way -function isTidalTeplateLiteral(node) { - return node.type === 'TaggedTemplateExpression' && node.tag.name === 'tidal'; +function isTemplateLiteral(node, value) { + return node.type === 'TaggedTemplateExpression' && node.tag.name === value; } function collectHaskellMiniLocations(haskellCode, offset) { @@ -262,3 +288,33 @@ function tidalWithLocation(value, offset) { optional: false, }; } + +function mondoWithLocation(value, offset) { + return { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'mondo', + }, + arguments: [ + { type: 'Literal', value }, + { type: 'Literal', value: offset }, + ], + optional: false, + }; +} + +function languageWithLocation(name, value, offset) { + return { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: name, + }, + arguments: [ + { type: 'Literal', value }, + { type: 'Literal', value: offset }, + ], + optional: false, + }; +} From 95526ac99c7db610b7246e4f48f75895f490c63b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 21:04:28 +0100 Subject: [PATCH 031/538] fix: formatting --- packages/mondough/mondough.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 03437be64..2388a9353 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -35,4 +35,3 @@ export function mondo(code, offset = 0) { registerLanguage('mondo', { getLocations: (code, offset) => runner.parser.get_locations(code, offset), }); - From ea61627963e28ac15714fcba1d278fffb1010a4b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 21:05:04 +0100 Subject: [PATCH 032/538] fix: lint error --- packages/mondo/mondo.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index be5e819fe..da8c31ecd 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,7 +19,7 @@ export class MondoParser { pipe: /^\./, stack: /^,/, op: /^[*/]/, - plain: /^[a-zA-Z0-9-_\^]+/, + plain: /^[a-zA-Z0-9-_^]+/, }; // matches next token next_token(code, offset = 0) { From 902012759ae16d9cd4ff29243751c767b63724d9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 21:24:22 +0100 Subject: [PATCH 033/538] fix:support negative numbers --- packages/mondo/mondo.mjs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index da8c31ecd..5ca3a0f81 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -15,7 +15,7 @@ export class MondoParser { close_cat: /^>/, open_seq: /^\[/, close_seq: /^\]/, - number: /^[0-9]*\.?[0-9]+/, // before pipe! + number: /^-?[0-9]*\.?[0-9]+/, // before pipe! pipe: /^\./, stack: /^,/, op: /^[*/]/, @@ -299,6 +299,13 @@ export class MondoRunner { // console.log(printAst(ast)); return this.call(ast); } + // todo: always use lib.call? + libcall(fn, args, name) { + if (this.lib.call) { + return this.lib.call(fn, args, name); + } + return fn(...args); + } call(ast) { // for a node to be callable, it needs to be a list this.assert(ast.type === 'list', `function call: expected list, got ${ast.type}`); @@ -325,15 +332,12 @@ export class MondoRunner { const callee = ast.children[1].value; const innerFn = this.lib[callee]; this.assert(innerFn, `function call: unknown function name "${callee}"`); - return (pat) => innerFn(pat, args.slice(1)); + return (pat) => this.libcall(innerFn, [pat, ...args.slice(1)], callee); } // look up function in lib const fn = this.lib[name]; this.assert(fn, `function call: unknown function name "${name}"`); - if (this.lib.call) { - return this.lib.call(fn, args, name); - } - return fn(...args); + return this.libcall(fn, args, name); } } From 77ade0758e95c28383bde99b145fdf6d399688db Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 22:07:03 +0100 Subject: [PATCH 034/538] mondo: slightly improve error handling --- packages/mondo/mondo.mjs | 20 ++++++++++++++------ packages/mondough/mondough.mjs | 1 + 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 5ca3a0f81..829f34278 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -306,11 +306,19 @@ export class MondoRunner { } return fn(...args); } + errorhead(ast) { + return `[mondo ${ast.loc?.join(':') || ''}]`; + } call(ast) { // for a node to be callable, it needs to be a list - this.assert(ast.type === 'list', `function call: expected list, got ${ast.type}`); + this.assert(ast.type === 'list', `${this.errorhead(ast)} function call: expected list, got ${ast.type}`); // the first element is expected to be the function name - this.assert(ast.children[0]?.type === 'plain', `function call: expected first child to be plain, got ${ast.type}`); + const first = ast.children[0]; + const name = first.value; + this.assert( + first?.type === 'plain', + `${this.errorhead(first)} expected first child to be function name, got ${first.type}${name ? ` "${name}"` : ''}.`, + ); // process args const args = ast.children.slice(1).map((arg) => { @@ -326,18 +334,18 @@ export class MondoRunner { return this.call(arg); }); - const name = ast.children[0].value; if (name === '.') { // lambda : (.fast 2) = x=>fast(2, x) - const callee = ast.children[1].value; + const second = ast.children[1]; + const callee = second.value; const innerFn = this.lib[callee]; - this.assert(innerFn, `function call: unknown function name "${callee}"`); + this.assert(innerFn, `${this.errorhead(second)} lambda error: unknown function name "${callee}"`); return (pat) => this.libcall(innerFn, [pat, ...args.slice(1)], callee); } // look up function in lib const fn = this.lib[name]; - this.assert(fn, `function call: unknown function name "${name}"`); + this.assert(fn, `${this.errorhead(first)} function call: unknown function name "${name}"`); return this.libcall(fn, args, name); } } diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 2388a9353..87089de73 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -11,6 +11,7 @@ let getLeaf = (value, token) => { strudelScope.plain = getLeaf; strudelScope.number = getLeaf; +strudelScope.string = getLeaf; strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; From 9b8761bc454ac36767e2684542c2dce2aa922f4d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 22:31:40 +0100 Subject: [PATCH 035/538] fix: top-level functions arp/arpWith --- packages/core/pattern.mjs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index c1e95211d..ebd5115cf 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -904,12 +904,13 @@ Pattern.prototype.collect = function () { * note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>") * .arpWith(haps => haps[2]) * */ -Pattern.prototype.arpWith = function (func) { - return this.collect() +export const arpWith = register('arpWith', (func, pat) => { + return pat + .collect() .fmap((v) => reify(func(v))) .innerJoin() .withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value))); -}; +}); /** * Selects indices in in stacked notes. @@ -917,9 +918,11 @@ Pattern.prototype.arpWith = function (func) { * note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>") * .arp("0 [0,2] 1 [0,2]") * */ -Pattern.prototype.arp = function (pat) { - return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length])); -}; +export const arp = register( + 'arp', + (indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])), + false, +); /* * Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are From 3c3832dbcecf3c94d81b4bb1a408c95f547741ae Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 23:08:31 +0100 Subject: [PATCH 036/538] superdough: native support for rest symbols - ~ in s --- packages/superdough/superdough.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fc81f565e..729f4316d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -481,6 +481,9 @@ export const superdough = async (value, t, hapDuration) => { const onended = () => { toDisconnect.forEach((n) => n?.disconnect()); }; + if (['-', '~'].includes(s)) { + return; + } if (bank && s) { s = `${bank}_${s}`; value.s = s; From 2dd445c1024c22d071d49649fa7b22d0d312dbea Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 23:10:23 +0100 Subject: [PATCH 037/538] mondo: support variables --- packages/mondough/mondough.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 87089de73..002a7ae5a 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -6,6 +6,9 @@ let runner = new MondoRunner(strudelScope, { pipepost: true, loc: true }); let getLeaf = (value, token) => { const [from, to] = token.loc; + if (strudelScope[value]) { + return strudelScope[value].withLoc(from, to); + } return reify(value).withLoc(from, to); }; From 118b619b740535bb06aff9a9b0df54b57572a17c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 23:10:52 +0100 Subject: [PATCH 038/538] mondo: support - ~ in plain type + simplify errors --- packages/mondo/mondo.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 829f34278..cb088fa9f 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,7 +19,7 @@ export class MondoParser { pipe: /^\./, stack: /^,/, op: /^[*/]/, - plain: /^[a-zA-Z0-9-_^]+/, + plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token next_token(code, offset = 0) { @@ -317,7 +317,7 @@ export class MondoRunner { const name = first.value; this.assert( first?.type === 'plain', - `${this.errorhead(first)} expected first child to be function name, got ${first.type}${name ? ` "${name}"` : ''}.`, + `${this.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, ); // process args @@ -339,13 +339,13 @@ export class MondoRunner { const second = ast.children[1]; const callee = second.value; const innerFn = this.lib[callee]; - this.assert(innerFn, `${this.errorhead(second)} lambda error: unknown function name "${callee}"`); + this.assert(innerFn, `${this.errorhead(second)} unknown function name "${callee}"`); return (pat) => this.libcall(innerFn, [pat, ...args.slice(1)], callee); } // look up function in lib const fn = this.lib[name]; - this.assert(fn, `${this.errorhead(first)} function call: unknown function name "${name}"`); + this.assert(fn, `${this.errorhead(first)} unknown function name "${name}"`); return this.libcall(fn, args, name); } } From 40f64891118acf33f3dabe965c336c12bfb8794e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 16 Mar 2025 23:30:41 +0100 Subject: [PATCH 039/538] reify variables --- packages/mondough/mondough.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 002a7ae5a..21250ad20 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,4 +1,4 @@ -import { strudelScope, reify, fast, slow } from '@strudel/core'; +import { strudelScope, reify, fast, slow, isPattern } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -7,7 +7,7 @@ let runner = new MondoRunner(strudelScope, { pipepost: true, loc: true }); let getLeaf = (value, token) => { const [from, to] = token.loc; if (strudelScope[value]) { - return strudelScope[value].withLoc(from, to); + return reify(strudelScope[value]).withLoc(from, to); } return reify(value).withLoc(from, to); }; From 7db52b3dc26105e7f79db0eeabebab7926f8d318 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 18 Mar 2025 22:33:14 +0100 Subject: [PATCH 040/538] mondo improvements: - add second string type - add $ as alias for stack - simplify leaf handling --- packages/mondo/mondo.mjs | 38 +++++++++++++++++------------- packages/mondo/test/mondo.test.mjs | 2 +- packages/mondough/mondough.mjs | 15 ++++++------ 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index cb088fa9f..68ce390d4 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -8,7 +8,8 @@ This program is free software: you can redistribute it and/or modify it under th export class MondoParser { // these are the tokens we expect token_types = { - string: /^"(.*?)"/, + quotes_double: /^"(.*?)"/, + quotes_single: /^'(.*?)'/, open_list: /^\(/, close_list: /^\)/, open_cat: /^ 1 || expressions[0].type !== 'list') { return { type: 'list', - children: this.desugar_children(expressions), + children: this.desugar(expressions), }; } // we have a single list @@ -110,7 +111,7 @@ export class MondoParser { let commaIndex = children.findIndex((child) => child.type === split_type); if (commaIndex === -1) break; const chunk = children.slice(0, commaIndex); - chunks.push(chunk); + chunk.length && chunks.push(chunk); children = children.slice(commaIndex + 1); } if (!chunks.length) { @@ -227,24 +228,26 @@ export class MondoParser { this.consume(close_type); return children; } + desugar(children, type) { + // not really needed but more readable and might be extended in the future + children = this.desugar_stack(children, type); + return children; + } parse_list() { let children = this.parse_pair('open_list', 'close_list'); - children = this.desugar_stack(children); - children = this.desugar_children(children); + children = this.desugar(children); return { type: 'list', children }; } parse_cat() { let children = this.parse_pair('open_cat', 'close_cat'); children = [{ type: 'plain', value: 'cat' }, ...children]; - children = this.desugar_stack(children, 'cat'); - children = this.desugar_children(children); + children = this.desugar(children, 'cat'); return { type: 'list', children }; } parse_seq() { let children = this.parse_pair('open_seq', 'close_seq'); children = [{ type: 'plain', value: 'seq' }, ...children]; - children = this.desugar_stack(children, 'seq'); - children = this.desugar_children(children); + children = this.desugar(children, 'seq'); return { type: 'list', children }; } consume(type) { @@ -322,16 +325,19 @@ export class MondoRunner { // process args const args = ast.children.slice(1).map((arg) => { - if (arg.type === 'string') { - return this.lib.string(arg.value.slice(1, -1), arg); + if (arg.type === 'list') { + return this.call(arg); } - if (arg.type === 'plain') { - return this.lib.plain(arg.value, arg); + if (!this.lib.leaf) { + throw new Error(`no handler for leaft nodes! add leaf to your lib`); } + if (arg.type === 'number') { - return this.lib.number(Number(arg.value), arg); + arg.value = Number(arg.value); + } else if (['quotes_double', 'quotes_single'].includes(arg.type)) { + arg.value = arg.value.slice(1, -1); } - return this.call(arg); + return this.lib.leaf(arg); }); if (name === '.') { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 40595004d..a88041821 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import { describe, expect, it } from 'vitest'; -import { MondoParser, MondoRunner, printAst } from '../mondo.mjs'; +import { MondoParser, printAst } from '../mondo.mjs'; const parser = new MondoParser(); const p = (code) => parser.parse(code, -1); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 21250ad20..52b169cae 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,21 +1,20 @@ -import { strudelScope, reify, fast, slow, isPattern } from '@strudel/core'; +import { strudelScope, reify, fast, slow } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; -let runner = new MondoRunner(strudelScope, { pipepost: true, loc: true }); +let runner = new MondoRunner(strudelScope); -let getLeaf = (value, token) => { +strudelScope.leaf = (token) => { + let { value } = token; const [from, to] = token.loc; - if (strudelScope[value]) { + if (token.type === 'plain' && strudelScope[value]) { + // what if we want a string that happens to also be a variable name? + // example: "s sine" -> sine is also a variable return reify(strudelScope[value]).withLoc(from, to); } return reify(value).withLoc(from, to); }; -strudelScope.plain = getLeaf; -strudelScope.number = getLeaf; -strudelScope.string = getLeaf; - strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; if (!['seq', 'cat', 'stack'].includes(name)) { From 129fd7d6b0b7639c7e504d2e04e6daac61803c72 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 19 Mar 2025 00:13:06 +0100 Subject: [PATCH 041/538] add notes --- packages/mondo/README.md | 135 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index d7ee20ac4..a6a59dd94 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -27,3 +27,138 @@ the above code will create the following call structure: ``` you can pass all available functions to *MondoRunner* as an object. + +## snippets / thoughts + +### variants of add + +```plaintext +n[0 1 2].add(<0 -4>.n).scale"C minor" +``` + +```js +n("0 1 2").add("<0 -4>".n()).scale("C:minor") +``` + +--- + +```plaintext +n[0 1 2].add(n<0 -4>).scale"C minor" +``` + +```js +n("0 1 2").add(n("<0 -4>")).scale("C:minor") +``` + +--- + +```plaintext +n[0 1 2].(add<0 -4>).scale"C minor" +``` + +```js +n("0 1 2".add("<0 -4>")).scale("C:minor") +``` + +--- + +```plaintext +n[0 1 2].scale"C minor" +.sometimes (12.note.add) +``` + +```js +n("0 1 2").scale("C:minor") +.sometimes(add(note("12"))) +``` + +--- + +```plaintext +note g2*8.dec /2.(range .1 .4) +``` + +```js +note("g2*8").dec(cat(sine, saw).slow(2).range(.1, .4)) +``` + +--- + +```plaintext +n <0 1 2 3 4>*4 .scale"C minor" .jux +``` + +```js +n("<0 1 2 3 4>*4").scale("C:minor").jux(cat(rev,press)) +``` + +--- + +mondo` +sound [bd sd.(every 3 (.fast 4))].jux +` +// og "Alternate Timelines for TidalCycles" example: +// jux <(rev) (iter 4)> $ sound [bd (every 3 (fast 4) [sn])] + +### things mondo cant do + +how to write lists? + +```js +arrange( + [4, "(3,8)"], + [2, "(5,8)"] +).note() +``` + +how to write objects: + +```js +samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/dirt-samples') +``` + +how to access array indices? + +```js +note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>").arpWith(haps => haps[2]) +``` + +s hh .struct(binaryN 55532 16) + +comments + +variables: note g2*8.dec sine + +sine.(range 0 4)/2 doesnt work +sine/2.(range 0 4) works + +n (irand 8. ribbon 0 2) .scale"C minor" => lags because no whole + +### reference + +- arp: note <[c,eb,g] [c,f,ab] [d,f,ab]> .arp [0 [0,2] 1 [0,2]] +- bank: s [bd sd [- bd] sd].bank TR909 +- beat: s sd .beat [4,12] 16 +- binary: s hh .struct (binary 5) +- binaryN: s hh .struct(binaryN 55532 16) => is wrong +- bite: n[0 1 2 3 4 5 6 7].scale"c mixolydian".bite 4 [3 2 1 0] +- bpattack: note [c2 e2 f2 g2].s sawtooth.bpf 500.bpa <.5 .25 .1 .01>/4.bpenv 4 + +### dot is a bit ambiguous + +```plaintext +n[0 1 2].scale"C minor".ad.1 +``` + +decimal vs pipe + +### less ambiguity with [] and "" + +in js, s("hh cp") implcitily does [hh cp] +in mondo, s[hh cp] always shows the type of bracket used + +### todo + +- lists: C:minor +- spread: [0 .. 2] +- replicate: ! From 88ca54461ee69783073a363e59604206547a3cb8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 19 Mar 2025 00:15:53 +0100 Subject: [PATCH 042/538] rename @strudel/mondough to @strudel/mondo --- packages/mondough/package.json | 2 +- pnpm-lock.yaml | 5 ++++- website/package.json | 2 +- website/src/repl/util.mjs | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/mondough/package.json b/packages/mondough/package.json index f444c5560..eae22519e 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -1,5 +1,5 @@ { - "name": "@strudel/mondough", + "name": "@strudel/mondo", "version": "1.1.0", "description": "mondo notation for strudel", "main": "mondough.mjs", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5b63c56f..7666d3fc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -358,6 +358,9 @@ importers: '@strudel/core': specifier: workspace:* version: link:../core + '@strudel/transpiler': + specifier: workspace:* + version: link:../transpiler devDependencies: mondo: specifier: '*' @@ -690,7 +693,7 @@ importers: '@strudel/mini': specifier: workspace:* version: link:../packages/mini - '@strudel/mondough': + '@strudel/mondo': specifier: workspace:* version: link:../packages/mondough '@strudel/motion': diff --git a/website/package.json b/website/package.json index e42493520..069a26c73 100644 --- a/website/package.json +++ b/website/package.json @@ -42,7 +42,7 @@ "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", "@strudel/webaudio": "workspace:*", - "@strudel/mondough": "workspace:*", + "@strudel/mondo": "workspace:*", "@strudel/xen": "workspace:*", "@supabase/supabase-js": "^2.48.1", "@tailwindcss/forms": "^0.5.10", diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index cb5e6f36e..fb9df4b90 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -84,7 +84,7 @@ export function loadModules() { import('@strudel/gamepad'), import('@strudel/motion'), import('@strudel/mqtt'), - import('@strudel/mondough'), + import('@strudel/mondo'), ]; if (isTauri()) { modules = modules.concat([ From 642ddcdb59aa2df3202915936fab1f562734e9ce Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 19 Mar 2025 17:03:17 +0100 Subject: [PATCH 043/538] support : in mondo --- packages/mondo/mondo.mjs | 31 ++++++++++++++++++++++++++++++ packages/mondo/test/mondo.test.mjs | 2 ++ packages/mondough/mondough.mjs | 5 ++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 68ce390d4..4402aa525 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -20,6 +20,7 @@ export class MondoParser { pipe: /^\./, stack: /^[,$]/, op: /^[*/]/, + tail: /^:/, plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token @@ -96,6 +97,7 @@ export class MondoParser { return this.consume(next); } desugar_children(children) { + children = this.resolve_tails(children); children = this.resolve_ops(children); children = this.resolve_pipes(children); return children; @@ -144,6 +146,35 @@ export class MondoParser { }); return [{ type: 'plain', value: 'stack' }, ...args]; } + resolve_tails(children) { + while (true) { + let opIndex = children.findIndex((child) => child.type === 'tail'); + if (opIndex === -1) break; + const op = { type: 'plain', value: children[opIndex].value }; + if (opIndex === children.length - 1) { + throw new Error(`cannot use operator as last child.`); + } + if (opIndex === 0) { + // regular function call (assuming each operator exists as function) + children[opIndex] = op; + continue; + } + const left = children[opIndex - 1]; + const right = children[opIndex + 1]; + + // convert infix to prefix notation + const call = { type: 'list', children: [op, left, right] }; + + // insert call while keeping other siblings + children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; + // unwrap double list.. e.g. (s jazz) * 2 + if (children.length === 1) { + // there might be a cleaner solution + children = children[0].children; + } + } + return children; + } resolve_ops(children) { while (true) { let opIndex = children.findIndex((child) => child.type === 'op'); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index a88041821..c78712e9d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -99,6 +99,8 @@ describe('mondo sugar', () => { it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(seq a (* b 2) c (/ d 3) e)')); it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(seq a (* (seq b c) 3))')); + it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); + it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 52b169cae..c7fa4bcef 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -17,7 +17,7 @@ strudelScope.leaf = (token) => { strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack'].includes(name)) { + if (!['seq', 'cat', 'stack', ':'].includes(name)) { args = [...rest, pat]; } return fn(...args); @@ -26,6 +26,9 @@ strudelScope.call = (fn, args, name) => { strudelScope['*'] = fast; strudelScope['/'] = slow; +const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); +strudelScope[':'] = tail; + export function mondo(code, offset = 0) { if (Array.isArray(code)) { code = code.join(''); From e04f250adbd1f4fa89b370dcdfb29af47651a8a8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 19 Mar 2025 18:16:37 +0100 Subject: [PATCH 044/538] mondo: proper lambda functions with local scope --- packages/mondo/mondo.mjs | 42 +++++++++++++++++++--------------- packages/mondough/mondough.mjs | 6 ++++- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 4402aa525..fc4069434 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -214,11 +214,14 @@ export class MondoParser { if (pipeIndex === -1) break; // pipe up front => lambda if (pipeIndex === 0) { - // . as lambda: (.fast 2) = x=>x.fast(2) - // TODO: this doesn't work for (.fast 2 .speed 2) - // probably needs proper ast representation of lambda - children[pipeIndex] = { type: 'plain', value: '.' }; - continue; + // . as lambda: (.fast 2) = (lambda (_) (fast _ 2)) + const args = [{ type: 'plain', value: '_' }]; + const body = this.desugar([args[0], ...children]); + return [ + { type: 'plain', value: 'lambda' }, + { type: 'list', children: args }, + { type: 'list', children: body }, + ]; } const rightSide = children.slice(pipeIndex + 2); const right = children[pipeIndex + 1]; @@ -343,7 +346,7 @@ export class MondoRunner { errorhead(ast) { return `[mondo ${ast.loc?.join(':') || ''}]`; } - call(ast) { + call(ast, scope = []) { // for a node to be callable, it needs to be a list this.assert(ast.type === 'list', `${this.errorhead(ast)} function call: expected list, got ${ast.type}`); // the first element is expected to be the function name @@ -354,10 +357,22 @@ export class MondoRunner { `${this.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, ); + if (name === 'lambda') { + const [_, args, body] = ast.children; + const argNames = args.children.map((child) => child.value); + // console.log('lambda', argNames, body.children); + return (x) => { + scope = { + [argNames[0]]: x, // TODO: merge scope... + support multiple args + }; + return this.call(body, scope); + }; + } + // process args const args = ast.children.slice(1).map((arg) => { if (arg.type === 'list') { - return this.call(arg); + return this.call(arg, scope); } if (!this.lib.leaf) { throw new Error(`no handler for leaft nodes! add leaf to your lib`); @@ -368,21 +383,12 @@ export class MondoRunner { } else if (['quotes_double', 'quotes_single'].includes(arg.type)) { arg.value = arg.value.slice(1, -1); } - return this.lib.leaf(arg); + return this.lib.leaf(arg, scope); }); - if (name === '.') { - // lambda : (.fast 2) = x=>fast(2, x) - const second = ast.children[1]; - const callee = second.value; - const innerFn = this.lib[callee]; - this.assert(innerFn, `${this.errorhead(second)} unknown function name "${callee}"`); - return (pat) => this.libcall(innerFn, [pat, ...args.slice(1)], callee); - } - // look up function in lib const fn = this.lib[name]; this.assert(fn, `${this.errorhead(first)} unknown function name "${name}"`); - return this.libcall(fn, args, name); + return this.libcall(fn, args, name, scope); } } diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index c7fa4bcef..868198ca0 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -4,8 +4,12 @@ import { MondoRunner } from '../mondo/mondo.mjs'; let runner = new MondoRunner(strudelScope); -strudelScope.leaf = (token) => { +strudelScope.leaf = (token, scope) => { let { value } = token; + // local scope + if (token.type === 'plain' && scope[value]) { + return reify(scope[value]); // -> local scope has no location + } const [from, to] = token.loc; if (token.type === 'plain' && strudelScope[value]) { // what if we want a string that happens to also be a variable name? From 208706fb52436b99341c384e5a5ee5068c6c3547 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:09:29 +0100 Subject: [PATCH 045/538] simplify mondo: ":" is just another operator --- packages/mondo/mondo.mjs | 42 +++++------------------------- packages/mondo/test/mondo.test.mjs | 1 + 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index fc4069434..5997feb40 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,8 +19,7 @@ export class MondoParser { number: /^-?[0-9]*\.?[0-9]+/, // before pipe! pipe: /^\./, stack: /^[,$]/, - op: /^[*/]/, - tail: /^:/, + op: /^[*/:]/, plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token @@ -97,7 +96,6 @@ export class MondoParser { return this.consume(next); } desugar_children(children) { - children = this.resolve_tails(children); children = this.resolve_ops(children); children = this.resolve_pipes(children); return children; @@ -146,32 +144,10 @@ export class MondoParser { }); return [{ type: 'plain', value: 'stack' }, ...args]; } - resolve_tails(children) { - while (true) { - let opIndex = children.findIndex((child) => child.type === 'tail'); - if (opIndex === -1) break; - const op = { type: 'plain', value: children[opIndex].value }; - if (opIndex === children.length - 1) { - throw new Error(`cannot use operator as last child.`); - } - if (opIndex === 0) { - // regular function call (assuming each operator exists as function) - children[opIndex] = op; - continue; - } - const left = children[opIndex - 1]; - const right = children[opIndex + 1]; - - // convert infix to prefix notation - const call = { type: 'list', children: [op, left, right] }; - - // insert call while keeping other siblings - children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; - // unwrap double list.. e.g. (s jazz) * 2 - if (children.length === 1) { - // there might be a cleaner solution - children = children[0].children; - } + // prevents to get a list, e.g. ((x y)) => (x y) + unwrap_children(children) { + if (children.length === 1) { + return children[0].children; } return children; } @@ -188,6 +164,7 @@ export class MondoParser { children[opIndex] = op; continue; } + // convert infix to prefix notation const left = children[opIndex - 1]; const right = children[opIndex + 1]; if (left.type === 'pipe') { @@ -195,15 +172,10 @@ export class MondoParser { children[opIndex] = op; continue; } - // convert infix to prefix notation const call = { type: 'list', children: [op, left, right] }; // insert call while keeping other siblings children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; - // unwrap double list.. e.g. (s jazz) * 2 - if (children.length === 1) { - // there might be a cleaner solution - children = children[0].children; - } + children = this.unwrap_children(children); } return children; } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index c78712e9d..e21ec75b0 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -101,6 +101,7 @@ describe('mondo sugar', () => { it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(seq a (* (seq b c) 3))')); it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); + it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* (: bd 0) 2)')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( From 65a7b3030e674c4bbd5e588fb93c81c5e178449a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:10:12 +0100 Subject: [PATCH 046/538] fix: markcss can now override styles (like color) --- packages/codemirror/highlight.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/highlight.mjs b/packages/codemirror/highlight.mjs index d333986cc..f9f977c6b 100644 --- a/packages/codemirror/highlight.mjs +++ b/packages/codemirror/highlight.mjs @@ -1,4 +1,4 @@ -import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state'; +import { RangeSetBuilder, StateEffect, StateField, Prec } from '@codemirror/state'; import { Decoration, EditorView } from '@codemirror/view'; export const setMiniLocations = StateEffect.define(); @@ -134,5 +134,5 @@ export const isPatternHighlightingEnabled = (on, config) => { setTimeout(() => { updateMiniLocations(config.editor, config.miniLocations); }, 100); - return on ? highlightExtension : []; + return on ? Prec.highest(highlightExtension) : []; }; From 3657e2fd65e11af40bbe9f2a3b66f67e8423e871 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:10:45 +0100 Subject: [PATCH 047/538] mondo: change default markcss --- 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 868198ca0..33e0116df 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -38,7 +38,7 @@ export function mondo(code, offset = 0) { code = code.join(''); } const pat = runner.run(code, offset); - return pat; + return pat.markcss('color: var(--foreground);text-decoration:underline'); } // tell transpiler how to get locations for mondo`` calls From 55a5f1d62a187493da3dfda9f0a22a0c4ab604b7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:10:54 +0100 Subject: [PATCH 048/538] transpiler cleanup --- packages/transpiler/transpiler.mjs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 5eeecd62b..0ee371e6a 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -289,21 +289,6 @@ function tidalWithLocation(value, offset) { }; } -function mondoWithLocation(value, offset) { - return { - type: 'CallExpression', - callee: { - type: 'Identifier', - name: 'mondo', - }, - arguments: [ - { type: 'Literal', value }, - { type: 'Literal', value: offset }, - ], - optional: false, - }; -} - function languageWithLocation(name, value, offset) { return { type: 'CallExpression', From 3505732afad89870286d2c0083887375c62e6b44 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 09:34:47 +0100 Subject: [PATCH 049/538] mondo: add .. operator --- packages/mondo/mondo.mjs | 2 +- packages/mondo/test/mondo.test.mjs | 1 + packages/mondough/mondough.mjs | 11 ++++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 5997feb40..70c58aa6d 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -17,9 +17,9 @@ export class MondoParser { open_seq: /^\[/, close_seq: /^\]/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! + op: /^[*\/:]|^\.{2}/, // * / : .. pipe: /^\./, stack: /^[,$]/, - op: /^[*/:]/, plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index e21ec75b0..aadbafbeb 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -102,6 +102,7 @@ describe('mondo sugar', () => { it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* (: bd 0) 2)')); + it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 0 2)')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 33e0116df..e69ea0994 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -21,7 +21,7 @@ strudelScope.leaf = (token, scope) => { strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', ':'].includes(name)) { + if (!['seq', 'cat', 'stack', ':', '..'].includes(name)) { args = [...rest, pat]; } return fn(...args); @@ -30,9 +30,18 @@ strudelScope.call = (fn, args, name) => { strudelScope['*'] = fast; strudelScope['/'] = slow; +// : operator const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); strudelScope[':'] = tail; +// .. operator +const arrayRange = (start, stop, step = 1) => + Array.from({ length: Math.abs(stop - start) / step + 1 }, (_, index) => + start < stop ? start + index * step : start - index * step, + ); +const range = (min, max) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); +strudelScope['..'] = range; + export function mondo(code, offset = 0) { if (Array.isArray(code)) { code = code.join(''); From d2e93a99f11a5ccf0475acd5888f9a84435b1421 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 10:24:22 +0100 Subject: [PATCH 050/538] fix: lint --- packages/mondo/mondo.mjs | 2 +- packages/mondough/mondough.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 70c58aa6d..ff0b6741e 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -17,7 +17,7 @@ export class MondoParser { open_seq: /^\[/, close_seq: /^\]/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*\/:]|^\.{2}/, // * / : .. + op: /^[*/:]|^\.{2}/, // * / : .. pipe: /^\./, stack: /^[,$]/, plain: /^[a-zA-Z0-9-~_^]+/, diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index e69ea0994..857a9f000 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,4 +1,4 @@ -import { strudelScope, reify, fast, slow } from '@strudel/core'; +import { strudelScope, reify, fast, slow, seq } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; From 432e8dcccc7806ee42f8ed059decb485969edefd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 13:15:48 +0100 Subject: [PATCH 051/538] mondo: support ! and @, also express seq and cat with stepcat --- packages/mondo/mondo.mjs | 2 +- packages/mondough/mondough.mjs | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index ff0b6741e..d7c90139f 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -17,7 +17,7 @@ export class MondoParser { open_seq: /^\[/, close_seq: /^\]/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*/:]|^\.{2}/, // * / : .. + op: /^[*/:!@]|^\.{2}/, // * / : .. pipe: /^\./, stack: /^[,$]/, plain: /^[a-zA-Z0-9-~_^]+/, diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 857a9f000..d442a0262 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -21,14 +21,22 @@ strudelScope.leaf = (token, scope) => { strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', ':', '..'].includes(name)) { + if (!['seq', 'cat', 'stack', ':', '..', '!', '@'].includes(name)) { args = [...rest, pat]; } + if (name === 'seq') { + return stepcat(...args).setSteps(1); + } + if (name === 'cat') { + return stepcat(...args).pace(1); + } return fn(...args); }; strudelScope['*'] = fast; strudelScope['/'] = slow; +strudelScope['!'] = (pat, n) => pat.extend(n); +strudelScope['@'] = (pat, n) => pat.expand(n); // : operator const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); From 9626f3cc9a58be793fd4be7cd9eacc6e0b38dadf Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 13:24:31 +0100 Subject: [PATCH 052/538] mondo add % for pace --- packages/mondo/mondo.mjs | 2 +- packages/mondough/mondough.mjs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index d7c90139f..bcd48aafe 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -17,7 +17,7 @@ export class MondoParser { open_seq: /^\[/, close_seq: /^\]/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*/:!@]|^\.{2}/, // * / : .. + op: /^[*/:!@%]|^\.{2}/, // * / : ! @ % .. pipe: /^\./, stack: /^[,$]/, plain: /^[a-zA-Z0-9-~_^]+/, diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index d442a0262..6de612324 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -21,7 +21,7 @@ strudelScope.leaf = (token, scope) => { strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', ':', '..', '!', '@'].includes(name)) { + if (!['seq', 'cat', 'stack', ':', '..', '!', '@', '%'].includes(name)) { args = [...rest, pat]; } if (name === 'seq') { @@ -37,6 +37,7 @@ strudelScope['*'] = fast; strudelScope['/'] = slow; strudelScope['!'] = (pat, n) => pat.extend(n); strudelScope['@'] = (pat, n) => pat.expand(n); +strudelScope['%'] = (pat, n) => pat.pace(n); // : operator const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); From e16295623d3a4861774791e2306d9398f676554f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 14:09:32 +0100 Subject: [PATCH 053/538] mondo: use stepcat for curly braces --- packages/mondo/mondo.mjs | 15 +++++++++++++-- packages/mondough/mondough.mjs | 13 +++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index bcd48aafe..4ab8a793c 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -12,10 +12,12 @@ export class MondoParser { quotes_single: /^'(.*?)'/, open_list: /^\(/, close_list: /^\)/, - open_cat: /^/, - open_seq: /^\[/, + open_seq: /^\[/, // todo: rename square close_seq: /^\]/, + open_curly: /^\{/, + close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%]|^\.{2}/, // * / : ! @ % .. pipe: /^\./, @@ -93,6 +95,9 @@ export class MondoParser { if (next === 'open_seq') { return this.parse_seq(); } + if (next === 'open_curly') { + return this.parse_curly(); + } return this.consume(next); } desugar_children(children) { @@ -256,6 +261,12 @@ export class MondoParser { children = this.desugar(children, 'seq'); return { type: 'list', children }; } + parse_curly() { + let children = this.parse_pair('open_curly', 'close_curly'); + children = [{ type: 'plain', value: 'curly' }, ...children]; + children = this.desugar(children, 'curly'); + return { type: 'list', children }; + } consume(type) { // shift removes first element and returns it const token = this.tokens.shift(); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 6de612324..e81cfecb7 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,4 +1,4 @@ -import { strudelScope, reify, fast, slow, seq } from '@strudel/core'; +import { strudelScope, reify, fast, slow, seq, stepcat } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -18,18 +18,15 @@ strudelScope.leaf = (token, scope) => { } return reify(value).withLoc(from, to); }; +strudelScope.curly = stepcat; +strudelScope.seq = (...args) => stepcat(...args).setSteps(1); +strudelScope.cat = (...args) => stepcat(...args).pace(1); strudelScope.call = (fn, args, name) => { const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', ':', '..', '!', '@', '%'].includes(name)) { + if (!['seq', 'cat', 'stack', 'curly', ':', '..', '!', '@', '%'].includes(name)) { args = [...rest, pat]; } - if (name === 'seq') { - return stepcat(...args).setSteps(1); - } - if (name === 'cat') { - return stepcat(...args).pace(1); - } return fn(...args); }; From c691916cbfe0f00cd0c0d99186a1d95d5eb0d2c7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 17:55:40 +0100 Subject: [PATCH 054/538] mondo: generic bracket names + simplify call handler + refactor mondough --- packages/mondo/mondo.mjs | 54 ++++++++----------- packages/mondo/test/mondo.test.mjs | 46 ++++++++-------- packages/mondough/mondough.mjs | 84 ++++++++++++++++-------------- 3 files changed, 90 insertions(+), 94 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 4ab8a793c..19ed27e95 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -12,10 +12,10 @@ export class MondoParser { quotes_single: /^'(.*?)'/, open_list: /^\(/, close_list: /^\)/, - open_cat: /^/, - open_seq: /^\[/, // todo: rename square - close_seq: /^\]/, + open_angle: /^/, + open_square: /^\[/, + close_square: /^\]/, open_curly: /^\{/, close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! @@ -89,11 +89,11 @@ export class MondoParser { if (next === 'open_list') { return this.parse_list(); } - if (next === 'open_cat') { - return this.parse_cat(); + if (next === 'open_angle') { + return this.parse_angle(); } - if (next === 'open_seq') { - return this.parse_seq(); + if (next === 'open_square') { + return this.parse_square(); } if (next === 'open_curly') { return this.parse_curly(); @@ -126,7 +126,7 @@ export class MondoParser { return chunks; } desugar_stack(children, sequence_type) { - // children is expected to contain seq or cat as first item + // children is expected to contain square or angle as first item const chunks = this.split_children(children, 'stack', sequence_type); if (!chunks.length) { return this.desugar_children(children); @@ -140,7 +140,7 @@ export class MondoParser { // chunks of multiple args if (sequence_type) { // if given, each chunk needs to be prefixed - // [a b, c d] => (stack (seq a b) (seq c d)) + // [a b, c d] => (stack (square a b) (square c d)) chunk = [{ type: 'plain', value: sequence_type }, ...chunk]; } chunk = this.desugar_children(chunk); @@ -249,16 +249,16 @@ export class MondoParser { children = this.desugar(children); return { type: 'list', children }; } - parse_cat() { - let children = this.parse_pair('open_cat', 'close_cat'); - children = [{ type: 'plain', value: 'cat' }, ...children]; - children = this.desugar(children, 'cat'); + parse_angle() { + let children = this.parse_pair('open_angle', 'close_angle'); + children = [{ type: 'plain', value: 'angle' }, ...children]; + children = this.desugar(children, 'angle'); return { type: 'list', children }; } - parse_seq() { - let children = this.parse_pair('open_seq', 'close_seq'); - children = [{ type: 'plain', value: 'seq' }, ...children]; - children = this.desugar(children, 'seq'); + parse_square() { + let children = this.parse_pair('open_square', 'close_square'); + children = [{ type: 'plain', value: 'square' }, ...children]; + children = this.desugar(children, 'square'); return { type: 'list', children }; } parse_curly() { @@ -307,6 +307,8 @@ export class MondoRunner { constructor(lib) { this.parser = new MondoParser(); this.lib = lib; + this.assert(!!this.lib.leaf, `no handler for leaft nodes! add "leaf" to your lib`); + this.assert(!!this.lib.call, `no handler for call nodes! add "call" to your lib`); } // a helper to check conditions and throw if they are not met assert(condition, error) { @@ -316,16 +318,9 @@ export class MondoRunner { } run(code, offset = 0) { const ast = this.parser.parse(code, offset); - // console.log(printAst(ast)); + console.log(printAst(ast)); return this.call(ast); } - // todo: always use lib.call? - libcall(fn, args, name) { - if (this.lib.call) { - return this.lib.call(fn, args, name); - } - return fn(...args); - } errorhead(ast) { return `[mondo ${ast.loc?.join(':') || ''}]`; } @@ -357,9 +352,6 @@ export class MondoRunner { if (arg.type === 'list') { return this.call(arg, scope); } - if (!this.lib.leaf) { - throw new Error(`no handler for leaft nodes! add leaf to your lib`); - } if (arg.type === 'number') { arg.value = Number(arg.value); @@ -370,8 +362,6 @@ export class MondoRunner { }); // look up function in lib - const fn = this.lib[name]; - this.assert(fn, `${this.errorhead(first)} unknown function name "${name}"`); - return this.libcall(fn, args, name, scope); + return this.lib.call(name, args, scope); } } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index aadbafbeb..1b9a41e3d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -12,15 +12,15 @@ const p = (code) => parser.parse(code, -1); describe('mondo tokenizer', () => { const parser = new MondoParser(); - it('should tokenize with locations', () => + it('should tokenize with loangleions', () => expect( parser .tokenize('(one two three)') .map((t) => t.value + '=' + t.loc.join('-')) .join(' '), ).toEqual('(=0-1 one=1-4 two=5-8 three=9-14 )=14-15')); - // it('should parse with locations', () => expect(parser.parse('(one two three)')).toEqual()); - it('should get locations', () => + // it('should parse with loangleions', () => expect(parser.parse('(one two three)')).toEqual()); + it('should get loangleions', () => expect(parser.get_locations('s bd rim')).toEqual([ [2, 4], [5, 8], @@ -73,32 +73,34 @@ let desguar = (a) => { }; describe('mondo sugar', () => { - it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(seq a b c)')); - it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(seq a (seq b c) d)')); - it('should desugar <>', () => expect(desguar('')).toEqual('(cat a b c)')); - it('should desugar <> nested', () => expect(desguar(' d>')).toEqual('(cat a (cat b c) d)')); - it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(seq a (cat b c))')); - it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(cat a (seq b c))')); + it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(square a b c)')); + it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(square a (square b c) d)')); + it('should desugar <>', () => expect(desguar('')).toEqual('(angle a b c)')); + it('should desugar <> nested', () => expect(desguar(' d>')).toEqual('(angle a (angle b c) d)')); + it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(square a (angle b c))')); + it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(angle a (square b c))')); it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast (s jazz) 2)')); - it('should desugar . seq', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (seq bd cp) 2)')); + it('should desugar . square', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (square bd cp) 2)')); it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow (fast (s jazz) 2) 2)')); it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast (s cp) 2)')); - it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (seq bd cp) 2)')); + it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (square bd cp) 2)')); it('should desugar . within , within []', () => - expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast (seq bd cp) 2) x)')); + expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast (square bd cp) 2) x)')); - it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(seq jazz (fast hh 2))')); + it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast hh 2))')); - it('should desugar , seq', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)')); - it('should desugar , seq 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (seq hh oh))')); - it('should desugar , seq 3', () => expect(desguar('[bd cp, hh oh]')).toEqual('(stack (seq bd cp) (seq hh oh))')); - it('should desugar , cat', () => expect(desguar('')).toEqual('(stack bd hh)')); - it('should desugar , cat 2', () => expect(desguar('')).toEqual('(stack bd (cat hh oh))')); - it('should desugar , cat 3', () => expect(desguar('')).toEqual('(stack (cat bd cp) (cat hh oh))')); + it('should desugar , square', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)')); + it('should desugar , square 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (square hh oh))')); + it('should desugar , square 3', () => + expect(desguar('[bd cp, hh oh]')).toEqual('(stack (square bd cp) (square hh oh))')); + it('should desugar , angle', () => expect(desguar('')).toEqual('(stack bd hh)')); + it('should desugar , angle 2', () => expect(desguar('')).toEqual('(stack bd (angle hh oh))')); + it('should desugar , angle 3', () => + expect(desguar('')).toEqual('(stack (angle bd cp) (angle hh oh))')); it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); - it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(seq a (* b 2) c (/ d 3) e)')); - it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(seq a (* (seq b c) 3))')); + it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* b 2) c (/ d 3) e)')); + it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* (square b c) 3))')); it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* (: bd 0) 2)')); @@ -106,6 +108,6 @@ describe('mondo sugar', () => { it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( - '(speed (s (seq bd (* hh 2) (crush cp 4) (cat mt ht lt))) .8)', + '(speed (s (square bd (* hh 2) (crush cp 4) (angle mt ht lt))) .8)', )); }); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index e81cfecb7..13b0f602f 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,52 +1,56 @@ -import { strudelScope, reify, fast, slow, seq, stepcat } from '@strudel/core'; +import { strudelScope, reify, fast, slow, seq, stepcat, extend, expand, pace } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; -let runner = new MondoRunner(strudelScope); +const tail = (friend, pat) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); -strudelScope.leaf = (token, scope) => { - let { value } = token; - // local scope - if (token.type === 'plain' && scope[value]) { - return reify(scope[value]); // -> local scope has no location - } - const [from, to] = token.loc; - if (token.type === 'plain' && strudelScope[value]) { - // what if we want a string that happens to also be a variable name? - // example: "s sine" -> sine is also a variable - return reify(strudelScope[value]).withLoc(from, to); - } - return reify(value).withLoc(from, to); -}; -strudelScope.curly = stepcat; -strudelScope.seq = (...args) => stepcat(...args).setSteps(1); -strudelScope.cat = (...args) => stepcat(...args).pace(1); - -strudelScope.call = (fn, args, name) => { - const [pat, ...rest] = args; - if (!['seq', 'cat', 'stack', 'curly', ':', '..', '!', '@', '%'].includes(name)) { - args = [...rest, pat]; - } - return fn(...args); -}; - -strudelScope['*'] = fast; -strudelScope['/'] = slow; -strudelScope['!'] = (pat, n) => pat.extend(n); -strudelScope['@'] = (pat, n) => pat.expand(n); -strudelScope['%'] = (pat, n) => pat.pace(n); - -// : operator -const tail = (pat, friend) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); -strudelScope[':'] = tail; - -// .. operator const arrayRange = (start, stop, step = 1) => Array.from({ length: Math.abs(stop - start) / step + 1 }, (_, index) => start < stop ? start + index * step : start - index * step, ); const range = (min, max) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); -strudelScope['..'] = range; + +let lib = {}; +lib.curly = stepcat; +lib.square = (...args) => stepcat(...args).setSteps(1); +lib.angle = (...args) => stepcat(...args).pace(1); +lib['*'] = fast; +lib['/'] = slow; +lib['!'] = extend; +lib['@'] = expand; +lib['%'] = pace; +lib[':'] = tail; +lib['..'] = range; + +let runner = new MondoRunner({ + call(name, args, scope) { + console.log('call', name, args, scope); + const fn = lib[name] || strudelScope[name]; + if (!fn) { + throw new Error(`[moundough]: unknown function "${name}"`); + } + if (!['square', 'angle', 'stack', 'curly'].includes(name)) { + // flip args (pat to end) + const [pat, ...rest] = args; + args = [...rest, pat]; + } + return fn(...args); + }, + leaf(token, scope) { + let { value } = token; + // local scope + if (token.type === 'plain' && scope[value]) { + return reify(scope[value]); // -> local scope has no location + } + const [from, to] = token.loc; + if (token.type === 'plain' && strudelScope[value]) { + // what if we want a string that happens to also be a variable name? + // example: "s sine" -> sine is also a variable + return reify(strudelScope[value]).withLoc(from, to); + } + return reify(value).withLoc(from, to); + }, +}); export function mondo(code, offset = 0) { if (Array.isArray(code)) { From 05ae31838e2e108f29c9de3d4a90c7e8dbeddbe1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 18:01:41 +0100 Subject: [PATCH 055/538] add sin sqr cos aliases --- packages/core/signal.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index aa8dd9a6b..140b12c6c 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -71,25 +71,28 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t)); /** * A sine signal between 0 and 1. - * * @return {Pattern} + * @synonyms sin * @example * n(sine.segment(16).range(0,15)) * .scale("C:minor") * */ export const sine = sine2.fromBipolar(); +export const sin = sine; /** * A cosine signal between 0 and 1. * * @return {Pattern} + * @synonyms cos * @example * n(stack(sine,cosine).segment(16).range(0,15)) * .scale("C:minor") * */ export const cosine = sine._early(Fraction(1).div(4)); +export const cos = cosine; /** * A cosine signal between -1 and 1 (like `cosine`, but bipolar). @@ -102,11 +105,13 @@ export const cosine2 = sine2._early(Fraction(1).div(4)); * A square signal between 0 and 1. * * @return {Pattern} + * @synonyms sqr * @example * n(square.segment(4).range(0,7)).scale("C:minor") * */ export const square = signal((t) => Math.floor((t * 2) % 2)); +export const sqr = square; /** * A square signal between -1 and 1 (like `square`, but bipolar). From 989fdfa20bc3633389cfbb47e4a75ada603f6f11 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 21:18:32 +0100 Subject: [PATCH 056/538] transpiler: add mechanism to register custom mini language --- packages/transpiler/transpiler.mjs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 0ee371e6a..17f5ae30b 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -29,8 +29,15 @@ export function transpiler(input, options = {}) { let miniLocations = []; const collectMiniLocations = (value, node) => { - const leafLocs = getLeafLocations(`"${value}"`, node.start, input); - miniLocations = miniLocations.concat(leafLocs); + const minilang = languages.get('minilang'); + if (minilang) { + const code = `[${value}]`; + const locs = minilang.getLocations(code, node.start); + miniLocations = miniLocations.concat(locs); + } else { + const leafLocs = getLeafLocations(`"${value}"`, node.start, input); + miniLocations = miniLocations.concat(leafLocs); + } }; let widgets = []; @@ -142,11 +149,18 @@ function isBackTickString(node, parent) { function miniWithLocation(value, node) { const { start: fromOffset } = node; + + const minilang = languages.get('minilang'); + let name = 'm'; + if (minilang && minilang.name) { + name = minilang.name; // name is expected to be exported from the package of the minilang + } + return { type: 'CallExpression', callee: { type: 'Identifier', - name: 'm', + name, }, arguments: [ { type: 'Literal', value }, From ac6472a43aaa9eadfd8334cec5e1cf4098680670 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 21:19:21 +0100 Subject: [PATCH 057/538] mondo: mondo as minilang (inactive) --- packages/mondough/mondough.mjs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 13b0f602f..26c9785e0 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -24,7 +24,6 @@ lib['..'] = range; let runner = new MondoRunner({ call(name, args, scope) { - console.log('call', name, args, scope); const fn = lib[name] || strudelScope[name]; if (!fn) { throw new Error(`[moundough]: unknown function "${name}"`); @@ -60,7 +59,19 @@ export function mondo(code, offset = 0) { return pat.markcss('color: var(--foreground);text-decoration:underline'); } +let getLocations = (code, offset) => runner.parser.get_locations(code, offset); + +export const mondi = (str, offset) => { + const code = `[${str}]`; + return mondo(code, offset); +}; + // tell transpiler how to get locations for mondo`` calls registerLanguage('mondo', { - getLocations: (code, offset) => runner.parser.get_locations(code, offset), + getLocations, }); +// uncomment the following to use mondo as mini notation language +/* registerLanguage('minilang', { + name: 'mondi', + getLocations, +}); */ From c33cfc78c5c29216c6d8ad4d4603c39b097b7cc9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Mar 2025 21:19:47 +0100 Subject: [PATCH 058/538] waveform aliases: tri, sqr, saw, sin --- packages/superdough/synth.mjs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 86c073d0b..569da5560 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,5 +1,5 @@ import { clamp, midiToFreq, noteToMidi } from './util.mjs'; -import { registerSound, getAudioContext } from './superdough.mjs'; +import { registerSound, getAudioContext, soundMap } from './superdough.mjs'; import { applyFM, gainNode, @@ -27,6 +27,12 @@ const getFrequencyFromValue = (value) => { }; const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; +const waveformAliases = [ + ['tri', 'triangle'], + ['sqr', 'square'], + ['saw', 'sawtooth'], + ['sin', 'sine'], +]; const noises = ['pink', 'white', 'brown', 'crackle']; export function registerSynthSounds() { @@ -235,6 +241,7 @@ export function registerSynthSounds() { { type: 'synth', prebake: true }, ); }); + waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] })); } export function waveformN(partials, type) { From 12bb82d29564b75a51c3b7360107e081bd350faa Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 11:10:06 +0100 Subject: [PATCH 059/538] add chooseIn + chooseOut --- packages/core/signal.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 140b12c6c..64a177253 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -403,6 +403,10 @@ export const chooseInWith = (pat, xs) => { */ export const choose = (...xs) => chooseWith(rand, xs); +// todo: doc +export const chooseIn = (...xs) => chooseInWith(rand, xs); +export const chooseOut = choose; + /** * Chooses from the given list of values (or patterns of values), according * to the pattern that the method is called on. The pattern should be in From cf1f4ec35c065870d8fcc3e4cdaea02bdbfcff68 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 11:10:25 +0100 Subject: [PATCH 060/538] fix: patterns without structure would error on draw --- packages/draw/draw.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 0576c297b..6edca9882 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -150,7 +150,7 @@ export class Drawer { this.lastFrame = phase; this.visibleHaps = (this.visibleHaps || []) // filter out haps that are too far in the past (think left edge of screen for pianoroll) - .filter((h) => h.endClipped >= phase - lookbehind - lookahead) + .filter((h) => h.whole && h.endClipped >= phase - lookbehind - lookahead) // add new haps with onset (think right edge bars scrolling in) .concat(haps.filter((h) => h.hasOnset())); const time = phase - lookahead; @@ -175,7 +175,7 @@ export class Drawer { // +0.1 = workaround for weird holes in query.. const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1]; // remove all future haps - this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); + this.visibleHaps = this.visibleHaps.filter((h) => h.whole?.begin < t); this.painters = []; // will get populated by .onPaint calls attached to the pattern // query future haps const futureHaps = scheduler.pattern.queryArc(begin, end, { painters: this.painters }); From 6fd89ddee7095f955dadc05e09a41a866b3c807b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 11:10:43 +0100 Subject: [PATCH 061/538] mondo: add | and ? --- packages/mondo/mondo.mjs | 12 +++++++----- packages/mondough/mondough.mjs | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 19ed27e95..815450263 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,9 +19,10 @@ export class MondoParser { open_curly: /^\{/, close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*/:!@%]|^\.{2}/, // * / : ! @ % .. + op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. pipe: /^\./, stack: /^[,$]/, + or: /^[|]/, plain: /^[a-zA-Z0-9-~_^]+/, }; // matches next token @@ -125,9 +126,9 @@ export class MondoParser { chunks.push(children); return chunks; } - desugar_stack(children, sequence_type) { + desugar_split(children, split_type, sequence_type) { // children is expected to contain square or angle as first item - const chunks = this.split_children(children, 'stack', sequence_type); + const chunks = this.split_children(children, split_type, sequence_type); if (!chunks.length) { return this.desugar_children(children); } @@ -147,7 +148,7 @@ export class MondoParser { return { type: 'list', children: chunk }; } }); - return [{ type: 'plain', value: 'stack' }, ...args]; + return [{ type: 'plain', value: split_type }, ...args]; } // prevents to get a list, e.g. ((x y)) => (x y) unwrap_children(children) { @@ -241,7 +242,8 @@ export class MondoParser { } desugar(children, type) { // not really needed but more readable and might be extended in the future - children = this.desugar_stack(children, type); + children = this.desugar_split(children, 'or', type); + children = this.desugar_split(children, 'stack', type); return children; } parse_list() { diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 26c9785e0..dcfb1c3c2 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -1,4 +1,16 @@ -import { strudelScope, reify, fast, slow, seq, stepcat, extend, expand, pace } from '@strudel/core'; +import { + strudelScope, + reify, + fast, + slow, + seq, + stepcat, + extend, + expand, + pace, + chooseIn, + degradeBy, +} from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -19,8 +31,11 @@ lib['/'] = slow; lib['!'] = extend; lib['@'] = expand; lib['%'] = pace; +lib['?'] = degradeBy; // todo: default 0.5 not working.. lib[':'] = tail; lib['..'] = range; +lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]" +//lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct let runner = new MondoRunner({ call(name, args, scope) { From bd93e441546cd4a9a28dc4b121413f149fbf9b61 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 22:34:53 +0100 Subject: [PATCH 062/538] mondo: fix combination of | and , --- packages/mondo/mondo.mjs | 36 +++++++++++++++--------------- packages/mondo/test/mondo.test.mjs | 3 +++ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 815450263..ba1b3c568 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -107,11 +107,7 @@ export class MondoParser { return children; } // Token[] => Token[][] . returns empty list if type not found - split_children(children, split_type, sequence_type) { - if (sequence_type) { - // if given, the first child is ignored - children = children.slice(1); - } + split_children(children, split_type) { const chunks = []; while (true) { let commaIndex = children.findIndex((child) => child.type === split_type); @@ -126,11 +122,10 @@ export class MondoParser { chunks.push(children); return chunks; } - desugar_split(children, split_type, sequence_type) { - // children is expected to contain square or angle as first item - const chunks = this.split_children(children, split_type, sequence_type); + desugar_split(children, split_type, next) { + const chunks = this.split_children(children, split_type); if (!chunks.length) { - return this.desugar_children(children); + return next(children); } // collect args of stack function const args = chunks.map((chunk) => { @@ -139,12 +134,7 @@ export class MondoParser { return chunk[0]; } else { // chunks of multiple args - if (sequence_type) { - // if given, each chunk needs to be prefixed - // [a b, c d] => (stack (square a b) (square c d)) - chunk = [{ type: 'plain', value: sequence_type }, ...chunk]; - } - chunk = this.desugar_children(chunk); + chunk = next(chunk); return { type: 'list', children: chunk }; } }); @@ -241,9 +231,19 @@ export class MondoParser { return children; } desugar(children, type) { - // not really needed but more readable and might be extended in the future - children = this.desugar_split(children, 'or', type); - children = this.desugar_split(children, 'stack', type); + // if type is given, the first element is expected to contain it as plain value + // e.g. with (square a b, c), we want to split (a b, c) and ignore "square" + children = type ? children.slice(1) : children; + children = this.desugar_split(children, 'stack', (children) => + this.desugar_split(children, 'or', (children) => { + // chunks of multiple args + if (type) { + // the type we've removed before splitting needs to be added back + children = [{ type: 'plain', value: type }, ...children]; + } + return this.desugar_children(children); + }), + ); return children; } parse_list() { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 1b9a41e3d..c51844be0 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -90,6 +90,9 @@ describe('mondo sugar', () => { it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast hh 2))')); + it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))')); + it('should desugar , | of []', () => + expect(desguar('[bd, hh | [oh rim]]')).toEqual('(stack bd (or hh (square oh rim)))')); it('should desugar , square', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)')); it('should desugar , square 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (square hh oh))')); it('should desugar , square 3', () => From 5d4ef46ac7255e9440d3303fd253cfef0ab8e4e4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 22:43:39 +0100 Subject: [PATCH 063/538] mondo cleanup --- packages/mondo/mondo.mjs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index ba1b3c568..da969c231 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -217,10 +217,6 @@ export class MondoParser { } return children; } - flip_call(children) { - let [name, first, ...rest] = children; - return [name, ...rest, first]; - } parse_pair(open_type, close_type) { this.consume(open_type); const children = []; @@ -340,7 +336,6 @@ export class MondoRunner { if (name === 'lambda') { const [_, args, body] = ast.children; const argNames = args.children.map((child) => child.value); - // console.log('lambda', argNames, body.children); return (x) => { scope = { [argNames[0]]: x, // TODO: merge scope... + support multiple args @@ -354,7 +349,6 @@ export class MondoRunner { if (arg.type === 'list') { return this.call(arg, scope); } - if (arg.type === 'number') { arg.value = Number(arg.value); } else if (['quotes_double', 'quotes_single'].includes(arg.type)) { @@ -363,7 +357,6 @@ export class MondoRunner { return this.lib.leaf(arg, scope); }); - // look up function in lib return this.lib.call(name, args, scope); } } From f71db4aeed3907e3a4e611100ef746fcbf5cdcf3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Mar 2025 23:50:14 +0100 Subject: [PATCH 064/538] mondo: flip pipe order: now pining to the end of the function... --- packages/mondo/mondo.mjs | 67 +++++++++--------------------- packages/mondo/test/mondo.test.mjs | 28 ++++++------- packages/mondough/mondough.mjs | 5 --- 3 files changed, 34 insertions(+), 66 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index da969c231..1a211e609 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -106,25 +106,22 @@ export class MondoParser { children = this.resolve_pipes(children); return children; } - // Token[] => Token[][] . returns empty list if type not found + // Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']] split_children(children, split_type) { const chunks = []; while (true) { - let commaIndex = children.findIndex((child) => child.type === split_type); - if (commaIndex === -1) break; - const chunk = children.slice(0, commaIndex); + let splitIndex = children.findIndex((child) => child.type === split_type); + if (splitIndex === -1) break; + const chunk = children.slice(0, splitIndex); chunk.length && chunks.push(chunk); - children = children.slice(commaIndex + 1); - } - if (!chunks.length) { - return []; + children = children.slice(splitIndex + 1); } chunks.push(children); return chunks; } desugar_split(children, split_type, next) { const chunks = this.split_children(children, split_type); - if (!chunks.length) { + if (chunks.length === 1) { return next(children); } // collect args of stack function @@ -168,7 +165,8 @@ export class MondoParser { children[opIndex] = op; continue; } - const call = { type: 'list', children: [op, left, right] }; + //const call = { type: 'list', children: [op, left, right] }; + const call = { type: 'list', children: [op, right, left] }; // insert call while keeping other siblings children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; children = this.unwrap_children(children); @@ -176,46 +174,21 @@ export class MondoParser { return children; } resolve_pipes(children) { - while (true) { - let pipeIndex = children.findIndex((child) => child.type === 'pipe'); - // no pipe => we're done - if (pipeIndex === -1) break; - // pipe up front => lambda - if (pipeIndex === 0) { - // . as lambda: (.fast 2) = (lambda (_) (fast _ 2)) - const args = [{ type: 'plain', value: '_' }]; - const body = this.desugar([args[0], ...children]); - return [ - { type: 'plain', value: 'lambda' }, - { type: 'list', children: args }, - { type: 'list', children: body }, - ]; - } - const rightSide = children.slice(pipeIndex + 2); - const right = children[pipeIndex + 1]; - if (right.type === 'list') { - // apply function only to left sibling (high precedence) - // s jazz.(fast 2) => s (fast jazz 2) - const [callee, ...rest] = right.children; - const leftSide = children.slice(0, pipeIndex - 1); - const left = children[pipeIndex - 1]; - let args = [callee, left, ...rest]; - const call = { type: 'list', children: args }; - children = [...leftSide, call, ...rightSide]; + let chunks = this.split_children(children, 'pipe'); + while (chunks.length > 1) { + let [left, right, ...rest] = chunks; + if (right.length && right[0].type === 'list') { + // s jazz hh.(fast 2) => s jazz (fast 2 hh) + const target = left[left.length - 1]; // hh + const call = { type: 'list', children: [...right[0].children, target] }; + chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) } else { - // apply function to all left siblings (low precedence) - // s jazz . fast 2 => fast (s jazz) 2 - let leftSide = children.slice(0, pipeIndex); - if (leftSide.length === 1) { - leftSide = leftSide[0]; - } else { - // wrap in (..) if multiple items on the left side - leftSide = { type: 'list', children: leftSide }; - } - children = [right, leftSide, ...rightSide]; + //s jazz hh.fast 2 => (fast 2 (s jazz hh)) + const call = left.length > 1 ? { type: 'list', children: left } : left[0]; + chunks = [[...right, call], ...rest]; } } - return children; + return chunks[0]; } parse_pair(open_type, close_type) { this.consume(open_type); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index c51844be0..342a3f9fd 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -80,15 +80,15 @@ describe('mondo sugar', () => { it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(square a (angle b c))')); it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(angle a (square b c))')); - it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast (s jazz) 2)')); - it('should desugar . square', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (square bd cp) 2)')); - it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow (fast (s jazz) 2) 2)')); - it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast (s cp) 2)')); - it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast (square bd cp) 2)')); + it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast 2 (s jazz))')); + it('should desugar . square', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast 2 (square bd cp))')); + it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow 2 (fast 2 (s jazz)))')); + it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast 2 (s cp))')); + it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast 2 (square bd cp))')); it('should desugar . within , within []', () => - expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast (square bd cp) 2) x)')); + expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); - it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast hh 2))')); + it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast 2 hh))')); it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))')); it('should desugar , | of []', () => @@ -102,15 +102,15 @@ describe('mondo sugar', () => { it('should desugar , angle 3', () => expect(desguar('')).toEqual('(stack (angle bd cp) (angle hh oh))')); it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); - it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* b 2) c (/ d 3) e)')); - it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* (square b c) 3))')); - it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: x y)')); - it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: (: x y) z)')); - it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* (: bd 0) 2)')); - it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 0 2)')); + it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* 2 b) c (/ 3 d) e)')); + it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* 3 (square b c)))')); + it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)')); + it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); + it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); + it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( - '(speed (s (square bd (* hh 2) (crush cp 4) (angle mt ht lt))) .8)', + '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); }); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index dcfb1c3c2..0db3667c3 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -43,11 +43,6 @@ let runner = new MondoRunner({ if (!fn) { throw new Error(`[moundough]: unknown function "${name}"`); } - if (!['square', 'angle', 'stack', 'curly'].includes(name)) { - // flip args (pat to end) - const [pat, ...rest] = args; - args = [...rest, pat]; - } return fn(...args); }, leaf(token, scope) { From 492271d786f57921ee3bff62e30f386dd87b698d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 22 Mar 2025 23:28:57 +0100 Subject: [PATCH 065/538] mondo: support $ tidal style --- packages/mondo/mondo.mjs | 19 +++++++++++++++---- packages/mondo/test/mondo.test.mjs | 3 +++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 1a211e609..ce9e960a1 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -20,8 +20,9 @@ export class MondoParser { close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. + dollar: /^\$/, pipe: /^\./, - stack: /^[,$]/, + stack: /^[,]/, or: /^[|]/, plain: /^[a-zA-Z0-9-~_^]+/, }; @@ -103,7 +104,7 @@ export class MondoParser { } desugar_children(children) { children = this.resolve_ops(children); - children = this.resolve_pipes(children); + children = this.resolve_pipes(children, (children) => this.resolve_dollars(children)); return children; } // Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']] @@ -173,7 +174,7 @@ export class MondoParser { } return children; } - resolve_pipes(children) { + resolve_pipes(children, next) { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -184,10 +185,20 @@ export class MondoParser { chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) } else { //s jazz hh.fast 2 => (fast 2 (s jazz hh)) - const call = left.length > 1 ? { type: 'list', children: left } : left[0]; + const call = left.length > 1 ? { type: 'list', children: next(left) } : left[0]; chunks = [[...right, call], ...rest]; } } + return next(chunks[0]); + } + resolve_dollars(children) { + let chunks = this.split_children(children, 'dollar'); + while (chunks.length > 1) { + let [left, right, ...rest] = chunks; + //fast 2 $ s jazz hh => (fast 2 (s jazz hh)) + const call = right.length > 1 ? { type: 'list', children: right } : right[0]; + chunks = [[...left, call], ...rest]; + } return chunks[0]; } parse_pair(open_type, close_type) { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 342a3f9fd..0e859a8b2 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -108,6 +108,9 @@ describe('mondo sugar', () => { it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)')); + it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)')); + it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))')); + it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( From bca16cdf99614dbc8f1ef4728a13957045752075 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 22 Mar 2025 23:51:23 +0100 Subject: [PATCH 066/538] mondo: rename resolve_ -> desugar_ --- packages/mondo/mondo.mjs | 15 ++++++--------- packages/mondo/test/mondo.test.mjs | 1 + 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index ce9e960a1..a14dcc5de 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -102,11 +102,6 @@ export class MondoParser { } return this.consume(next); } - desugar_children(children) { - children = this.resolve_ops(children); - children = this.resolve_pipes(children, (children) => this.resolve_dollars(children)); - return children; - } // Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']] split_children(children, split_type) { const chunks = []; @@ -145,7 +140,7 @@ export class MondoParser { } return children; } - resolve_ops(children) { + desugar_ops(children) { while (true) { let opIndex = children.findIndex((child) => child.type === 'op'); if (opIndex === -1) break; @@ -174,7 +169,7 @@ export class MondoParser { } return children; } - resolve_pipes(children, next) { + desugar_pipes(children, next) { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -191,7 +186,7 @@ export class MondoParser { } return next(chunks[0]); } - resolve_dollars(children) { + desugar_dollars(children) { let chunks = this.split_children(children, 'dollar'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -221,7 +216,9 @@ export class MondoParser { // the type we've removed before splitting needs to be added back children = [{ type: 'plain', value: type }, ...children]; } - return this.desugar_children(children); + children = this.desugar_ops(children); + children = this.desugar_pipes(children, (children) => this.desugar_dollars(children)); + return children; }), ); return children; diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 0e859a8b2..f302174e8 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -104,6 +104,7 @@ describe('mondo sugar', () => { it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))')); it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* 2 b) c (/ 3 d) e)')); it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* 3 (square b c)))')); + it('should desugar []*', () => expect(desguar('[a b*<2 3> c]')).toEqual('(square a (* (angle 2 3) b) c)')); it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)')); it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); From 6d214564230d77dbbb1e18df91a7a1a3884ac90f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 11:18:18 +0100 Subject: [PATCH 067/538] mondo: allow # character in plain values (for sharps) --- packages/mondo/mondo.mjs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index a14dcc5de..cc05e131e 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -24,7 +24,7 @@ export class MondoParser { pipe: /^\./, stack: /^[,]/, or: /^[|]/, - plain: /^[a-zA-Z0-9-~_^]+/, + plain: /^[a-zA-Z0-9-~_^#]+/, }; // matches next token next_token(code, offset = 0) { @@ -109,7 +109,7 @@ export class MondoParser { let splitIndex = children.findIndex((child) => child.type === split_type); if (splitIndex === -1) break; const chunk = children.slice(0, splitIndex); - chunk.length && chunks.push(chunk); + chunks.push(chunk); children = children.slice(splitIndex + 1); } chunks.push(children); @@ -173,6 +173,16 @@ export class MondoParser { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; + if (!left.length) { + // . as lambda: (.fast 2) = (lambda (_) (fast _ 2)) + const args = [{ type: 'plain', value: '_' }]; + const body = this.desugar([args[0], ...children]); + return [ + { type: 'plain', value: 'lambda' }, + { type: 'list', children: args }, + { type: 'list', children: body }, + ]; + } if (right.length && right[0].type === 'list') { // s jazz hh.(fast 2) => s jazz (fast 2 hh) const target = left[left.length - 1]; // hh From 5eabcf061875c5241ccd84add4e9b3c5b111bc17 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 22:47:33 +0100 Subject: [PATCH 068/538] mondo mode for StrudelMirror / repl / mini repl --- packages/codemirror/codemirror.mjs | 5 +++-- packages/core/repl.mjs | 5 +++++ website/src/config.ts | 1 + website/src/docs/MiniRepl.jsx | 2 ++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index e96b533ec..193d96b55 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -62,7 +62,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS }); // https://codemirror.net/docs/guide/ -export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root }) { +export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) { const settings = codemirrorSettings.get(); const initialSettings = Object.keys(compartments).map((key) => compartments[key].of(extensions[key](parseBooleans(settings[key]))), @@ -75,7 +75,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo /* search(), highlightSelectionMatches(), */ ...initialSettings, - javascript(), + mondo ? [] : javascript(), sliderPlugin, widgetPlugin, // indentOnInput(), // works without. already brought with javascript extension? @@ -209,6 +209,7 @@ export class StrudelMirror { }, onEvaluate: () => this.evaluate(), onStop: () => this.stop(), + mondo: replOptions.mondo, }); const cmEditor = this.root.querySelector('.cm-editor'); if (cmEditor) { diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index e703909ff..147155fbe 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -21,6 +21,7 @@ export function repl({ setInterval, clearInterval, id, + mondo = false, }) { const state = { schedulerError: undefined, @@ -180,6 +181,10 @@ export function repl({ setTime(() => scheduler.now()); // TODO: refactor? await beforeEval?.({ code }); shouldHush && hush(); + + if (mondo) { + code = `mondolang\`${code}\``; + } let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { let patterns = Object.values(pPatterns); diff --git a/website/src/config.ts b/website/src/config.ts index 2f499d55f..124cfb3f6 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -81,6 +81,7 @@ export const SIDEBAR: Sidebar = { { text: 'Visual Feedback', link: 'learn/visual-feedback' }, { text: 'Offline', link: 'learn/pwa' }, { text: 'Patterns', link: 'technical-manual/patterns' }, + { text: 'Mondo Notation', link: 'learn/mondo-notation' }, { text: 'Music metadata', link: 'learn/metadata' }, { text: 'CSound', link: 'learn/csound' }, { text: 'Hydra', link: 'learn/hydra' }, diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index 0f3d3ce10..09ebb7692 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -30,6 +30,7 @@ export function MiniRepl({ maxHeight, autodraw, drawTime, + mondo = false, }) { const code = tunes ? tunes[0] : tune; const id = useMemo(() => s4(), []); @@ -85,6 +86,7 @@ export function MiniRepl({ }, beforeStart: () => audioReady, afterEval: ({ code }) => setVersionDefaultsFrom(code), + mondo, }); // init settings editor.setCode(code); From 51e3aa257ccbf1e1f21a4caadcd9cf7d25ce4268 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 22:49:03 +0100 Subject: [PATCH 069/538] mondolang function for mondo repl + fix rests --- packages/mondough/mondough.mjs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 0db3667c3..f42f0ca76 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -10,6 +10,7 @@ import { pace, chooseIn, degradeBy, + silence, } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -20,9 +21,11 @@ const arrayRange = (start, stop, step = 1) => Array.from({ length: Math.abs(stop - start) / step + 1 }, (_, index) => start < stop ? start + index * step : start - index * step, ); -const range = (min, max) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); +const range = (max, min) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); let lib = {}; +lib['-'] = silence; +lib['~'] = silence; lib.curly = stepcat; lib.square = (...args) => stepcat(...args).setSteps(1); lib.angle = (...args) => stepcat(...args).pace(1); @@ -46,14 +49,15 @@ let runner = new MondoRunner({ return fn(...args); }, leaf(token, scope) { - let { value } = token; + let { value, type } = token; // local scope - if (token.type === 'plain' && scope[value]) { + if (type === 'plain' && scope[value]) { return reify(scope[value]); // -> local scope has no location } const [from, to] = token.loc; - if (token.type === 'plain' && strudelScope[value]) { - // what if we want a string that happens to also be a variable name? + const variable = lib[value] ?? strudelScope[value]; + if (type === 'plain' && typeof variable !== 'undefined') { + // problem: collisions when we want a string that happens to also be a variable name // example: "s sine" -> sine is also a variable return reify(strudelScope[value]).withLoc(from, to); } @@ -66,7 +70,7 @@ export function mondo(code, offset = 0) { code = code.join(''); } const pat = runner.run(code, offset); - return pat.markcss('color: var(--foreground);text-decoration:underline'); + return pat.markcss('color: var(--caret,--foreground);text-decoration:underline'); } let getLocations = (code, offset) => runner.parser.get_locations(code, offset); @@ -80,6 +84,12 @@ export const mondi = (str, offset) => { registerLanguage('mondo', { getLocations, }); + +// this is like mondo, but with a zero offset +export const mondolang = (code) => mondo(code, 0); +registerLanguage('mondolang', { + getLocations: (code) => getLocations(code, 0), +}); // uncomment the following to use mondo as mini notation language /* registerLanguage('minilang', { name: 'mondi', From dd5743ab8c3e0ae057a0859e57badfaf98a00e1e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 22:49:23 +0100 Subject: [PATCH 070/538] mondo: bring back $ for stacking --- packages/mondo/mondo.mjs | 37 ++++++++++++++++++------------ packages/mondo/test/mondo.test.mjs | 4 ++-- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index cc05e131e..3f0eb0385 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -20,9 +20,9 @@ export class MondoParser { close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. - dollar: /^\$/, + // dollar: /^\$/, pipe: /^\./, - stack: /^[,]/, + stack: /^[,$]/, or: /^[|]/, plain: /^[a-zA-Z0-9-~_^#]+/, }; @@ -121,16 +121,20 @@ export class MondoParser { return next(children); } // collect args of stack function - const args = chunks.map((chunk) => { - if (chunk.length === 1) { - // chunks of one element can be added to the stack as is - return chunk[0]; - } else { + const args = chunks + .map((chunk) => { + if (!chunk.length) { + return; // useful for things like "$ s bd $ s hh*8" (first chunk is empty) + } + if (chunk.length === 1) { + // chunks of one element can be added to the stack as is + return chunk[0]; + } // chunks of multiple args chunk = next(chunk); return { type: 'list', children: chunk }; - } - }); + }) + .filter(Boolean); // ignore empty chunks return [{ type: 'plain', value: split_type }, ...args]; } // prevents to get a list, e.g. ((x y)) => (x y) @@ -169,7 +173,7 @@ export class MondoParser { } return children; } - desugar_pipes(children, next) { + desugar_pipes(children) { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -190,13 +194,15 @@ export class MondoParser { chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) } else { //s jazz hh.fast 2 => (fast 2 (s jazz hh)) - const call = left.length > 1 ? { type: 'list', children: next(left) } : left[0]; + // const call = left.length > 1 ? { type: 'list', children: next(left) } : left[0]; + const call = left.length > 1 ? { type: 'list', children: left } : left[0]; chunks = [[...right, call], ...rest]; } } - return next(chunks[0]); + // return next(chunks[0]); + return chunks[0]; } - desugar_dollars(children) { + /* desugar_dollars(children) { let chunks = this.split_children(children, 'dollar'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; @@ -205,7 +211,7 @@ export class MondoParser { chunks = [[...left, call], ...rest]; } return chunks[0]; - } + } */ parse_pair(open_type, close_type) { this.consume(open_type); const children = []; @@ -227,7 +233,8 @@ export class MondoParser { children = [{ type: 'plain', value: type }, ...children]; } children = this.desugar_ops(children); - children = this.desugar_pipes(children, (children) => this.desugar_dollars(children)); + // children = this.desugar_pipes(children, (children) => this.desugar_dollars(children)); + children = this.desugar_pipes(children); return children; }), ); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index f302174e8..a35eb8aa8 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -109,9 +109,9 @@ describe('mondo sugar', () => { it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)')); - it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)')); + /* it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)')); it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))')); - it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); + it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */ it('should desugar README example', () => expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( From 3a19e23473299a6664e085f9aa1620a262663f38 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 22:49:56 +0100 Subject: [PATCH 071/538] mondo: doc --- website/src/pages/learn/mondo-notation.mdx | 145 +++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 website/src/pages/learn/mondo-notation.mdx diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx new file mode 100644 index 000000000..caf9f67fe --- /dev/null +++ b/website/src/pages/learn/mondo-notation.mdx @@ -0,0 +1,145 @@ +--- +title: Mondo Notation +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Mondo Notation + +"Mondo Notation" is a new kind of notation that is similar to [Mini Notation](/learn/mini-notation/), but with enough abilities to make it work as a standalone pattern language. +Here's an example: + +/2 +.jux /2 +.rarely (12.note.add) .delay .4 .vib 1:.3 +.fm (sine/4.range .5 4).fmh 2.02 +.lpf (sine/2.range 200 2000).lpq 4 +.s piano .clip 2 +.add (perlin.range 0 .2 .note)`} +/> + +## Calling Functions + +Compared to Mini Notation, the most notable feature of Mondo Notation is the ability to call functions using round brackets: + + + +The first element inside the brackets is the function name. In JS, this would look like: + + + +The outermost parens are not needed, so we can drop them: + + + +## Mini Notation Features + +Besides function calling with round parens, Mondo Notation has a lot in common with Mini Notation: + +- `[]` for 1-cycle sequences +- `<>` for multi-cycle sequences +- `{}` for stepped sequences (more on that later) +- \* => [fast](/learn/time-modifiers/#fast) +- / => [slow](/learn/time-modifiers/#slow) +- , => [stack](/learn/factories/#stack) +- ! => [extend](/learn/stepwise/#extend) +- @ => [expand](/learn/stepwise/#expand) +- % => [pace](/learn/stepwise/#pace) +- ? => [degradeBy](/learn/random-modifiers/#degradeby) (currently requires right operand) +- : => tail (creates a list) +- .. => range (between numbers) +- | => [chooseIn](/learn/random-modifiers/#choose) + +Example: + +`} +/> + +## Chaining Functions + +Similar to how it works in JS, we can chain functions calls with the "." operator: + +*4 +.scale C4:minor +.jux rev +.dec .2 +.delay .5`} +/> + +Here's the same written in JS: + +*4") +.scale("C4:minor") +.jux(rev) +.dec(.2) +.delay(.5)`} +/> + +### Chaining Functions Locally + +A function can be applied "locally" by wrapping it in round parens: + + + +in this case, `delay .6` will only be applied to `cp`. compare this with the JS version: + + + +here we can see much we can save when there's no boundary between mini notation and function calls! + +### Lambda Functions + +Some functions in strudel expect a function as input, for example: + +x.dec(.1))`} /> + +in mondo, the `x=>x.` can be shortened to: + + + +chaining works as expected: + + + +## Strings + +You can use "double quotes" and 'single quotes' to get a string: + + + +## Multiple Patterns + +The `$` sign can be used to separate multiple patterns: + +.voicing +.struct[x - - x - x - -].delay.5`} +/> + +The `$` sign is an alias for `,` so it will create a stack behind the scenes. From 92b5b6538f91a300c1c2ca0aef04d3ed3830b06a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 23:12:52 +0100 Subject: [PATCH 072/538] mondo: improve doc --- website/src/pages/learn/mondo-notation.mdx | 42 ++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index caf9f67fe..6a2f07ff9 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -14,14 +14,20 @@ Here's an example: /2 -.jux /2 -.rarely (12.note.add) .delay .4 .vib 1:.3 -.fm (sine/4.range .5 4).fmh 2.02 -.lpf (sine/2.range 200 2000).lpq 4 -.s piano .clip 2 -.add (perlin.range 0 .2 .note)`} + tune={`$ note c2 .(euclid <3 6 3> <8 16>) . *2 +.s "sine" .add (note [0 <12 24>]*2) +.dec(sine .range .2 2) .room .5 +.lpf(sine/3.range 120 400) +.lpenv(rand .range .5 4) +.lpq(perlin .range 5 12 . * 2) +.dist 1 .fm 4 .fmh 5.01 .fmdecay <.1 .2> +.postgain .6 .delay .1 .clip 5 + +$ s [bd bd bd bd] .bank tr909.clip.5 +.ply<1 [1 [2 4]]> + +$ s oh*4 .press .bank tr909 .speed.8 +.dec <.02 .05>*2 .(add (saw/8.range 0 1))`} /> ## Calling Functions @@ -42,21 +48,29 @@ The outermost parens are not needed, so we can drop them: Besides function calling with round parens, Mondo Notation has a lot in common with Mini Notation: +### Brackets + - `[]` for 1-cycle sequences - `<>` for multi-cycle sequences - `{}` for stepped sequences (more on that later) + +### Infix Operators + - \* => [fast](/learn/time-modifiers/#fast) - / => [slow](/learn/time-modifiers/#slow) -- , => [stack](/learn/factories/#stack) - ! => [extend](/learn/stepwise/#extend) - @ => [expand](/learn/stepwise/#expand) - % => [pace](/learn/stepwise/#pace) - ? => [degradeBy](/learn/random-modifiers/#degradeby) (currently requires right operand) - : => tail (creates a list) - .. => range (between numbers) + +### Separators + +- , => [stack](/learn/factories/#stack) - | => [chooseIn](/learn/random-modifiers/#choose) -Example: +### Example + +In this case, the \*2 will be applied to the whole pattern. + ### Lambda Functions Some functions in strudel expect a function as input, for example: From 222e479e30fede8f8cf567207b0fc691d57508d3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 23 Mar 2025 23:18:57 +0100 Subject: [PATCH 073/538] mondo: more docs --- website/src/pages/learn/mondo-notation.mdx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index 6a2f07ff9..aa7904987 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -30,6 +30,15 @@ $ s oh*4 .press .bank tr909 .speed.8 .dec <.02 .05>*2 .(add (saw/8.range 0 1))`} /> +## Mondo in the REPL + +For now, you can only use mondo in the repl like this: + + + +The rest of this site will only use the mondo notation itself. +In the future, the REPL might get a way to use mondo notation directly. + ## Calling Functions Compared to Mini Notation, the most notable feature of Mondo Notation is the ability to call functions using round brackets: @@ -64,9 +73,6 @@ Besides function calling with round parens, Mondo Notation has a lot in common w - ? => [degradeBy](/learn/random-modifiers/#degradeby) (currently requires right operand) - : => tail (creates a list) - .. => range (between numbers) - -### Separators - - , => [stack](/learn/factories/#stack) - | => [chooseIn](/learn/random-modifiers/#choose) From 2938bfd88f9e74449a585d49c41462dda2cb9beb Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 02:46:26 +0100 Subject: [PATCH 074/538] mondo: add highly impractical .(. notation for local infix application --- packages/mondo/README.md | 2 +- packages/mondo/mondo.mjs | 79 ++++++++++++++++++++++++------ packages/mondo/test/mondo.test.mjs | 12 ++++- 3 files changed, 74 insertions(+), 19 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index a6a59dd94..096d77d56 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -9,7 +9,7 @@ an experimental parser for an *uzulang*, a custom dsl for patterns that can stan import { MondoRunner } from 'uzu' const runner = MondoRunner({ seq, cat, s, crush, speed, '*': fast }); -const pat = runner.run('s [bd hh*2 cp.(crush 4) ] . speed .8') +const pat = runner.run('s [bd hh*2 cp.(.crush 4) ] . speed .8') ``` the above code will create the following call structure: diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 3f0eb0385..70015cc2d 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -63,6 +63,8 @@ export class MondoParser { } // take code, return abstract syntax tree parse(code, offset) { + this.code = code; + this.offset = offset; this.tokens = this.tokenize(code, offset); const expressions = []; while (this.tokens.length) { @@ -165,7 +167,6 @@ export class MondoParser { children[opIndex] = op; continue; } - //const call = { type: 'list', children: [op, left, right] }; const call = { type: 'list', children: [op, right, left] }; // insert call while keeping other siblings children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; @@ -173,24 +174,73 @@ export class MondoParser { } return children; } + get_lambda(args, children) { + // (.fast 2) = (lambda (_) (fast _ 2)) + const body = this.desugar(children); + return [ + { type: 'plain', value: 'lambda' }, + { type: 'list', children: args }, + { type: 'list', children: body }, + ]; + } + // inserts target into lambda body + desugar_lambda(lambda, target) { + // lambda looks like return value from this.get_lambda + const [_, args, body] = lambda; + if (args.length > 1) { + throw new Error('desugar_lambda with >1 arg is unsupported rn'); + } + const argNames = args.children.map((child) => child.value); + let desugar = (child) => { + if (child.type === 'plain' && child.value === argNames[0]) { + return target; + } + if (child.type === 'list') { + child.children = child.children.map(desugar); + } + return child; + }; + return desugar(body); + } + // returns location range of given ast (even if desugared) + get_range(ast, range = [Infinity, 0]) { + let union = (a, b) => [Math.min(a[0], b[0]), Math.max(a[1], b[1])]; + if (ast.loc) { + return union(range, ast.loc); + } + if (ast.type !== 'list') { + return range; + } + return ast.children.reduce((range, child) => { + const childrange = this.get_range(child, range); + return union(range, childrange); + }, range); + } + errorhead(ast) { + return `[mondo ${this.get_range(ast)?.join(':') || '?'}]`; + } + // returns original user code where the given ast originates (even if desugared) + get_code_snippet(ast) { + const [min, max] = this.get_range(ast); + return this.code.slice(min - this.offset, max - this.offset); + } desugar_pipes(children) { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; if (!left.length) { - // . as lambda: (.fast 2) = (lambda (_) (fast _ 2)) - const args = [{ type: 'plain', value: '_' }]; - const body = this.desugar([args[0], ...children]); - return [ - { type: 'plain', value: 'lambda' }, - { type: 'list', children: args }, - { type: 'list', children: body }, - ]; + const arg = { type: 'plain', value: '_' }; + return this.get_lambda([arg], [arg, ...children]); } if (right.length && right[0].type === 'list') { - // s jazz hh.(fast 2) => s jazz (fast 2 hh) + // s jazz hh.(.fast 2) => s jazz (hh.fast 2) = s jazz (fast 2 hh) const target = left[left.length - 1]; // hh - const call = { type: 'list', children: [...right[0].children, target] }; + + if (right[0].children[0].value !== 'lambda') { + const snip = this.get_code_snippet(right[0]); + throw new Error(`${this.errorhead(right[0])} no lambda: expected "${snip}" to start with "."`); + } + const call = this.desugar_lambda(right[0].children, target); chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) } else { //s jazz hh.fast 2 => (fast 2 (s jazz hh)) @@ -317,18 +367,15 @@ export class MondoRunner { console.log(printAst(ast)); return this.call(ast); } - errorhead(ast) { - return `[mondo ${ast.loc?.join(':') || ''}]`; - } call(ast, scope = []) { // for a node to be callable, it needs to be a list - this.assert(ast.type === 'list', `${this.errorhead(ast)} function call: expected list, got ${ast.type}`); + this.assert(ast.type === 'list', `${this.parser.errorhead(ast)} function call: expected list, got ${ast.type}`); // the first element is expected to be the function name const first = ast.children[0]; const name = first.value; this.assert( first?.type === 'plain', - `${this.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, + `${this.parser.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, ); if (name === 'lambda') { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index a35eb8aa8..d5e33cbce 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -88,7 +88,7 @@ describe('mondo sugar', () => { it('should desugar . within , within []', () => expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); - it('should desugar . ()', () => expect(desguar('[jazz hh.(fast 2)]')).toEqual('(square jazz (fast 2 hh))')); + it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))')); it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))')); it('should desugar , | of []', () => @@ -114,7 +114,15 @@ describe('mondo sugar', () => { it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */ it('should desugar README example', () => - expect(desguar('s [bd hh*2 cp.(crush 4) ] . speed .8')).toEqual( + expect(desguar('s [bd hh*2 cp.(.crush 4) ] . speed .8')).toEqual( '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); + + it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(lambda (_) (fast 2 _))')); + it('should desugar lambda with pipe', () => + expect(desguar('(.fast 2 .room 1)')).toEqual('(lambda (_) (room 1 (fast 2 _)))')); + const lambda = parser.parse('(lambda (_) (fast 2 _))'); + const target = { type: 'plain', value: 'xyz' }; + it('should desugar_lambda', () => + expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); }); From 7716fdb98e8ee64f70ef77472b7b38c6f7f3afd1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 03:22:23 +0100 Subject: [PATCH 075/538] mondo: refactor - rename MondoRunner.call => .evaluate - can now evaluate leafs - remove .( because it's confusing and half baked - support (.) as id function --- packages/mondo/README.md | 21 ++----- packages/mondo/mondo.mjs | 97 +++++++++--------------------- packages/mondo/test/mondo.test.mjs | 9 +-- 3 files changed, 37 insertions(+), 90 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 096d77d56..bd9104b9f 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -9,7 +9,7 @@ an experimental parser for an *uzulang*, a custom dsl for patterns that can stan import { MondoRunner } from 'uzu' const runner = MondoRunner({ seq, cat, s, crush, speed, '*': fast }); -const pat = runner.run('s [bd hh*2 cp.(.crush 4) ] . speed .8') +const pat = runner.run('s [bd hh*2 (cp.crush 4) ] . speed .8') ``` the above code will create the following call structure: @@ -52,16 +52,6 @@ n("0 1 2").add(n("<0 -4>")).scale("C:minor") --- -```plaintext -n[0 1 2].(add<0 -4>).scale"C minor" -``` - -```js -n("0 1 2".add("<0 -4>")).scale("C:minor") -``` - ---- - ```plaintext n[0 1 2].scale"C minor" .sometimes (12.note.add) @@ -75,11 +65,11 @@ n("0 1 2").scale("C:minor") --- ```plaintext -note g2*8.dec /2.(range .1 .4) +note g2*8.dec /2 ``` ```js -note("g2*8").dec(cat(sine, saw).slow(2).range(.1, .4)) +note("g2*8").dec(cat(sine, saw).range(.1, .4).slow(2)) ``` --- @@ -95,7 +85,7 @@ n("<0 1 2 3 4>*4").scale("C:minor").jux(cat(rev,press)) --- mondo` -sound [bd sd.(every 3 (.fast 4))].jux +sound [bd (sd.every 3 (.fast 4))].jux ` // og "Alternate Timelines for TidalCycles" example: // jux <(rev) (iter 4)> $ sound [bd (every 3 (fast 4) [sn])] @@ -129,9 +119,6 @@ comments variables: note g2*8.dec sine -sine.(range 0 4)/2 doesnt work -sine/2.(range 0 4) works - n (irand 8. ribbon 0 2) .scale"C minor" => lags because no whole ### reference diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 70015cc2d..165e597bb 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -176,31 +176,9 @@ export class MondoParser { } get_lambda(args, children) { // (.fast 2) = (lambda (_) (fast _ 2)) - const body = this.desugar(children); - return [ - { type: 'plain', value: 'lambda' }, - { type: 'list', children: args }, - { type: 'list', children: body }, - ]; - } - // inserts target into lambda body - desugar_lambda(lambda, target) { - // lambda looks like return value from this.get_lambda - const [_, args, body] = lambda; - if (args.length > 1) { - throw new Error('desugar_lambda with >1 arg is unsupported rn'); - } - const argNames = args.children.map((child) => child.value); - let desugar = (child) => { - if (child.type === 'plain' && child.value === argNames[0]) { - return target; - } - if (child.type === 'list') { - child.children = child.children.map(desugar); - } - return child; - }; - return desugar(body); + children = this.desugar(children); + const body = children.length === 1 ? children[0] : { type: 'list', children }; + return [{ type: 'plain', value: 'lambda' }, { type: 'list', children: args }, body]; } // returns location range of given ast (even if desugared) get_range(ast, range = [Infinity, 0]) { @@ -228,40 +206,23 @@ export class MondoParser { let chunks = this.split_children(children, 'pipe'); while (chunks.length > 1) { let [left, right, ...rest] = chunks; + + if (right.length && right[0].type === 'list') { + // x.(y) => not allowed anymore for now.. + const snip = this.get_code_snippet(right[0]); + throw new Error(`${this.errorhead(right[0])} cannot apply list: expected "(${snip})" to be a word`); + } if (!left.length) { const arg = { type: 'plain', value: '_' }; return this.get_lambda([arg], [arg, ...children]); } - if (right.length && right[0].type === 'list') { - // s jazz hh.(.fast 2) => s jazz (hh.fast 2) = s jazz (fast 2 hh) - const target = left[left.length - 1]; // hh - - if (right[0].children[0].value !== 'lambda') { - const snip = this.get_code_snippet(right[0]); - throw new Error(`${this.errorhead(right[0])} no lambda: expected "${snip}" to start with "."`); - } - const call = this.desugar_lambda(right[0].children, target); - chunks = [[...left.slice(0, -1), call, ...right.slice(1)], ...rest]; // jazz (fast 2 hh) - } else { - //s jazz hh.fast 2 => (fast 2 (s jazz hh)) - // const call = left.length > 1 ? { type: 'list', children: next(left) } : left[0]; - const call = left.length > 1 ? { type: 'list', children: left } : left[0]; - chunks = [[...right, call], ...rest]; - } + // s jazz hh.fast 2 => (fast 2 (s jazz hh)) + const call = left.length > 1 ? { type: 'list', children: left } : left[0]; + chunks = [[...right, call], ...rest]; } // return next(chunks[0]); return chunks[0]; } - /* desugar_dollars(children) { - let chunks = this.split_children(children, 'dollar'); - while (chunks.length > 1) { - let [left, right, ...rest] = chunks; - //fast 2 $ s jazz hh => (fast 2 (s jazz hh)) - const call = right.length > 1 ? { type: 'list', children: right } : right[0]; - chunks = [[...left, call], ...rest]; - } - return chunks[0]; - } */ parse_pair(open_type, close_type) { this.consume(open_type); const children = []; @@ -365,11 +326,20 @@ export class MondoRunner { run(code, offset = 0) { const ast = this.parser.parse(code, offset); console.log(printAst(ast)); - return this.call(ast); + return this.evaluate(ast); } - call(ast, scope = []) { - // for a node to be callable, it needs to be a list - this.assert(ast.type === 'list', `${this.parser.errorhead(ast)} function call: expected list, got ${ast.type}`); + evaluate(ast, scope = []) { + if (ast.type !== 'list') { + // is leaf + if (ast.type === 'number') { + ast.value = Number(ast.value); + } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { + arg.value = arg.value.slice(1, -1); + } + return this.lib.leaf(ast, scope); + } + + // is list // the first element is expected to be the function name const first = ast.children[0]; const name = first.value; @@ -385,23 +355,12 @@ export class MondoRunner { scope = { [argNames[0]]: x, // TODO: merge scope... + support multiple args }; - return this.call(body, scope); + return this.evaluate(body, scope); }; } - // process args - const args = ast.children.slice(1).map((arg) => { - if (arg.type === 'list') { - return this.call(arg, scope); - } - if (arg.type === 'number') { - arg.value = Number(arg.value); - } else if (['quotes_double', 'quotes_single'].includes(arg.type)) { - arg.value = arg.value.slice(1, -1); - } - return this.lib.leaf(arg, scope); - }); - + // evaluate args + const args = ast.children.slice(1).map((arg) => this.evaluate(arg, scope)); return this.lib.call(name, args, scope); } } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index d5e33cbce..2fd45d46d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -88,7 +88,7 @@ describe('mondo sugar', () => { it('should desugar . within , within []', () => expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); - it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))')); + // it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))')); it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))')); it('should desugar , | of []', () => @@ -114,15 +114,16 @@ describe('mondo sugar', () => { it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */ it('should desugar README example', () => - expect(desguar('s [bd hh*2 cp.(.crush 4) ] . speed .8')).toEqual( + expect(desguar('s [bd hh*2 (cp.crush 4) ] . speed .8')).toEqual( '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); + it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(lambda (_) _)')); it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(lambda (_) (fast 2 _))')); it('should desugar lambda with pipe', () => expect(desguar('(.fast 2 .room 1)')).toEqual('(lambda (_) (room 1 (fast 2 _)))')); - const lambda = parser.parse('(lambda (_) (fast 2 _))'); + /* const lambda = parser.parse('(lambda (_) (fast 2 _))'); const target = { type: 'plain', value: 'xyz' }; it('should desugar_lambda', () => - expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); + expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); */ }); From abc7a1e48d3e8cfb301d39a711ddb59ffcf8ae38 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 03:55:00 +0100 Subject: [PATCH 076/538] mondo: this is the way --- packages/mondo/mondo.mjs | 17 +++++++---------- packages/mondough/mondough.mjs | 5 +++++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 165e597bb..3ff781256 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -207,11 +207,6 @@ export class MondoParser { while (chunks.length > 1) { let [left, right, ...rest] = chunks; - if (right.length && right[0].type === 'list') { - // x.(y) => not allowed anymore for now.. - const snip = this.get_code_snippet(right[0]); - throw new Error(`${this.errorhead(right[0])} cannot apply list: expected "(${snip})" to be a word`); - } if (!left.length) { const arg = { type: 'plain', value: '_' }; return this.get_lambda([arg], [arg, ...children]); @@ -342,11 +337,13 @@ export class MondoRunner { // is list // the first element is expected to be the function name const first = ast.children[0]; - const name = first.value; - this.assert( - first?.type === 'plain', - `${this.parser.errorhead(first)} expected function name, got ${first.type}${name ? ` "${name}"` : ''}.`, - ); + let name; + if (first?.type !== 'list') { + name = first.value; // regular function call e.g. (fast 2 (s bd)) + } else { + // dynamic function name e.g. "( 2 (s bd))" + name = this.evaluate(first); + } if (name === 'lambda') { const [_, args, body] = ast.children; diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index f42f0ca76..081d054b9 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -11,6 +11,7 @@ import { chooseIn, degradeBy, silence, + isPattern, } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -42,6 +43,10 @@ lib['or'] = (...children) => chooseIn(...children); // always has structure but let runner = new MondoRunner({ call(name, args, scope) { + if (isPattern(name)) { + // patterned function name, e.g. "s bd . 2" + return name.fmap((fn) => fn(...args)).innerJoin(); + } const fn = lib[name] || strudelScope[name]; if (!fn) { throw new Error(`[moundough]: unknown function "${name}"`); From 1738e4d53892d0434082b4b3af0b957530fa804b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 03:55:54 +0100 Subject: [PATCH 077/538] fix: lint --- packages/mondo/mondo.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 3ff781256..5419d15b5 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -329,7 +329,7 @@ export class MondoRunner { if (ast.type === 'number') { ast.value = Number(ast.value); } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { - arg.value = arg.value.slice(1, -1); + ast.value = ast.value.slice(1, -1); } return this.lib.leaf(ast, scope); } From 64a6dacc1ed8975ff99c7022aac1495db412fb79 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 09:27:02 +0100 Subject: [PATCH 078/538] mondo: patternable function names --- packages/mondo/mondo.mjs | 19 ++++------- packages/mondough/mondough.mjs | 37 +++++++++++++++------- website/src/pages/learn/mondo-notation.mdx | 4 +-- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 5419d15b5..d031af4eb 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -280,7 +280,7 @@ export class MondoParser { get_locations(code, offset = 0) { let walk = (ast, locations = []) => { if (ast.type === 'list') { - return ast.children.slice(1).forEach((child) => walk(child, locations)); + return ast.children.forEach((child) => walk(child, locations)); } if (ast.loc) { locations.push(ast.loc); @@ -335,17 +335,11 @@ export class MondoRunner { } // is list - // the first element is expected to be the function name - const first = ast.children[0]; - let name; - if (first?.type !== 'list') { - name = first.value; // regular function call e.g. (fast 2 (s bd)) - } else { - // dynamic function name e.g. "( 2 (s bd))" - name = this.evaluate(first); + if (!ast.children.length) { + throw new Error(`empty list`); } - if (name === 'lambda') { + if (ast.children[0].value === 'lambda') { const [_, args, body] = ast.children; const argNames = args.children.map((child) => child.value); return (x) => { @@ -356,8 +350,9 @@ export class MondoRunner { }; } + const args = ast.children.map((arg) => this.evaluate(arg, scope)); + // we could short circuit arg[0] if its plain... // evaluate args - const args = ast.children.slice(1).map((arg) => this.evaluate(arg, scope)); - return this.lib.call(name, args, scope); + return this.lib.call(args[0], args.slice(1), scope); } } diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 081d054b9..01247e78b 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -11,7 +11,6 @@ import { chooseIn, degradeBy, silence, - isPattern, } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from '../mondo/mondo.mjs'; @@ -24,7 +23,10 @@ const arrayRange = (start, stop, step = 1) => ); const range = (max, min) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b)))); +let nope = (...args) => args[args.length - 1]; + let lib = {}; +lib['nope'] = nope; lib['-'] = silence; lib['~'] = silence; lib.curly = stepcat; @@ -43,15 +45,19 @@ lib['or'] = (...children) => chooseIn(...children); // always has structure but let runner = new MondoRunner({ call(name, args, scope) { - if (isPattern(name)) { - // patterned function name, e.g. "s bd . 2" - return name.fmap((fn) => fn(...args)).innerJoin(); + // name is expected to be a pattern of functions! + const first = name.firstCycle(true)[0]; + if (typeof first?.value !== 'function') { + throw new Error(`[mondough] "${first}" is not a function`); } - const fn = lib[name] || strudelScope[name]; - if (!fn) { - throw new Error(`[moundough]: unknown function "${name}"`); - } - return fn(...args); + return name + .fmap((fn) => { + if (typeof fn !== 'function') { + throw new Error(`[mondough] "${fn}" is not a function`); + } + return fn(...args); + }) + .innerJoin(); }, leaf(token, scope) { let { value, type } = token; @@ -59,14 +65,21 @@ let runner = new MondoRunner({ if (type === 'plain' && scope[value]) { return reify(scope[value]); // -> local scope has no location } - const [from, to] = token.loc; const variable = lib[value] ?? strudelScope[value]; + let pat; if (type === 'plain' && typeof variable !== 'undefined') { // problem: collisions when we want a string that happens to also be a variable name // example: "s sine" -> sine is also a variable - return reify(strudelScope[value]).withLoc(from, to); + pat = reify(variable); + } else { + pat = reify(value); } - return reify(value).withLoc(from, to); + + if (token.loc) { + pat = pat.withLoc(token.loc[0], token.loc[1]); + } + pat.foo = true; + return pat; }, }); diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index aa7904987..2bc952341 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -14,7 +14,7 @@ Here's an example: <8 16>) . *2 + tune={`$ note (c2 .euclid <3 6 3> <8 16>) . *2 .s "sine" .add (note [0 <12 24>]*2) .dec(sine .range .2 2) .room .5 .lpf(sine/3.range 120 400) @@ -27,7 +27,7 @@ $ s [bd bd bd bd] .bank tr909.clip.5 .ply<1 [1 [2 4]]> $ s oh*4 .press .bank tr909 .speed.8 -.dec <.02 .05>*2 .(add (saw/8.range 0 1))`} +.dec (<.02 .05>*2 .add (saw/8.range 0 1))`} /> ## Mondo in the REPL From 6b2b8b5124c9eefa1d25904958a09871b1e2cb55 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 09:30:49 +0100 Subject: [PATCH 079/538] fix: test --- packages/mondo/test/mondo.test.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 2fd45d46d..f987cbc2b 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -12,7 +12,7 @@ const p = (code) => parser.parse(code, -1); describe('mondo tokenizer', () => { const parser = new MondoParser(); - it('should tokenize with loangleions', () => + it('should tokenize with locations', () => expect( parser .tokenize('(one two three)') @@ -22,6 +22,7 @@ describe('mondo tokenizer', () => { // it('should parse with loangleions', () => expect(parser.parse('(one two three)')).toEqual()); it('should get loangleions', () => expect(parser.get_locations('s bd rim')).toEqual([ + [0, 1], [2, 4], [5, 8], ])); From e9de45993e2c9fe9fa002ccf2af361fc8dea6214 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 29 Mar 2025 19:54:04 +0100 Subject: [PATCH 080/538] signal: remove signal synonyms for now --- packages/core/signal.mjs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 64a177253..9152b6a32 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -72,27 +72,23 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t)); /** * A sine signal between 0 and 1. * @return {Pattern} - * @synonyms sin * @example * n(sine.segment(16).range(0,15)) * .scale("C:minor") * */ export const sine = sine2.fromBipolar(); -export const sin = sine; /** * A cosine signal between 0 and 1. * * @return {Pattern} - * @synonyms cos * @example * n(stack(sine,cosine).segment(16).range(0,15)) * .scale("C:minor") * */ export const cosine = sine._early(Fraction(1).div(4)); -export const cos = cosine; /** * A cosine signal between -1 and 1 (like `cosine`, but bipolar). @@ -105,13 +101,11 @@ export const cosine2 = sine2._early(Fraction(1).div(4)); * A square signal between 0 and 1. * * @return {Pattern} - * @synonyms sqr * @example * n(square.segment(4).range(0,7)).scale("C:minor") * */ export const square = signal((t) => Math.floor((t * 2) % 2)); -export const sqr = square; /** * A square signal between -1 and 1 (like `square`, but bipolar). From c9f58220a3c8bf02a35559ac9489d00becb29c2e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 29 Mar 2025 19:55:29 +0100 Subject: [PATCH 081/538] remove minor change --- packages/core/signal.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 9152b6a32..ac8aa3e3d 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -99,7 +99,6 @@ export const cosine2 = sine2._early(Fraction(1).div(4)); /** * A square signal between 0 and 1. - * * @return {Pattern} * @example * n(square.segment(4).range(0,7)).scale("C:minor") From 2e017e46f94962b2ec43a8db44853bcdd4d6ddc0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 29 Mar 2025 20:17:09 +0100 Subject: [PATCH 082/538] trim readme --- packages/mondo/README.md | 124 --------------------------------------- 1 file changed, 124 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index bd9104b9f..551690467 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -25,127 +25,3 @@ the above code will create the following call structure: ) .8 ) ``` - -you can pass all available functions to *MondoRunner* as an object. - -## snippets / thoughts - -### variants of add - -```plaintext -n[0 1 2].add(<0 -4>.n).scale"C minor" -``` - -```js -n("0 1 2").add("<0 -4>".n()).scale("C:minor") -``` - ---- - -```plaintext -n[0 1 2].add(n<0 -4>).scale"C minor" -``` - -```js -n("0 1 2").add(n("<0 -4>")).scale("C:minor") -``` - ---- - -```plaintext -n[0 1 2].scale"C minor" -.sometimes (12.note.add) -``` - -```js -n("0 1 2").scale("C:minor") -.sometimes(add(note("12"))) -``` - ---- - -```plaintext -note g2*8.dec /2 -``` - -```js -note("g2*8").dec(cat(sine, saw).range(.1, .4).slow(2)) -``` - ---- - -```plaintext -n <0 1 2 3 4>*4 .scale"C minor" .jux -``` - -```js -n("<0 1 2 3 4>*4").scale("C:minor").jux(cat(rev,press)) -``` - ---- - -mondo` -sound [bd (sd.every 3 (.fast 4))].jux -` -// og "Alternate Timelines for TidalCycles" example: -// jux <(rev) (iter 4)> $ sound [bd (every 3 (fast 4) [sn])] - -### things mondo cant do - -how to write lists? - -```js -arrange( - [4, "(3,8)"], - [2, "(5,8)"] -).note() -``` - -how to write objects: - -```js -samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/dirt-samples') -``` - -how to access array indices? - -```js -note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>").arpWith(haps => haps[2]) -``` - -s hh .struct(binaryN 55532 16) - -comments - -variables: note g2*8.dec sine - -n (irand 8. ribbon 0 2) .scale"C minor" => lags because no whole - -### reference - -- arp: note <[c,eb,g] [c,f,ab] [d,f,ab]> .arp [0 [0,2] 1 [0,2]] -- bank: s [bd sd [- bd] sd].bank TR909 -- beat: s sd .beat [4,12] 16 -- binary: s hh .struct (binary 5) -- binaryN: s hh .struct(binaryN 55532 16) => is wrong -- bite: n[0 1 2 3 4 5 6 7].scale"c mixolydian".bite 4 [3 2 1 0] -- bpattack: note [c2 e2 f2 g2].s sawtooth.bpf 500.bpa <.5 .25 .1 .01>/4.bpenv 4 - -### dot is a bit ambiguous - -```plaintext -n[0 1 2].scale"C minor".ad.1 -``` - -decimal vs pipe - -### less ambiguity with [] and "" - -in js, s("hh cp") implcitily does [hh cp] -in mondo, s[hh cp] always shows the type of bracket used - -### todo - -- lists: C:minor -- spread: [0 .. 2] -- replicate: ! From c9cafa37fdb521d87d4a690d68e8092b841bf209 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 29 Mar 2025 21:33:51 +0100 Subject: [PATCH 083/538] mondo: improve mondo package api + update readme --- packages/mondo/README.md | 45 +++++++++++++++---------- packages/mondo/mondo.mjs | 24 +++++--------- packages/mondo/test/mondo.test.mjs | 27 ++++++++++++++- packages/mondough/mondough.mjs | 53 ++++++++++++++++-------------- 4 files changed, 89 insertions(+), 60 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 551690467..47812e83d 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -5,23 +5,32 @@ an experimental parser for an *uzulang*, a custom dsl for patterns that can stan - [uzulang I](https://garten.salat.dev/uzu/uzulang1.html) - [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) +## Example Usage + ```js -import { MondoRunner } from 'uzu' - -const runner = MondoRunner({ seq, cat, s, crush, speed, '*': fast }); -const pat = runner.run('s [bd hh*2 (cp.crush 4) ] . speed .8') -``` - -the above code will create the following call structure: - -```lisp -(speed - (s - (seq bd - (* hh 2) - (crush cp 4) - (cat mt ht lt) - ) - ) .8 -) +import { MondoRunner } from 'mondo' +// define our library of functions and variables +let lib = { + add: (a, b) => a + b, + mul: (a, b) => a * b, + PI: Math.PI, +}; +// this function will evaluate nodes in the syntax tree +function evaluator(node) { + // check if node is a leaf node (!= list) + if (node.type !== 'list') { + // check lib if we find a match in the lib, otherwise return value + return lib[node.value] ?? node.value; + } + // now it can only be a list.. + const [fn, ...args] = node.children; + // children in a list will already be evaluated + // the first child is expected to be a function + if (typeof fn !== 'function') { + throw new Error(`"${fn}" is not a function ${typeof fn}`); + } + return fn(...args); +} +const runner = new MondoRunner(evaluator); +const pat = runner.run('add 1 (mul 2 PI)') // 7.283185307179586 ``` diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index d031af4eb..673841cde 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -306,11 +306,10 @@ export function printAst(ast, compact = false, lvl = 0) { // lisp runner export class MondoRunner { - constructor(lib) { + constructor(evaluator) { this.parser = new MondoParser(); - this.lib = lib; - this.assert(!!this.lib.leaf, `no handler for leaft nodes! add "leaf" to your lib`); - this.assert(!!this.lib.call, `no handler for call nodes! add "call" to your lib`); + this.evaluator = evaluator; + this.assert(typeof evaluator === 'function', `expected an evaluator function to be passed to new MondoRunner`); } // a helper to check conditions and throw if they are not met assert(condition, error) { @@ -331,15 +330,10 @@ export class MondoRunner { } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { ast.value = ast.value.slice(1, -1); } - return this.lib.leaf(ast, scope); + return this.evaluator(ast, scope); } - // is list - if (!ast.children.length) { - throw new Error(`empty list`); - } - - if (ast.children[0].value === 'lambda') { + if (ast.children[0]?.value === 'lambda') { const [_, args, body] = ast.children; const argNames = args.children.map((child) => child.value); return (x) => { @@ -349,10 +343,8 @@ export class MondoRunner { return this.evaluate(body, scope); }; } - - const args = ast.children.map((arg) => this.evaluate(arg, scope)); - // we could short circuit arg[0] if its plain... - // evaluate args - return this.lib.call(args[0], args.slice(1), scope); + // evaluate all children before evaluating list + ast.children = ast.children.map((arg) => this.evaluate(arg, scope)); + return this.evaluator(ast, scope); } } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index f987cbc2b..338a1fec8 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import { describe, expect, it } from 'vitest'; -import { MondoParser, printAst } from '../mondo.mjs'; +import { MondoParser, printAst, MondoRunner } from '../mondo.mjs'; const parser = new MondoParser(); const p = (code) => parser.parse(code, -1); @@ -128,3 +128,28 @@ describe('mondo sugar', () => { it('should desugar_lambda', () => expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); */ }); + +describe('mondo arithmetic', () => { + let lib = { + add: (a, b) => a + b, + mul: (a, b) => a * b, + PI: Math.PI, + }; + function evaluator(node) { + // check if node is a leaf node (!= list) + if (node.type !== 'list') { + // check lib if we find a match in the lib, otherwise return value + return lib[node.value] ?? node.value; + } + // now it can only be a list.. + const [fn, ...args] = node.children; + // children in a list will already be evaluated + // the first child is expected to be a function + if (typeof fn !== 'function') { + throw new Error(`"${fn}" is not a function ${typeof fn}`); + } + return fn(...args); + } + const runner = new MondoRunner(evaluator); + it('should desugar (.)', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28')); +}); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 01247e78b..d84ceb0cf 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -43,8 +43,12 @@ lib['..'] = range; lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]" //lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct -let runner = new MondoRunner({ - call(name, args, scope) { +function evaluator(node, scope) { + const { type } = node; + // node is list + if (type === 'list') { + const { children } = node; + const [name, ...args] = children; // name is expected to be a pattern of functions! const first = name.firstCycle(true)[0]; if (typeof first?.value !== 'function') { @@ -58,30 +62,29 @@ let runner = new MondoRunner({ return fn(...args); }) .innerJoin(); - }, - leaf(token, scope) { - let { value, type } = token; - // local scope - if (type === 'plain' && scope[value]) { - return reify(scope[value]); // -> local scope has no location - } - const variable = lib[value] ?? strudelScope[value]; - let pat; - if (type === 'plain' && typeof variable !== 'undefined') { - // problem: collisions when we want a string that happens to also be a variable name - // example: "s sine" -> sine is also a variable - pat = reify(variable); - } else { - pat = reify(value); - } + } + // node is leaf + let { value } = node; + if (type === 'plain' && scope[value]) { + return reify(scope[value]); // -> local scope has no location + } + const variable = lib[value] ?? strudelScope[value]; + let pat; + if (type === 'plain' && typeof variable !== 'undefined') { + // problem: collisions when we want a string that happens to also be a variable name + // example: "s sine" -> sine is also a variable + pat = reify(variable); + } else { + pat = reify(value); + } + if (node.loc) { + pat = pat.withLoc(node.loc[0], node.loc[1]); + } + pat.foo = true; + return pat; +} - if (token.loc) { - pat = pat.withLoc(token.loc[0], token.loc[1]); - } - pat.foo = true; - return pat; - }, -}); +let runner = new MondoRunner(evaluator); export function mondo(code, offset = 0) { if (Array.isArray(code)) { From b4027fd92ac38f9426e29982bd444fa197754f33 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 30 Mar 2025 11:36:50 +0200 Subject: [PATCH 084/538] fix: mondo dont mutate (breaks lambdas) --- packages/mondo/mondo.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 673841cde..0e8dc8576 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -343,8 +343,9 @@ export class MondoRunner { return this.evaluate(body, scope); }; } - // evaluate all children before evaluating list - ast.children = ast.children.map((arg) => this.evaluate(arg, scope)); - return this.evaluator(ast, scope); + // evaluate all children before evaluating list (dont mutate!!!) + const args = ast.children.map((arg) => this.evaluate(arg, scope)); + const node = { type: 'list', children: args }; + return this.evaluator(node, scope); } } From a0fb8fb37f36644e5e344a938e828ec22ffb0cd0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 30 Mar 2025 11:43:01 +0200 Subject: [PATCH 085/538] mondo: def node --- packages/mondo/mondo.mjs | 21 +++++++++++++++++---- packages/mondo/test/mondo.test.mjs | 6 +++--- packages/mondough/mondough.mjs | 11 +++++++---- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 0e8dc8576..89236a3bb 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -175,10 +175,10 @@ export class MondoParser { return children; } get_lambda(args, children) { - // (.fast 2) = (lambda (_) (fast _ 2)) + // (.fast 2) = (fn (_) (fast _ 2)) children = this.desugar(children); const body = children.length === 1 ? children[0] : { type: 'list', children }; - return [{ type: 'plain', value: 'lambda' }, { type: 'list', children: args }, body]; + return [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, body]; } // returns location range of given ast (even if desugared) get_range(ast, range = [Infinity, 0]) { @@ -322,7 +322,7 @@ export class MondoRunner { console.log(printAst(ast)); return this.evaluate(ast); } - evaluate(ast, scope = []) { + evaluate(ast, scope = {}) { if (ast.type !== 'list') { // is leaf if (ast.type === 'number') { @@ -333,7 +333,20 @@ export class MondoRunner { return this.evaluator(ast, scope); } - if (ast.children[0]?.value === 'lambda') { + if (ast.children[0]?.value === 'def') { + if (ast.children.length !== 3) { + throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`); + } + // (def myfn (fn (_) (ply 2 _))) + // ^name ^body + const name = ast.children[1].value; + const body = this.evaluate(ast.children[2], scope); + scope[name] = body; + return this.evaluator(ast, scope); + } + if (ast.children[0]?.value === 'fn') { + // (fn (_) (ply 2 _) + // ^args ^ body const [_, args, body] = ast.children; const argNames = args.children.map((child) => child.value); return (x) => { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 338a1fec8..47e0e165d 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -119,10 +119,10 @@ describe('mondo sugar', () => { '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); - it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(lambda (_) _)')); - it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(lambda (_) (fast 2 _))')); + it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(fn (_) _)')); + it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(fn (_) (fast 2 _))')); it('should desugar lambda with pipe', () => - expect(desguar('(.fast 2 .room 1)')).toEqual('(lambda (_) (room 1 (fast 2 _)))')); + expect(desguar('(.fast 2 .room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))')); /* const lambda = parser.parse('(lambda (_) (fast 2 _))'); const target = { type: 'plain', value: 'xyz' }; it('should desugar_lambda', () => diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index d84ceb0cf..543dcfeb2 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -49,15 +49,19 @@ function evaluator(node, scope) { if (type === 'list') { const { children } = node; const [name, ...args] = children; + if (name.value === 'def') { + return silence; + } // name is expected to be a pattern of functions! const first = name.firstCycle(true)[0]; - if (typeof first?.value !== 'function') { - throw new Error(`[mondough] "${first}" is not a function`); + const type = typeof first?.value; + if (type !== 'function') { + throw new Error(`[mondough] "${first}" is not a function, got ${type} ...`); } return name .fmap((fn) => { if (typeof fn !== 'function') { - throw new Error(`[mondough] "${fn}" is not a function`); + throw new Error(`[mondough] "${fn}" is not a function b`); } return fn(...args); }) @@ -80,7 +84,6 @@ function evaluator(node, scope) { if (node.loc) { pat = pat.withLoc(node.loc[0], node.loc[1]); } - pat.foo = true; return pat; } From f2372e7a415da0edce6d9460327d652a487e18ba Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 30 Mar 2025 17:55:37 +0200 Subject: [PATCH 086/538] mondo: refactor evaluate function into bits --- packages/mondo/README.md | 2 +- packages/mondo/mondo.mjs | 75 +++++++++++++++++------------- packages/mondo/test/mondo.test.mjs | 2 +- packages/mondough/mondough.mjs | 2 +- 4 files changed, 45 insertions(+), 36 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 47812e83d..d283ccf31 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -31,6 +31,6 @@ function evaluator(node) { } return fn(...args); } -const runner = new MondoRunner(evaluator); +const runner = new MondoRunner({ evaluator }); const pat = runner.run('add 1 (mul 2 PI)') // 7.283185307179586 ``` diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 89236a3bb..e19116ae6 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -306,7 +306,7 @@ export function printAst(ast, compact = false, lvl = 0) { // lisp runner export class MondoRunner { - constructor(evaluator) { + constructor({ evaluator } = {}) { this.parser = new MondoParser(); this.evaluator = evaluator; this.assert(typeof evaluator === 'function', `expected an evaluator function to be passed to new MondoRunner`); @@ -322,43 +322,52 @@ export class MondoRunner { console.log(printAst(ast)); return this.evaluate(ast); } - evaluate(ast, scope = {}) { - if (ast.type !== 'list') { - // is leaf - if (ast.type === 'number') { - ast.value = Number(ast.value); - } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { - ast.value = ast.value.slice(1, -1); - } - return this.evaluator(ast, scope); + evaluate_def(ast, scope) { + // (def name body) + if (ast.children.length !== 3) { + throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`); } - - if (ast.children[0]?.value === 'def') { - if (ast.children.length !== 3) { - throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`); - } - // (def myfn (fn (_) (ply 2 _))) - // ^name ^body - const name = ast.children[1].value; - const body = this.evaluate(ast.children[2], scope); - scope[name] = body; - return this.evaluator(ast, scope); - } - if (ast.children[0]?.value === 'fn') { - // (fn (_) (ply 2 _) - // ^args ^ body - const [_, args, body] = ast.children; - const argNames = args.children.map((child) => child.value); - return (x) => { - scope = { - [argNames[0]]: x, // TODO: merge scope... + support multiple args - }; - return this.evaluate(body, scope); + const name = ast.children[1].value; + const body = this.evaluate(ast.children[2], scope); + scope[name] = body; + return this.evaluator(ast, scope); + } + evaluate_lambda(ast, scope) { + // (fn (_) (ply 2 _) + // ^args ^ body + const [_, args, body] = ast.children; + const argNames = args.children.map((child) => child.value); + return (x) => { + scope = { + [argNames[0]]: x, // TODO: merge scope... + support multiple args }; - } + return this.evaluate(body, scope); + }; + } + evaluate_list(ast, scope) { // evaluate all children before evaluating list (dont mutate!!!) const args = ast.children.map((arg) => this.evaluate(arg, scope)); const node = { type: 'list', children: args }; return this.evaluator(node, scope); } + evaluate_leaf(ast, scope) { + if (ast.type === 'number') { + ast.value = Number(ast.value); + } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { + ast.value = ast.value.slice(1, -1); + } + return this.evaluator(ast, scope); + } + evaluate(ast, scope = {}) { + if (ast.type !== 'list') { + return this.evaluate_leaf(ast, scope); + } + if (ast.children[0]?.value === 'def') { + return this.evaluate_def(ast, scope); + } + if (ast.children[0]?.value === 'fn') { + return this.evaluate_lambda(ast, scope); + } + return this.evaluate_list(ast, scope); + } } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 47e0e165d..6f8090cc5 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -150,6 +150,6 @@ describe('mondo arithmetic', () => { } return fn(...args); } - const runner = new MondoRunner(evaluator); + const runner = new MondoRunner({ evaluator }); it('should desugar (.)', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28')); }); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 543dcfeb2..effaf367c 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -87,7 +87,7 @@ function evaluator(node, scope) { return pat; } -let runner = new MondoRunner(evaluator); +let runner = new MondoRunner({ evaluator }); export function mondo(code, offset = 0) { if (Array.isArray(code)) { From 94a3a60e4917e7c5592aea35b1ded71e6005ad60 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 1 Apr 2025 21:49:28 +0200 Subject: [PATCH 087/538] mondo: remember pair locations + allow passing a scope to MondoRunner.run + make def a side effect + proper lambda with multiple args + closure --- packages/mondo/README.md | 2 +- packages/mondo/mondo.mjs | 65 +++++++++++++++++++--------------- packages/mondough/mondough.mjs | 2 +- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index d283ccf31..8bdd93143 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -27,7 +27,7 @@ function evaluator(node) { // children in a list will already be evaluated // the first child is expected to be a function if (typeof fn !== 'function') { - throw new Error(`"${fn}" is not a function ${typeof fn}`); + throw new Error(`"${fn}" is not a function`); } return fn(...args); } diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index e19116ae6..2553f74fb 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -24,7 +24,7 @@ export class MondoParser { pipe: /^\./, stack: /^[,$]/, or: /^[|]/, - plain: /^[a-zA-Z0-9-~_^#]+/, + plain: /^[a-zA-Z0-9-~_^#+-]+/, }; // matches next token next_token(code, offset = 0) { @@ -88,6 +88,8 @@ export class MondoParser { parse_expr() { if (!this.tokens[0]) { throw new Error(`unexpected end of file`); + // TODO: could we allow that? like (((((((( s bd + // return { type: 'list', children: [] }; } let next = this.tokens[0]?.type; if (next === 'open_list') { @@ -219,13 +221,17 @@ export class MondoParser { return chunks[0]; } parse_pair(open_type, close_type) { + const begin = this.tokens[0].loc?.[0]; this.consume(open_type); const children = []; while (this.tokens[0]?.type !== close_type) { children.push(this.parse_expr()); } + const end = this.tokens[0].loc?.[1]; this.consume(close_type); - return children; + const node = { type: 'list', children }; + begin !== undefined && (node.loc = [begin, end]); + return node; } desugar(children, type) { // if type is given, the first element is expected to contain it as plain value @@ -247,27 +253,27 @@ export class MondoParser { return children; } parse_list() { - let children = this.parse_pair('open_list', 'close_list'); - children = this.desugar(children); - return { type: 'list', children }; + let node = this.parse_pair('open_list', 'close_list'); + node.children = this.desugar(node.children); + return node; } parse_angle() { - let children = this.parse_pair('open_angle', 'close_angle'); - children = [{ type: 'plain', value: 'angle' }, ...children]; - children = this.desugar(children, 'angle'); - return { type: 'list', children }; + let node = this.parse_pair('open_angle', 'close_angle'); + node.children.unshift({ type: 'plain', value: 'angle' }); + node.children = this.desugar(node.children, 'angle'); + return node; } parse_square() { - let children = this.parse_pair('open_square', 'close_square'); - children = [{ type: 'plain', value: 'square' }, ...children]; - children = this.desugar(children, 'square'); - return { type: 'list', children }; + let node = this.parse_pair('open_square', 'close_square'); + node.children.unshift({ type: 'plain', value: 'square' }); + node.children = this.desugar(node.children, 'square'); + return node; } parse_curly() { - let children = this.parse_pair('open_curly', 'close_curly'); - children = [{ type: 'plain', value: 'curly' }, ...children]; - children = this.desugar(children, 'curly'); - return { type: 'list', children }; + let node = this.parse_pair('open_curly', 'close_curly'); + node.children.unshift({ type: 'plain', value: 'curly' }); + node.children = this.desugar(node.children, 'curly'); + return node; } consume(type) { // shift removes first element and returns it @@ -317,10 +323,10 @@ export class MondoRunner { throw new Error(error); } } - run(code, offset = 0) { + run(code, scope, offset = 0) { const ast = this.parser.parse(code, offset); console.log(printAst(ast)); - return this.evaluate(ast); + return this.evaluate(ast, scope); } evaluate_def(ast, scope) { // (def name body) @@ -330,18 +336,19 @@ export class MondoRunner { const name = ast.children[1].value; const body = this.evaluate(ast.children[2], scope); scope[name] = body; - return this.evaluator(ast, scope); + // def with fall through } evaluate_lambda(ast, scope) { // (fn (_) (ply 2 _) // ^args ^ body - const [_, args, body] = ast.children; - const argNames = args.children.map((child) => child.value); - return (x) => { - scope = { - [argNames[0]]: x, // TODO: merge scope... + support multiple args + const [_, formalArgs, body] = ast.children; + return (...args) => { + const params = Object.fromEntries(formalArgs.children.map((arg, i) => [arg.value, args[i]])); + const closure = { + ...scope, + ...params, }; - return this.evaluate(body, scope); + return this.evaluate(body, closure); }; } evaluate_list(ast, scope) { @@ -362,12 +369,12 @@ export class MondoRunner { if (ast.type !== 'list') { return this.evaluate_leaf(ast, scope); } - if (ast.children[0]?.value === 'def') { - return this.evaluate_def(ast, scope); - } if (ast.children[0]?.value === 'fn') { return this.evaluate_lambda(ast, scope); } + if (ast.children[0]?.value === 'def') { + this.evaluate_def(ast, scope); + } return this.evaluate_list(ast, scope); } } diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index effaf367c..e995c0e4e 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -93,7 +93,7 @@ export function mondo(code, offset = 0) { if (Array.isArray(code)) { code = code.join(''); } - const pat = runner.run(code, offset); + const pat = runner.run(code, undefined, offset); return pat.markcss('color: var(--caret,--foreground);text-decoration:underline'); } From 077aac1888da1d9c4d307161158dfe50f875da67 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 1 Apr 2025 21:50:03 +0200 Subject: [PATCH 088/538] mondo: add some examples from sicp book --- packages/mondo/test/mondo.test.mjs | 84 ++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 6f8090cc5..c9ec8aa02 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -121,6 +121,7 @@ describe('mondo sugar', () => { it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(fn (_) _)')); it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(fn (_) (fast 2 _))')); + it('should desugar lambda call', () => expect(desguar('((.mul 2) 2)')).toEqual('((fn (_) (mul 2 _)) 2)')); it('should desugar lambda with pipe', () => expect(desguar('(.fast 2 .room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))')); /* const lambda = parser.parse('(lambda (_) (fast 2 _))'); @@ -130,26 +131,87 @@ describe('mondo sugar', () => { }); describe('mondo arithmetic', () => { + let multi = + (op) => + (init, ...rest) => + rest.reduce((acc, arg) => op(acc, arg), init); + let lib = { - add: (a, b) => a + b, - mul: (a, b) => a * b, + '+': multi((a, b) => a + b), + '-': multi((a, b) => a - b), + '*': multi((a, b) => a * b), + '/': multi((a, b) => a / b), + run: (...args) => args[args.length - 1], + def: () => 0, PI: Math.PI, }; - function evaluator(node) { - // check if node is a leaf node (!= list) + function evaluator(node, scope) { if (node.type !== 'list') { - // check lib if we find a match in the lib, otherwise return value - return lib[node.value] ?? node.value; + // is leaf + return scope[node.value] ?? lib[node.value] ?? node.value; } - // now it can only be a list.. + // is list const [fn, ...args] = node.children; - // children in a list will already be evaluated - // the first child is expected to be a function if (typeof fn !== 'function') { - throw new Error(`"${fn}" is not a function ${typeof fn}`); + throw new Error(`"${fn}": expected function, got ${typeof fn} "${JSON.stringify(fn)}"`); } return fn(...args); } const runner = new MondoRunner({ evaluator }); - it('should desugar (.)', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28')); + let evaluate = (exp, scope) => runner.run(`run ${exp}`, scope); + let pretty = (exp) => printAst(runner.parser.parse(exp), false); + //it('should eval nested expression', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28')); + + it('eval number', () => expect(evaluate('2')).toEqual(2)); + it('eval string', () => expect(evaluate('abc')).toEqual('abc')); + it('eval list', () => expect(evaluate('(+ 1 2)')).toEqual(3)); + it('eval nested list', () => expect(evaluate('(+ 1 (+ 2 3))')).toEqual(6)); + it('def number', () => expect(evaluate('(def a 2) a')).toEqual(2)); + it('def + ref number', () => expect(evaluate('(def a 2) (* a a)')).toEqual(4)); + it('def + call lambda', () => expect(evaluate('(def sqr (fn (x) (* x x))) (sqr 3)')).toEqual(9)); + + // sicp + it('sicp 8.1', () => expect(evaluate('(+ 137 349)')).toEqual(486)); + it('sicp 8.2', () => expect(evaluate('(- 1000 334)')).toEqual(666)); + it('sicp 8.3', () => expect(evaluate('(* 5 99)')).toEqual(495)); + it('sicp 8.4', () => expect(evaluate('(/ 10 5)')).toEqual(2)); + it('sicp 8.5', () => expect(evaluate('(+ 2.7 10)')).toEqual(12.7)); + it('sicp 9.1', () => expect(evaluate('(+ 21 35 12 7)')).toEqual(75)); + it('sicp 9.2', () => expect(evaluate('(* 25 4 12)')).toEqual(1200)); + it('sicp 9.3', () => expect(evaluate('(+ (* 3 5) (- 10 6))')).toEqual(19)); + it('sicp 9.4', () => + expect(pretty('(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))')).toEqual(`(+ + (* 3 + (+ + (* 2 4) + (+ 3 5) + ) + ) + (+ + (- 10 7) 6 + ) +)`)); // this is not exactly pretty printing by convention.. + + let scope = {}; + it('sicp 11.1', () => expect(evaluate('(def size 2) (* 5 size)', scope)).toEqual(10)); + it('sicp 11.2', () => + expect(evaluate('(def pi 3.14159) (def radius 10) (* pi (* radius radius))', scope)).toEqual(314.159)); + it('sicp 11.3', () => expect(evaluate('(def circumference (* 2 pi radius))', scope)).toEqual(0)); + it('sicp 11.4', () => expect(evaluate('circumference', scope)).toEqual(62.8318)); + it('sicp 13.1', () => expect(evaluate('(* (+ 2 (* 4 6)) (+ 3 5 7))')).toEqual(390)); + it('sicp 16.1', () => expect(evaluate('(def square (fn (x) (* x x)))', scope)).toEqual(0)); + // it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0)); + it('sicp 17.1', () => expect(evaluate('(square 21)', scope)).toEqual(441)); + it('sicp 17.2', () => expect(evaluate('(square (+ 2 5))', scope)).toEqual(49)); + it('sicp 17.3', () => expect(evaluate('(square (square 3))', scope)).toEqual(81)); + it('sicp 17.4', () => + expect(evaluate(`(def sum-of-squares (fn (x y) (+ (square x) (square y))))`, scope)).toEqual(0)); + it('sicp 17.5', () => expect(evaluate(`(sum-of-squares 3 4)`, scope)).toEqual(25)); + it('sicp 17.6', () => + expect(evaluate(`(def f (fn (a) (sum-of-squares (+ a 1) (* a 2)))) (f 5)`, scope)).toEqual(136)); + it('sicp 21.1', () => expect(evaluate(`(sum-of-squares (+ 5 1) (* 5 2))`, scope)).toEqual(136)); + + /* it('sicp 11.1', () => expect(evaluate('(* 5 size)', { size: 3 })).toEqual(15)); + it('sicp 11.1', () => expect(evaluate('(def b 3) (* a b)', scope)).toEqual(12)); + it('sicp 11.1', () => expect(scope.b).toEqual(3)); */ }); From f6ffdd6ae6779698b1a84ab902d2771e9752ad15 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 1 Apr 2025 22:31:42 +0200 Subject: [PATCH 089/538] mondo: move +- to ops + add "raw" to parsed pairs + implement match + if + add more sicp examples --- packages/mondo/mondo.mjs | 57 +++++++++++++++++++++++++++--- packages/mondo/test/mondo.test.mjs | 43 ++++++++++++++++++---- 2 files changed, 89 insertions(+), 11 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 2553f74fb..cdbaf964a 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -19,12 +19,12 @@ export class MondoParser { open_curly: /^\{/, close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. + op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, pipe: /^\./, stack: /^[,$]/, or: /^[|]/, - plain: /^[a-zA-Z0-9-~_^#+-]+/, + plain: /^[a-zA-Z0-9-~_^#]+/, }; // matches next token next_token(code, offset = 0) { @@ -230,7 +230,10 @@ export class MondoParser { const end = this.tokens[0].loc?.[1]; this.consume(close_type); const node = { type: 'list', children }; - begin !== undefined && (node.loc = [begin, end]); + if (begin !== undefined) { + node.loc = [begin, end]; + node.raw = this.code.slice(begin, end); + } return node; } desugar(children, type) { @@ -338,6 +341,43 @@ export class MondoRunner { scope[name] = body; // def with fall through } + evaluate_match(ast, scope) { + // (match (p1 e1) (p2 e2) ... (pn en)) + // = cond in lisp + if (ast.children.length < 2) { + return; + } + const [_, ...body] = ast.children; + for (let i = 0; i < body.length; ++i) { + const [predicate, exp] = body[i].children; + if (predicate.value === 'else') { + return this.evaluate(exp, scope); + } + const outcome = this.evaluate(predicate, scope); + if (outcome) { + return this.evaluate(exp, scope); + } + } + return undefined; // nothing was matched + } + evaluate_if(ast, scope) { + // if is a special case of match + if (ast.children.length !== 4) { + return; + } + // (if predicate consequent alternative) + const [_, predicate, consequent, alternative] = ast.children; + // (match (predicate consequent) (else alternative)) + const matcher = { + type: 'list', + children: [ + { type: 'plain', value: 'match' }, + { type: 'list', children: [predicate, consequent] }, + { type: 'list', children: [{ type: 'plain', value: 'else' }, alternative] }, + ], + }; + return this.evaluate_match(matcher, scope); + } evaluate_lambda(ast, scope) { // (fn (_) (ply 2 _) // ^args ^ body @@ -369,10 +409,17 @@ export class MondoRunner { if (ast.type !== 'list') { return this.evaluate_leaf(ast, scope); } - if (ast.children[0]?.value === 'fn') { + const name = ast.children[0]?.value; + if (name === 'fn') { return this.evaluate_lambda(ast, scope); } - if (ast.children[0]?.value === 'def') { + if (name === 'match') { + return this.evaluate_match(ast, scope); + } + if (name === 'if') { + return this.evaluate_if(ast, scope); + } + if (name === 'def') { this.evaluate_def(ast, scope); } return this.evaluate_list(ast, scope); diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index c9ec8aa02..15cb42218 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -141,6 +141,12 @@ describe('mondo arithmetic', () => { '-': multi((a, b) => a - b), '*': multi((a, b) => a * b), '/': multi((a, b) => a / b), + eq: (a, b) => a === b, + lt: (a, b) => a < b, + gt: (a, b) => a > b, + and: (a, b) => a && b, + or: (a, b) => a || b, + not: (a) => !a, run: (...args) => args[args.length - 1], def: () => 0, PI: Math.PI, @@ -204,12 +210,37 @@ describe('mondo arithmetic', () => { it('sicp 17.1', () => expect(evaluate('(square 21)', scope)).toEqual(441)); it('sicp 17.2', () => expect(evaluate('(square (+ 2 5))', scope)).toEqual(49)); it('sicp 17.3', () => expect(evaluate('(square (square 3))', scope)).toEqual(81)); - it('sicp 17.4', () => - expect(evaluate(`(def sum-of-squares (fn (x y) (+ (square x) (square y))))`, scope)).toEqual(0)); - it('sicp 17.5', () => expect(evaluate(`(sum-of-squares 3 4)`, scope)).toEqual(25)); - it('sicp 17.6', () => - expect(evaluate(`(def f (fn (a) (sum-of-squares (+ a 1) (* a 2)))) (f 5)`, scope)).toEqual(136)); - it('sicp 21.1', () => expect(evaluate(`(sum-of-squares (+ 5 1) (* 5 2))`, scope)).toEqual(136)); + it('sicp 17.4', () => expect(evaluate(`(def sumofsquares (fn (x y) (+ (square x) (square y))))`, scope)).toEqual(0)); + it('sicp 17.5', () => expect(evaluate(`(sumofsquares 3 4)`, scope)).toEqual(25)); + it('sicp 17.6', () => expect(evaluate(`(def f (fn (a) (sumofsquares (+ a 1) (* a 2)))) (f 5)`, scope)).toEqual(136)); + it('sicp 21.1', () => expect(evaluate(`(sumofsquares (+ 5 1) (* 5 2))`, scope)).toEqual(136)); + + it('sicp 22.1', () => + expect( + evaluate( + `(def abs (fn (x) + (match + ((gt x 0) x) + ((eq x 0) 0) + ((lt x 0) (- 0 x)) + )))`, // sicp was doing (- x), which doesnt work with our - + scope, + ), + ).toEqual(0)); + + it('sicp gt1', () => expect(evaluate(`(gt -12 0)`, scope)).toEqual(false)); + it('sicp gt2', () => expect(evaluate(`(gt 0 -12)`, scope)).toEqual(true)); + it('sicp lt1', () => expect(evaluate(`(lt -12 0)`, scope)).toEqual(true)); + it('sicp lt2', () => expect(evaluate(`(lt 0 -12)`, scope)).toEqual(false)); + + it('sicp 24.1', () => expect(evaluate(`(abs (- 3))`, scope)).toEqual(3)); + it('sicp 24.2', () => expect(evaluate(`(abs (+ 3))`, scope)).toEqual(3)); + it('sicp 24.3', () => expect(evaluate(`(abs -12)`, scope)).toEqual(12)); + + it('sicp 24.4', () => expect(evaluate(`(def abs (fn (x) (if (lt x 0) (- 0 x) x)))`, scope)).toEqual(0)); + it('sicp 24.5', () => expect(evaluate(`(abs -13)`, scope)).toEqual(13)); + it('sicp 25.1', () => expect(evaluate(`(and (gt 6 5) (lt 6 10))`, scope)).toEqual(true)); + it('sicp 25.2', () => expect(evaluate(`(and (gt 4 5) (lt 6 10))`, scope)).toEqual(false)); /* it('sicp 11.1', () => expect(evaluate('(* 5 size)', { size: 3 })).toEqual(15)); it('sicp 11.1', () => expect(evaluate('(def b 3) (* a b)', scope)).toEqual(12)); From aa70246afcc484d70899886965df9b1d473dd6e1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 1 Apr 2025 23:24:47 +0200 Subject: [PATCH 090/538] mondo: more sicp --- packages/mondo/test/mondo.test.mjs | 114 ++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 15cb42218..d52fc004b 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -138,7 +138,9 @@ describe('mondo arithmetic', () => { let lib = { '+': multi((a, b) => a + b), + add: multi((a, b) => a + b), '-': multi((a, b) => a - b), + sub: multi((a, b) => a - b), '*': multi((a, b) => a * b), '/': multi((a, b) => a / b), eq: (a, b) => a === b, @@ -242,7 +244,113 @@ describe('mondo arithmetic', () => { it('sicp 25.1', () => expect(evaluate(`(and (gt 6 5) (lt 6 10))`, scope)).toEqual(true)); it('sicp 25.2', () => expect(evaluate(`(and (gt 4 5) (lt 6 10))`, scope)).toEqual(false)); - /* it('sicp 11.1', () => expect(evaluate('(* 5 size)', { size: 3 })).toEqual(15)); - it('sicp 11.1', () => expect(evaluate('(def b 3) (* a b)', scope)).toEqual(12)); - it('sicp 11.1', () => expect(scope.b).toEqual(3)); */ + it('sicp ex1.1.1', () => expect(evaluate(`(def a 3)`, scope)).toEqual(0)); + it('sicp ex1.1.2', () => expect(evaluate(`(def b (+ a 1))`, scope)).toEqual(0)); + it('sicp ex1.1.3', () => expect(evaluate(`(+ a b (* a b))`, scope)).toEqual(19)); + it('sicp ex1.1.4', () => expect(evaluate(`(if (and (gt b a) (lt b (* a b))) b a)`, scope)).toEqual(4)); + it('sicp ex1.1.5', () => expect(evaluate(`(match ((eq a 4) 6) ((eq b 4) (+ 6 7 a)) (else 25))`, scope)).toEqual(16)); + it('sicp ex1.1.6', () => expect(evaluate(`(+ 2 (if (gt b a) b a))`, scope)).toEqual(6)); + it('sicp ex1.1.7', () => + expect(evaluate(`(* (match ((gt a b) a) ((lt a b) b) (else -1)) (+ a 1))`, scope)).toEqual(16)); + + // .. cant use "+" and "-" as standalone expressions, because they are parsed as operators... + it('sicp ex1.4.1', () => expect(evaluate(`(def foo (fn (a b) ((if (gt b 0) add sub) a b)))`, scope)).toEqual(0)); + it('sicp ex1.4.1', () => expect(evaluate(`(foo 3 1)`, scope)).toEqual(4)); + it('sicp ex1.4.2', () => expect(evaluate(`(foo 3 -1)`, scope)).toEqual(4)); + + // 1.1.7 Example: Square Roots by Newton’s Method + it('sicp 30.1', () => + expect(evaluate(`(def goodenuf (fn (guess x) (lt (abs (- (square guess) x)) 0.001)))`, scope)).toEqual(0)); + it('sicp 30.2', () => expect(evaluate(`(goodenuf 1 1.001)`, scope)).toEqual(true)); + it('sicp 30.3', () => expect(evaluate(`(goodenuf 1 1.002)`, scope)).toEqual(false)); + it('sicp 30.4', () => expect(evaluate(`(def average (fn (x y) (/ (+ x y) 2)))`, scope)).toEqual(0)); + it('sicp 30.5', () => expect(evaluate(`(average 18 20)`, scope)).toEqual(19)); + it('sicp 30.6', () => expect(evaluate(`(def improve (fn (guess x) (average guess (/ x guess))))`, scope)).toEqual(0)); + it('sicp 31.1', () => + expect( + evaluate( + `(def sqrtiter (fn (guess x) (if (goodenuf guess x) + guess + (sqrtiter (improve guess x) x))))`, + scope, + ), + ).toEqual(0)); + it('sicp 31.2', () => expect(evaluate(`(def sqrt (fn (x) (sqrtiter 1.0 x)))`, scope)).toEqual(0)); + it('sicp 31.3', () => expect(evaluate(`(sqrt 9)`, scope)).toEqual(3.00009155413138)); + it('sicp 31.4', () => expect(evaluate(`(sqrt (+ 100 37))`, scope)).toEqual(11.704699917758145)); + it('sicp 31.5', () => expect(evaluate(`(sqrt (+ (sqrt 2) (sqrt 3)))`, scope)).toEqual(1.77392790232078925)); + it('sicp 31.6', () => expect(evaluate(`(square (sqrt 1000))`, scope)).toEqual(1000.000369924366)); + + // lexical scoping + // doesnt work... + /* it('sicp 39.1', () => + expect( + evaluate( + `(def sqrt (fn (x) +(def (goodenuf guess) +(lt (abs (- (square guess) x)) 0.001)) (def (improve guess) +(average guess (/ x guess))) (def sqrtiter (fn (guess) +(if (goodenuf guess) guess + (sqrtiter (improve guess))))) + (sqrtiter 1.0))) (sqrt 7)`, + scope, + ), + ).toEqual(0)); */ + + // recursive fac + it('sicp 41.1', () => expect(evaluate(`(def fac (fn (n) (if (eq n 1) 1 (* n (fac (- n 1))))))`, scope)).toEqual(0)); + it('sicp 41.2', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); + + // iterative fac + // uses lexical scoping -> doesnt work + /* it('sicp 41.3', () => + expect( + evaluate( + `(def fac (fn (n) (faciter 1 1 n))) +(def faciter (fn (product counter maxcount) (if (gt counter maxcount) + product + (faciter (* counter product) + (+ counter 1) + max-count))))`, + scope, + ), + ).toEqual(0)); + it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); */ + + // 46.1 + /* (define (+ a b) +(if (= a 0) b (inc (+ (dec a) b)))) +(define (+ a b) +(if (= a 0) b (+ (dec a) (inc b)))) */ + + // Exercise 1.10 + // Ackermann’s function + it('sicp 47.1', () => + expect( + evaluate( + ` +(def A (fn (x y) (match ((eq y 0) 0) +((eq x 0) (* 2 y)) +((eq y 1) 2) +(else (A (- x 1) (A x (- y 1))))))) +`, + scope, + ), + ).toEqual(0)); + it('sicp 47.2', () => expect(evaluate(`(A 1 10)`, scope)).toEqual(1024)); + it('sicp 47.3', () => expect(evaluate(`(A 2 4)`, scope)).toEqual(65536)); + it('sicp 47.4', () => expect(evaluate(`(A 3 3)`, scope)).toEqual(65536)); + it('sicp 47.5', () => + expect( + evaluate( + ` +(def f (fn (n) (A 0 n))) +(def g (fn (n) (A 1 n))) +(def h (fn (n) (A 2 n))) +(def k (fn (n) (* 5 n n))) + `, + scope, + ), + ).toEqual(0)); + // Tree Recursion }); From 39d27844411f0ec88c25d733d64845bb2a750811 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 2 Apr 2025 22:09:17 +0200 Subject: [PATCH 091/538] mondo: more sicp tests + support special form for function def (might remove later) + support multiple expressions in fn body + support lexical scoping --- packages/mondo/mondo.mjs | 28 ++- packages/mondo/test/mondo.test.mjs | 302 ++++++++++++++++++++++++----- 2 files changed, 279 insertions(+), 51 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index cdbaf964a..8db36bd5d 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -328,10 +328,29 @@ export class MondoRunner { } run(code, scope, offset = 0) { const ast = this.parser.parse(code, offset); - console.log(printAst(ast)); + //console.log(printAst(ast)); return this.evaluate(ast, scope); } evaluate_def(ast, scope) { + // function definition special form? + if (ast.children[1].type === 'list') { + // (def (add a b) (+ a b)) + // => (def add (fn (a b) (+ a b)) ) + const args = ast.children[1].children.slice(1); + const lambda = { + // lambda + type: 'list', + children: [ + { type: 'plain', value: 'fn' }, + { type: 'list', children: args }, + ...ast.children.slice(2), // body + ], + }; + // we mutate to make sure the old ast wont make a mess later + ast.children[1] = ast.children[1].children[0]; + ast.children[2] = lambda; + ast.children = ast.children.slice(0, 3); // throw away rest + } // (def name body) if (ast.children.length !== 3) { throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`); @@ -381,14 +400,17 @@ export class MondoRunner { evaluate_lambda(ast, scope) { // (fn (_) (ply 2 _) // ^args ^ body - const [_, formalArgs, body] = ast.children; + const [_, formalArgs, ...body] = ast.children; return (...args) => { const params = Object.fromEntries(formalArgs.children.map((arg, i) => [arg.value, args[i]])); const closure = { ...scope, ...params, }; - return this.evaluate(body, closure); + // body can have multiple expressions + const res = body.map((exp) => this.evaluate(exp, closure)); + // last expression is the return value + return res[res.length - 1]; }; } evaluate_list(ast, scope) { diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index d52fc004b..d5b9a094f 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -143,6 +143,7 @@ describe('mondo arithmetic', () => { sub: multi((a, b) => a - b), '*': multi((a, b) => a * b), '/': multi((a, b) => a / b), + mod: multi((a, b) => a % b), eq: (a, b) => a === b, lt: (a, b) => a < b, gt: (a, b) => a > b, @@ -207,25 +208,25 @@ describe('mondo arithmetic', () => { it('sicp 11.3', () => expect(evaluate('(def circumference (* 2 pi radius))', scope)).toEqual(0)); it('sicp 11.4', () => expect(evaluate('circumference', scope)).toEqual(62.8318)); it('sicp 13.1', () => expect(evaluate('(* (+ 2 (* 4 6)) (+ 3 5 7))')).toEqual(390)); - it('sicp 16.1', () => expect(evaluate('(def square (fn (x) (* x x)))', scope)).toEqual(0)); + it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0)); // it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0)); it('sicp 17.1', () => expect(evaluate('(square 21)', scope)).toEqual(441)); it('sicp 17.2', () => expect(evaluate('(square (+ 2 5))', scope)).toEqual(49)); it('sicp 17.3', () => expect(evaluate('(square (square 3))', scope)).toEqual(81)); - it('sicp 17.4', () => expect(evaluate(`(def sumofsquares (fn (x y) (+ (square x) (square y))))`, scope)).toEqual(0)); + it('sicp 17.4', () => expect(evaluate(`(def (sumofsquares x y) (+ (square x) (square y)))`, scope)).toEqual(0)); it('sicp 17.5', () => expect(evaluate(`(sumofsquares 3 4)`, scope)).toEqual(25)); - it('sicp 17.6', () => expect(evaluate(`(def f (fn (a) (sumofsquares (+ a 1) (* a 2)))) (f 5)`, scope)).toEqual(136)); + it('sicp 17.6', () => expect(evaluate(`(def (f a) (sumofsquares (+ a 1) (* a 2))) (f 5)`, scope)).toEqual(136)); it('sicp 21.1', () => expect(evaluate(`(sumofsquares (+ 5 1) (* 5 2))`, scope)).toEqual(136)); it('sicp 22.1', () => expect( evaluate( - `(def abs (fn (x) + `(def (abs x) (match ((gt x 0) x) ((eq x 0) 0) ((lt x 0) (- 0 x)) - )))`, // sicp was doing (- x), which doesnt work with our - + ))`, // sicp was doing (- x), which doesnt work with our - scope, ), ).toEqual(0)); @@ -239,7 +240,7 @@ describe('mondo arithmetic', () => { it('sicp 24.2', () => expect(evaluate(`(abs (+ 3))`, scope)).toEqual(3)); it('sicp 24.3', () => expect(evaluate(`(abs -12)`, scope)).toEqual(12)); - it('sicp 24.4', () => expect(evaluate(`(def abs (fn (x) (if (lt x 0) (- 0 x) x)))`, scope)).toEqual(0)); + it('sicp 24.4', () => expect(evaluate(`(def (abs x) (if (lt x 0) (- 0 x) x))`, scope)).toEqual(0)); it('sicp 24.5', () => expect(evaluate(`(abs -13)`, scope)).toEqual(13)); it('sicp 25.1', () => expect(evaluate(`(and (gt 6 5) (lt 6 10))`, scope)).toEqual(true)); it('sicp 25.2', () => expect(evaluate(`(and (gt 4 5) (lt 6 10))`, scope)).toEqual(false)); @@ -254,68 +255,73 @@ describe('mondo arithmetic', () => { expect(evaluate(`(* (match ((gt a b) a) ((lt a b) b) (else -1)) (+ a 1))`, scope)).toEqual(16)); // .. cant use "+" and "-" as standalone expressions, because they are parsed as operators... - it('sicp ex1.4.1', () => expect(evaluate(`(def foo (fn (a b) ((if (gt b 0) add sub) a b)))`, scope)).toEqual(0)); + it('sicp ex1.4.1', () => expect(evaluate(`(def (foo a b) ((if (gt b 0) add sub) a b))`, scope)).toEqual(0)); it('sicp ex1.4.1', () => expect(evaluate(`(foo 3 1)`, scope)).toEqual(4)); it('sicp ex1.4.2', () => expect(evaluate(`(foo 3 -1)`, scope)).toEqual(4)); // 1.1.7 Example: Square Roots by Newton’s Method it('sicp 30.1', () => - expect(evaluate(`(def goodenuf (fn (guess x) (lt (abs (- (square guess) x)) 0.001)))`, scope)).toEqual(0)); + expect(evaluate(`(def (goodenuf guess x) (lt (abs (- (square guess) x)) 0.001))`, scope)).toEqual(0)); it('sicp 30.2', () => expect(evaluate(`(goodenuf 1 1.001)`, scope)).toEqual(true)); it('sicp 30.3', () => expect(evaluate(`(goodenuf 1 1.002)`, scope)).toEqual(false)); - it('sicp 30.4', () => expect(evaluate(`(def average (fn (x y) (/ (+ x y) 2)))`, scope)).toEqual(0)); + it('sicp 30.4', () => expect(evaluate(`(def (average x y) (/ (+ x y) 2))`, scope)).toEqual(0)); it('sicp 30.5', () => expect(evaluate(`(average 18 20)`, scope)).toEqual(19)); - it('sicp 30.6', () => expect(evaluate(`(def improve (fn (guess x) (average guess (/ x guess))))`, scope)).toEqual(0)); + it('sicp 30.6', () => expect(evaluate(`(def (improve guess x) (average guess (/ x guess)))`, scope)).toEqual(0)); it('sicp 31.1', () => expect( evaluate( - `(def sqrtiter (fn (guess x) (if (goodenuf guess x) + `(def (sqrtiter guess x) (if (goodenuf guess x) guess - (sqrtiter (improve guess x) x))))`, + (sqrtiter (improve guess x) x)))`, scope, ), ).toEqual(0)); - it('sicp 31.2', () => expect(evaluate(`(def sqrt (fn (x) (sqrtiter 1.0 x)))`, scope)).toEqual(0)); + it('sicp 31.2', () => expect(evaluate(`(def (sqrt x) (sqrtiter 1.0 x))`, scope)).toEqual(0)); it('sicp 31.3', () => expect(evaluate(`(sqrt 9)`, scope)).toEqual(3.00009155413138)); it('sicp 31.4', () => expect(evaluate(`(sqrt (+ 100 37))`, scope)).toEqual(11.704699917758145)); it('sicp 31.5', () => expect(evaluate(`(sqrt (+ (sqrt 2) (sqrt 3)))`, scope)).toEqual(1.77392790232078925)); it('sicp 31.6', () => expect(evaluate(`(square (sqrt 1000))`, scope)).toEqual(1000.000369924366)); // lexical scoping - // doesnt work... - /* it('sicp 39.1', () => + it('sicp 39.1', () => expect( evaluate( - `(def sqrt (fn (x) -(def (goodenuf guess) -(lt (abs (- (square guess) x)) 0.001)) (def (improve guess) -(average guess (/ x guess))) (def sqrtiter (fn (guess) -(if (goodenuf guess) guess - (sqrtiter (improve guess))))) - (sqrtiter 1.0))) (sqrt 7)`, - scope, - ), - ).toEqual(0)); */ - - // recursive fac - it('sicp 41.1', () => expect(evaluate(`(def fac (fn (n) (if (eq n 1) 1 (* n (fac (- n 1))))))`, scope)).toEqual(0)); - it('sicp 41.2', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); - - // iterative fac - // uses lexical scoping -> doesnt work - /* it('sicp 41.3', () => - expect( - evaluate( - `(def fac (fn (n) (faciter 1 1 n))) -(def faciter (fn (product counter maxcount) (if (gt counter maxcount) - product - (faciter (* counter product) - (+ counter 1) - max-count))))`, + ` +(def (sqrt x) + (def (goodenough guess) + (lt (abs (- (square guess) x)) 0.001)) +(def (improve guess) + (average guess (/ x guess))) +(def (sqrt-iter guess) + (if (goodenough guess) guess (sqrt-iter (improve guess)))) +(sqrtiter 1.0)) + + `, scope, ), ).toEqual(0)); - it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); */ + + // recursive fac + it('sicp 41.1', () => expect(evaluate(`(def (fac n) (if (eq n 1) 1 (* n (fac (- n 1)))))`, scope)).toEqual(0)); + it('sicp 41.2', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); + + // iterative fac + it('sicp 41.3', () => + expect( + evaluate( + ` +(def (factorial n) (factiter 1 1 n)) +(def (factiter product counter maxcount) + (if (gt counter maxcount) + product + (factiter (* counter product) + (+ counter 1) + maxcount))) +`, + scope, + ), + ).toEqual(0)); + it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); // 46.1 /* (define (+ a b) @@ -329,10 +335,10 @@ describe('mondo arithmetic', () => { expect( evaluate( ` -(def A (fn (x y) (match ((eq y 0) 0) +(def (A x y) (match ((eq y 0) 0) ((eq x 0) (* 2 y)) ((eq y 1) 2) -(else (A (- x 1) (A x (- y 1))))))) +(else (A (- x 1) (A x (- y 1)))))) `, scope, ), @@ -344,13 +350,213 @@ describe('mondo arithmetic', () => { expect( evaluate( ` -(def f (fn (n) (A 0 n))) -(def g (fn (n) (A 1 n))) -(def h (fn (n) (A 2 n))) -(def k (fn (n) (* 5 n n))) +(def (f n) (A 0 n)) +(def (g n) (A 1 n))) +(def (h n) (A 2 n)) +(def (k n) (* 5 n n)) `, scope, ), ).toEqual(0)); + // Tree Recursion + // recursive process + it('sicp 48.1', () => + expect( + evaluate( + ` +(def (fib n) (match ((eq n 0) 0) ((eq n 1) 1) +(else (+ (fib (- n 1)) (fib (- n 2)))))) +(fib 7) + `, + scope, + ), + ).toEqual(13)); + + // iterative process + it('sicp 48.2', () => + expect( + evaluate( + ` +(def (fib n) (fibiter 1 0 n)) +(def (fibiter a b count) (if (eq count 0) + b + (fibiter (+ a b) a (- count 1)))) +(fib 7) + `, + scope, + ), + ).toEqual(13)); + + // example: counting change + it('sicp 52.2', () => + expect( + evaluate( + ` +(def (countchange amount) (cc amount 5)) +(def (cc amount kindsofcoins) + (match + ((eq amount 0) 1) + ((or (lt amount 0) (eq kindsofcoins 0)) 0) + (else (+ + (cc amount (- kindsofcoins 1)) + (cc (- amount (firstdenomination kindsofcoins)) kindsofcoins))))) + +(def (firstdenomination kindsofcoins) +(match + ((eq kindsofcoins 1) 1) + ((eq kindsofcoins 2) 5) + ((eq kindsofcoins 3) 10) + ((eq kindsofcoins 4) 25) + ((eq kindsofcoins 5) 50))) + +(countchange 100) + `, + scope, + ), + ).toEqual(292)); + + // todo: pascals triangle + it('sicp 57.1', () => + expect( + evaluate( + ` +(def (cube x) (* x x x)) +(def (p x) (sub (* 3 x) (* 4 (cube x)))) +(def (sine angle) +(if (not (gt (abs angle) 0.1)) angle +(p (sine (/ angle 3.0))))) + +(sine 12.15) + `, + scope, + ), + ).toEqual(-0.39980345741334)); + + // exponentiation recursive + it('sicp 57.2', () => + expect( + evaluate( + ` +(def (expt b n) (if (eq n 0) 1 (* b (expt b (- n 1))))) +(expt 2 4) +`, + scope, + ), + ).toEqual(16)); + + // exponentiation iterative + it('sicp 58.1b', () => + expect( + evaluate( + ` +(def (expt b n) (exptiter b n 1)) +(def (exptiter b counter product) (if (eq counter 0) + product + (exptiter b (- counter 1) (* b product)))) +(expt 2 5) + `, + scope, + ), + ).toEqual(32)); + + // exponentiation fast + it('sicp 58.2', () => + expect( + evaluate( + ` +(def (fastexpt b n) (match ((eq n 0) 1) +((iseven n) (square (fastexpt b (/ n 2)))) (else (* b (fastexpt b (- n 1)))))) +(def (iseven n) +(eq (mod n 2) 0)) +(fastexpt 2 5) + `, + scope, + ), + ).toEqual(32)); + + // * = repeated addition + it('sicp 60.1', () => + expect( + evaluate( + `(def (mult a b) (if (eq b 0) + 0 + (+ a (* a (- b 1))))) + (mult 3 15) + `, + ), + ).toEqual(45)); + + // gcd / euclid + it('sicp 63.1', () => + expect( + evaluate( + `(def (gcd a b) (if (eq b 0) + a + (gcd b (mod a b)))) + (gcd 20 6) + `, + ), + ).toEqual(2)); + + // 65 smallest divisor + // 67 fermat test + // .... + + // higher order procedures + + it('sicp 77.1', () => + expect( + evaluate( + ` +(def (sum term a next b) +(if (gt a b) 0 (+ (term a) +(sum term (next a) next b)))) +`, + scope, + ), + ).toEqual(0)); + + it('sicp 78.1', () => + expect( + evaluate( + ` +(def (inc n) (+ n 1)) +(def (cube a) (* a a a)) +(def (sumcubes a b) +(sum cube a inc b)) +(sumcubes 1 10) + `, + scope, + ), + ).toEqual(3025)); + + it('sicp 78.2', () => + expect( + evaluate( + ` + (def (identity x) x) + (def (sumintegers a b) + (sum identity a inc b)) + (sumintegers 1 10) + `, + scope, + ), + ).toEqual(55)); + + // pisum pulled out defs to make it work + it('sicp 79.1', () => + expect( + evaluate( + ` +(def (piterm x) +(/ 1.0 (* x (+ x 2)))) +(def (pinext x) (+ x 4)) +(def (pisum a b) +(sum piterm a pinext b)) +(* 8 (pisum 1 1000)) +`, + scope, + ), + ).toEqual(3.139592655589783)); }); From b2588e8c935a6e066c449a5749fabe8e9caf83b0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 2 Apr 2025 22:11:06 +0200 Subject: [PATCH 092/538] fix: lint --- packages/mondo/test/mondo.test.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index d5b9a094f..9f7f16be4 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -279,6 +279,7 @@ describe('mondo arithmetic', () => { it('sicp 31.2', () => expect(evaluate(`(def (sqrt x) (sqrtiter 1.0 x))`, scope)).toEqual(0)); it('sicp 31.3', () => expect(evaluate(`(sqrt 9)`, scope)).toEqual(3.00009155413138)); it('sicp 31.4', () => expect(evaluate(`(sqrt (+ 100 37))`, scope)).toEqual(11.704699917758145)); + // eslint-disable-next-line no-loss-of-precision it('sicp 31.5', () => expect(evaluate(`(sqrt (+ (sqrt 2) (sqrt 3)))`, scope)).toEqual(1.77392790232078925)); it('sicp 31.6', () => expect(evaluate(`(square (sqrt 1000))`, scope)).toEqual(1000.000369924366)); From 60d09adb449409842734a12a5ef4a545574528d0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 2 Apr 2025 22:33:18 +0200 Subject: [PATCH 093/538] mondo: more tests --- packages/mondo/test/mondo.test.mjs | 58 +++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 9f7f16be4..0fac441e8 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -545,19 +545,69 @@ describe('mondo arithmetic', () => { ), ).toEqual(55)); - // pisum pulled out defs to make it work + // pisum it('sicp 79.1', () => expect( evaluate( ` -(def (piterm x) -(/ 1.0 (* x (+ x 2)))) -(def (pinext x) (+ x 4)) (def (pisum a b) + (def (piterm x) + (/ 1.0 (* x (+ x 2)))) +(def (pinext x) (+ x 4)) (sum piterm a pinext b)) (* 8 (pisum 1 1000)) `, scope, ), ).toEqual(3.139592655589783)); + + // integral + it('sicp 79.2', () => + expect( + evaluate( + ` +(def (integral f a b dx) + (def (adddx x) (+ x dx)) +(* (sum f (+ a (/ dx 2.0)) adddx b) dx)) +(integral cube 0 1 0.01) + `, + scope, + ), + ).toEqual(0.24998750000000042)); + // maximum callstack... + //it('sicp 79.3', () => expect(evaluate(`(integral cube 0 1 0.001)`, scope)).toEqual(0.249999875000001)); + + //lambdas + it('sicp 83.1', () => expect(evaluate(`((fn (x) (+ x 4)) 5)`)).toEqual(9)); + + it('sicp 83.2', () => + expect( + evaluate( + ` +(def (pisum a b) + (sum (fn (x) (/ 1.0 (* x (+ x 2)))) + a + (fn (x) (+ x 4)) + b)) +(* 8 (pisum 1 1000)) +`, + scope, + ), + ).toEqual(3.139592655589783)); + it('sicp 83.3', () => + expect( + evaluate( + ` +(def (integral f a b dx) + (* (sum f + (+ a (/ dx 2.0)) + (fn (x) (+ x dx)) + b) + dx)) +(integral cube 0 1 0.01) +`, + scope, + ), + ).toEqual(0.24998750000000042)); + it('sicp 84.1', () => expect(evaluate(`((fn (x y z) (+ x y (square z))) 1 2 3)`, scope)).toEqual(12)); }); From 989bd0990e638f19d694826c5509683459f1a6b9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 3 Apr 2025 17:01:36 +0200 Subject: [PATCH 094/538] mondo: let expressions --- packages/mondo/mondo.mjs | 16 +++++++++++ packages/mondo/test/mondo.test.mjs | 43 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 8db36bd5d..705b9de91 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -331,6 +331,19 @@ export class MondoRunner { //console.log(printAst(ast)); return this.evaluate(ast, scope); } + evaluate_let(ast, scope) { + // (let ((x 3) (y 4)) ...body) + // = ((fn (x y) ...body) 3 4) + const defs = ast.children[1].children; + const args = defs.map((pair) => pair.children[0]); + const vals = defs.map((pair) => pair.children[1]); + const body = ast.children.slice(2); + const lambda = { + type: 'list', + children: [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, ...body], + }; + return this.evaluate({ type: 'list', children: [lambda, ...vals] }, scope); + } evaluate_def(ast, scope) { // function definition special form? if (ast.children[1].type === 'list') { @@ -441,6 +454,9 @@ export class MondoRunner { if (name === 'if') { return this.evaluate_if(ast, scope); } + if (name === 'let') { + return this.evaluate_let(ast, scope); + } if (name === 'def') { this.evaluate_def(ast, scope); } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 0fac441e8..236fa2911 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -610,4 +610,47 @@ describe('mondo arithmetic', () => { ), ).toEqual(0.24998750000000042)); it('sicp 84.1', () => expect(evaluate(`((fn (x y z) (+ x y (square z))) 1 2 3)`, scope)).toEqual(12)); + + // let expressions + it('sicp 87.1', () => + expect( + evaluate( + ` +(+ (let ((x 3)) +(+ x (* x 10))) x) +`, + { x: 5 }, + ), + ).toEqual(38)); + it('sicp 87.2', () => + expect( + evaluate( + ` +(let ((x 3) +(y (+ x 2))) +(* x y)) + `, + { x: 2 }, + ), + ).toEqual(12)); + it('sicp 88.1', () => + expect( + evaluate( + ` +(def (f g) (g 2)) +(f square) + `, + scope, + ), + ).toEqual(4)); + it('sicp 88.2', () => + expect( + evaluate( + ` +(def (f g) (g 2)) +(f (fn (z) (* z (+ z 1)))) + `, + scope, + ), + ).toEqual(6)); }); From d4733a30aba4d6ec9dfaf53046b1e3ac0193fbc7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 3 Apr 2025 17:30:03 +0200 Subject: [PATCH 095/538] mondo: add half interval method example --- packages/mondo/test/mondo.test.mjs | 45 ++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 236fa2911..9c2931966 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -152,6 +152,7 @@ describe('mondo arithmetic', () => { not: (a) => !a, run: (...args) => args[args.length - 1], def: () => 0, + sin: Math.sin, PI: Math.PI, }; function evaluator(node, scope) { @@ -647,10 +648,48 @@ describe('mondo arithmetic', () => { expect( evaluate( ` -(def (f g) (g 2)) -(f (fn (z) (* z (+ z 1)))) - `, + (def (f g) (g 2)) + (f (fn (z) (* z (+ z 1)))) + `, scope, ), ).toEqual(6)); + + // Finding roots of equations by the half-interval method + it('sicp 89.1', () => + expect( + evaluate( + ` +(def (search f negpoint pospoint) + (let ((midpoint (average negpoint pospoint))) + (if (closeenough negpoint pospoint) + midpoint + (let ((testvalue (f midpoint))) + (match ((positive testvalue) + (search f negpoint midpoint)) + ((negative testvalue) + (search f midpoint pospoint)) + (else midpoint)))))) + +(def (closeenough x y) (lt (abs (- x y)) 0.001)) + +(def (negative x) (lt x 0)) +(def (positive x) (gt x 0)) + +(def (halfintervalmethod f a b) (let ((avalue (f a)) +(bvalue (f b))) +(match ((and (negative avalue) (positive bvalue)) +(search f a b)) +((and (negative bvalue) (positive avalue)) +(search f b a)) (else +(error "Values are not of opposite sign" a b))))) + +(halfintervalmethod sin 2.0 4.0) +`, + scope, + ), + ).toEqual(3.14111328125)); + + it('sicp 89.1', () => + expect(evaluate(`(halfintervalmethod (fn (x) (- (* x x x) (* 2 x) 3)) 1.0 2.0)`, scope)).toEqual(1.89306640625)); }); From d5ef686fe0fc3036363502e31556007412a64fa4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 4 Apr 2025 00:45:00 +0200 Subject: [PATCH 096/538] mondo: sicp 100 --- packages/mondo/test/mondo.test.mjs | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 9c2931966..c685bcb28 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -153,6 +153,7 @@ describe('mondo arithmetic', () => { run: (...args) => args[args.length - 1], def: () => 0, sin: Math.sin, + cos: Math.cos, PI: Math.PI, }; function evaluator(node, scope) { @@ -692,4 +693,69 @@ describe('mondo arithmetic', () => { it('sicp 89.1', () => expect(evaluate(`(halfintervalmethod (fn (x) (- (* x x x) (* 2 x) 3)) 1.0 2.0)`, scope)).toEqual(1.89306640625)); + + // Finding fixed points of functions + it('sicp 92.1', () => + expect( + evaluate( + ` +(def tolerance 0.00001) +(def (fixedpoint f first-guess) + (def (closeenough v1 v2) (lt (abs (- v1 v2)) tolerance)) + (def (try guess) + (let ((next (f guess))) + (if (closeenough guess next) next (try next)))) + (try first-guess)) + +(fixedpoint cos 1.0) +`, + scope, + ), + ).toEqual(0.7390822985224023)); + it('sicp 93.1', () => + expect(evaluate(`(fixedpoint (fn (y) (+ (sin y) (cos y))) 1.0)`, scope)).toEqual(1.2587315962971173)); + // Maximum call stack size exceeded (expected) + /* it('sicp 93.2', () => + expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (/ x y)) 1.0)) (sqrt 4)`, scope)).toEqual(0)); */ + it('sicp 93.3', () => + expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (average y (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual( + 2.6457513110645907, + )); + // Procedures as Returned Values + it('sicp 97.1', () => + expect(evaluate(`(def (averagedamp f) (fn (x) (average x (f x)))) ((averagedamp square) 10)`, scope)).toEqual(55)); + it('sicp 98.1', () => + expect(evaluate(`(def (sqrt x) (fixedpoint (averagedamp (fn (y) (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual( + 2.6457513110645907, + )); + it('sicp 98.2', () => + expect( + evaluate(`(def (cuberoot x) (fixedpoint (averagedamp (fn (y) (/ x (square y)))) 1.0)) (cuberoot 7)`, scope), + ).toEqual(1.912934258514886)); + it('sicp 99.1', () => + expect( + evaluate( + ` + (def (deriv g) (fn (x) (/ (- (g (+ x dx)) (g x)) dx))) + (def dx 0.00001) + (def (cube x) (* x x x)) + ((deriv cube) 5) + `, + scope, + ), + ).toEqual(75.00014999664018)); + // With the aid of deriv, we can express Newton’s method as a fixed-point process: + it('sicp 100.1', () => + expect( + evaluate( + ` +(def (newtontransform g) +(fn (x) (- x (/ (g x) ((deriv g) x))))) +(def (newtonsmethod g guess) (fixedpoint (newtontransform g) guess)) +(def (sqrt x) (newtonsmethod (fn (y) (- (square y) x)) 1.0)) +(sqrt 7) + `, + scope, + ), + ).toEqual(2.6457513110645907)); }); From 7fc21d55d7904f5047806ef50e1ddbd308fde8c5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 4 Apr 2025 10:04:22 +0200 Subject: [PATCH 097/538] mondo: rational number test --- packages/mondo/mondo.mjs | 1 + packages/mondo/test/mondo.test.mjs | 113 ++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 3 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 705b9de91..bed568661 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -437,6 +437,7 @@ export class MondoRunner { ast.value = Number(ast.value); } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { ast.value = ast.value.slice(1, -1); + ast.type = 'plain'; // is this problematic? } return this.evaluator(ast, scope); } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index c685bcb28..7029c5772 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -155,6 +155,13 @@ describe('mondo arithmetic', () => { sin: Math.sin, cos: Math.cos, PI: Math.PI, + cons: (a, b) => [a, b], + car: (pair) => pair[0], + cdr: (pair) => pair[1], + concat: (...msgs) => msgs.join(''), + error: (...msgs) => { + throw new Error(msgs.join(' ')); + }, }; function evaluator(node, scope) { if (node.type !== 'list') { @@ -164,7 +171,7 @@ describe('mondo arithmetic', () => { // is list const [fn, ...args] = node.children; if (typeof fn !== 'function') { - throw new Error(`"${fn}": expected function, got ${typeof fn} "${JSON.stringify(fn)}"`); + throw new Error(`"${fn}": expected function, got ${typeof fn} "${fn}"`); } return fn(...args); } @@ -327,9 +334,9 @@ describe('mondo arithmetic', () => { it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24)); // 46.1 - /* (define (+ a b) + /* (def (+ a b) (if (= a 0) b (inc (+ (dec a) b)))) -(define (+ a b) +(def (+ a b) (if (= a 0) b (+ (dec a) (inc b)))) */ // Exercise 1.10 @@ -499,6 +506,7 @@ describe('mondo arithmetic', () => { (gcd b (mod a b)))) (gcd 20 6) `, + scope, ), ).toEqual(2)); @@ -758,4 +766,103 @@ describe('mondo arithmetic', () => { scope, ), ).toEqual(2.6457513110645907)); + // whatever this is + it('sicp 101.1', () => + expect( + evaluate( + ` + (def (fixedpointoftransform g transform guess) (fixedpoint (transform g) guess)) + (def (sqrt x) (fixedpointoftransform + (fn (y) (/ x y)) averagedamp 1.0)) + (sqrt 7) + `, + scope, + ), + ).toEqual(2.6457513110645907)); + it('sicp 101.2', () => + expect( + evaluate( + ` +(def (sqrt x) (fixedpointoftransform +(fn (y) (- (square y) x)) newtontransform 1.0)) +(sqrt 7) + `, + scope, + ), + ).toEqual(2.6457513110645907)); + + // data abstraction + + // rational arithmetic + it('sicp 114.1', () => + expect( + evaluate( + ` +(def (addrat x y) + (makerat (+ (* + (numer x) (denom y)) + (* (numer y) (denom x))) + (* (denom x) (denom y)))) +(def (subrat x y) + (makerat (- (* (numer x) (denom y)) + (* (numer y) (denom x))) + (* (denom x) (denom y)))) +(def (mulrat x y) + (makerat (* (numer x) (numer y)) + (* (denom x) (denom y)))) +(def (divrat x y) + (makerat (* (numer x) (denom y)) + (* (denom x) (numer y)))) +(def (equalrat x y) + (eq (* (numer x) (denom y)) + (* (numer y) (denom x)))) + `, + scope, + ), + ).toEqual(0)); + + // markerat number denom + it('sicp 117.1', () => + expect( + evaluate( + ` +(def (makerat n d) (cons n d)) +(def (numer x) (car x)) +(def (denom x) (cdr x)) +(def (printrat x) (concat (numer x) ':' (denom x))) + `, + scope, + ), + ).toEqual(0)); + + it('sicp 117.1', () => expect(evaluate(`(def onehalf (makerat 1 2)) (printrat onehalf)`, scope)).toEqual('1:2')); + it('sicp 117.2', () => + expect(evaluate(`(def onethird (makerat 1 3)) (printrat (addrat onehalf onethird))`, scope)).toEqual('5:6')); + it('sicp 117.3', () => expect(evaluate(`(printrat (mulrat onehalf onethird))`, scope)).toEqual('1:6')); + it('sicp 117.4', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('6:9')); + it('sicp 118.1', () => + expect(evaluate(`(def (makerat n d) (let ((g (gcd n d))) (cons (/ n g) (/ d g))))`, scope)).toEqual(0)); + it('sicp 118.1', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('2:3')); + + let lscope = {}; + // pairs with lambda + it('sicp 124.1', () => + expect( + evaluate( + ` +(def (cons x y) + (def (dispatch m) + (match + ((eq m 0) x) + ((eq m 1) y) + (else (error "argument not 0 or 1: CONS" m))) + ) dispatch) + (def (car z) (z 0)) + (def (cdr z) (z 1)) + `, + lscope, + ), + ).toEqual(0)); + it('sicp 124.1', () => expect(evaluate(`(car (cons first second))`, lscope)).toEqual('first')); + it('sicp 124.2', () => expect(evaluate(`(cdr (cons first second))`, lscope)).toEqual('second')); }); From 243adda3ddc2627eb51c3f93f8b2280d4da7ef12 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 1 May 2025 10:56:33 +0200 Subject: [PATCH 098/538] mondo: more sicp tests --- packages/mondo/test/mondo.test.mjs | 106 ++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 7029c5772..4ec18decb 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -155,9 +155,12 @@ describe('mondo arithmetic', () => { sin: Math.sin, cos: Math.cos, PI: Math.PI, - cons: (a, b) => [a, b], + cons: (a, b) => [a, ...(Array.isArray(b) ? b : [b])], car: (pair) => pair[0], - cdr: (pair) => pair[1], + cdr: (pair) => pair.slice(1), + list: (...items) => items, + nil: [], + isnull: (items) => items.length === 0, concat: (...msgs) => msgs.join(''), error: (...msgs) => { throw new Error(msgs.join(' ')); @@ -865,4 +868,103 @@ describe('mondo arithmetic', () => { ).toEqual(0)); it('sicp 124.1', () => expect(evaluate(`(car (cons first second))`, lscope)).toEqual('first')); it('sicp 124.2', () => expect(evaluate(`(cdr (cons first second))`, lscope)).toEqual('second')); + // lists + it('sicp 135.1', () => expect(evaluate(`(list 1 2 3 4)`)).toEqual([1, 2, 3, 4])); + it('sicp 137.1', () => expect(evaluate(`(car (list 1 2 3 4))`)).toEqual(1)); + it('sicp 137.2', () => expect(evaluate(`(cdr (list 1 2 3 4))`)).toEqual([2, 3, 4])); + it('sicp 137.3', () => expect(evaluate(`(car (cdr (list 1 2 3 4)))`)).toEqual(2)); + it('sicp 137.4', () => expect(evaluate(`(cons 10 (list 1 2 3 4))`)).toEqual([10, 1, 2, 3, 4])); + // listref + it('sicp 138.1', () => + expect( + evaluate( + ` +(def (listref items n) (if (eq n 0) (car items) + (listref (cdr items) (- n 1)))) +(def squares (list 1 4 9 16 25)) +(listref squares 3)`, + scope, + ), + ).toEqual(16)); + // length recursive + it('sicp 138.2', () => + expect( + evaluate( + ` + (def (length items) + (if (isnull items) 0 + (+ 1 (length (cdr items))))) + (def odds (list 1 3 5 7)) + (length odds)`, + ), + ).toEqual(4)); + // length iterative + it('sicp 139.1', () => + expect( + evaluate( + ` + (def (length items) + (def (lengthiter a count) + (if (isnull a) count + (lengthiter (cdr a) (+ 1 count)))) + (lengthiter items 0)) + (def odds (list 1 3 5 7)) + (length odds) + `, + scope, + ), + ).toEqual(4)); + // append + it('sicp 139.1', () => + expect( + evaluate( + ` +(def (append list1 list2) +(if (isnull list1) + list2 + (cons (car list1) (append (cdr list1) list2)))) + (append squares odds) + `, + scope, + ), + ).toEqual([1, 4, 9, 16, 25, 1, 3, 5, 7])); + // (define (f x y . z) ⟨body⟩) <- tbd: variable argument count + // Mapping over lists + + it('sicp 143.1', () => + expect( + evaluate( + ` +(def (scalelist items factor) (if (isnull items) nil + (cons (* (car items) factor) + (scalelist (cdr items) factor)))) +(scalelist (list 1 2 3 4 5) 10) + `, + scope, + ), + ).toEqual([10, 20, 30, 40, 50])); + + it('sicp 143.1', () => + expect( + evaluate( + ` + (def (map proc items) (if (isnull items) nil + (cons (proc (car items)) + (map proc (cdr items))))) + (map abs (list -10 2.5 -11.6 17)) + `, + scope, + ), + ).toEqual([10, 2.5, 11.6, 17])); + it('sicp 143.1', () => expect(evaluate(`(map (fn (x) (* x x)) (list 1 2 3 4))`, scope)).toEqual([1, 4, 9, 16])); + it('sicp 143.1', () => + expect( + evaluate( + ` +(def (scalelist items factor) (map (fn (x) (* x factor)) items)) +(scalelist (list 1 2 3 4 5) 10) +`, + scope, + ), + ).toEqual([10, 20, 30, 40, 50])); }); From e3a6444f413ee01bb23aef9ac44bb0e69306296e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:30:09 +0200 Subject: [PATCH 099/538] mondo: support line comments --- packages/mondo/mondo.mjs | 5 ++++- packages/mondo/test/mondo.test.mjs | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index bed568661..be7b2a203 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -8,6 +8,7 @@ This program is free software: you can redistribute it and/or modify it under th export class MondoParser { // these are the tokens we expect token_types = { + comment: /^\/\/(.*?)(?=\n|$)/, quotes_double: /^"(.*?)"/, quotes_single: /^'(.*?)'/, open_list: /^\(/, @@ -428,7 +429,9 @@ export class MondoRunner { } evaluate_list(ast, scope) { // evaluate all children before evaluating list (dont mutate!!!) - const args = ast.children.map((arg) => this.evaluate(arg, scope)); + const args = ast.children + .filter((child) => child.type !== 'comment') // ignore comments + .map((arg) => this.evaluate(arg, scope)); const node = { type: 'list', children: args }; return this.evaluator(node, scope); } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 4ec18decb..6e9260f08 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -67,6 +67,14 @@ describe('mondo s-expressions parser', () => { { type: 'number', value: '22.3' }, ], })); + it('should parse comments', () => + expect(p('a // hello')).toEqual({ + type: 'list', + children: [ + { type: 'plain', value: 'a' }, + { type: 'comment', value: '// hello' }, + ], + })); }); let desguar = (a) => { From 29e5833c8c48818289b436864af332f15b04cf39 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:32:46 +0200 Subject: [PATCH 100/538] tweak --- packages/mondo/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 8bdd93143..b1f504bf9 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -1,6 +1,6 @@ # mondo -an experimental parser for an *uzulang*, a custom dsl for patterns that can stand on its own feet. more info: +an experimental parser for a maxi notation, a custom dsl for patterns that can stand on its own feet. more info: - [uzulang I](https://garten.salat.dev/uzu/uzulang1.html) - [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) From 60dd8bd763d510197daafe6830dce8c4a3f075ec Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:35:16 +0200 Subject: [PATCH 101/538] tweak again --- packages/mondo/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index b1f504bf9..5233c73d3 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -1,6 +1,9 @@ # mondo -an experimental parser for a maxi notation, a custom dsl for patterns that can stand on its own feet. more info: +a lisp-based language intended to be used as a custom dsl for patterns that can stand on its own feet. +see the `test` folder for usage examples + +more info: - [uzulang I](https://garten.salat.dev/uzu/uzulang1.html) - [uzulang II](https://garten.salat.dev/uzu/uzulang2.html) From ffe831d24bdbd16d63e30ab8cb67c4b50c4f17d3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:53:55 +0200 Subject: [PATCH 102/538] mondo: strings now get type "string" to be discernable from plain variables --- packages/mondo/mondo.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index be7b2a203..626793d8e 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -440,7 +440,7 @@ export class MondoRunner { ast.value = Number(ast.value); } else if (['quotes_double', 'quotes_single'].includes(ast.type)) { ast.value = ast.value.slice(1, -1); - ast.type = 'plain'; // is this problematic? + ast.type = 'string'; } return this.evaluator(ast, scope); } From 7565354274bca05ec29156bf084baefdca1be4ad Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 08:54:34 +0200 Subject: [PATCH 103/538] superdough: fallback to triangle when non-string is given --- packages/superdough/superdough.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 02832b433..8c26910b5 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -101,6 +101,10 @@ export async function aliasBank(...args) { } export function getSound(s) { + if (typeof s !== 'string') { + console.warn(`getSound: expected string got "${s}". fall back to triangle`); + return soundMap.get().triangle; // is this good? + } return soundMap.get()[s.toLowerCase()]; } From 4208c79c0951ef963026b1454a0cc76ed7294555 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 15:13:04 +0200 Subject: [PATCH 104/538] fix: ! and @ operators --- packages/mondough/mondough.mjs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index e995c0e4e..d995c3d8f 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -49,6 +49,10 @@ function evaluator(node, scope) { if (type === 'list') { const { children } = node; const [name, ...args] = children; + // some functions wont be reified to make sure they work (e.g. see extend below) + if (typeof name === 'function') { + return name(...args); + } if (name.value === 'def') { return silence; } @@ -56,7 +60,7 @@ function evaluator(node, scope) { const first = name.firstCycle(true)[0]; const type = typeof first?.value; if (type !== 'function') { - throw new Error(`[mondough] "${first}" is not a function, got ${type} ...`); + throw new Error(`[mondough] expected function, got "${first?.value}"`); } return name .fmap((fn) => { @@ -73,10 +77,14 @@ function evaluator(node, scope) { return reify(scope[value]); // -> local scope has no location } const variable = lib[value] ?? strudelScope[value]; + // problem: collisions when we want a string that happens to also be a variable name + // example: "s sine" -> sine is also a variable let pat; if (type === 'plain' && typeof variable !== 'undefined') { - // problem: collisions when we want a string that happens to also be a variable name - // example: "s sine" -> sine is also a variable + // some function names are not patternable, so we skip reification here + if (['!', 'extend', '@', 'expand'].includes(value)) { + return variable; + } pat = reify(variable); } else { pat = reify(value); From 607a6121bcd8fc3ebef851b5c6f4fefb25727132 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 2 May 2025 16:20:23 +0200 Subject: [PATCH 105/538] rename mondo package to mondolang --- packages/mondo/README.md | 2 +- packages/mondo/package.json | 2 +- packages/mondough/mondough.mjs | 2 +- packages/mondough/package.json | 3 ++- pnpm-lock.yaml | 3 +++ 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/mondo/README.md b/packages/mondo/README.md index 5233c73d3..954f425a9 100644 --- a/packages/mondo/README.md +++ b/packages/mondo/README.md @@ -11,7 +11,7 @@ more info: ## Example Usage ```js -import { MondoRunner } from 'mondo' +import { MondoRunner } from 'mondolang' // define our library of functions and variables let lib = { add: (a, b) => a + b, diff --git a/packages/mondo/package.json b/packages/mondo/package.json index d8a4a0bf2..277bddb1c 100644 --- a/packages/mondo/package.json +++ b/packages/mondo/package.json @@ -1,5 +1,5 @@ { - "name": "mondo", + "name": "mondolang", "version": "1.1.0", "description": "a language for functional composition that translates to js", "main": "mondo.mjs", diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index d995c3d8f..3731e8cee 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -13,7 +13,7 @@ import { silence, } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; -import { MondoRunner } from '../mondo/mondo.mjs'; +import { MondoRunner } from 'mondolang'; const tail = (friend, pat) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); diff --git a/packages/mondough/package.json b/packages/mondough/package.json index eae22519e..a034424c0 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -33,7 +33,8 @@ "homepage": "https://github.com/tidalcycles/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", - "@strudel/transpiler": "workspace:*" + "@strudel/transpiler": "workspace:*", + "mondolang": "workspace:*" }, "devDependencies": { "mondo": "*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67e5d85d1..7aecd35bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -364,6 +364,9 @@ importers: '@strudel/transpiler': specifier: workspace:* version: link:../transpiler + mondolang: + specifier: workspace:* + version: link:../mondo devDependencies: mondo: specifier: '*' From 006ce9d1da2af36364d2acce6f4aada71a965766 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 2 May 2025 23:33:58 -0400 Subject: [PATCH 106/538] delaytime --- packages/superdough/superdough.mjs | 8 ++++---- packages/superdough/util.mjs | 4 ++++ packages/webaudio/webaudio.mjs | 5 +++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a1dc30b7c..2a205e18d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -7,7 +7,7 @@ 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 } from './util.mjs'; +import { clamp, nanFallback, _mod, cycleToSeconds } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs'; import { map } from 'nanostores'; @@ -132,7 +132,7 @@ const defaultDefaultValues = { distortvol: 1, delay: 0, delayfeedback: 0.5, - delaytime: 0.25, + delaytime: 3 / 16, orbit: 1, i: 1, velocity: 1, @@ -429,7 +429,7 @@ export function resetGlobalEffects() { let activeSoundSources = new Map(); -export const superdough = async (value, t, hapDuration) => { +export const superdough = async (value, t, hapDuration, cps = 1) => { const ac = getAudioContext(); t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; let { stretch } = value; @@ -699,7 +699,7 @@ export const superdough = async (value, t, hapDuration) => { // delay let delaySend; if (delay > 0 && delaytime > 0 && delayfeedback > 0) { - const delyNode = getDelay(orbit, delaytime, delayfeedback, t); + const delyNode = getDelay(orbit, cycleToSeconds(delaytime, cps), delayfeedback, t); delaySend = effectSend(post, delyNode, delay); audioNodes.push(delaySend); } diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 4e3c7e41e..ea63a16f0 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -68,3 +68,7 @@ export const _mod = (n, m) => ((n % m) + m) % m; export const getSoundIndex = (n, numSounds) => { return _mod(Math.round(nanFallback(n, 0)), numSounds); }; + +export function cycleToSeconds(cycle, cps) { + return cycle / cps; +} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 44a683480..3ce4f91b3 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -17,8 +17,9 @@ const hap2value = (hap) => { export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps); // uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 -export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => - superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration); +export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => { + return superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration,cps); +}; Pattern.prototype.webaudio = function () { return this.onTrigger(webaudioOutputTrigger); From 94257e81ee6c43053ffa739eff88c1454ba36604 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 2 May 2025 23:44:55 -0400 Subject: [PATCH 107/538] lint --- packages/webaudio/webaudio.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 3ce4f91b3..640449db0 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -18,7 +18,7 @@ const hap2value = (hap) => { export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps); // uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => { - return superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration,cps); + return superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration, cps); }; Pattern.prototype.webaudio = function () { From 2951a60fac778ee1b7a8b04d51282957425d7758 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 3 May 2025 11:44:02 +0200 Subject: [PATCH 108/538] fix: rests at the end --- packages/mondo/mondo.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 626793d8e..6ec63c8d0 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -155,7 +155,9 @@ export class MondoParser { if (opIndex === -1) break; const op = { type: 'plain', value: children[opIndex].value }; if (opIndex === children.length - 1) { - throw new Error(`cannot use operator as last child.`); + //throw new Error(`cannot use operator as last child.`); + children[opIndex] = op; // ignore operator if last child.. e.g. "note [c -]" + continue; } if (opIndex === 0) { // regular function call (assuming each operator exists as function) From 9b10dc85351a2ef23e71d708b0595b69132aeb10 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 3 May 2025 20:37:44 +0100 Subject: [PATCH 109/538] change pipe symbol from '.' to '#' --- packages/mondo/mondo.mjs | 7 +++---- packages/mondo/test/mondo.test.mjs | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 6ec63c8d0..fe48509c5 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -22,7 +22,7 @@ export class MondoParser { number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, - pipe: /^\./, + pipe: /^\#/, stack: /^[,$]/, or: /^[|]/, plain: /^[a-zA-Z0-9-~_^#]+/, @@ -309,9 +309,8 @@ export function printAst(ast, compact = false, lvl = 0) { const br = compact ? '' : '\n'; const spaces = compact ? '' : Array(lvl).fill(' ').join(''); if (ast.type === 'list') { - return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ - ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' - }`; + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; } return `${ast.value}`; } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 6e9260f08..41558aaab 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -89,13 +89,13 @@ describe('mondo sugar', () => { it('should desugar mixed [] <>', () => expect(desguar('[a ]')).toEqual('(square a (angle b c))')); it('should desugar mixed <> []', () => expect(desguar('')).toEqual('(angle a (square b c))')); - it('should desugar .', () => expect(desguar('s jazz . fast 2')).toEqual('(fast 2 (s jazz))')); - it('should desugar . square', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast 2 (square bd cp))')); - it('should desugar . twice', () => expect(desguar('s jazz . fast 2 . slow 2')).toEqual('(slow 2 (fast 2 (s jazz)))')); - it('should desugar . nested', () => expect(desguar('(s cp . fast 2)')).toEqual('(fast 2 (s cp))')); - it('should desugar . within []', () => expect(desguar('[bd cp . fast 2]')).toEqual('(fast 2 (square bd cp))')); - it('should desugar . within , within []', () => - expect(desguar('[bd cp . fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); + it('should desugar #', () => expect(desguar('s jazz # fast 2')).toEqual('(fast 2 (s jazz))')); + it('should desugar # square', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))')); + it('should desugar # twice', () => expect(desguar('s jazz # fast 2 # slow 2')).toEqual('(slow 2 (fast 2 (s jazz)))')); + it('should desugar # nested', () => expect(desguar('(s cp # fast 2)')).toEqual('(fast 2 (s cp))')); + it('should desugar # within []', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))')); + it('should desugar # within , within []', () => + expect(desguar('[bd cp # fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)')); // it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))')); @@ -123,15 +123,15 @@ describe('mondo sugar', () => { it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */ it('should desugar README example', () => - expect(desguar('s [bd hh*2 (cp.crush 4) ] . speed .8')).toEqual( + expect(desguar('s [bd hh*2 (cp # crush 4) ] # speed .8')).toEqual( '(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))', )); - it('should desugar (.)', () => expect(desguar('(.)')).toEqual('(fn (_) _)')); - it('should desugar lambda', () => expect(desguar('(.fast 2)')).toEqual('(fn (_) (fast 2 _))')); - it('should desugar lambda call', () => expect(desguar('((.mul 2) 2)')).toEqual('((fn (_) (mul 2 _)) 2)')); + it('should desugar (#)', () => expect(desguar('(#)')).toEqual('(fn (_) _)')); + it('should desugar lambda', () => expect(desguar('(# fast 2)')).toEqual('(fn (_) (fast 2 _))')); + it('should desugar lambda call', () => expect(desguar('((# mul 2) 2)')).toEqual('((fn (_) (mul 2 _)) 2)')); it('should desugar lambda with pipe', () => - expect(desguar('(.fast 2 .room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))')); + expect(desguar('(# fast 2 # room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))')); /* const lambda = parser.parse('(lambda (_) (fast 2 _))'); const target = { type: 'plain', value: 'xyz' }; it('should desugar_lambda', () => @@ -141,8 +141,8 @@ describe('mondo sugar', () => { describe('mondo arithmetic', () => { let multi = (op) => - (init, ...rest) => - rest.reduce((acc, arg) => op(acc, arg), init); + (init, ...rest) => + rest.reduce((acc, arg) => op(acc, arg), init); let lib = { '+': multi((a, b) => a + b), From 1eb5f6a99505d6db5c72909ab4559c4b695597b7 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 3 May 2025 20:38:43 +0100 Subject: [PATCH 110/538] format --- packages/mondo/mondo.mjs | 5 +- packages/mondo/test/mondo.test.mjs | 4 +- website/src/pages/learn/mondo-notation.mdx | 67 ++++++++++++---------- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index fe48509c5..142b856dc 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -309,8 +309,9 @@ export function printAst(ast, compact = false, lvl = 0) { const br = compact ? '' : '\n'; const spaces = compact ? '' : Array(lvl).fill(' ').join(''); if (ast.type === 'list') { - return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' - }`; + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ + ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; } return `${ast.value}`; } diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 41558aaab..6393b10f9 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -141,8 +141,8 @@ describe('mondo sugar', () => { describe('mondo arithmetic', () => { let multi = (op) => - (init, ...rest) => - rest.reduce((acc, arg) => op(acc, arg), init); + (init, ...rest) => + rest.reduce((acc, arg) => op(acc, arg), init); let lib = { '+': multi((a, b) => a + b), diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index 2bc952341..ccf5b72b6 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -14,20 +14,25 @@ Here's an example: <8 16>) . *2 -.s "sine" .add (note [0 <12 24>]*2) -.dec(sine .range .2 2) .room .5 -.lpf(sine/3.range 120 400) -.lpenv(rand .range .5 4) -.lpq(perlin .range 5 12 . * 2) -.dist 1 .fm 4 .fmh 5.01 .fmdecay <.1 .2> -.postgain .6 .delay .1 .clip 5 + tune={`$ note (c2 # euclid <3 6 3> <8 16>) # *2 + # s "sine" # add (note [0 <12 24>]*2) + # dec(sine # range .2 2) + # room .5 + # lpf (sine/3 # range 120 400) + # lpenv (rand # range .5 4) + # lpq (perlin # range 5 12 # * 2) + # dist 1 # fm 4 # fmh 5.01 # fmdecay <.1 .2> + # postgain .6 # delay .1 # clip 5 -$ s [bd bd bd bd] .bank tr909.clip.5 -.ply<1 [1 [2 4]]> +$ s [bd bd bd bd] # bank tr909 # clip .5 -$ s oh*4 .press .bank tr909 .speed.8 -.dec (<.02 .05>*2 .add (saw/8.range 0 1))`} +# ply <1 [1 [2 4]]> + +$ s oh\*4 # press # bank tr909 # speed.8 + +# dec (<.02 .05>\*2 # add (saw/8 # range 0 1)) + +`} /> ## Mondo in the REPL @@ -95,16 +100,16 @@ Besides function calling with round parens, Mondo Notation has a lot in common w ## Chaining Functions -Similar to how it works in JS, we can chain functions calls with the "." operator: +Similar to how "." works in javascript (JS), we can chain functions calls with the "#" operator: *4 -.scale C4:minor -.jux rev -.dec .2 -.delay .5`} + # scale C4:minor + # jux rev + # dec .2 + # delay .5`} /> Here's the same written in JS: @@ -112,29 +117,29 @@ Here's the same written in JS: *4") -.scale("C4:minor") -.jux(rev) -.dec(.2) -.delay(.5)`} + # scale("C4:minor") + # jux(rev) + # dec(.2) + # delay(.5)`} /> ### Chaining Functions Locally -A function can be applied "locally" by wrapping it in round parens: +A function can be applied to a single element by wrapping it in round parens: - + in this case, `delay .6` will only be applied to `cp`. compare this with the JS version: -here we can see much we can save when there's no boundary between mini notation and function calls! +here we can see how much we can save when there's no boundary between mini notation and function calls! ### Chaining Infix Operators Infix operators exist as regular functions, so they can be chained as well: - + In this case, the \*2 will be applied to the whole pattern. @@ -146,17 +151,17 @@ Some functions in strudel expect a function as input, for example: in mondo, the `x=>x.` can be shortened to: - + chaining works as expected: - + ## Strings You can use "double quotes" and 'single quotes' to get a string: - + ## Multiple Patterns @@ -165,9 +170,9 @@ The `$` sign can be used to separate multiple patterns: .voicing -.struct[x - - x - x - -].delay.5`} + tune={`$ s [bd rim [~ bd] rim] # bank tr707 +$ chord # voicing + # struct[x ~ ~ x ~ x ~ ~] # delay .5`} /> The `$` sign is an alias for `,` so it will create a stack behind the scenes. From 98efd72ab496231f9d67e1bfc6aa60976f2373fb Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 3 May 2025 20:59:05 +0100 Subject: [PATCH 111/538] delint --- packages/mondo/mondo.mjs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 142b856dc..0f9da0eb9 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -22,7 +22,7 @@ export class MondoParser { number: /^-?[0-9]*\.?[0-9]+/, // before pipe! op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, - pipe: /^\#/, + pipe: /^#/, stack: /^[,$]/, or: /^[|]/, plain: /^[a-zA-Z0-9-~_^#]+/, @@ -309,9 +309,8 @@ export function printAst(ast, compact = false, lvl = 0) { const br = compact ? '' : '\n'; const spaces = compact ? '' : Array(lvl).fill(' ').join(''); if (ast.type === 'list') { - return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ - ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' - }`; + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; } return `${ast.value}`; } From 1b3b07842ec86ac65715803d3776fe5b6dc6f300 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 3 May 2025 21:57:01 +0100 Subject: [PATCH 112/538] format again.. --- packages/mondo/mondo.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 0f9da0eb9..73dbf1c7a 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -309,8 +309,9 @@ export function printAst(ast, compact = false, lvl = 0) { const br = compact ? '' : '\n'; const spaces = compact ? '' : Array(lvl).fill(' ').join(''); if (ast.type === 'list') { - return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' - }`; + return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ + ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')' + }`; } return `${ast.value}`; } From 483843bd0a776ff0690712b5495499a0535c2c1f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 1 Jun 2025 18:06:36 +0200 Subject: [PATCH 113/538] 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 114/538] 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 e0a21a936bad89b1b8cf349fcde5b9a596122250 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 2 Jun 2025 12:57:37 +0100 Subject: [PATCH 115/538] berlin --- packages/core/signal.mjs | 43 ++++++++++++++++++++--- packages/superdough/worklets.mjs | 1 - test/__snapshots__/examples.test.mjs.snap | 2 ++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index b6ba5d5cc..8e2d12b74 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -486,13 +486,34 @@ export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pair export const wrandcat = wchooseCycles; -// this function expects pat to be a pattern of floats... -export const perlinWith = (pat) => { - const pata = pat.fmap(Math.floor); - const patb = pat.fmap((t) => Math.floor(t) + 1); +function _perlin(t) { + let ta = Math.floor(t); + let tb = ta + 1; const smootherStep = (x) => 6.0 * x ** 5 - 15.0 * x ** 4 + 10.0 * x ** 3; const interp = (x) => (a) => (b) => a + smootherStep(x) * (b - a); - return pat.sub(pata).fmap(interp).appBoth(pata.fmap(timeToRand)).appBoth(patb.fmap(timeToRand)); + const v = interp(t - ta)(timeToRand(ta))(timeToRand(tb)); + return v; +} +export const perlinWith = (tpat) => { + return tpat.fmap(_perlin); +}; + +function _berlin(t) { + const prevRidgeStartIndex = Math.floor(t); + const nextRidgeStartIndex = prevRidgeStartIndex + 1; + + const prevRidgeBottomPoint = timeToRand(prevRidgeStartIndex); + const nextRidgeTopPoint = timeToRand(nextRidgeStartIndex) + prevRidgeBottomPoint; + + const currentPercent = (t - prevRidgeStartIndex) / (nextRidgeStartIndex - prevRidgeStartIndex); + const interp = (a, b, t) => { + return a + (b - a) * t; + }; + return interp(prevRidgeBottomPoint, nextRidgeTopPoint, currentPercent) / 2; +} + +export const berlinWith = (tpat) => { + return tpat.fmap(_berlin); }; /** @@ -506,6 +527,18 @@ export const perlinWith = (pat) => { */ export const perlin = perlinWith(time.fmap((v) => Number(v))); +/** + * Generates a continuous pattern of [berlin noise](conceived by Jame Coyne and Jade Rowland as a joke but turned out to be surprisingly cool and useful, + * like perlin noise but with sawtooth waves), in the range 0..1. + * + * @name berlin + * @example + * // ascending arpeggios + * $: n("0!16".add(berlin.fast(4).mul(14))).scale("d:minor")._pianoroll() + * + */ +export const berlin = berlinWith(time.fmap((v) => Number(v))); + export const degradeByWith = register( 'degradeByWith', (withPat, x, pat) => pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)), diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 24f7434a9..6f526ec8f 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -882,7 +882,6 @@ class ByteBeatProcessor extends AudioWorkletProcessor { const funcValue = this.func(local_t); let signal = (funcValue & 255) / 127.5 - 1; const out = signal * 0.2; - for (let c = 0; c < output.length; c++) { //prevent speaker blowout via clipping if threshold exceeds output[c][i] = clamp(out, -0.4, 0.4); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 34914664d..074803057 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1051,6 +1051,8 @@ exports[`runs examples > example "begin" example index 0 1`] = ` ] `; +exports[`runs examples > example "berlin" example index 0 1`] = `[]`; + exports[`runs examples > example "binary" example index 0 1`] = ` [ "[ 0/1 → 1/3 | s:hh ]", From cde943d73712d8db15908093a8f1fac46be854f5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 2 Jun 2025 13:02:41 +0100 Subject: [PATCH 116/538] fix test --- packages/core/signal.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 69 ++++++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 8e2d12b74..5fd83bce3 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -534,7 +534,7 @@ export const perlin = perlinWith(time.fmap((v) => Number(v))); * @name berlin * @example * // ascending arpeggios - * $: n("0!16".add(berlin.fast(4).mul(14))).scale("d:minor")._pianoroll() + * n("0!16".add(berlin.fast(4).mul(14))).scale("d:minor") * */ export const berlin = berlinWith(time.fmap((v) => Number(v))); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 074803057..b57d319a7 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1051,7 +1051,74 @@ exports[`runs examples > example "begin" example index 0 1`] = ` ] `; -exports[`runs examples > example "berlin" example index 0 1`] = `[]`; +exports[`runs examples > example "berlin" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:D3 ]", + "[ 1/16 → 1/8 | note:E3 ]", + "[ 1/8 → 3/16 | note:F3 ]", + "[ 3/16 → 1/4 | note:G3 ]", + "[ 1/4 → 5/16 | note:A3 ]", + "[ 5/16 → 3/8 | note:C4 ]", + "[ 3/8 → 7/16 | note:D4 ]", + "[ 7/16 → 1/2 | note:F4 ]", + "[ 1/2 → 9/16 | note:D4 ]", + "[ 9/16 → 5/8 | note:E4 ]", + "[ 5/8 → 11/16 | note:E4 ]", + "[ 11/16 → 3/4 | note:E4 ]", + "[ 3/4 → 13/16 | note:F3 ]", + "[ 13/16 → 7/8 | note:F3 ]", + "[ 7/8 → 15/16 | note:F3 ]", + "[ 15/16 → 1/1 | note:F3 ]", + "[ 1/1 → 17/16 | note:E3 ]", + "[ 17/16 → 9/8 | note:E3 ]", + "[ 9/8 → 19/16 | note:E3 ]", + "[ 19/16 → 5/4 | note:F3 ]", + "[ 5/4 → 21/16 | note:E3 ]", + "[ 21/16 → 11/8 | note:F3 ]", + "[ 11/8 → 23/16 | note:G3 ]", + "[ 23/16 → 3/2 | note:A3 ]", + "[ 3/2 → 25/16 | note:A3 ]", + "[ 25/16 → 13/8 | note:Bb3 ]", + "[ 13/8 → 27/16 | note:Bb3 ]", + "[ 27/16 → 7/4 | note:Bb3 ]", + "[ 7/4 → 29/16 | note:F3 ]", + "[ 29/16 → 15/8 | note:G3 ]", + "[ 15/8 → 31/16 | note:Bb3 ]", + "[ 31/16 → 2/1 | note:C4 ]", + "[ 2/1 → 33/16 | note:C4 ]", + "[ 33/16 → 17/8 | note:D4 ]", + "[ 17/8 → 35/16 | note:F4 ]", + "[ 35/16 → 9/4 | note:G4 ]", + "[ 9/4 → 37/16 | note:Bb3 ]", + "[ 37/16 → 19/8 | note:Bb3 ]", + "[ 19/8 → 39/16 | note:Bb3 ]", + "[ 39/16 → 5/2 | note:C4 ]", + "[ 5/2 → 41/16 | note:F3 ]", + "[ 41/16 → 21/8 | note:F3 ]", + "[ 21/8 → 43/16 | note:G3 ]", + "[ 43/16 → 11/4 | note:A3 ]", + "[ 11/4 → 45/16 | note:A3 ]", + "[ 45/16 → 23/8 | note:A3 ]", + "[ 23/8 → 47/16 | note:A3 ]", + "[ 47/16 → 3/1 | note:A3 ]", + "[ 3/1 → 49/16 | note:E3 ]", + "[ 49/16 → 25/8 | note:G3 ]", + "[ 25/8 → 51/16 | note:Bb3 ]", + "[ 51/16 → 13/4 | note:C4 ]", + "[ 13/4 → 53/16 | note:D4 ]", + "[ 53/16 → 27/8 | note:E4 ]", + "[ 27/8 → 55/16 | note:G4 ]", + "[ 55/16 → 7/2 | note:A4 ]", + "[ 7/2 → 57/16 | note:Bb3 ]", + "[ 57/16 → 29/8 | note:Bb3 ]", + "[ 29/8 → 59/16 | note:C4 ]", + "[ 59/16 → 15/4 | note:C4 ]", + "[ 15/4 → 61/16 | note:F3 ]", + "[ 61/16 → 31/8 | note:G3 ]", + "[ 31/8 → 63/16 | note:G3 ]", + "[ 63/16 → 4/1 | note:A3 ]", +] +`; exports[`runs examples > example "binary" example index 0 1`] = ` [ From df923174ede5ba5df5d54db5a22d90ff05778e1f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 3 Jun 2025 01:22:37 +0100 Subject: [PATCH 117/538] working --- packages/core/pattern.mjs | 19 ++++++++++++ test/__snapshots__/examples.test.mjs.snap | 37 +++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 56a6b77d1..efbae1f3e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3192,6 +3192,25 @@ export const slice = register( false, // turns off auto-patternification ); +/** + * + * make something happen on event time + * uses browser timeout which is innacurate for audio tasks + * @name onTriggerTime + * @memberof Pattern + * @returns Pattern + * @example + * s("bd!8").onTriggerTime((hap) => {console.info(hap)}) + */ +Pattern.prototype.onTriggerTime = function (func) { + return this.onTrigger((t_deprecate, hap, currentTime, cps = 1, targetTime) => { + const diff = targetTime - currentTime; + window.setTimeout(() => { + func(hap); + }, diff * 1000); + }, false); +}; + /** * Works the same as slice, but changes the playback speed of each slice to match the duration of its step. * @name splice diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 34914664d..2ee135b9b 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -5950,6 +5950,43 @@ exports[`runs examples > example "often" example index 0 1`] = ` ] `; +exports[`runs examples > example "onTriggerTime" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:bd ]", + "[ 1/8 → 1/4 | s:bd ]", + "[ 1/4 → 3/8 | s:bd ]", + "[ 3/8 → 1/2 | s:bd ]", + "[ 1/2 → 5/8 | s:bd ]", + "[ 5/8 → 3/4 | s:bd ]", + "[ 3/4 → 7/8 | s:bd ]", + "[ 7/8 → 1/1 | s:bd ]", + "[ 1/1 → 9/8 | s:bd ]", + "[ 9/8 → 5/4 | s:bd ]", + "[ 5/4 → 11/8 | s:bd ]", + "[ 11/8 → 3/2 | s:bd ]", + "[ 3/2 → 13/8 | s:bd ]", + "[ 13/8 → 7/4 | s:bd ]", + "[ 7/4 → 15/8 | s:bd ]", + "[ 15/8 → 2/1 | s:bd ]", + "[ 2/1 → 17/8 | s:bd ]", + "[ 17/8 → 9/4 | s:bd ]", + "[ 9/4 → 19/8 | s:bd ]", + "[ 19/8 → 5/2 | s:bd ]", + "[ 5/2 → 21/8 | s:bd ]", + "[ 21/8 → 11/4 | s:bd ]", + "[ 11/4 → 23/8 | s:bd ]", + "[ 23/8 → 3/1 | s:bd ]", + "[ 3/1 → 25/8 | s:bd ]", + "[ 25/8 → 13/4 | s:bd ]", + "[ 13/4 → 27/8 | s:bd ]", + "[ 27/8 → 7/2 | s:bd ]", + "[ 7/2 → 29/8 | s:bd ]", + "[ 29/8 → 15/4 | s:bd ]", + "[ 15/4 → 31/8 | s:bd ]", + "[ 31/8 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "orbit" example index 0 1`] = ` [ "[ 0/1 → 1/6 | s:hh delay:0.5 delaytime:0.25 orbit:1 ]", From 5129fa667791abb0498f189a0ea3c7c506eac61c Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 4 Jun 2025 00:48:32 +0100 Subject: [PATCH 118/538] 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 119/538] 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 120/538] 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 121/538] 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 122/538] 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 123/538] 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 124/538] 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 125/538] 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 126/538] 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 127/538] 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 128/538] 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 129/538] 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 130/538] 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 131/538] 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 132/538] 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 133/538] 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 134/538] 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 135/538] 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 136/538] 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 137/538] 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 138/538] 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 139/538] 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 140/538] 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 141/538] 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 142/538] 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 143/538] 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 144/538] 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 145/538] 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 146/538] 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 147/538] 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 148/538] 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 149/538] 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 150/538] 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 151/538] 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 152/538] 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 ae15bb72740000bd1ac563cf4a0f96d2e9c73d1f Mon Sep 17 00:00:00 2001 From: Alice Wyan <121136+wyan@users.noreply.github.com> Date: Mon, 9 Jun 2025 15:03:41 +0200 Subject: [PATCH 153/538] Add browser cache explanation in samples.mdx Several users (including me) have been bitten by this, so it's perhaps worth mentioning in the docs that the strudel.json file gets cached by the browser and doesn't reload properly unless forced. --- website/src/pages/learn/samples.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index 890de3aae..88a0a3b29 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -163,6 +163,12 @@ The last section could be written as: } ``` +Please note that browsers will often cache `strudel.json` on first load, and keep using the cached +version even if the orginal has been updated. If this bites you (for example while developing a new +sample pack), you can force the browser to download a new copy by i.e. changing capitalization of one +character in the URL, or by removing 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). + ## Github Shortcut Because loading samples from github is common, there is a shortcut: From aeaed50446e6f31e0a722ed3f19b8bd99e25efbb Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 9 Jun 2025 15:18:47 +0200 Subject: [PATCH 154/538] 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 155/538] 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 156/538] 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 117cf7e18aa3e481ffde92964fc41df3439d83c7 Mon Sep 17 00:00:00 2001 From: Alice Wyan <121136+wyan@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:45:21 +0200 Subject: [PATCH 157/538] Update samples.mdx --- website/src/pages/learn/samples.mdx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index 88a0a3b29..bc6627d29 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -166,8 +166,15 @@ The last section could be written as: Please note that browsers will often cache `strudel.json` on first load, and keep using the cached version even if the orginal has been updated. If this bites you (for example while developing a new sample pack), you can force the browser to download a new copy by i.e. changing capitalization of one -character in the URL, or by removing 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). +character in the URL, or adding a URL attribute, such as: +```javascript +samples('https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/strudel.json?version=2') +``` +that gets ignored by GitHub (but changes the URL, forcing the browser to reload every time we increase +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). ## Github Shortcut From c9f494d8657e5990ded89a02dbaf8ab80caa1e40 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 9 Jun 2025 22:10:09 +0200 Subject: [PATCH 158/538] 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 159/538] 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 160/538] 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 161/538] 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 1b7fbecf50c8385497005cd8c8df36bc004f8f0e Mon Sep 17 00:00:00 2001 From: Alice Wyan <121136+wyan@users.noreply.github.com> Date: Tue, 10 Jun 2025 13:20:21 +0200 Subject: [PATCH 162/538] Fix style issue --- website/src/pages/learn/samples.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index bc6627d29..daaecd06c 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -167,9 +167,11 @@ Please note that browsers will often cache `strudel.json` on first load, and kee version even if the orginal has been updated. If this bites you (for example while developing a new sample pack), you can force the browser to download a new copy by i.e. changing capitalization of one character in the URL, or adding a URL attribute, such as: + ```javascript -samples('https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/strudel.json?version=2') +samples('https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/strudel.json?version=2'); ``` + that gets ignored by GitHub (but changes the URL, forcing the browser to reload every time we increase the version number). From 43504d16b66e739590ec80c4fd18e10f54c31802 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 10 Jun 2025 16:03:42 +0200 Subject: [PATCH 163/538] 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 164/538] 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 165/538] 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 166/538] 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 167/538] 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 2c9f9d785f13e76d5c7304372d0e8c9f4df615d1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 08:39:27 +0200 Subject: [PATCH 168/538] hotfix: close bakery --- website/src/config.ts | 2 +- website/src/repl/components/Header.jsx | 8 ++-- .../src/repl/components/panel/PatternsTab.jsx | 37 +++++++++++-------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/website/src/config.ts b/website/src/config.ts index 2f499d55f..e88b7f3e1 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -58,7 +58,7 @@ export const SIDEBAR: Sidebar = { { text: 'What is Strudel?', link: 'workshop/getting-started' }, { text: 'Showcase', link: 'intro/showcase' }, { text: 'Blog', link: 'blog' }, - { text: 'Community Bakery', link: 'bakery' }, + /* { text: 'Community Bakery', link: 'bakery' }, */ ], Workshop: [ // { text: 'Getting Started', link: 'workshop/getting-started' }, diff --git a/website/src/repl/components/Header.jsx b/website/src/repl/components/Header.jsx index 2963a5f97..a97e2bed0 100644 --- a/website/src/repl/components/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -93,7 +93,7 @@ export function Header({ context, embedded = false }) { > {!isEmbedded && update} - {!isEmbedded && ( + {/* !isEmbedded && ( - )} - {!isEmbedded && ( + ) */} + {/* !isEmbedded && ( - )} + ) */} {!isEmbedded && (
- {patternFilter === patternFilterName.user && ( - - updateCodeWindow( - context, - { ...userPatterns[id], collection: userPattern.collection }, - autoResetPatternOnChange, - ) - } - patterns={userPatterns} - started={context.started} - activePattern={activePattern} - viewingPatternID={viewingPatternID} - /> - )} + {/* {patternFilter === patternFilterName.user && ( */} + + updateCodeWindow( + context, + { ...userPatterns[id], collection: userPattern.collection }, + autoResetPatternOnChange, + ) + } + patterns={userPatterns} + started={context.started} + activePattern={activePattern} + viewingPatternID={viewingPatternID} + /> + {/* )} */}
); @@ -250,6 +250,11 @@ export function PatternsTab({ context }) { const { patternFilter } = useSettings(); return ( +
+ +
+ ); + /* return (
)}
- ); + ); */ } From 79f7938b3381b426e85b67ec163abaee5423ed92 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 08:51:03 +0200 Subject: [PATCH 169/538] hotfix: add default code --- website/src/repl/useReplContext.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index f88e5a6e2..708506cc5 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -59,6 +59,8 @@ async function getModule(name) { return modules.find((m) => m.packageName === name); } +const initialCode = `$: s("[bd ]*2").bank("tr909").dec(.4)`; + export function useReplContext() { const { isSyncEnabled, audioEngineTarget } = useSettings(); const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc; @@ -77,7 +79,7 @@ export function useReplContext() { transpiler, autodraw: false, root: containerRef.current, - initialCode: '// LOADING', + initialCode, pattern: silence, drawTime, drawContext, From 40ba5d44652d18dc84bcc4d9f9d542f18d573551 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 09:11:19 +0200 Subject: [PATCH 170/538] hotfix: don't load patterns from db --- website/src/repl/util.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index f49dd568f..9db280dad 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -7,7 +7,7 @@ import './Repl.css'; import { createClient } from '@supabase/supabase-js'; import { nanoid } from 'nanoid'; import { writeText } from '@tauri-apps/plugin-clipboard-manager'; -import { $featuredPatterns, loadDBPatterns } from '@src/user_pattern_utils.mjs'; +import { $featuredPatterns /* , loadDBPatterns */ } from '@src/user_pattern_utils.mjs'; // Create a single supabase client for interacting with your database export const supabase = createClient( @@ -16,9 +16,9 @@ export const supabase = createClient( ); let dbLoaded; -if (typeof window !== 'undefined') { +/* if (typeof window !== 'undefined') { dbLoaded = loadDBPatterns(); -} +} */ export async function initCode() { // load code from url hash (either short hash from database or decode long hash) From ef3b9a68e362218bef0a79f0954fad5fa6a38aae Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 09:21:57 +0200 Subject: [PATCH 171/538] hotfix: leave out part about shuffle in welcome tab --- website/src/repl/components/panel/WelcomeTab.jsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/src/repl/components/panel/WelcomeTab.jsx b/website/src/repl/components/panel/WelcomeTab.jsx index 1049a581d..a43f02ad4 100644 --- a/website/src/repl/components/panel/WelcomeTab.jsx +++ b/website/src/repl/components/panel/WelcomeTab.jsx @@ -15,17 +15,18 @@ export function WelcomeTab({ context }) {
1. hit play - 2. change something -{' '} 3. hit update -
- If you don't like what you hear, try shuffle! + {/*
+ If you don't like what you hear, try shuffle! */}

- To learn more about what this all means, check out the{' '} + {/* To learn more about what this all means, check out the{' '} */} + To get started, check out the interactive tutorial . Also feel free to join the{' '} - tidalcycles discord channel + discord channel {' '} to ask any questions, give feedback or just say hello.

From 694a22af85494444c3d07c3c48da48d20afbe390 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 09:51:04 +0200 Subject: [PATCH 172/538] hotfix: bring back loading code + fill in default pattern later --- website/src/repl/useReplContext.jsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 708506cc5..36e8099cb 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -59,7 +59,7 @@ async function getModule(name) { return modules.find((m) => m.packageName === name); } -const initialCode = `$: s("[bd ]*2").bank("tr909").dec(.4)`; +const initialCode = `// LOADING`; export function useReplContext() { const { isSyncEnabled, audioEngineTarget } = useSettings(); @@ -135,9 +135,10 @@ export function useReplContext() { code = latestCode; msg = `Your last session has been loaded!`; } else { - const { code: randomTune, name } = await getRandomTune(); - code = randomTune; - msg = `A random code snippet named "${name}" has been loaded!`; + /* const { code: randomTune, name } = await getRandomTune(); + code = randomTune; */ + code = '$: s("[bd ]*2").bank("tr909").dec(.4)'; + msg = `Default code has been loaded`; } editor.setCode(code); setDocumentTitle(code); From 8363ba5a41ca3210d6cd4b436ead3692d7d29d3c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 12 Jun 2025 10:22:35 +0200 Subject: [PATCH 173/538] 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 174/538] 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 372542dcd86f7f201d6b187c875be4b2032f972a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 12 Jun 2025 10:23:21 +0200 Subject: [PATCH 175/538] working --- website/src/repl/components/Header.jsx | 4 +- website/src/repl/util.mjs | 76 ++++++++++++++++---------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/website/src/repl/components/Header.jsx b/website/src/repl/components/Header.jsx index a97e2bed0..ca4e1ecf9 100644 --- a/website/src/repl/components/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -102,7 +102,7 @@ export function Header({ context, embedded = false }) { shuffle ) */} - {/* !isEmbedded && ( + {!isEmbedded && ( - ) */} + )} {!isEmbedded && ( { - const hash = nanoid(12); - const shareUrl = window.location.origin + window.location.pathname + '?' + hash; - const { error } = await supabase.from('code_v1').insert([{ code: codeToShare, hash, ['public']: isPublic }]); - if (!error) { - lastShared = codeToShare; - // copy shareUrl to clipboard - if (isTauri()) { - await writeText(shareUrl); - } else { - await navigator.clipboard.writeText(shareUrl); - } - const message = `Link copied to clipboard: ${shareUrl}`; - alert(message); - // alert(message); - logger(message, 'highlight'); +//RIP due to SPAM +// export async function shareCode(codeToShare) { +// // const codeToShare = activeCode || code; +// if (lastShared === codeToShare) { +// logger(`Link already generated!`, 'error'); +// return; +// } + +// confirmDialog( +// 'Do you want your pattern to be public? If no, press cancel and you will get just a private link.', +// ).then(async (isPublic) => { +// const hash = nanoid(12); +// const shareUrl = window.location.origin + window.location.pathname + '?' + hash; +// const { error } = await supabase.from('code_v1').insert([{ code: codeToShare, hash, ['public']: isPublic }]); +// if (!error) { +// lastShared = codeToShare; +// // copy shareUrl to clipboard +// if (isTauri()) { +// await writeText(shareUrl); +// } else { +// await navigator.clipboard.writeText(shareUrl); +// } +// const message = `Link copied to clipboard: ${shareUrl}`; +// alert(message); +// // alert(message); +// logger(message, 'highlight'); +// } else { +// console.log('error', error); +// const message = `Error: ${error.message}`; +// // alert(message); +// logger(message); +// } +// }); +// } + +export async function shareCode() { + try { + const shareUrl = window.location.href; + if (isTauri()) { + await writeText(shareUrl); } else { - console.log('error', error); - const message = `Error: ${error.message}`; - // alert(message); - logger(message); + await navigator.clipboard.writeText(shareUrl); } - }); + const message = `Link copied to clipboard!`; + alert(message); + logger(message, 'highlight'); + } catch (e) { + console.error(e); + } } export const isIframe = () => window.location !== window.parent.location; From 94746b5fae73f34b287ca1c9ae20e65a7ff0202f Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 12 Jun 2025 13:37:58 +0100 Subject: [PATCH 176/538] Update README.md --- README.md | 53 +---------------------------------------------------- 1 file changed, 1 insertion(+), 52 deletions(-) diff --git a/README.md b/README.md index 12ee85035..0d576ac6d 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,3 @@ # strudel -[![Strudel test status](https://github.com/tidalcycles/strudel/actions/workflows/test.yml/badge.svg)](https://github.com/tidalcycles/strudel/actions) [![DOI](https://zenodo.org/badge/450927247.svg)](https://doi.org/10.5281/zenodo.6659278) - -An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is a bit more stable now, but please continue to tread carefully. - -- Try it here: -- Docs: -- Technical Blog Post: -- 1 Year of Strudel Blog Post: -- 2 Years of Strudel Blog Post: - -## Running Locally - -After cloning the project, you can run the REPL locally: - -1. Install [Node.js](https://nodejs.org/) -2. Install [pnpm](https://pnpm.io/installation) -3. Install dependencies by running the following command: - ```bash - pnpm i - ``` -4. Run the development server: - ```bash - pnpm dev - ``` - -## Using Strudel In Your Project - -This project is organized into many [packages](./packages), which are also available on [npm](https://www.npmjs.com/search?q=%40strudel). - -Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start). - -You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE.md). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details. - -Licensing info for the default sound banks can be found over on the [dough-samples](https://github.com/felixroos/dough-samples/blob/main/README.md) repository. - -## Contributing - -There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). - - - - - -Made with [contrib.rocks](https://contrib.rocks). - -## Community - -There is a #strudel channel on the TidalCycles discord: - -You can also ask questions and find related discussions on the tidal club forum: - -The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project. +Moving to https://codeberg.org/uzu/strudel From 84efa664caa4c78a0675326d6f76894d01e3310c Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 12 Jun 2025 13:39:00 +0100 Subject: [PATCH 177/538] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d576ac6d..792245236 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ # strudel -Moving to https://codeberg.org/uzu/strudel +Live coding patterns on the web +https://strudel.cc/ + +Development is moving to https://codeberg.org/uzu/strudel + +Please update your bookmarks. From b6e67f34743fee69a92b0d98ba9996d5409d1821 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 12 Jun 2025 14:12:42 +0100 Subject: [PATCH 178/538] > codeberg --- CONTRIBUTING.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d8170cfc..618830b9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,11 +2,14 @@ Thanks for wanting to contribute!!! There are many ways you can add value to this project +## Move to codeberg + +We are currently in the process of moving from github to codeberg -- not everything is working, please bear with us. + ## Communication Channels To get in touch with the contributors, either -- open a [github discussion](https://github.com/tidalcycles/strudel/discussions) or - [join the Tidal Discord Channel](https://discord.gg/remJ6gQA) and go to the #strudel channel - Find related discussions on the [tidal club forum](https://club.tidalcycles.org/) @@ -32,7 +35,7 @@ Use one of the Communication Channels listed above. ## Improve the Docs If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/), -you can edit each file directly on github via the "Edit this page" link located in the right sidebar. +you can edit each file directly on codeburg. (we are currently fixing the "Edit this page" links in the right sidebar) ## Propose a Feature @@ -41,7 +44,7 @@ Maybe you even want to help with the implementation of that feature! ## Report a Bug -If you've found a bug, or some behaviour that does not seem right, you are welcome to file an [issue](https://github.com/tidalcycles/strudel/issues). +If you've found a bug, or some behaviour that does not seem right, you are welcome to file an [issue](https://codeberg.org/uzu/strudel/issues). Please check that it has not been reported before. ## Fix a Bug @@ -71,7 +74,7 @@ To get the project up and running for development, make sure you have installed: then, do the following: ```sh -git clone https://github.com/tidalcycles/strudel.git && cd strudel +git clone https://codeberg.org/uzu/strudel.git && cd strudel pnpm i # install at root to symlink packages pnpm start # start repl ``` @@ -113,7 +116,7 @@ You can run the same check with `pnpm check` ## Package Workflow -The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning. +The project is split into multiple [packages](https://codeberg.org/uzu/strudel/src/branch/main/packages) with independent versioning. When you run `pnpm i` on the root folder, [pnpm workspaces](https://pnpm.io/workspaces) will install all dependencies of all subpackages. This will allow any js file to import `@strudel/` to get the local version, allowing to develop multiple packages at the same time. From 580447d00af888513357b2a8f8ee74acace1fbb6 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 12 Jun 2025 14:15:07 +0100 Subject: [PATCH 179/538] move technical manual from https://github.com/tidalcycles/strudel/wiki/Technical-Manual --- technical.manual.md | 193 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 technical.manual.md diff --git a/technical.manual.md b/technical.manual.md new file mode 100644 index 000000000..73180b145 --- /dev/null +++ b/technical.manual.md @@ -0,0 +1,193 @@ +This document introduces you to Strudel in a technical sense. If you just want to *use* Strudel, have a look at the [Tutorial](https://strudel.tidalcycles.org/tutorial/). + +## Strudel Packages + +There are different packages for different purposes. They.. + +- split up the code into smaller chunks +- can be selectively used to implement some sort of time based system + +Please refer to the individual README files in the [packages folder](https://github.com/tidalcycles/strudel/tree/main/packages) + +## REPL + +The [REPL](https://strudel.tidalcycles.org/) 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. + +More info in the [REPL README](https://github.com/tidalcycles/strudel/tree/main/repl) + +# High Level Overview + + + +## 1. End User Code + +The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://github.com/tidalcycles/strudel/tree/main/packages/eval#strudelcycleseval) evaluates the user code +after a transpilation step, which resolves the syntax sugar. If you don't want the syntax sugar, you can omit the eval package and call the native javascript `eval` instead. + +### 🍭 Syntax Sugar + +JavaScript Transpilation = converting valid JavaScript to valid JavaScript: + +```js +"c3 [e3 g3]".fast(2) +``` + +becomes + +```js +mini('c3 [e3 g3]') + .withMiniLocation([1, 0, 0], [1, 11, 11]) // source location + .fast(2); +``` + +- double quoted strings and backtick strings are turned into `mini` calls (single quoted strings are left as is) +- The source location is added by chaining `withMiniLocation`, which enables the real time highlighting later +- (psuedo) variable names that look like notes (like `c4`, `bb2` or `fs3`) are turned into strings +- support for top level await +- operator overloading could be implemented in the future + +This is how it works: + + + +- The user code is parsed with a [shift parser](https://github.com/shapesecurity/shift-parser-js), generating an AST +- The AST is transformed to resolve the syntax sugar +- The AST is used to generate code again (shift-codegen) + +Shift will most likely be replaced with acorn in the future, see https://github.com/tidalcycles/strudel/issues/174 + +### Mini Notation + +Another important part of the user code is the mini notation, which allows to express rhythms in a short manner. + +- the mini notation is [implemented as a PEG grammar](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), living in the [mini package](https://github.com/tidalcycles/strudel/tree/main/packages/mini) +- it is based on [krill](https://github.com/Mdashdotdashn/krill) by Mdashdotdashn +- the peg grammar is used to generate a parser with [peggyjs](https://peggyjs.org/) +- the generated parser takes a mini notation string and outputs an AST +- the AST can then be used to construct a pattern using the regular Strudel API + +Here's an example AST: + +```json +{ + "type_": "pattern", + "arguments_": { "alignment": "h" }, + "source_": [ + { + "type_": "element", "source_": "c3", + "location_": { "start": { "offset": 1, "line": 1, "column": 2 }, "end": { "offset": 4, "line": 1, "column": 5 } } + }, + { + "type_": "element", + "location_": { "start": { "offset": 4, "line": 1, "column": 5 }, "end": { "offset": 11, "line": 1, "column": 12 } } + "source_": { + "type_": "pattern", "arguments_": { "alignment": "h" }, + "source_": [ + { + "type_": "element", "source_": "e3", + "location_": { "start": { "offset": 5, "line": 1, "column": 6 }, "end": { "offset": 8, "line": 1, "column": 9 } } + }, + { + "type_": "element", "source_": "g3", + "location_": { "start": { "offset": 8, "line": 1, "column": 9 }, "end": { "offset": 10, "line": 1, "column": 11 } } + } + ] + }, + } + ] +} +``` + +which translates to `seq(c3, seq(e3, g3))` + +## 2. Querying & Scheduling + +When the user code has been evaluated, we hopefully get a Pattern instance, which we can use to query events from. +These events can then be used to trigger side effects in the real world. On that note, Events are mostly called Hap(s) in the codebase, because JS already has a built in `Event` class. + +### Querying + +> Querying = Asking a Pattern for Events within a certain time span + +```js +seq('c3', ['e3', 'g3']) // <--- Pattern + .queryArc(0, 2) // query events within 0 and 2 cycles + .map((hap) => hap.showWhole()); // make readable +``` + +yields + +```js +[ + '0/1 -> 1/2: c3', // cycle 0 + '1/2 -> 3/4: e3', + '3/4 -> 1/1: g3', + '1/1 -> 3/2: c3', // cycle 1 + '3/2 -> 7/4: e3', + '7/4 -> 2/1: g3', +]; +``` + +### 🗓️ Scheduling + +The scheduler will query events repeatedly, creating a possibly endless loop of time slices. +Here is a simplified example of how it works + +```js +let step = 0.5; // query interval in seconds +let tick = 0; // how many intervals have passed +let pattern = seq('c3', ['e3', 'g3']); // pattern from user +setInterval(() => { + const events = pattern.queryArc(tick * step, ++tick * step); + events.forEach((event) => { + console.log(event.showWhole()); + const o = getAudioContext().createOscillator(); + o.frequency.value = getFreq(event.value); + o.start(event.whole.begin); + o.stop(event.whole.begin + event.duration); + o.connect(getAudioContext().destination); + }); +}, step * 1000); // query each "step" seconds +``` + +## 3. Sound Output + +The third and last step is to use the scheduled events to make sound. +Patterns are wrapped with param functions to compose different properties of the sound. + +```js +note("[c2(3,8) [ bb1]]") // sets frequency + .s("") // sound source + .gain(.5) // turn down volume + .cutoff(sine.range(200,1000).slow(4)) // modulated cutoff + .slow(2) + .out().logValues()`, + ]} +/> +``` + +Here is an example Hap value with different properties: + +```js +{ note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 } +``` + + +
+ +- Patterns represent just values in time! +- Suitable for any time based output (music, visuals, movement, .. ?) + +### Supported Outputs + +At the time of writing this doc, the following outputs are supported: + +- Web Audio API `.out()` see [/webaudio](https://github.com/tidalcycles/strudel/tree/main/packages/webaudio) +- MIDI `.midi()` see [/midi](https://github.com/tidalcycles/strudel/tree/main/packages/midi) +- OSC `.osc()` see [/osc](https://github.com/tidalcycles/strudel/tree/main/packages/osc) +- Serial `.serial()` see [/serial](https://github.com/tidalcycles/strudel/tree/main/packages/serial) +- Tone.js `.tone()` (deprecated?) [/tone](https://github.com/tidalcycles/strudel/tree/main/packages/tone) +- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://github.com/tidalcycles/strudel/tree/main/packages/webdirt) +- Speech `.speak()` (experimental) part of [/core](https://github.com/tidalcycles/strudel/tree/main/packages/core) + +These could change, so make sure to check the [packages folder](https://github.com/tidalcycles/strudel/tree/main/packages). From 5fdea7fd8002ae7454bccb88e81e1689e26f55bb Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 12 Jun 2025 14:36:45 +0100 Subject: [PATCH 180/538] github > strudel --- packages/README.md | 2 +- packages/codemirror/package.json | 6 +++--- packages/core/clockworker.js | 2 +- packages/core/controls.mjs | 12 ++++++------ packages/core/cyclist.mjs | 6 +++--- packages/core/drawLine.mjs | 2 +- packages/core/euclid.mjs | 2 +- packages/core/evaluate.mjs | 2 +- packages/core/fraction.mjs | 2 +- packages/core/hap.mjs | 2 +- packages/core/index.mjs | 2 +- packages/core/neocyclist.mjs | 2 +- packages/core/package.json | 4 ++-- packages/core/pattern.mjs | 2 +- packages/core/pick.mjs | 2 +- packages/core/repl.mjs | 2 +- packages/core/signal.mjs | 2 +- packages/core/speak.mjs | 2 +- packages/core/state.mjs | 2 +- packages/core/timespan.mjs | 2 +- packages/core/ui.mjs | 2 +- packages/core/util.mjs | 2 +- packages/core/value.mjs | 4 ++-- packages/csound/package.json | 6 +++--- packages/desktopbridge/index.mjs | 2 +- packages/desktopbridge/package.json | 6 +++--- packages/draw/draw.mjs | 2 +- packages/draw/package.json | 6 +++--- packages/draw/pianoroll.mjs | 2 +- packages/embed/package.json | 6 +++--- packages/gamepad/package.json | 6 +++--- packages/hs2js/package.json | 6 +++--- packages/hydra/package.json | 6 +++--- packages/midi/midi.mjs | 2 +- packages/midi/package.json | 6 +++--- packages/mini/krill.pegjs | 2 +- packages/mini/mini.mjs | 2 +- packages/mini/package.json | 6 +++--- packages/motion/package.json | 6 +++--- packages/mqtt/mqtt.mjs | 2 +- packages/mqtt/package.json | 6 +++--- packages/osc/osc.mjs | 2 +- packages/osc/package.json | 6 +++--- packages/osc/server.js | 2 +- packages/osc/tidal-sniffer.js | 2 +- packages/reference/package.json | 6 +++--- packages/repl/package.json | 6 +++--- packages/serial/package.json | 6 +++--- packages/serial/serial.mjs | 2 +- packages/soundfonts/package.json | 6 +++--- packages/superdough/index.mjs | 2 +- packages/superdough/package.json | 6 +++--- packages/superdough/superdough.mjs | 2 +- packages/tidal/package.json | 6 +++--- packages/tonal/package.json | 6 +++--- packages/tonal/tonal.mjs | 2 +- packages/tonal/tonleiter.mjs | 2 +- packages/tonal/voicings.mjs | 2 +- packages/transpiler/package.json | 6 +++--- packages/transpiler/transpiler.mjs | 2 +- packages/web/package.json | 6 +++--- packages/webaudio/index.mjs | 2 +- packages/webaudio/package.json | 6 +++--- packages/webaudio/webaudio.mjs | 4 ++-- packages/xen/package.json | 6 +++--- packages/xen/tune.mjs | 2 +- packages/xen/tunejs.js | 2 +- packages/xen/xen.mjs | 2 +- 68 files changed, 126 insertions(+), 126 deletions(-) diff --git a/packages/README.md b/packages/README.md index 7a7681b67..a5b93d321 100644 --- a/packages/README.md +++ b/packages/README.md @@ -2,4 +2,4 @@ Each folder represents one of the @strudel/* packages [published to npm](https://www.npmjs.com/org/strudel). -To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs. +To understand how those pieces connect, refer to the [Technical Manual](https://codeberg.org/uzu/strudel/src/branch/main/technical-manual.md) or the individual READMEs. diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 5036cd8a3..1ffe1724c 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -13,7 +13,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -28,9 +28,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@codemirror/autocomplete": "^6.18.4", "@codemirror/commands": "^6.8.0", diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index bcaf28725..77a45362e 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -113,7 +113,7 @@ self.onconnect = function (e) { port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. }; -// used to consistently schedule events, for use in a service worker - see +// used to consistently schedule events, for use in a service worker - see function createClock( getTime, callback, // called slightly before each cycle diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 21c183da9..058f4536a 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1,6 +1,6 @@ /* controls.mjs - Registers audio controls for pattern manipulation and effects. -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ @@ -96,7 +96,7 @@ export const { source, src } = registerControl('source', 'src'); * @example * s("bd sd [~ bd] sd,hh*6").n("<0 1>") */ -// also see https://github.com/tidalcycles/strudel/pull/63 +// also see https://codeberg.org/uzu/strudel/pulls/63 export const { n } = registerControl('n'); /** * Plays the given note name or midi number. A note name consists of @@ -348,7 +348,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], ' * s("bd sd [~ bd] sd").bpf(500).bpq("<0 1 2 3>") * */ -// currently an alias of 'bandq' https://github.com/tidalcycles/strudel/issues/496 +// currently an alias of 'bandq' https://codeberg.org/uzu/strudel/issues/496 // ['bpq'], export const { bandq, bpq } = registerControl('bandq', 'bpq'); /** @@ -855,7 +855,7 @@ export const { fanchor } = registerControl('fanchor'); * s("bd sd [~ bd] sd,hh*8").hpf("<2000 2000:25>") * */ -// currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496 +// currently an alias of 'hcutoff' https://codeberg.org/uzu/strudel/issues/496 // ['hpf'], /** * Applies a vibrato to the frequency of the oscillator. @@ -922,7 +922,7 @@ export const { hresonance, hpq } = registerControl('hresonance', 'hpq'); * s("bd sd [~ bd] sd,hh*8").lpf(2000).lpq("<0 10 20 30>") * */ -// currently an alias of 'resonance' https://github.com/tidalcycles/strudel/issues/496 +// currently an alias of 'resonance' https://codeberg.org/uzu/strudel/issues/496 export const { resonance, lpq } = registerControl('resonance', 'lpq'); /** * DJ filter, below 0.5 is low pass filter, above is high pass filter. @@ -1288,7 +1288,7 @@ export const { semitone } = registerControl('semitone'); // TODO: synth param export const { voice } = registerControl('voice'); -// voicings // https://github.com/tidalcycles/strudel/issues/506 +// voicings // https://codeberg.org/uzu/strudel/issues/506 // chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings export const { chord } = registerControl('chord'); // which dictionary to use for the voicings diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index bd2db1223..f28dc604c 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -1,6 +1,6 @@ /* -cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see -Copyright (C) 2022 Strudel contributors - see +cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see +Copyright (C) 2022 Strudel contributors - see 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 . */ @@ -65,7 +65,7 @@ export class Cyclist { (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency; const duration = hap.duration / this.cps; // the following line is dumb and only here for backwards compatibility - // see https://github.com/tidalcycles/strudel/pull/1004 + // see https://codeberg.org/uzu/strudel/pulls/1004 const deadline = targetTime - phase; onTrigger?.(hap, deadline, duration, this.cps, targetTime); if (hap.value.cps !== undefined && this.cps != hap.value.cps) { diff --git a/packages/core/drawLine.mjs b/packages/core/drawLine.mjs index 91b86b4ab..7509c0f6f 100644 --- a/packages/core/drawLine.mjs +++ b/packages/core/drawLine.mjs @@ -1,6 +1,6 @@ /* drawLine.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index ad0b01486..1a5be78b4 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -2,7 +2,7 @@ euclid.mjs - Bjorklund/Euclidean/Diaspora rhythms Copyright (C) 2023 Rohan Drape and strudel contributors -See for authors of this file. +See for authors of this file. The Bjorklund algorithm implementation is ported from the Haskell Music Theory Haskell module by Rohan Drape - https://rohandrape.net/?t=hmt diff --git a/packages/core/evaluate.mjs b/packages/core/evaluate.mjs index e3e73d596..0559a93a2 100644 --- a/packages/core/evaluate.mjs +++ b/packages/core/evaluate.mjs @@ -1,6 +1,6 @@ /* evaluate.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/core/fraction.mjs b/packages/core/fraction.mjs index dc7fe27a7..2e3bc68ea 100644 --- a/packages/core/fraction.mjs +++ b/packages/core/fraction.mjs @@ -1,6 +1,6 @@ /* fraction.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/core/hap.mjs b/packages/core/hap.mjs index 7a9e0a620..a6e3c55ad 100644 --- a/packages/core/hap.mjs +++ b/packages/core/hap.mjs @@ -1,6 +1,6 @@ /* hap.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 Fraction from './fraction.mjs'; diff --git a/packages/core/index.mjs b/packages/core/index.mjs index a10b68b09..e4daf445a 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -1,6 +1,6 @@ /* index.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index ad22cf006..5c175dc1f 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -1,6 +1,6 @@ /* neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances. -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/core/package.json b/packages/core/package.json index 89b047d2a..d6853c961 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -15,7 +15,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -27,7 +27,7 @@ "author": "Alex McLean (https://slab.org)", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, "homepage": "https://strudel.cc", "dependencies": { diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index efbae1f3e..76b02c21e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1,6 +1,6 @@ /* pattern.mjs - Core pattern representation for strudel -Copyright (C) 2025 Strudel contributors - see +Copyright (C) 2025 Strudel contributors - see 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 . */ diff --git a/packages/core/pick.mjs b/packages/core/pick.mjs index 702201fa6..206fa619b 100644 --- a/packages/core/pick.mjs +++ b/packages/core/pick.mjs @@ -1,6 +1,6 @@ /* pick.mjs - methods that use one pattern to pick events from other patterns. -Copyright (C) 2024 Strudel contributors - see +Copyright (C) 2024 Strudel contributors - see 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 . */ diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index e703909ff..7a8cbbab2 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -225,7 +225,7 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => async (hap, deadline, duration, cps, t) => { - // TODO: get rid of deadline after https://github.com/tidalcycles/strudel/pull/1004 + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 try { if (!hap.context.onTrigger || !hap.context.dominantTrigger) { await defaultOutput(hap, deadline, duration, cps, t); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 5fd83bce3..54da989a5 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -1,6 +1,6 @@ /* signal.mjs - continuous patterns -Copyright (C) 2024 Strudel contributors - see +Copyright (C) 2024 Strudel contributors - see 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 . */ diff --git a/packages/core/speak.mjs b/packages/core/speak.mjs index 6ae959544..7e548a73b 100644 --- a/packages/core/speak.mjs +++ b/packages/core/speak.mjs @@ -1,6 +1,6 @@ /* speak.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/core/state.mjs b/packages/core/state.mjs index db1f77eff..162dc7da9 100644 --- a/packages/core/state.mjs +++ b/packages/core/state.mjs @@ -1,6 +1,6 @@ /* state.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/core/timespan.mjs b/packages/core/timespan.mjs index 4cbfb999a..0dbc74fc8 100644 --- a/packages/core/timespan.mjs +++ b/packages/core/timespan.mjs @@ -1,6 +1,6 @@ /* timespan.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/core/ui.mjs b/packages/core/ui.mjs index 86ceb2863..5a2c54a30 100644 --- a/packages/core/ui.mjs +++ b/packages/core/ui.mjs @@ -1,6 +1,6 @@ /* ui.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/core/util.mjs b/packages/core/util.mjs index b7b1e8413..b81811bae 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -1,6 +1,6 @@ /* util.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/core/value.mjs b/packages/core/value.mjs index ef98bc370..9496405eb 100644 --- a/packages/core/value.mjs +++ b/packages/core/value.mjs @@ -1,6 +1,6 @@ /* value.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ @@ -9,7 +9,7 @@ import { logger } from './logger.mjs'; export function unionWithObj(a, b, func) { if (b?.value !== undefined && Object.keys(b).length === 1) { - // https://github.com/tidalcycles/strudel/issues/1026 + // https://codeberg.org/uzu/strudel/issues/1026 logger(`[warn]: Can't do arithmetic on control pattern.`); return a; } diff --git a/packages/csound/package.json b/packages/csound/package.json index f200cdaf4..837a397f9 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -28,9 +28,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@csound/browser": "6.18.7", "@strudel/core": "workspace:*", diff --git a/packages/desktopbridge/index.mjs b/packages/desktopbridge/index.mjs index 591bbe34f..ffb88783f 100644 --- a/packages/desktopbridge/index.mjs +++ b/packages/desktopbridge/index.mjs @@ -1,6 +1,6 @@ /* index.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/desktopbridge/package.json b/packages/desktopbridge/package.json index 45e89f44e..6609016e0 100644 --- a/packages/desktopbridge/package.json +++ b/packages/desktopbridge/package.json @@ -7,7 +7,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -19,11 +19,11 @@ "author": "Jade Rowland ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, "dependencies": { "@strudel/core": "workspace:*", "@tauri-apps/api": "^2.2.0" }, - "homepage": "https://github.com/tidalcycles/strudel#readme" + "homepage": "https://codeberg.org/uzu/strudel#readme" } \ No newline at end of file diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 0576c297b..c727c1d81 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -1,6 +1,6 @@ /* draw.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/draw/package.json b/packages/draw/package.json index 51750da2b..f2555a5c4 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/draw/pianoroll.mjs b/packages/draw/pianoroll.mjs index d874c9686..1cf218fa0 100644 --- a/packages/draw/pianoroll.mjs +++ b/packages/draw/pianoroll.mjs @@ -1,6 +1,6 @@ /* pianoroll.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/embed/package.json b/packages/embed/package.json index afd887887..3b88acc62 100644 --- a/packages/embed/package.json +++ b/packages/embed/package.json @@ -6,7 +6,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -18,7 +18,7 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme" + "homepage": "https://codeberg.org/uzu/strudel#readme" } diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json index 8b91a15e9..25b4a87b5 100644 --- a/packages/gamepad/package.json +++ b/packages/gamepad/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Yuta Nakayama ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/hs2js/package.json b/packages/hs2js/package.json index c650dfc83..3b93a3ede 100644 --- a/packages/hs2js/package.json +++ b/packages/hs2js/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "haskell", @@ -25,9 +25,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel", + "homepage": "https://codeberg.org/uzu/strudel/", "dependencies": { "web-tree-sitter": "^0.24.7" }, diff --git a/packages/hydra/package.json b/packages/hydra/package.json index 4375ebefc..c1294eb00 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -29,9 +29,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index ce7cdb0e2..af2dd3a62 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -1,6 +1,6 @@ /* midi.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/midi/package.json b/packages/midi/package.json index 2956b99f3..513dbe197 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/webaudio": "workspace:*", diff --git a/packages/mini/krill.pegjs b/packages/mini/krill.pegjs index a593b4167..8af82caee 100644 --- a/packages/mini/krill.pegjs +++ b/packages/mini/krill.pegjs @@ -1,6 +1,6 @@ /* krill.pegjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 6277daa91..d138d9fa0 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -1,6 +1,6 @@ /* mini.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/mini/package.json b/packages/mini/package.json index 5ae0dc242..9c96292d7 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -28,9 +28,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/motion/package.json b/packages/motion/package.json index 850a75bd1..1fa87d878 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Yuta Nakayama ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/mqtt/mqtt.mjs b/packages/mqtt/mqtt.mjs index c2322600c..aef01bd93 100644 --- a/packages/mqtt/mqtt.mjs +++ b/packages/mqtt/mqtt.mjs @@ -1,6 +1,6 @@ /* mqtt.mjs - for patterning the internet of things from strudel -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index 9b9cdaa3b..0eba694c3 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Alex McLean ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "paho-mqtt": "^1.1.0" diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index 3c7b92d4e..ac70b9e73 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -1,6 +1,6 @@ /* osc.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/osc/package.json b/packages/osc/package.json index 7e87c1e1a..62b00090a 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -32,9 +32,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "osc-js": "^2.4.1" diff --git a/packages/osc/server.js b/packages/osc/server.js index 7e0b90592..75fc5b1c0 100644 --- a/packages/osc/server.js +++ b/packages/osc/server.js @@ -1,6 +1,6 @@ /* server.js - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/osc/tidal-sniffer.js b/packages/osc/tidal-sniffer.js index 10bf0ad05..95e1fc329 100644 --- a/packages/osc/tidal-sniffer.js +++ b/packages/osc/tidal-sniffer.js @@ -1,6 +1,6 @@ /* tidal-sniffer.js - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/reference/package.json b/packages/reference/package.json index fd7edadff..289520ddd 100644 --- a/packages/reference/package.json +++ b/packages/reference/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -28,9 +28,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "devDependencies": { "vite": "^6.0.11" } diff --git a/packages/repl/package.json b/packages/repl/package.json index 5487ce443..1c10aebf7 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -29,9 +29,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/codemirror": "workspace:*", "@strudel/core": "workspace:*", diff --git a/packages/serial/package.json b/packages/serial/package.json index bf6e0dcab..df671cb1a 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "titdalcycles", @@ -25,9 +25,9 @@ "author": "Alex McLean ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/serial/serial.mjs b/packages/serial/serial.mjs index e0eeacedd..692109522 100644 --- a/packages/serial/serial.mjs +++ b/packages/serial/serial.mjs @@ -1,6 +1,6 @@ /* serial.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index bf4fa2960..d227359c7 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -13,7 +13,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -25,9 +25,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/webaudio": "workspace:*", diff --git a/packages/superdough/index.mjs b/packages/superdough/index.mjs index 3247c5b49..fd49fe338 100644 --- a/packages/superdough/index.mjs +++ b/packages/superdough/index.mjs @@ -1,6 +1,6 @@ /* index.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/superdough/package.json b/packages/superdough/package.json index a835f252d..8fc3cca78 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -28,9 +28,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "devDependencies": { "vite": "^6.0.11", "vite-plugin-bundle-audioworklet": "workspace:*" diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 1069d4e84..819cdeb37 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -1,6 +1,6 @@ /* superdough.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/tidal/package.json b/packages/tidal/package.json index 8e4871617..960b09001 100644 --- a/packages/tidal/package.json +++ b/packages/tidal/package.json @@ -6,7 +6,7 @@ "module": "tidal.mjs", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel/tree/main/packages/tidal" + "url": "git+https://codeberg.org/uzu/strudel/src/branch/main/packages/tidal" }, "keywords": [ "haskell", @@ -15,9 +15,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel/tree/main/packages/hs2js", + "homepage": "https://codeberg.org/uzu/strudel/src/branch/main/packages/hs2js", "dependencies": { "@strudel/core": "workspace:*", "@strudel/mini": "workspace:*", diff --git a/packages/tonal/package.json b/packages/tonal/package.json index 89f02b301..98b2a5325 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -26,9 +26,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@tonaljs/tonal": "^4.10.0", diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 78183d228..4fd622158 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -1,6 +1,6 @@ /* tonal.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index 15288fcc8..3814394f6 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -178,7 +178,7 @@ export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below' return notes; } -// https://github.com/tidalcycles/strudel/blob/14184993d0ee7d69c47df57ac864a1a0f99a893f/packages/tonal/tonleiter.mjs +// https://codeberg.org/uzu/strudel/blob/14184993d0ee7d69c47df57ac864a1a0f99a893f/packages/tonal/tonleiter.mjs const steps = [1, 0, 2, 0, 3, 4, 0, 5, 0, 6, 0, 7]; const notes = ['C', '', 'D', '', 'E', 'F', '', 'G', '', 'A', '', 'B']; const noteLetters = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; diff --git a/packages/tonal/voicings.mjs b/packages/tonal/voicings.mjs index 0a08575db..d81911e01 100644 --- a/packages/tonal/voicings.mjs +++ b/packages/tonal/voicings.mjs @@ -1,6 +1,6 @@ /* voicings.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 93648bbc7..1a20a78f8 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -26,9 +26,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/mini": "workspace:*", diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 2e566305f..2f5ed5309 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -194,7 +194,7 @@ function isLabelStatement(node) { } // converts label expressions to p calls: "x: y" to "y.p('x')" -// see https://github.com/tidalcycles/strudel/issues/990 +// see https://codeberg.org/uzu/strudel/issues/990 function labelToP(node) { return { type: 'ExpressionStatement', diff --git a/packages/web/package.json b/packages/web/package.json index 652496fee..6264849eb 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -29,9 +29,9 @@ ], "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/mini": "workspace:*", diff --git a/packages/webaudio/index.mjs b/packages/webaudio/index.mjs index 59672b617..362e61c44 100644 --- a/packages/webaudio/index.mjs +++ b/packages/webaudio/index.mjs @@ -1,6 +1,6 @@ /* index.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 5714fddf3..f984d5321 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -29,9 +29,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 44a683480..f80f114fd 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -1,6 +1,6 @@ /* webaudio.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ @@ -16,7 +16,7 @@ const hap2value = (hap) => { }; export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps); -// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 +// uses more precise, absolute t if available, see https://codeberg.org/uzu/strudel/pulls/1004 export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration); diff --git a/packages/xen/package.json b/packages/xen/package.json index 76fecf525..b79aea6e6 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -26,9 +26,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*" }, diff --git a/packages/xen/tune.mjs b/packages/xen/tune.mjs index feed38f4f..01303bf52 100644 --- a/packages/xen/tune.mjs +++ b/packages/xen/tune.mjs @@ -1,6 +1,6 @@ /* tune.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/xen/tunejs.js b/packages/xen/tunejs.js index 7b1a804a0..6b5e7cb7c 100644 --- a/packages/xen/tunejs.js +++ b/packages/xen/tunejs.js @@ -1,6 +1,6 @@ /* tunejs.js - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 4077632a5..cc96f4110 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -1,6 +1,6 @@ /* xen.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ From 307e5ea2d7536dcef26b2e7d453b797bd0f78e87 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 12 Jun 2025 14:45:35 +0100 Subject: [PATCH 181/538] less github --- package.json | 4 ++-- technical.manual.md | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 01c3e7d5d..be3e1f1c8 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "tidalcycles", @@ -43,7 +43,7 @@ "author": "Alex McLean (https://slab.org)", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, "homepage": "https://strudel.cc", "dependencies": { diff --git a/technical.manual.md b/technical.manual.md index 73180b145..9b09c741d 100644 --- a/technical.manual.md +++ b/technical.manual.md @@ -7,13 +7,13 @@ There are different packages for different purposes. They.. - split up the code into smaller chunks - can be selectively used to implement some sort of time based system -Please refer to the individual README files in the [packages folder](https://github.com/tidalcycles/strudel/tree/main/packages) +Please refer to the individual README files in the [packages folder](https://codeberg.org/uzu/strudel/src/branch/main/packages) ## REPL The [REPL](https://strudel.tidalcycles.org/) 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. -More info in the [REPL README](https://github.com/tidalcycles/strudel/tree/main/repl) +More info in the [REPL README](https://codeberg.org/uzu/strudel/src/branch/main/packages/repl/README.md) # High Level Overview @@ -21,7 +21,7 @@ More info in the [REPL README](https://github.com/tidalcycles/strudel/tree/main/ ## 1. End User Code -The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://github.com/tidalcycles/strudel/tree/main/packages/eval#strudelcycleseval) evaluates the user code +The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://codeberg.org/uzu/strudel/src/branch/main/packages/eval#strudelcycleseval) evaluates the user code after a transpilation step, which resolves the syntax sugar. If you don't want the syntax sugar, you can omit the eval package and call the native javascript `eval` instead. ### 🍭 Syntax Sugar @@ -60,7 +60,7 @@ Shift will most likely be replaced with acorn in the future, see https://github. Another important part of the user code is the mini notation, which allows to express rhythms in a short manner. -- the mini notation is [implemented as a PEG grammar](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), living in the [mini package](https://github.com/tidalcycles/strudel/tree/main/packages/mini) +- the mini notation is [implemented as a PEG grammar](https://codeberg.org/uzu/strudel/src/branch/main/packages/mini/krill.pegjs), living in the [mini package](https://codeberg.org/uzu/strudel/src/branch/main/packages/mini) - it is based on [krill](https://github.com/Mdashdotdashn/krill) by Mdashdotdashn - the peg grammar is used to generate a parser with [peggyjs](https://peggyjs.org/) - the generated parser takes a mini notation string and outputs an AST @@ -182,12 +182,12 @@ Here is an example Hap value with different properties: At the time of writing this doc, the following outputs are supported: -- Web Audio API `.out()` see [/webaudio](https://github.com/tidalcycles/strudel/tree/main/packages/webaudio) -- MIDI `.midi()` see [/midi](https://github.com/tidalcycles/strudel/tree/main/packages/midi) -- OSC `.osc()` see [/osc](https://github.com/tidalcycles/strudel/tree/main/packages/osc) -- Serial `.serial()` see [/serial](https://github.com/tidalcycles/strudel/tree/main/packages/serial) -- Tone.js `.tone()` (deprecated?) [/tone](https://github.com/tidalcycles/strudel/tree/main/packages/tone) -- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://github.com/tidalcycles/strudel/tree/main/packages/webdirt) -- Speech `.speak()` (experimental) part of [/core](https://github.com/tidalcycles/strudel/tree/main/packages/core) +- Web Audio API `.out()` see [/webaudio](https://codeberg.org/uzu/strudel/src/branch/main/packages/webaudio) +- MIDI `.midi()` see [/midi](https://codeberg.org/uzu/strudel/src/branch/main/packages/midi) +- OSC `.osc()` see [/osc](https://codeberg.org/uzu/strudel/src/branch/main/packages/osc) +- Serial `.serial()` see [/serial](https://codeberg.org/uzu/strudel/src/branch/main/packages/serial) +- Tone.js `.tone()` (deprecated?) [/tone](https://codeberg.org/uzu/strudel/src/branch/main/packages/tone) +- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://codeberg.org/uzu/strudel/src/branch/main/packages/webdirt) +- Speech `.speak()` (experimental) part of [/core](https://codeberg.org/uzu/strudel/src/branch/main/packages/core) -These could change, so make sure to check the [packages folder](https://github.com/tidalcycles/strudel/tree/main/packages). +These could change, so make sure to check the [packages folder](https://codeberg.org/uzu/strudel/src/branch/main/packages). From c0ae12f32f49b3386bd3e07d8ffc754545df84bd Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 12 Jun 2025 17:26:19 +0100 Subject: [PATCH 182/538] add codeberg repo update command --- CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 618830b9c..aa84cbce7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,13 @@ Thanks for wanting to contribute!!! There are many ways you can add value to thi We are currently in the process of moving from github to codeberg -- not everything is working, please bear with us. +To update your local clone, you can run this command: + +``` +git remote set-url origin git@codeberg.org:uzu/strudel.git +``` + + ## Communication Channels To get in touch with the contributors, either From 990f1c6ece10f61dca48d23b81d95c755bf99d54 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 12 Jun 2025 17:29:29 +0100 Subject: [PATCH 183/538] degithub --- README.md | 2 +- technical.manual.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 12ee85035..e225d9bde 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Licensing info for the default sound banks can be found over on the [dough-sampl There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). - + diff --git a/technical.manual.md b/technical.manual.md index 9b09c741d..58f7cd934 100644 --- a/technical.manual.md +++ b/technical.manual.md @@ -54,7 +54,7 @@ This is how it works: - The AST is transformed to resolve the syntax sugar - The AST is used to generate code again (shift-codegen) -Shift will most likely be replaced with acorn in the future, see https://github.com/tidalcycles/strudel/issues/174 +Shift will most likely be replaced with acorn in the future, see https://codeberg.org/uzu/strudel/issues/174 ### Mini Notation From a3d9d68c45fb0ccbf883acfff68b15bcbb235b7b Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 12 Jun 2025 18:00:39 +0100 Subject: [PATCH 184/538] less github --- examples/tidal-repl/package.json | 6 +++--- jsdoc/jsdoc-synonyms.js | 2 +- my-patterns/README.md | 22 +++++++++------------- packages/core/test/value.test.mjs | 2 +- packages/hs2js/README.md | 2 +- website/agpl-header.txt | 4 ++-- website/src/config.ts | 2 +- website/src/user_pattern_utils.mjs | 2 +- 8 files changed, 19 insertions(+), 23 deletions(-) diff --git a/examples/tidal-repl/package.json b/examples/tidal-repl/package.json index 4da2f086b..7c1f55c38 100644 --- a/examples/tidal-repl/package.json +++ b/examples/tidal-repl/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" + "url": "git+https://codeberg.org/uzu/strudel.git }, "keywords": [ "titdalcycles", @@ -23,9 +23,9 @@ "author": "Felix Roos ", "license": "AGPL-3.0-or-later", "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" + "url": "https://codeberg.org/uzu/strudel/issues" }, - "homepage": "https://github.com/tidalcycles/strudel#readme", + "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/web": "workspace:*", "hs2js": "workspace:*" diff --git a/jsdoc/jsdoc-synonyms.js b/jsdoc/jsdoc-synonyms.js index 09190846f..0b52420bc 100644 --- a/jsdoc/jsdoc-synonyms.js +++ b/jsdoc/jsdoc-synonyms.js @@ -1,6 +1,6 @@ /* jsdoc-synonyms.js - Add support for @synonym tag -Copyright (C) 2023 Strudel contributors - see +Copyright (C) 2023 Strudel contributors - see 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 . */ diff --git a/my-patterns/README.md b/my-patterns/README.md index c8d694ea8..5283ad81b 100644 --- a/my-patterns/README.md +++ b/my-patterns/README.md @@ -5,22 +5,24 @@ made into a pattern swatch. Example: +Please note: These instructions have not been fully tested/adapted since strudel moved to codeberg from github. PRs welcome! + ## deploy -### 1. fork the [strudel repo on github](https://github.com/tidalcycles/strudel.git) +### 1. fork the [strudel repo on codeberg](https://codeberg.org/uzu/strudel.git) -### 2. clone your fork to your machine `git clone https://github.com//strudel.git strudel && cd strudel` +### 2. clone your fork to your machine `git clone https://codeberg.org//strudel.git strudel && cd strudel` ### 3. create a separate branch like `git branch patternuary && git checkout patternuary` ### 4. save one or more .txt files in the my-patterns folder -### 5. edit `website/public/CNAME` to contain `.github.io/strudel` +### 5. edit `website/public/CNAME` to contain `.codeberg.page/strudel` -### 6. edit `website/astro.config.mjs` to use site: `https://.github.io` and base `/strudel`, like this +### 6. edit `website/astro.config.mjs` to use site: `https://.codeberg.page` and base `/strudel`, like this ```js -const site = 'https://.github.io'; +const site = 'https://.codeberg.page'; const base = '/strudel'; ``` @@ -30,15 +32,9 @@ const base = '/strudel'; git add . && git commit -m "site config" && git push --set-upstream origin ``` -### 8. deploy to github pages +### 8. deploy to codeberg pages -- go to settings -> pages and select "Github Actions" as source -- go to settings -> environments -> github-pages and press the edit button next to `main` and type in `patternuary` (under "Deployment branches") -- go to Actions -> `Build and Deploy` and click `Run workflow` with branch `patternuary` - -### 9. view your patterns at `.github.io/strudel/swatch/` - -Alternatively, github pages allows you to use a custom domain, like https://mycooldomain.org/swatch/. [See their documentation for details](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site). +### 9. view your patterns at `.codeberg.page/strudel/swatch/` ### 10. optional: automatic deployment diff --git a/packages/core/test/value.test.mjs b/packages/core/test/value.test.mjs index 87cba57d0..35d9f5e10 100644 --- a/packages/core/test/value.test.mjs +++ b/packages/core/test/value.test.mjs @@ -1,6 +1,6 @@ /* value.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2025 Strudel contributors - see 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 . */ diff --git a/packages/hs2js/README.md b/packages/hs2js/README.md index 24d9c7749..f84f9cded 100644 --- a/packages/hs2js/README.md +++ b/packages/hs2js/README.md @@ -2,7 +2,7 @@ Experimental haskell in javascript interpreter. Many haskell features are not implemented. This projects mainly exists to be able to write and interpret [Tidal Cycles](https://tidalcycles.org/) code in the browser, -as part of [Strudel](https://github.com/tidalcycles/strudel). This project could only exist thanks to [tree-sitter-haskell](https://github.com/tree-sitter/tree-sitter-haskell). +as part of [Strudel](https://codeberg.org/uzu/strudel). This project could only exist thanks to [tree-sitter-haskell](https://github.com/tree-sitter/tree-sitter-haskell). ## Installation diff --git a/website/agpl-header.txt b/website/agpl-header.txt index 6fd0c0fc9..8b7b6d631 100644 --- a/website/agpl-header.txt +++ b/website/agpl-header.txt @@ -1,10 +1,10 @@ /* Strudel - javascript-based environment for live coding algorithmic (musical) patterns -https://strudel.cc / https://github.com/tidalcycles/strudel/ +https://strudel.cc / https://codeberg.org/uzu/strudel/ Copyright (C) Strudel contributors -https://github.com/tidalcycles/strudel/graphs/contributors +https://codeberg.org/uzu/strudel/activity/contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by diff --git a/website/src/config.ts b/website/src/config.ts index e88b7f3e1..490a067b2 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -28,7 +28,7 @@ export const KNOWN_LANGUAGES = { } as const; export const KNOWN_LANGUAGE_CODES = Object.values(KNOWN_LANGUAGES); -export const GITHUB_EDIT_URL = `https://github.com/tidalcycles/strudel/tree/main/website`; +export const GITHUB_EDIT_URL = `https://codeberg.org/uzu/strudel/src/branch/main/website`; export const COMMUNITY_INVITE_URL = `https://discord.com/invite/HGEdXmRkzT`; diff --git a/website/src/user_pattern_utils.mjs b/website/src/user_pattern_utils.mjs index 18442cb4c..791c6a8f9 100644 --- a/website/src/user_pattern_utils.mjs +++ b/website/src/user_pattern_utils.mjs @@ -96,7 +96,7 @@ export async function loadDBPatterns() { } } -// reason: https://github.com/tidalcycles/strudel/issues/857 +// reason: https://codeberg.org/uzu/strudel/issues/857 const $activePattern = sessionAtom('activePattern', ''); export function setActivePattern(key) { From fec38bd5d2d0f0a0162bbc08325cecf6fcc84d31 Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 00:07:24 +0200 Subject: [PATCH 185/538] Update .github/workflows/test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 180f87521..6f2246790 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest + runs-on: toplap-runner strategy: matrix: node-version: [20] From 1141b1803b92469ef87a7be3022c7cf7a321599d Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 00:08:39 +0200 Subject: [PATCH 186/538] Update .github/workflows/test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f2246790..180f87521 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: build: - runs-on: toplap-runner + runs-on: ubuntu-latest strategy: matrix: node-version: [20] From 8c8e91417079fd90629d7067d21e19697d53fe3c Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 08:53:53 +0200 Subject: [PATCH 187/538] Update .github/workflows/test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 180f87521..498cd800b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest + runs-on: docker strategy: matrix: node-version: [20] From 062201d1dce00779f400236abd38724843571f1d Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 13 Jun 2025 08:07:35 +0100 Subject: [PATCH 188/538] fix json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index be3e1f1c8..d18291eb2 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -74,4 +74,4 @@ "vitest": "^3.0.4", "vite-plugin-bundle-audioworklet": "workspace:*" } -} +} \ No newline at end of file From dcac254790eafe8a35155da2c77595ea2ddc0b09 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 13 Jun 2025 08:17:55 +0100 Subject: [PATCH 189/538] missing double quotes --- examples/tidal-repl/package.json | 2 +- packages/codemirror/package.json | 2 +- packages/core/package.json | 2 +- packages/csound/package.json | 2 +- packages/desktopbridge/package.json | 4 ++-- packages/draw/package.json | 2 +- packages/embed/package.json | 2 +- packages/gamepad/package.json | 2 +- packages/hs2js/package.json | 2 +- packages/hydra/package.json | 2 +- packages/midi/package.json | 2 +- packages/mini/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 +- 25 files changed, 26 insertions(+), 26 deletions(-) diff --git a/examples/tidal-repl/package.json b/examples/tidal-repl/package.json index 7c1f55c38..21c8ee177 100644 --- a/examples/tidal-repl/package.json +++ b/examples/tidal-repl/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 1ffe1724c..4f8508c90 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -13,7 +13,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/core/package.json b/packages/core/package.json index d6853c961..f4170f2b5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -15,7 +15,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/csound/package.json b/packages/csound/package.json index 837a397f9..04a5ff246 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/desktopbridge/package.json b/packages/desktopbridge/package.json index 6609016e0..a01ec1f8a 100644 --- a/packages/desktopbridge/package.json +++ b/packages/desktopbridge/package.json @@ -7,7 +7,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", @@ -26,4 +26,4 @@ "@tauri-apps/api": "^2.2.0" }, "homepage": "https://codeberg.org/uzu/strudel#readme" -} \ No newline at end of file +} diff --git a/packages/draw/package.json b/packages/draw/package.json index f2555a5c4..ee1b8dd00 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", diff --git a/packages/embed/package.json b/packages/embed/package.json index 3b88acc62..a0cc33de1 100644 --- a/packages/embed/package.json +++ b/packages/embed/package.json @@ -6,7 +6,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json index 25b4a87b5..3efb2e084 100644 --- a/packages/gamepad/package.json +++ b/packages/gamepad/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", diff --git a/packages/hs2js/package.json b/packages/hs2js/package.json index 3b93a3ede..c0bf8fa8c 100644 --- a/packages/hs2js/package.json +++ b/packages/hs2js/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "haskell", diff --git a/packages/hydra/package.json b/packages/hydra/package.json index c1294eb00..b022de87d 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/midi/package.json b/packages/midi/package.json index 513dbe197..4efd329d8 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", diff --git a/packages/mini/package.json b/packages/mini/package.json index 9c96292d7..5d94301d4 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/motion/package.json b/packages/motion/package.json index 1fa87d878..a7db05680 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index 0eba694c3..f522e3354 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", diff --git a/packages/osc/package.json b/packages/osc/package.json index 62b00090a..7d19fbbfc 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/reference/package.json b/packages/reference/package.json index 289520ddd..8dc966cc2 100644 --- a/packages/reference/package.json +++ b/packages/reference/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/repl/package.json b/packages/repl/package.json index 1c10aebf7..bfa404c75 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/serial/package.json b/packages/serial/package.json index df671cb1a..c04a69cd0 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -13,7 +13,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "titdalcycles", diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index d227359c7..2c87a6e05 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -13,7 +13,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/superdough/package.json b/packages/superdough/package.json index 8fc3cca78..439b83718 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/tonal/package.json b/packages/tonal/package.json index 98b2a5325..614e86f74 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 1a20a78f8..2a5e39776 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/web/package.json b/packages/web/package.json index 6264849eb..0feddc82d 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -14,7 +14,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index f984d5321..5cc0a5538 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", diff --git a/packages/xen/package.json b/packages/xen/package.json index b79aea6e6..88c2bb082 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git + "url": "git+https://codeberg.org/uzu/strudel.git" }, "keywords": [ "tidalcycles", From 3dbae7907cba29b340aea371bdaac2ba4fdec8b1 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 13 Jun 2025 08:22:46 +0100 Subject: [PATCH 190/538] ignore .pnpm-store --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index 950e59f19..c9584aca0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,3 +13,4 @@ pnpm-workspace.yaml website/.astro !tidal-drum-machines.json !tidal-drum-machines-alias.json +.pnpm-store From d7b83e200c01820a0733dcb99bef577a34089209 Mon Sep 17 00:00:00 2001 From: Bernhard Wagner Date: Thu, 12 Jun 2025 19:32:41 +0200 Subject: [PATCH 191/538] fix issue #1368 euclidLegatoRot --- .gitignore | 1 + packages/core/euclid.mjs | 44 ++++++----- packages/core/test/euclid.test.js | 89 +++++++++++++++++++++++ packages/core/test/util.test.mjs | 65 ++++++++++++++--- packages/core/util.mjs | 2 + pnpm-workspace.yaml | 16 ++-- test/__snapshots__/examples.test.mjs.snap | 25 ++++--- test/__snapshots__/tunes.test.mjs.snap | 24 +++--- 8 files changed, 205 insertions(+), 61 deletions(-) create mode 100644 packages/core/test/euclid.test.js diff --git a/.gitignore b/.gitignore index 59d9940e1..2be3ee698 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,4 @@ fabric.properties samples/* !samples/README.md +.idea/ diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 1a5be78b4..c3bb7a6d5 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -10,9 +10,8 @@ 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 { Pattern, timeCat, register, silence } from './pattern.mjs'; +import { timeCat, register, silence } from './pattern.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs'; -import Fraction from './fraction.mjs'; const left = function (n, x) { const [ons, offs] = n; @@ -42,29 +41,26 @@ const _bjork = function (n, x) { export const bjork = function (ons, steps) { const inverted = ons < 0; - ons = Math.abs(ons); - const offs = steps - ons; - const x = Array(ons).fill([1]); - const y = Array(offs).fill([0]); - const result = _bjork([ons, offs], [x, y]); - const p = flatten(result[1][0]).concat(flatten(result[1][1])); - if (inverted) { - return p.map((x) => (x === 0 ? 1 : 0)); - } - return p; + const absOns = Math.abs(ons); + const offs = steps - absOns; + const ones = Array(absOns).fill([1]); + const zeros = Array(offs).fill([0]); + const result = _bjork([absOns, offs], [ones, zeros]); + const pattern = flatten(result[1][0]).concat(flatten(result[1][1])); + return inverted ? pattern.map((x) => 1 - x) : pattern; }; /** - * Changes the structure of the pattern to form an euclidean rhythm. - * Euclidian rhythms are rhythms obtained using the greatest common + * Changes the structure of the pattern to form an Euclidean rhythm. + * Euclidean rhythms are rhythms obtained using the greatest common * divisor of two numbers. They were described in 2004 by Godfried - * Toussaint, a canadian computer scientist. Euclidian rhythms are + * Toussaint, a Canadian computer scientist. Euclidean rhythms are * really useful for computer/algorithmic music because they can * describe a large number of rhythms with a couple of numbers. * * @memberof Pattern * @name euclid - * @param {number} pulses the number of onsets / beats + * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill * @returns Pattern * @example @@ -76,7 +72,7 @@ export const bjork = function (ons, steps) { * Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence. * @memberof Pattern * @name euclidRot - * @param {number} pulses the number of onsets / beats + * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill * @param {number} rotation offset in steps * @returns Pattern @@ -86,13 +82,13 @@ export const bjork = function (ons, steps) { */ /** - * @example // A thirteenth century Persian rhythm called Khafif-e-ramal. + * @example // A thirteenth-century Persian rhythm called Khafif-e-ramal. * note("c3").euclid(2,5) * @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad. * note("c3").euclid(3,4) * @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm. * note("c3").euclidRot(3,5,2) - * @example // A Ruchenitza rhythm used in a Bulgarian folk-dance. + * @example // A Ruchenitza rhythm used in a Bulgarian folk dance. * note("c3").euclid(3,7) * @example // The Cuban tresillo pattern. * note("c3").euclid(3,8) @@ -151,8 +147,10 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun * so there will be no gaps. * @name euclidLegato * @memberof Pattern - * @param {number} pulses the number of onsets / beats + * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill + * @param rotation offset in steps + * @param pat * @example * note("c3").euclidLegato(3,8) */ @@ -161,13 +159,13 @@ const _euclidLegato = function (pulses, steps, rotation, pat) { if (pulses < 1) { return silence; } - const bin_pat = _euclidRot(pulses, steps, rotation); + const bin_pat = _euclidRot(pulses, steps, 0); const gapless = bin_pat .join('') .split('1') .slice(1) .map((s) => [s.length + 1, true]); - return pat.struct(timeCat(...gapless)); + return pat.struct(timeCat(...gapless)).late(rotation / steps); }; export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) { @@ -180,7 +178,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps, * the resulting sequence * @name euclidLegatoRot * @memberof Pattern - * @param {number} pulses the number of onsets / beats + * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill * @param {number} rotation offset in steps * @example diff --git a/packages/core/test/euclid.test.js b/packages/core/test/euclid.test.js new file mode 100644 index 000000000..a33ec9514 --- /dev/null +++ b/packages/core/test/euclid.test.js @@ -0,0 +1,89 @@ +import { bjork } from '../euclid.mjs'; +import { describe, expect, it } from 'vitest'; +import { fastcat } from '../pattern.mjs'; + +describe('bjork', () => { + it('should apply bjorklund to ons and steps', () => { + expect(bjork(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]); + expect(bjork(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]); + expect(bjork(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]); + expect(bjork(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]); + expect(bjork(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]); + }); +}); + +describe('euclid', () => { + it('Can create euclid', () => { + expect( + fastcat('a') + .euclid(3, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/8: a', '3/8 → 1/2: a', '3/4 → 7/8: a']); + expect( + fastcat('a') + .euclid(5, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '3/8 → 1/2: a', '5/8 → 3/4: a', '3/4 → 7/8: a']); + }); +}); + +describe('euclidRot', () => { + it('Can create euclidRot', () => { + expect( + fastcat('a') + .euclidRot(3, 8, 2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '5/8 → 3/4: a']); + expect( + fastcat('a') + .euclidRot(5, 8, 2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '1/2 → 5/8: a', '5/8 → 3/4: a', '7/8 → 1/1: a']); + }); +}); + +describe('euclidLegato', () => { + it('Can create euclidLegato', () => { + expect( + fastcat('a') + .euclidLegato(3, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 3/8: a', '3/8 → 3/4: a', '3/4 → 1/1: a']); + expect( + fastcat('a') + .euclidLegato(5, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/4: a', '1/4 → 3/8: a', '3/8 → 5/8: a', '5/8 → 3/4: a', '3/4 → 1/1: a']); + }); +}); + +describe('euclidLegatoRot', () => { + it('Can create euclidLegatoRot', () => { + expect( + fastcat('a') + .euclidLegatoRot(3, 8, 2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/4: a', '1/4 → 5/8: a', '5/8 → 1/1: a']); + expect( + fastcat('a') + .euclidLegatoRot(5, 8, 2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/4: a', '1/4 → 1/2: a', '1/2 → 5/8: a', '5/8 → 7/8: a', '7/8 → 1/1: a']); + }); +}); diff --git a/packages/core/test/util.test.mjs b/packages/core/test/util.test.mjs index 6b1053a3f..a511e0fc2 100644 --- a/packages/core/test/util.test.mjs +++ b/packages/core/test/util.test.mjs @@ -6,21 +6,25 @@ This program is free software: you can redistribute it and/or modify it under th import { pure } from '../pattern.mjs'; import { - isNote, - tokenizeNote, - noteToMidi, - midiToFreq, - freqToMidi, _mod, compose, + flatten, + fractionalArgs, + freqToMidi, getFrequency, getPlayableNoteValue, - parseNumeral, - parseFractional, + isNote, + midiToFreq, + noteToMidi, numeralArgs, - fractionalArgs, + parseFractional, + parseNumeral, + rotate, + splitAt, + tokenizeNote, + zipWith, } from '../util.mjs'; -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; describe('isNote', () => { it('should recognize notes without accidentals', () => { @@ -233,3 +237,46 @@ describe('fractionalArgs', () => { expect(add('q', 2)).toBe(2.25); }); }); + +describe('rotate', () => { + it('should rotate array to the left', () => { + expect(rotate([0, 1, 2, 3], 2)).toStrictEqual([2, 3, 0, 1]); + expect(rotate([0, 1, 2, 3], 0)).toStrictEqual([0, 1, 2, 3]); + expect(rotate([0, 1, 2, 3], -3)).toStrictEqual([1, 2, 3, 0]); + expect(rotate([0, 1, 2, 3], 3)).toStrictEqual([3, 0, 1, 2]); + expect(rotate([0], 3)).toStrictEqual([0]); + expect(rotate([], 3)).toStrictEqual([]); + }); +}); + +describe('flatten', () => { + it('should flatten array by one level', () => { + expect(flatten([0, 1, 2, 3])).toStrictEqual([0, 1, 2, 3]); + expect(flatten([0, 1, [2, 3]])).toStrictEqual([0, 1, 2, 3]); + expect(flatten([0, [1, [2, 3]]])).toStrictEqual([0, 1, [2, 3]]); + expect(flatten([0])).toStrictEqual([0]); + expect(flatten([])).toStrictEqual([]); + }); +}); + +describe('splitAt', () => { + it('should split array into two', () => { + expect(splitAt(2, [0, 1, 2, 3])).toStrictEqual([ + [0, 1], + [2, 3], + ]); + expect(splitAt(0, [0, 1, 2, 3])).toStrictEqual([[], [0, 1, 2, 3]]); + expect(splitAt(-3, [0, 1, 2, 3])).toStrictEqual([[0], [1, 2, 3]]); + expect(splitAt(3, [0, 1, 2, 3])).toStrictEqual([[0, 1, 2], [3]]); + }); +}); + +describe('zipWith', () => { + it('should use the function to combine the two arrays element-wise', () => { + expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2, 3])).toStrictEqual([0, 2, 4, 6]); + expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2])).toStrictEqual([0, 2, 4, NaN]); + expect(zipWith((a, b) => a + b, [0, 1, 2], [0, 1, 2, 3])).toStrictEqual([0, 2, 4]); + expect(zipWith((a) => a, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([0, 1, 2]); + expect(zipWith((a, b) => b, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([1, 2, 3]); + }); +}); diff --git a/packages/core/util.mjs b/packages/core/util.mjs index b81811bae..756fac8e8 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -162,6 +162,7 @@ export const compose = (...funcs) => pipe(...funcs.reverse()); // Removes 'None' values from given list export const removeUndefineds = (xs) => xs.filter((x) => x != undefined); +// flattens by one level export const flatten = (arr) => [].concat(...arr); export const id = (a) => a; @@ -237,6 +238,7 @@ export const splitAt = function (index, value) { return [value.slice(0, index), value.slice(index)]; }; +// Uses the function f to combine the arrays xs, ys element-wise export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i])); export const pairs = function (xs) { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ef8cc99d5..a4fca9ff7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,12 @@ packages: - # all packages in direct subdirs of packages/ - - "packages/*" - - "examples/*" - - "tools/dbpatch" - - "website/" + - packages/* + - examples/* + - tools/dbpatch + - website/ + +onlyBuiltDependencies: + - esbuild + - nx + - sharp + - tree-sitter + - tree-sitter-haskell diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 0eacfdfb0..85948f0c0 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3145,18 +3145,19 @@ exports[`runs examples > example "euclidLegato" example index 0 1`] = ` exports[`runs examples > example "euclidLegatoRot" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c3 ]", - "[ 1/4 → 3/4 | note:c3 ]", - "[ 3/4 → 1/1 | note:c3 ]", - "[ 1/1 → 5/4 | note:c3 ]", - "[ 5/4 → 7/4 | note:c3 ]", - "[ 7/4 → 2/1 | note:c3 ]", - "[ 2/1 → 9/4 | note:c3 ]", - "[ 9/4 → 11/4 | note:c3 ]", - "[ 11/4 → 3/1 | note:c3 ]", - "[ 3/1 → 13/4 | note:c3 ]", - "[ 13/4 → 15/4 | note:c3 ]", - "[ 15/4 → 4/1 | note:c3 ]", + "[ -1/5 ⇜ (0/1 → 1/5) | note:c3 ]", + "[ 1/5 → 2/5 | note:c3 ]", + "[ 2/5 → 4/5 | note:c3 ]", + "[ 4/5 → 6/5 | note:c3 ]", + "[ 6/5 → 7/5 | note:c3 ]", + "[ 7/5 → 9/5 | note:c3 ]", + "[ 9/5 → 11/5 | note:c3 ]", + "[ 11/5 → 12/5 | note:c3 ]", + "[ 12/5 → 14/5 | note:c3 ]", + "[ 14/5 → 16/5 | note:c3 ]", + "[ 16/5 → 17/5 | note:c3 ]", + "[ 17/5 → 19/5 | note:c3 ]", + "[ (19/5 → 4/1) ⇝ 21/5 | note:c3 ]", ] `; diff --git a/test/__snapshots__/tunes.test.mjs.snap b/test/__snapshots__/tunes.test.mjs.snap index 06267b8f9..4a0ddd539 100644 --- a/test/__snapshots__/tunes.test.mjs.snap +++ b/test/__snapshots__/tunes.test.mjs.snap @@ -7318,12 +7318,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` [ "[ -9/8 ⇜ (0/1 → 3/8) | gain:0.6 note:A3 velocity:0.5989903202280402 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ -3/4 ⇜ (0/1 → 3/4) | gain:0.6 note:C5 velocity:0.8369929669424891 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 0/1 → 3/2 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 0/1 → 3/2 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 0/1 → 9/4 | gain:0.6 note:D3 velocity:0.5 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 3/8 → 21/8 | gain:0.6 note:F5 velocity:0.9213038925081491 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 3/4 → 3/1 | gain:0.6 note:C5 velocity:0.8426077850162983 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 3/2 → 9/4 | note:D2 s:bass clip:1 gain:0.8 ]", - "[ 9/4 → 3/1 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 3/2 → 9/4 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 9/4 → 3/1 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 9/4 → 9/2 | gain:0.6 note:D4 velocity:0.7006962578743696 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 21/8 → 39/8 | gain:0.6 note:C4 velocity:0.6507943943142891 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 3/1 → 9/2 | note:D2 s:bass clip:1 gain:0.8 ]", @@ -7335,12 +7335,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (21/4 → 6/1) ⇝ 27/4 | gain:0.6 note:D4 velocity:0.6988155404105783 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 39/8 ⇜ (6/1 → 51/8) | gain:0.6 note:D5 velocity:0.8758113365620375 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 21/4 ⇜ (6/1 → 27/4) | gain:0.6 note:D4 velocity:0.6988155404105783 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 6/1 → 15/2 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 6/1 → 15/2 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 6/1 → 33/4 | gain:0.6 note:G4 velocity:0.7597710825502872 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 51/8 → 69/8 | gain:0.6 note:G4 velocity:0.7743164440616965 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 27/4 → 9/1 | gain:0.6 note:C5 velocity:0.8362447572872043 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 15/2 → 33/4 | note:A2 s:bass clip:1 gain:0.8 ]", - "[ 33/4 → 9/1 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 15/2 → 33/4 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 33/4 → 9/1 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 33/4 → 21/2 | gain:0.6 note:A3 velocity:0.5914018759503961 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 69/8 → 87/8 | gain:0.6 note:G4 velocity:0.754063542932272 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 9/1 → 21/2 | note:A2 s:bass clip:1 gain:0.8 ]", @@ -7352,12 +7352,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (45/4 → 12/1) ⇝ 51/4 | gain:0.6 note:A4 velocity:0.7972785895690322 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 87/8 ⇜ (12/1 → 99/8) | gain:0.6 note:F4 velocity:0.7347871446982026 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 45/4 ⇜ (12/1 → 51/4) | gain:0.6 note:A4 velocity:0.7972785895690322 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 12/1 → 27/2 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 12/1 → 27/2 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 12/1 → 57/4 | gain:0.6 note:G5 velocity:0.9797635599970818 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 99/8 → 117/8 | gain:0.6 note:C4 velocity:0.6662392104044557 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 51/4 → 15/1 | gain:0.6 note:F5 velocity:0.9516951469704509 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 27/2 → 57/4 | note:G2 s:bass clip:1 gain:0.8 ]", - "[ 57/4 → 15/1 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 27/2 → 57/4 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 57/4 → 15/1 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 57/4 → 33/2 | gain:0.6 note:F5 velocity:0.9182533202692866 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 117/8 → 135/8 | gain:0.6 note:G3 velocity:0.5711571052670479 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 15/1 → 33/2 | note:G2 s:bass clip:1 gain:0.8 ]", @@ -7369,12 +7369,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (69/4 → 18/1) ⇝ 75/4 | gain:0.6 note:F3 velocity:0.5081270858645439 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 135/8 ⇜ (18/1 → 147/8) | gain:0.6 note:F5 velocity:0.9456470254808664 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 69/4 ⇜ (18/1 → 75/4) | gain:0.6 note:F3 velocity:0.5081270858645439 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 18/1 → 39/2 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 18/1 → 39/2 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 18/1 → 81/4 | gain:0.6 note:A3 velocity:0.6086445553228259 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 147/8 → 165/8 | gain:0.6 note:F3 velocity:0.5062594395130873 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 75/4 → 21/1 | gain:0.6 note:D4 velocity:0.6716219391673803 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 39/2 → 81/4 | note:F2 s:bass clip:1 gain:0.8 ]", - "[ 81/4 → 21/1 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 39/2 → 81/4 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 81/4 → 21/1 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 81/4 → 45/2 | gain:0.6 note:D4 velocity:0.7043459005653858 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 165/8 → 183/8 | gain:0.6 note:D5 velocity:0.8878388572484255 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 21/1 → 45/2 | note:F2 s:bass clip:1 gain:0.8 ]", From 7f50bcebd29a36d90b13678ebc6473a512b0f291 Mon Sep 17 00:00:00 2001 From: Bernhard Wagner Date: Fri, 13 Jun 2025 10:28:03 +0200 Subject: [PATCH 192/538] avoid floating point inaccuracy by using Fraction --- packages/core/euclid.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index c3bb7a6d5..2ee9962da 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -12,6 +12,7 @@ This program is free software: you can redistribute it and/or modify it under th import { timeCat, register, silence } from './pattern.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs'; +import Fraction, { lcm } from './fraction.mjs'; const left = function (n, x) { const [ons, offs] = n; @@ -165,7 +166,7 @@ const _euclidLegato = function (pulses, steps, rotation, pat) { .split('1') .slice(1) .map((s) => [s.length + 1, true]); - return pat.struct(timeCat(...gapless)).late(rotation / steps); + return pat.struct(timeCat(...gapless)).late(Fraction(rotation).div(steps)); }; export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) { From 38e7fe606c3a355277ca5dbf7ef88568f0762ca8 Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 16:47:40 +0200 Subject: [PATCH 193/538] Update .github/workflows/deploy.yml --- .github/workflows/deploy.yml | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9a853c783..669073887 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,13 +2,6 @@ name: Build and Deploy on: [workflow_dispatch] -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - deployments: write - # Allow one concurrent deployment concurrency: group: "pages" @@ -16,10 +9,9 @@ concurrency: jobs: build: - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} + runs-on: docker + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -35,15 +27,11 @@ jobs: - name: Build run: pnpm build - - name: Setup Pages - uses: actions/configure-pages@v2 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - # Upload entire repository - path: "./website/dist" - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 + - name: Deploy + run: | + eval $(ssh-agent -s) + echo "$SSH_PRIVATE_KEY" | ssh-add - + apt update && apt install -y rsync + mkdir ~/.ssh + ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts + rsync -atv --progress ./website/dist strudel@matrix.toplap.org:/home/strudel/dist \ No newline at end of file From 8fba92f447cab4a104b694f86dd74303dcc5cfc8 Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 17:10:37 +0200 Subject: [PATCH 194/538] Update .github/workflows/deploy.yml --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 669073887..8eefebc2c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -34,4 +34,4 @@ jobs: apt update && apt install -y rsync mkdir ~/.ssh ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts - rsync -atv --progress ./website/dist strudel@matrix.toplap.org:/home/strudel/dist \ No newline at end of file + rsync -atv --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy \ No newline at end of file From 3428e18e7d36142ed10acd54251cc2ccce87b6f1 Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 17:26:37 +0200 Subject: [PATCH 195/538] Update .github/workflows/deploy.yml --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8eefebc2c..f82f0e30f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -34,4 +34,4 @@ jobs: apt update && apt install -y rsync mkdir ~/.ssh ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts - rsync -atv --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy \ No newline at end of file + rsync -atv --delete --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy \ No newline at end of file From 0c193238c55217725045de3d1fdfe75a7a2636d5 Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 17:31:03 +0200 Subject: [PATCH 196/538] Update .github/workflows/deploy.yml --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f82f0e30f..d8049d767 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -34,4 +34,4 @@ jobs: apt update && apt install -y rsync mkdir ~/.ssh ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts - rsync -atv --delete --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy \ No newline at end of file + rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy \ No newline at end of file From 47de9e45ff9e976908f2aa59a54bda211933b6cb Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 17:31:33 +0200 Subject: [PATCH 197/538] Update .github/workflows/deploy.yml --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d8049d767..561fee658 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20 - cache: "pnpm" + # cache: "pnpm" - name: Install Dependencies run: pnpm install From aa20526963fe216fe9ed82f5f3bda114a31f024e Mon Sep 17 00:00:00 2001 From: yaxu Date: Fri, 13 Jun 2025 17:33:47 +0200 Subject: [PATCH 198/538] Update .github/workflows/test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 498cd800b..765f5958d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,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 2bc3d69fb0e240214cd24304aef4d85d0f7d12ad Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 14 Jun 2025 14:01:11 +0200 Subject: [PATCH 199/538] replace github links in release notes --- .../content/blog/release-0.0.2-schwindlig.mdx | 52 ++-- .../blog/release-0.0.2.1-stuermisch.mdx | 80 ++--- .../content/blog/release-0.0.3-maelstrom.mdx | 104 +++---- .../src/content/blog/release-0.0.4-gischt.mdx | 62 ++-- .../content/blog/release-0.3.0-donauwelle.mdx | 74 ++--- .../content/blog/release-0.4.0-brandung.mdx | 8 +- .../src/content/blog/release-0.5.0-wirbel.mdx | 54 ++-- .../blog/release-0.6.0-zimtschnecke.mdx | 104 +++---- .../content/blog/release-0.7.0-zuckerguss.mdx | 118 +++---- .../blog/release-0.8.0-himbeermuffin.mdx | 84 ++--- .../blog/release-0.9.0-bananenbrot.mdx | 82 ++--- .../blog/release-1.0.0-geburtstagskuchen.mdx | 288 +++++++++--------- website/src/content/blog/year-2.mdx | 4 +- 13 files changed, 557 insertions(+), 557 deletions(-) diff --git a/website/src/content/blog/release-0.0.2-schwindlig.mdx b/website/src/content/blog/release-0.0.2-schwindlig.mdx index cf4bc19d6..8dbff4928 100644 --- a/website/src/content/blog/release-0.0.2-schwindlig.mdx +++ b/website/src/content/blog/release-0.0.2-schwindlig.mdx @@ -8,33 +8,33 @@ author: froos ## What's Changed -- Most work done as [commits to main](https://github.com/tidalcycles/strudel/commits/2a0d8c3f77ff7b34e82602e2d02400707f367316) -- repl + reify functions by @felixroos in https://github.com/tidalcycles/strudel/pull/2 -- Fix path by @yaxu in https://github.com/tidalcycles/strudel/pull/3 -- update readme for local dev by @kindohm in https://github.com/tidalcycles/strudel/pull/4 -- Patternify all the things by @yaxu in https://github.com/tidalcycles/strudel/pull/5 -- krill parser + improved repl by @felixroos in https://github.com/tidalcycles/strudel/pull/6 -- fixed editor crash by @felixroos in https://github.com/tidalcycles/strudel/pull/7 -- timeCat by @yaxu in https://github.com/tidalcycles/strudel/pull/8 -- Bugfix every, and create more top level functions by @yaxu in https://github.com/tidalcycles/strudel/pull/9 -- Failing test for `when` WIP by @yaxu in https://github.com/tidalcycles/strudel/pull/10 -- Added mask() and struct() by @yaxu in https://github.com/tidalcycles/strudel/pull/11 -- Add continuous signals (sine, cosine, saw, etc) by @yaxu in https://github.com/tidalcycles/strudel/pull/13 -- add apply and layer, and missing div/mul methods by @yaxu in https://github.com/tidalcycles/strudel/pull/15 -- higher latencyHint by @felixroos in https://github.com/tidalcycles/strudel/pull/16 -- test: 📦 Add missing dependency and a CI check, to prevent oversights ;p by @puria in https://github.com/tidalcycles/strudel/pull/17 -- fix: 💄 Enhance visualisation of the Tutorial on mobile by @puria in https://github.com/tidalcycles/strudel/pull/19 -- Stateful queries and events (WIP) by @yaxu in https://github.com/tidalcycles/strudel/pull/14 -- Fix resolveState by @yaxu in https://github.com/tidalcycles/strudel/pull/22 -- added \_asNumber + interpret numbers as midi by @felixroos in https://github.com/tidalcycles/strudel/pull/21 -- Update package.json by @ChiakiUehira in https://github.com/tidalcycles/strudel/pull/23 -- packaging by @felixroos in https://github.com/tidalcycles/strudel/pull/24 +- Most work done as [commits to main](https://codeberg.org/uzu/strudel/commit/2a0d8c3f77ff7b34e82602e2d02400707f367316) +- repl + reify functions by @felixroos in https://codeberg.org/uzu/strudel/pulls/2 +- Fix path by @yaxu in https://codeberg.org/uzu/strudel/pulls/3 +- update readme for local dev by @kindohm in https://codeberg.org/uzu/strudel/pulls/4 +- Patternify all the things by @yaxu in https://codeberg.org/uzu/strudel/pulls/5 +- krill parser + improved repl by @felixroos in https://codeberg.org/uzu/strudel/pulls/6 +- fixed editor crash by @felixroos in https://codeberg.org/uzu/strudel/pulls/7 +- timeCat by @yaxu in https://codeberg.org/uzu/strudel/pulls/8 +- Bugfix every, and create more top level functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/9 +- Failing test for `when` WIP by @yaxu in https://codeberg.org/uzu/strudel/pulls/10 +- Added mask() and struct() by @yaxu in https://codeberg.org/uzu/strudel/pulls/11 +- Add continuous signals (sine, cosine, saw, etc) by @yaxu in https://codeberg.org/uzu/strudel/pulls/13 +- add apply and layer, and missing div/mul methods by @yaxu in https://codeberg.org/uzu/strudel/pulls/15 +- higher latencyHint by @felixroos in https://codeberg.org/uzu/strudel/pulls/16 +- test: 📦 Add missing dependency and a CI check, to prevent oversights ;p by @puria in https://codeberg.org/uzu/strudel/pulls/17 +- fix: 💄 Enhance visualisation of the Tutorial on mobile by @puria in https://codeberg.org/uzu/strudel/pulls/19 +- Stateful queries and events (WIP) by @yaxu in https://codeberg.org/uzu/strudel/pulls/14 +- Fix resolveState by @yaxu in https://codeberg.org/uzu/strudel/pulls/22 +- added \_asNumber + interpret numbers as midi by @felixroos in https://codeberg.org/uzu/strudel/pulls/21 +- Update package.json by @ChiakiUehira in https://codeberg.org/uzu/strudel/pulls/23 +- packaging by @felixroos in https://codeberg.org/uzu/strudel/pulls/24 ## New Contributors -- @felixroos made their first contribution in https://github.com/tidalcycles/strudel/pull/2 -- @kindohm made their first contribution in https://github.com/tidalcycles/strudel/pull/4 -- @puria made their first contribution in https://github.com/tidalcycles/strudel/pull/17 -- @ChiakiUehira made their first contribution in https://github.com/tidalcycles/strudel/pull/23 +- @felixroos made their first contribution in https://codeberg.org/uzu/strudel/pulls/2 +- @kindohm made their first contribution in https://codeberg.org/uzu/strudel/pulls/4 +- @puria made their first contribution in https://codeberg.org/uzu/strudel/pulls/17 +- @ChiakiUehira made their first contribution in https://codeberg.org/uzu/strudel/pulls/23 -**Full Changelog**: https://github.com/tidalcycles/strudel/commits/2a0d8c3f77ff7b34e82602e2d02400707f367316 +**Full Changelog**: https://codeberg.org/uzu/strudel/commit/2a0d8c3f77ff7b34e82602e2d02400707f367316 diff --git a/website/src/content/blog/release-0.0.2.1-stuermisch.mdx b/website/src/content/blog/release-0.0.2.1-stuermisch.mdx index edefd2b09..441d99f76 100644 --- a/website/src/content/blog/release-0.0.2.1-stuermisch.mdx +++ b/website/src/content/blog/release-0.0.2.1-stuermisch.mdx @@ -7,47 +7,47 @@ author: froos ## What's Changed -- Add chunk, chunkBack and iterBack by @yaxu in https://github.com/tidalcycles/strudel/pull/25 -- Update tutorial.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/37 -- Update tutorial.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/38 -- Compose by @felixroos in https://github.com/tidalcycles/strudel/pull/40 -- Fix polymeter by @yaxu in https://github.com/tidalcycles/strudel/pull/44 -- First run at squeezeBind, ref #32 by @yaxu in https://github.com/tidalcycles/strudel/pull/48 -- Implement `chop()` by @yaxu in https://github.com/tidalcycles/strudel/pull/50 -- OSC and SuperDirt support by @yaxu in https://github.com/tidalcycles/strudel/pull/27 -- More functions by @yaxu in https://github.com/tidalcycles/strudel/pull/56 -- More functions by @yaxu in https://github.com/tidalcycles/strudel/pull/61 -- Separate out strudel.mjs, make index.mjs aggregate module by @yaxu in https://github.com/tidalcycles/strudel/pull/62 -- Speech output by @felixroos in https://github.com/tidalcycles/strudel/pull/67 -- use new fixed version of osc-js package by @felixroos in https://github.com/tidalcycles/strudel/pull/68 -- First effort at rand() by @yaxu in https://github.com/tidalcycles/strudel/pull/69 -- More randomness, fix `rand`, and add `brand`, `irand` and `choose` by @yaxu in https://github.com/tidalcycles/strudel/pull/70 -- webaudio package by @felixroos in https://github.com/tidalcycles/strudel/pull/26 -- Port `perlin` noise, `rangex`, and `palindrome` by @yaxu in https://github.com/tidalcycles/strudel/pull/73 -- More random functions by @yaxu in https://github.com/tidalcycles/strudel/pull/74 -- Try to fix appLeft / appRight by @yaxu in https://github.com/tidalcycles/strudel/pull/75 -- Basic webserial support by @yaxu in https://github.com/tidalcycles/strudel/pull/80 -- Webaudio in REPL by @felixroos in https://github.com/tidalcycles/strudel/pull/77 -- add `striate()` by @yaxu in https://github.com/tidalcycles/strudel/pull/76 -- Tidy up a couple of old files by @mindofmatthew in https://github.com/tidalcycles/strudel/pull/84 -- Add pattern composers, implements #82 by @yaxu in https://github.com/tidalcycles/strudel/pull/83 -- Fiddles with cat/stack by @yaxu in https://github.com/tidalcycles/strudel/pull/90 -- Paper by @felixroos in https://github.com/tidalcycles/strudel/pull/98 -- Change to Affero GPL by @yaxu in https://github.com/tidalcycles/strudel/pull/101 -- Work on Codemirror 6 highlighting by @mindofmatthew in https://github.com/tidalcycles/strudel/pull/102 -- Codemirror 6 by @felixroos in https://github.com/tidalcycles/strudel/pull/97 -- Tune tests by @felixroos in https://github.com/tidalcycles/strudel/pull/104 -- /embed package: web component for repl by @felixroos in https://github.com/tidalcycles/strudel/pull/106 -- Reset, Restart and other composers by @felixroos in https://github.com/tidalcycles/strudel/pull/88 -- Embed style by @felixroos in https://github.com/tidalcycles/strudel/pull/109 -- In source doc by @yaxu in https://github.com/tidalcycles/strudel/pull/105 -- `.brak()`, `.inside()` and `.outside()` by @yaxu in https://github.com/tidalcycles/strudel/pull/112 -- loopAt by @yaxu in https://github.com/tidalcycles/strudel/pull/114 -- Osc timing improvements by @yaxu in https://github.com/tidalcycles/strudel/pull/113 +- Add chunk, chunkBack and iterBack by @yaxu in https://codeberg.org/uzu/strudel/pulls/25 +- Update tutorial.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/37 +- Update tutorial.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/38 +- Compose by @felixroos in https://codeberg.org/uzu/strudel/pulls/40 +- Fix polymeter by @yaxu in https://codeberg.org/uzu/strudel/pulls/44 +- First run at squeezeBind, ref #32 by @yaxu in https://codeberg.org/uzu/strudel/pulls/48 +- Implement `chop()` by @yaxu in https://codeberg.org/uzu/strudel/pulls/50 +- OSC and SuperDirt support by @yaxu in https://codeberg.org/uzu/strudel/pulls/27 +- More functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/56 +- More functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/61 +- Separate out strudel.mjs, make index.mjs aggregate module by @yaxu in https://codeberg.org/uzu/strudel/pulls/62 +- Speech output by @felixroos in https://codeberg.org/uzu/strudel/pulls/67 +- use new fixed version of osc-js package by @felixroos in https://codeberg.org/uzu/strudel/pulls/68 +- First effort at rand() by @yaxu in https://codeberg.org/uzu/strudel/pulls/69 +- More randomness, fix `rand`, and add `brand`, `irand` and `choose` by @yaxu in https://codeberg.org/uzu/strudel/pulls/70 +- webaudio package by @felixroos in https://codeberg.org/uzu/strudel/pulls/26 +- Port `perlin` noise, `rangex`, and `palindrome` by @yaxu in https://codeberg.org/uzu/strudel/pulls/73 +- More random functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/74 +- Try to fix appLeft / appRight by @yaxu in https://codeberg.org/uzu/strudel/pulls/75 +- Basic webserial support by @yaxu in https://codeberg.org/uzu/strudel/pulls/80 +- Webaudio in REPL by @felixroos in https://codeberg.org/uzu/strudel/pulls/77 +- add `striate()` by @yaxu in https://codeberg.org/uzu/strudel/pulls/76 +- Tidy up a couple of old files by @mindofmatthew in https://codeberg.org/uzu/strudel/pulls/84 +- Add pattern composers, implements #82 by @yaxu in https://codeberg.org/uzu/strudel/pulls/83 +- Fiddles with cat/stack by @yaxu in https://codeberg.org/uzu/strudel/pulls/90 +- Paper by @felixroos in https://codeberg.org/uzu/strudel/pulls/98 +- Change to Affero GPL by @yaxu in https://codeberg.org/uzu/strudel/pulls/101 +- Work on Codemirror 6 highlighting by @mindofmatthew in https://codeberg.org/uzu/strudel/pulls/102 +- Codemirror 6 by @felixroos in https://codeberg.org/uzu/strudel/pulls/97 +- Tune tests by @felixroos in https://codeberg.org/uzu/strudel/pulls/104 +- /embed package: web component for repl by @felixroos in https://codeberg.org/uzu/strudel/pulls/106 +- Reset, Restart and other composers by @felixroos in https://codeberg.org/uzu/strudel/pulls/88 +- Embed style by @felixroos in https://codeberg.org/uzu/strudel/pulls/109 +- In source doc by @yaxu in https://codeberg.org/uzu/strudel/pulls/105 +- `.brak()`, `.inside()` and `.outside()` by @yaxu in https://codeberg.org/uzu/strudel/pulls/112 +- loopAt by @yaxu in https://codeberg.org/uzu/strudel/pulls/114 +- Osc timing improvements by @yaxu in https://codeberg.org/uzu/strudel/pulls/113 ## New Contributors -- @bwagner made their first contribution in https://github.com/tidalcycles/strudel/pull/37 -- @mindofmatthew made their first contribution in https://github.com/tidalcycles/strudel/pull/84 +- @bwagner made their first contribution in https://codeberg.org/uzu/strudel/pulls/37 +- @mindofmatthew made their first contribution in https://codeberg.org/uzu/strudel/pulls/84 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.0.2...@strudel.cycles/core@0.1.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.0.2...@strudel.cycles/core@0.1.0 diff --git a/website/src/content/blog/release-0.0.3-maelstrom.mdx b/website/src/content/blog/release-0.0.3-maelstrom.mdx index 3710a9ce5..5141b34a7 100644 --- a/website/src/content/blog/release-0.0.3-maelstrom.mdx +++ b/website/src/content/blog/release-0.0.3-maelstrom.mdx @@ -8,59 +8,59 @@ author: froos ## What's Changed -- Add chunk, chunkBack and iterBack by @yaxu in https://github.com/tidalcycles/strudel/pull/25 -- Update tutorial.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/37 -- Update tutorial.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/38 -- Compose by @felixroos in https://github.com/tidalcycles/strudel/pull/40 -- Fix polymeter by @yaxu in https://github.com/tidalcycles/strudel/pull/44 -- First run at squeezeBind, ref #32 by @yaxu in https://github.com/tidalcycles/strudel/pull/48 -- Implement `chop()` by @yaxu in https://github.com/tidalcycles/strudel/pull/50 -- OSC and SuperDirt support by @yaxu in https://github.com/tidalcycles/strudel/pull/27 -- More functions by @yaxu in https://github.com/tidalcycles/strudel/pull/56 -- More functions by @yaxu in https://github.com/tidalcycles/strudel/pull/61 -- Separate out strudel.mjs, make index.mjs aggregate module by @yaxu in https://github.com/tidalcycles/strudel/pull/62 -- Speech output by @felixroos in https://github.com/tidalcycles/strudel/pull/67 -- use new fixed version of osc-js package by @felixroos in https://github.com/tidalcycles/strudel/pull/68 -- First effort at rand() by @yaxu in https://github.com/tidalcycles/strudel/pull/69 -- More randomness, fix `rand`, and add `brand`, `irand` and `choose` by @yaxu in https://github.com/tidalcycles/strudel/pull/70 -- webaudio package by @felixroos in https://github.com/tidalcycles/strudel/pull/26 -- Port `perlin` noise, `rangex`, and `palindrome` by @yaxu in https://github.com/tidalcycles/strudel/pull/73 -- More random functions by @yaxu in https://github.com/tidalcycles/strudel/pull/74 -- Try to fix appLeft / appRight by @yaxu in https://github.com/tidalcycles/strudel/pull/75 -- Basic webserial support by @yaxu in https://github.com/tidalcycles/strudel/pull/80 -- Webaudio in REPL by @felixroos in https://github.com/tidalcycles/strudel/pull/77 -- add `striate()` by @yaxu in https://github.com/tidalcycles/strudel/pull/76 -- Tidy up a couple of old files by @mindofmatthew in https://github.com/tidalcycles/strudel/pull/84 -- Add pattern composers, implements #82 by @yaxu in https://github.com/tidalcycles/strudel/pull/83 -- Fiddles with cat/stack by @yaxu in https://github.com/tidalcycles/strudel/pull/90 -- Paper by @felixroos in https://github.com/tidalcycles/strudel/pull/98 -- Change to Affero GPL by @yaxu in https://github.com/tidalcycles/strudel/pull/101 -- Work on Codemirror 6 highlighting by @mindofmatthew in https://github.com/tidalcycles/strudel/pull/102 -- Codemirror 6 by @felixroos in https://github.com/tidalcycles/strudel/pull/97 -- Tune tests by @felixroos in https://github.com/tidalcycles/strudel/pull/104 -- /embed package: web component for repl by @felixroos in https://github.com/tidalcycles/strudel/pull/106 -- Reset, Restart and other composers by @felixroos in https://github.com/tidalcycles/strudel/pull/88 -- Embed style by @felixroos in https://github.com/tidalcycles/strudel/pull/109 -- In source doc by @yaxu in https://github.com/tidalcycles/strudel/pull/105 -- `.brak()`, `.inside()` and `.outside()` by @yaxu in https://github.com/tidalcycles/strudel/pull/112 -- loopAt by @yaxu in https://github.com/tidalcycles/strudel/pull/114 -- Osc timing improvements by @yaxu in https://github.com/tidalcycles/strudel/pull/113 -- react package + vite build by @felixroos in https://github.com/tidalcycles/strudel/pull/116 -- In source doc by @felixroos in https://github.com/tidalcycles/strudel/pull/117 -- fix: #108 by @felixroos in https://github.com/tidalcycles/strudel/pull/123 -- fix: #122 ctrl enter would add newline by @felixroos in https://github.com/tidalcycles/strudel/pull/124 -- Webdirt by @felixroos in https://github.com/tidalcycles/strudel/pull/121 -- Fix link to contributing to tutorial docs by @stephendwolff in https://github.com/tidalcycles/strudel/pull/129 -- Pianoroll enhancements by @felixroos in https://github.com/tidalcycles/strudel/pull/131 -- add createParam + createParams by @felixroos in https://github.com/tidalcycles/strudel/pull/110 -- remove cycle + delta from onTrigger by @felixroos in https://github.com/tidalcycles/strudel/pull/135 -- Scheduler improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/134 -- add onTrigger helper by @felixroos in https://github.com/tidalcycles/strudel/pull/136 +- Add chunk, chunkBack and iterBack by @yaxu in https://codeberg.org/uzu/strudel/pulls/25 +- Update tutorial.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/37 +- Update tutorial.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/38 +- Compose by @felixroos in https://codeberg.org/uzu/strudel/pulls/40 +- Fix polymeter by @yaxu in https://codeberg.org/uzu/strudel/pulls/44 +- First run at squeezeBind, ref #32 by @yaxu in https://codeberg.org/uzu/strudel/pulls/48 +- Implement `chop()` by @yaxu in https://codeberg.org/uzu/strudel/pulls/50 +- OSC and SuperDirt support by @yaxu in https://codeberg.org/uzu/strudel/pulls/27 +- More functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/56 +- More functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/61 +- Separate out strudel.mjs, make index.mjs aggregate module by @yaxu in https://codeberg.org/uzu/strudel/pulls/62 +- Speech output by @felixroos in https://codeberg.org/uzu/strudel/pulls/67 +- use new fixed version of osc-js package by @felixroos in https://codeberg.org/uzu/strudel/pulls/68 +- First effort at rand() by @yaxu in https://codeberg.org/uzu/strudel/pulls/69 +- More randomness, fix `rand`, and add `brand`, `irand` and `choose` by @yaxu in https://codeberg.org/uzu/strudel/pulls/70 +- webaudio package by @felixroos in https://codeberg.org/uzu/strudel/pulls/26 +- Port `perlin` noise, `rangex`, and `palindrome` by @yaxu in https://codeberg.org/uzu/strudel/pulls/73 +- More random functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/74 +- Try to fix appLeft / appRight by @yaxu in https://codeberg.org/uzu/strudel/pulls/75 +- Basic webserial support by @yaxu in https://codeberg.org/uzu/strudel/pulls/80 +- Webaudio in REPL by @felixroos in https://codeberg.org/uzu/strudel/pulls/77 +- add `striate()` by @yaxu in https://codeberg.org/uzu/strudel/pulls/76 +- Tidy up a couple of old files by @mindofmatthew in https://codeberg.org/uzu/strudel/pulls/84 +- Add pattern composers, implements #82 by @yaxu in https://codeberg.org/uzu/strudel/pulls/83 +- Fiddles with cat/stack by @yaxu in https://codeberg.org/uzu/strudel/pulls/90 +- Paper by @felixroos in https://codeberg.org/uzu/strudel/pulls/98 +- Change to Affero GPL by @yaxu in https://codeberg.org/uzu/strudel/pulls/101 +- Work on Codemirror 6 highlighting by @mindofmatthew in https://codeberg.org/uzu/strudel/pulls/102 +- Codemirror 6 by @felixroos in https://codeberg.org/uzu/strudel/pulls/97 +- Tune tests by @felixroos in https://codeberg.org/uzu/strudel/pulls/104 +- /embed package: web component for repl by @felixroos in https://codeberg.org/uzu/strudel/pulls/106 +- Reset, Restart and other composers by @felixroos in https://codeberg.org/uzu/strudel/pulls/88 +- Embed style by @felixroos in https://codeberg.org/uzu/strudel/pulls/109 +- In source doc by @yaxu in https://codeberg.org/uzu/strudel/pulls/105 +- `.brak()`, `.inside()` and `.outside()` by @yaxu in https://codeberg.org/uzu/strudel/pulls/112 +- loopAt by @yaxu in https://codeberg.org/uzu/strudel/pulls/114 +- Osc timing improvements by @yaxu in https://codeberg.org/uzu/strudel/pulls/113 +- react package + vite build by @felixroos in https://codeberg.org/uzu/strudel/pulls/116 +- In source doc by @felixroos in https://codeberg.org/uzu/strudel/pulls/117 +- fix: #108 by @felixroos in https://codeberg.org/uzu/strudel/pulls/123 +- fix: #122 ctrl enter would add newline by @felixroos in https://codeberg.org/uzu/strudel/pulls/124 +- Webdirt by @felixroos in https://codeberg.org/uzu/strudel/pulls/121 +- Fix link to contributing to tutorial docs by @stephendwolff in https://codeberg.org/uzu/strudel/pulls/129 +- Pianoroll enhancements by @felixroos in https://codeberg.org/uzu/strudel/pulls/131 +- add createParam + createParams by @felixroos in https://codeberg.org/uzu/strudel/pulls/110 +- remove cycle + delta from onTrigger by @felixroos in https://codeberg.org/uzu/strudel/pulls/135 +- Scheduler improvements by @felixroos in https://codeberg.org/uzu/strudel/pulls/134 +- add onTrigger helper by @felixroos in https://codeberg.org/uzu/strudel/pulls/136 ## New Contributors -- @bwagner made their first contribution in https://github.com/tidalcycles/strudel/pull/37 -- @mindofmatthew made their first contribution in https://github.com/tidalcycles/strudel/pull/84 -- @stephendwolff made their first contribution in https://github.com/tidalcycles/strudel/pull/129 +- @bwagner made their first contribution in https://codeberg.org/uzu/strudel/pulls/37 +- @mindofmatthew made their first contribution in https://codeberg.org/uzu/strudel/pulls/84 +- @stephendwolff made their first contribution in https://codeberg.org/uzu/strudel/pulls/129 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.0.2...v0.0.3 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.0.2...v0.0.3 diff --git a/website/src/content/blog/release-0.0.4-gischt.mdx b/website/src/content/blog/release-0.0.4-gischt.mdx index 9149137ca..f06dd38e8 100644 --- a/website/src/content/blog/release-0.0.4-gischt.mdx +++ b/website/src/content/blog/release-0.0.4-gischt.mdx @@ -8,38 +8,38 @@ author: froos ## What's Changed -- Webaudio rewrite by @felixroos in https://github.com/tidalcycles/strudel/pull/138 -- Fix createParam() by @yaxu in https://github.com/tidalcycles/strudel/pull/140 -- Soundfont Support by @felixroos in https://github.com/tidalcycles/strudel/pull/139 -- Serial twiddles by @yaxu in https://github.com/tidalcycles/strudel/pull/141 -- Pianoroll Object Support by @felixroos in https://github.com/tidalcycles/strudel/pull/142 -- flash effect on ctrl enter by @felixroos in https://github.com/tidalcycles/strudel/pull/144 -- can now generate short link for sharing by @felixroos in https://github.com/tidalcycles/strudel/pull/146 -- Sampler optimizations and more by @felixroos in https://github.com/tidalcycles/strudel/pull/148 -- Final update to demo.pdf by @yaxu in https://github.com/tidalcycles/strudel/pull/151 -- add webdirt drum samples to prebake for general availability by @larkob in https://github.com/tidalcycles/strudel/pull/150 -- update to tutorial documentation by @larkob in https://github.com/tidalcycles/strudel/pull/162 -- add chooseInWith/chooseCycles by @yaxu in https://github.com/tidalcycles/strudel/pull/166 -- fix: jsdoc comments by @felixroos in https://github.com/tidalcycles/strudel/pull/169 -- Pianoroll fixes by @felixroos in https://github.com/tidalcycles/strudel/pull/163 -- Talk fixes by @felixroos in https://github.com/tidalcycles/strudel/pull/164 -- Amend shapeshifter to allow use of dynamic import by @debrisapron in https://github.com/tidalcycles/strudel/pull/171 -- add more shapeshifter flags by @felixroos in https://github.com/tidalcycles/strudel/pull/99 -- Replace react-codemirror6 with @uiw/react-codemirror by @felixroos in https://github.com/tidalcycles/strudel/pull/173 -- fix some annoying bugs by @felixroos in https://github.com/tidalcycles/strudel/pull/177 -- incorporate elements of randomness to the mini notation by @bpow in https://github.com/tidalcycles/strudel/pull/165 -- replace mocha with vitest by @felixroos in https://github.com/tidalcycles/strudel/pull/175 -- scheduler improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/181 -- Fix codemirror bug by @felixroos in https://github.com/tidalcycles/strudel/pull/186 -- wait for prebake to finish before evaluating by @felixroos in https://github.com/tidalcycles/strudel/pull/189 -- fix regression: old way of setting frequencies was broken by @felixroos in https://github.com/tidalcycles/strudel/pull/190 -- Soundfont file support by @felixroos in https://github.com/tidalcycles/strudel/pull/183 -- change "stride"/"offset" of successive degradeBy/chooseIn by @bpow in https://github.com/tidalcycles/strudel/pull/185 +- Webaudio rewrite by @felixroos in https://codeberg.org/uzu/strudel/pulls/138 +- Fix createParam() by @yaxu in https://codeberg.org/uzu/strudel/pulls/140 +- Soundfont Support by @felixroos in https://codeberg.org/uzu/strudel/pulls/139 +- Serial twiddles by @yaxu in https://codeberg.org/uzu/strudel/pulls/141 +- Pianoroll Object Support by @felixroos in https://codeberg.org/uzu/strudel/pulls/142 +- flash effect on ctrl enter by @felixroos in https://codeberg.org/uzu/strudel/pulls/144 +- can now generate short link for sharing by @felixroos in https://codeberg.org/uzu/strudel/pulls/146 +- Sampler optimizations and more by @felixroos in https://codeberg.org/uzu/strudel/pulls/148 +- Final update to demo.pdf by @yaxu in https://codeberg.org/uzu/strudel/pulls/151 +- add webdirt drum samples to prebake for general availability by @larkob in https://codeberg.org/uzu/strudel/pulls/150 +- update to tutorial documentation by @larkob in https://codeberg.org/uzu/strudel/pulls/162 +- add chooseInWith/chooseCycles by @yaxu in https://codeberg.org/uzu/strudel/pulls/166 +- fix: jsdoc comments by @felixroos in https://codeberg.org/uzu/strudel/pulls/169 +- Pianoroll fixes by @felixroos in https://codeberg.org/uzu/strudel/pulls/163 +- Talk fixes by @felixroos in https://codeberg.org/uzu/strudel/pulls/164 +- Amend shapeshifter to allow use of dynamic import by @debrisapron in https://codeberg.org/uzu/strudel/pulls/171 +- add more shapeshifter flags by @felixroos in https://codeberg.org/uzu/strudel/pulls/99 +- Replace react-codemirror6 with @uiw/react-codemirror by @felixroos in https://codeberg.org/uzu/strudel/pulls/173 +- fix some annoying bugs by @felixroos in https://codeberg.org/uzu/strudel/pulls/177 +- incorporate elements of randomness to the mini notation by @bpow in https://codeberg.org/uzu/strudel/pulls/165 +- replace mocha with vitest by @felixroos in https://codeberg.org/uzu/strudel/pulls/175 +- scheduler improvements by @felixroos in https://codeberg.org/uzu/strudel/pulls/181 +- Fix codemirror bug by @felixroos in https://codeberg.org/uzu/strudel/pulls/186 +- wait for prebake to finish before evaluating by @felixroos in https://codeberg.org/uzu/strudel/pulls/189 +- fix regression: old way of setting frequencies was broken by @felixroos in https://codeberg.org/uzu/strudel/pulls/190 +- Soundfont file support by @felixroos in https://codeberg.org/uzu/strudel/pulls/183 +- change "stride"/"offset" of successive degradeBy/chooseIn by @bpow in https://codeberg.org/uzu/strudel/pulls/185 ## New Contributors -- @larkob made their first contribution in https://github.com/tidalcycles/strudel/pull/150 -- @debrisapron made their first contribution in https://github.com/tidalcycles/strudel/pull/171 -- @bpow made their first contribution in https://github.com/tidalcycles/strudel/pull/165 +- @larkob made their first contribution in https://codeberg.org/uzu/strudel/pulls/150 +- @debrisapron made their first contribution in https://codeberg.org/uzu/strudel/pulls/171 +- @bpow made their first contribution in https://codeberg.org/uzu/strudel/pulls/165 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.0.3...v0.0.4 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.0.3...v0.0.4 diff --git a/website/src/content/blog/release-0.3.0-donauwelle.mdx b/website/src/content/blog/release-0.3.0-donauwelle.mdx index 054b36b31..a551dc53b 100644 --- a/website/src/content/blog/release-0.3.0-donauwelle.mdx +++ b/website/src/content/blog/release-0.3.0-donauwelle.mdx @@ -22,44 +22,44 @@ author: froos ## What's Changed -- Fix numbers in sampler by @felixroos in https://github.com/tidalcycles/strudel/pull/196 -- document random functions by @felixroos in https://github.com/tidalcycles/strudel/pull/199 -- add rollup-plugin-visualizer to build by @felixroos in https://github.com/tidalcycles/strudel/pull/200 -- add vowel to .out by @felixroos in https://github.com/tidalcycles/strudel/pull/201 -- Coarse crush shape by @felixroos in https://github.com/tidalcycles/strudel/pull/205 -- Webaudio guide by @felixroos in https://github.com/tidalcycles/strudel/pull/207 -- Even more docs by @felixroos in https://github.com/tidalcycles/strudel/pull/212 -- Just another docs PR by @felixroos in https://github.com/tidalcycles/strudel/pull/215 -- sampler features + fixes by @felixroos in https://github.com/tidalcycles/strudel/pull/217 -- samples now have envelopes by @felixroos in https://github.com/tidalcycles/strudel/pull/218 -- encapsulate webaudio output by @felixroos in https://github.com/tidalcycles/strudel/pull/219 -- Fix squeeze join by @yaxu in https://github.com/tidalcycles/strudel/pull/220 -- Feedback Delay by @felixroos in https://github.com/tidalcycles/strudel/pull/213 -- support negative speeds by @felixroos in https://github.com/tidalcycles/strudel/pull/222 -- focus tweak for squeezeJoin - another go at fixing #216 by @yaxu in https://github.com/tidalcycles/strudel/pull/221 -- Reverb by @felixroos in https://github.com/tidalcycles/strudel/pull/224 -- fix fastgap for events that go across cycle boundaries by @yaxu in https://github.com/tidalcycles/strudel/pull/225 -- Core util tests by @mystery-house in https://github.com/tidalcycles/strudel/pull/226 -- Refactor tunes away from tone by @felixroos in https://github.com/tidalcycles/strudel/pull/230 -- Just another docs branch by @felixroos in https://github.com/tidalcycles/strudel/pull/228 -- Patternify range by @yaxu in https://github.com/tidalcycles/strudel/pull/231 -- Out by default by @felixroos in https://github.com/tidalcycles/strudel/pull/232 -- Fix zero length queries WIP by @yaxu in https://github.com/tidalcycles/strudel/pull/234 -- add vcsl sample library by @felixroos in https://github.com/tidalcycles/strudel/pull/235 -- fx on stereo speakers by @felixroos in https://github.com/tidalcycles/strudel/pull/236 -- Tidal drum machines by @felixroos in https://github.com/tidalcycles/strudel/pull/237 -- Object arithmetic by @felixroos in https://github.com/tidalcycles/strudel/pull/238 -- Load samples from url by @felixroos in https://github.com/tidalcycles/strudel/pull/239 -- feat: support github: links by @felixroos in https://github.com/tidalcycles/strudel/pull/240 -- in source example tests by @felixroos in https://github.com/tidalcycles/strudel/pull/242 -- Readme + TLC by @felixroos in https://github.com/tidalcycles/strudel/pull/244 -- patchday by @felixroos in https://github.com/tidalcycles/strudel/pull/246 -- Some tunes by @felixroos in https://github.com/tidalcycles/strudel/pull/247 -- snapshot tests on shared snippets by @felixroos in https://github.com/tidalcycles/strudel/pull/243 -- General purpose scheduler by @felixroos in https://github.com/tidalcycles/strudel/pull/248 +- Fix numbers in sampler by @felixroos in https://codeberg.org/uzu/strudel/pulls/196 +- document random functions by @felixroos in https://codeberg.org/uzu/strudel/pulls/199 +- add rollup-plugin-visualizer to build by @felixroos in https://codeberg.org/uzu/strudel/pulls/200 +- add vowel to .out by @felixroos in https://codeberg.org/uzu/strudel/pulls/201 +- Coarse crush shape by @felixroos in https://codeberg.org/uzu/strudel/pulls/205 +- Webaudio guide by @felixroos in https://codeberg.org/uzu/strudel/pulls/207 +- Even more docs by @felixroos in https://codeberg.org/uzu/strudel/pulls/212 +- Just another docs PR by @felixroos in https://codeberg.org/uzu/strudel/pulls/215 +- sampler features + fixes by @felixroos in https://codeberg.org/uzu/strudel/pulls/217 +- samples now have envelopes by @felixroos in https://codeberg.org/uzu/strudel/pulls/218 +- encapsulate webaudio output by @felixroos in https://codeberg.org/uzu/strudel/pulls/219 +- Fix squeeze join by @yaxu in https://codeberg.org/uzu/strudel/pulls/220 +- Feedback Delay by @felixroos in https://codeberg.org/uzu/strudel/pulls/213 +- support negative speeds by @felixroos in https://codeberg.org/uzu/strudel/pulls/222 +- focus tweak for squeezeJoin - another go at fixing #216 by @yaxu in https://codeberg.org/uzu/strudel/pulls/221 +- Reverb by @felixroos in https://codeberg.org/uzu/strudel/pulls/224 +- fix fastgap for events that go across cycle boundaries by @yaxu in https://codeberg.org/uzu/strudel/pulls/225 +- Core util tests by @mystery-house in https://codeberg.org/uzu/strudel/pulls/226 +- Refactor tunes away from tone by @felixroos in https://codeberg.org/uzu/strudel/pulls/230 +- Just another docs branch by @felixroos in https://codeberg.org/uzu/strudel/pulls/228 +- Patternify range by @yaxu in https://codeberg.org/uzu/strudel/pulls/231 +- Out by default by @felixroos in https://codeberg.org/uzu/strudel/pulls/232 +- Fix zero length queries WIP by @yaxu in https://codeberg.org/uzu/strudel/pulls/234 +- add vcsl sample library by @felixroos in https://codeberg.org/uzu/strudel/pulls/235 +- fx on stereo speakers by @felixroos in https://codeberg.org/uzu/strudel/pulls/236 +- Tidal drum machines by @felixroos in https://codeberg.org/uzu/strudel/pulls/237 +- Object arithmetic by @felixroos in https://codeberg.org/uzu/strudel/pulls/238 +- Load samples from url by @felixroos in https://codeberg.org/uzu/strudel/pulls/239 +- feat: support github: links by @felixroos in https://codeberg.org/uzu/strudel/pulls/240 +- in source example tests by @felixroos in https://codeberg.org/uzu/strudel/pulls/242 +- Readme + TLC by @felixroos in https://codeberg.org/uzu/strudel/pulls/244 +- patchday by @felixroos in https://codeberg.org/uzu/strudel/pulls/246 +- Some tunes by @felixroos in https://codeberg.org/uzu/strudel/pulls/247 +- snapshot tests on shared snippets by @felixroos in https://codeberg.org/uzu/strudel/pulls/243 +- General purpose scheduler by @felixroos in https://codeberg.org/uzu/strudel/pulls/248 ## New Contributors -- @mystery-house made their first contribution in https://github.com/tidalcycles/strudel/pull/226 +- @mystery-house made their first contribution in https://codeberg.org/uzu/strudel/pulls/226 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.0.4...v0.3.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.0.4...v0.3.0 diff --git a/website/src/content/blog/release-0.4.0-brandung.mdx b/website/src/content/blog/release-0.4.0-brandung.mdx index 06023662b..01e2f9b89 100644 --- a/website/src/content/blog/release-0.4.0-brandung.mdx +++ b/website/src/content/blog/release-0.4.0-brandung.mdx @@ -8,8 +8,8 @@ author: froos ## What's Changed -- new transpiler based on acorn by @felixroos in https://github.com/tidalcycles/strudel/pull/249 -- Webaudio build by @felixroos in https://github.com/tidalcycles/strudel/pull/250 -- Repl refactoring by @felixroos in https://github.com/tidalcycles/strudel/pull/255 +- new transpiler based on acorn by @felixroos in https://codeberg.org/uzu/strudel/pulls/249 +- Webaudio build by @felixroos in https://codeberg.org/uzu/strudel/pulls/250 +- Repl refactoring by @felixroos in https://codeberg.org/uzu/strudel/pulls/255 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.3.0...v0.4.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.3.0...v0.4.0 diff --git a/website/src/content/blog/release-0.5.0-wirbel.mdx b/website/src/content/blog/release-0.5.0-wirbel.mdx index 032639fd9..3c1e1bfe2 100644 --- a/website/src/content/blog/release-0.5.0-wirbel.mdx +++ b/website/src/content/blog/release-0.5.0-wirbel.mdx @@ -26,34 +26,34 @@ author: froos ## What's Changed -- Binaries by @felixroos in https://github.com/tidalcycles/strudel/pull/254 -- fix tutorial bugs by @felixroos in https://github.com/tidalcycles/strudel/pull/263 -- fix performance bottleneck by @felixroos in https://github.com/tidalcycles/strudel/pull/266 -- Tidying up core by @yaxu in https://github.com/tidalcycles/strudel/pull/256 -- tonal update with fixed memory leak by @felixroos in https://github.com/tidalcycles/strudel/pull/272 -- add eslint by @felixroos in https://github.com/tidalcycles/strudel/pull/271 -- release version bumps by @felixroos in https://github.com/tidalcycles/strudel/pull/273 -- Support sending CRC16 bytes with serial messages by @yaxu in https://github.com/tidalcycles/strudel/pull/276 -- add licenses / credits to all tunes + remove some by @felixroos in https://github.com/tidalcycles/strudel/pull/277 -- add basic csound output by @felixroos in https://github.com/tidalcycles/strudel/pull/275 -- do not recompile orc by @felixroos in https://github.com/tidalcycles/strudel/pull/278 -- implement collect + arp function by @felixroos in https://github.com/tidalcycles/strudel/pull/281 -- Switch 'operators' from .whatHow to .what.how by @yaxu in https://github.com/tidalcycles/strudel/pull/285 -- Fancy hap show, include part in snapshots by @yaxu in https://github.com/tidalcycles/strudel/pull/291 -- Reorganise pattern.mjs with a 'toplevel first' regime by @yaxu in https://github.com/tidalcycles/strudel/pull/286 -- add prettier task by @felixroos in https://github.com/tidalcycles/strudel/pull/296 -- Move stuff to new register function by @felixroos in https://github.com/tidalcycles/strudel/pull/295 -- can now add bare numbers to numeral object props by @felixroos in https://github.com/tidalcycles/strudel/pull/287 -- update vitest by @felixroos in https://github.com/tidalcycles/strudel/pull/297 -- remove whitespace from highlighted region by @felixroos in https://github.com/tidalcycles/strudel/pull/298 -- .defragmentHaps() for merging touching haps that share a whole and value by @yaxu in https://github.com/tidalcycles/strudel/pull/299 -- fix whitespace trimming by @felixroos in https://github.com/tidalcycles/strudel/pull/300 -- add freq support to sampler by @felixroos in https://github.com/tidalcycles/strudel/pull/301 -- add lint + prettier check before test by @felixroos in https://github.com/tidalcycles/strudel/pull/305 -- Updated csoundm to use the register facility . by @gogins in https://github.com/tidalcycles/strudel/pull/303 +- Binaries by @felixroos in https://codeberg.org/uzu/strudel/pulls/254 +- fix tutorial bugs by @felixroos in https://codeberg.org/uzu/strudel/pulls/263 +- fix performance bottleneck by @felixroos in https://codeberg.org/uzu/strudel/pulls/266 +- Tidying up core by @yaxu in https://codeberg.org/uzu/strudel/pulls/256 +- tonal update with fixed memory leak by @felixroos in https://codeberg.org/uzu/strudel/pulls/272 +- add eslint by @felixroos in https://codeberg.org/uzu/strudel/pulls/271 +- release version bumps by @felixroos in https://codeberg.org/uzu/strudel/pulls/273 +- Support sending CRC16 bytes with serial messages by @yaxu in https://codeberg.org/uzu/strudel/pulls/276 +- add licenses / credits to all tunes + remove some by @felixroos in https://codeberg.org/uzu/strudel/pulls/277 +- add basic csound output by @felixroos in https://codeberg.org/uzu/strudel/pulls/275 +- do not recompile orc by @felixroos in https://codeberg.org/uzu/strudel/pulls/278 +- implement collect + arp function by @felixroos in https://codeberg.org/uzu/strudel/pulls/281 +- Switch 'operators' from .whatHow to .what.how by @yaxu in https://codeberg.org/uzu/strudel/pulls/285 +- Fancy hap show, include part in snapshots by @yaxu in https://codeberg.org/uzu/strudel/pulls/291 +- Reorganise pattern.mjs with a 'toplevel first' regime by @yaxu in https://codeberg.org/uzu/strudel/pulls/286 +- add prettier task by @felixroos in https://codeberg.org/uzu/strudel/pulls/296 +- Move stuff to new register function by @felixroos in https://codeberg.org/uzu/strudel/pulls/295 +- can now add bare numbers to numeral object props by @felixroos in https://codeberg.org/uzu/strudel/pulls/287 +- update vitest by @felixroos in https://codeberg.org/uzu/strudel/pulls/297 +- remove whitespace from highlighted region by @felixroos in https://codeberg.org/uzu/strudel/pulls/298 +- .defragmentHaps() for merging touching haps that share a whole and value by @yaxu in https://codeberg.org/uzu/strudel/pulls/299 +- fix whitespace trimming by @felixroos in https://codeberg.org/uzu/strudel/pulls/300 +- add freq support to sampler by @felixroos in https://codeberg.org/uzu/strudel/pulls/301 +- add lint + prettier check before test by @felixroos in https://codeberg.org/uzu/strudel/pulls/305 +- Updated csoundm to use the register facility . by @gogins in https://codeberg.org/uzu/strudel/pulls/303 ## New Contributors -- @gogins made their first contribution in https://github.com/tidalcycles/strudel/pull/303 +- @gogins made their first contribution in https://codeberg.org/uzu/strudel/pulls/303 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.4.0...v0.5.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.4.0...v0.5.0 diff --git a/website/src/content/blog/release-0.6.0-zimtschnecke.mdx b/website/src/content/blog/release-0.6.0-zimtschnecke.mdx index 2736974bf..19536581d 100644 --- a/website/src/content/blog/release-0.6.0-zimtschnecke.mdx +++ b/website/src/content/blog/release-0.6.0-zimtschnecke.mdx @@ -26,59 +26,59 @@ author: froos ## What's Changed -- support freq in pianoroll by @felixroos in https://github.com/tidalcycles/strudel/pull/308 -- ICLC2023 paper WIP by @yaxu in https://github.com/tidalcycles/strudel/pull/306 -- fix: copy share link to clipboard was broken for some browsers by @felixroos in https://github.com/tidalcycles/strudel/pull/311 -- Jsdoc component by @felixroos in https://github.com/tidalcycles/strudel/pull/312 -- object support for .scale by @felixroos in https://github.com/tidalcycles/strudel/pull/307 -- Astro build by @felixroos in https://github.com/tidalcycles/strudel/pull/315 -- Reference tab sort by @felixroos in https://github.com/tidalcycles/strudel/pull/318 -- tutorial updates by @jarmitage in https://github.com/tidalcycles/strudel/pull/320 -- support notes without octave by @felixroos in https://github.com/tidalcycles/strudel/pull/323 -- mini repl improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/324 -- fix: workaround Object.assign globalThis by @felixroos in https://github.com/tidalcycles/strudel/pull/326 -- add examples route by @felixroos in https://github.com/tidalcycles/strudel/pull/327 -- add my-patterns by @felixroos in https://github.com/tidalcycles/strudel/pull/328 -- my-patterns build + deploy by @felixroos in https://github.com/tidalcycles/strudel/pull/329 -- my-patterns: fix paths + update readme by @felixroos in https://github.com/tidalcycles/strudel/pull/330 -- improve displaying 's' in pianoroll by @felixroos in https://github.com/tidalcycles/strudel/pull/331 -- fix: can now multiply floats in mini notation by @felixroos in https://github.com/tidalcycles/strudel/pull/332 -- Embed mode improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/333 -- testing + docs docs by @felixroos in https://github.com/tidalcycles/strudel/pull/334 -- animate mvp by @felixroos in https://github.com/tidalcycles/strudel/pull/335 -- Tidy parser, implement polymeters by @yaxu in https://github.com/tidalcycles/strudel/pull/336 -- animation options by @felixroos in https://github.com/tidalcycles/strudel/pull/337 -- move /my-patterns to /swatch by @yaxu in https://github.com/tidalcycles/strudel/pull/338 -- more animate functions + mini repl fix by @felixroos in https://github.com/tidalcycles/strudel/pull/340 -- Patternify euclid, fast, slow and polymeter step parameters in mininotation by @yaxu in https://github.com/tidalcycles/strudel/pull/341 -- fixes #346 by @felixroos in https://github.com/tidalcycles/strudel/pull/347 -- Fix prebake base path by @felixroos in https://github.com/tidalcycles/strudel/pull/345 -- Fix Bjorklund by @yaxu in https://github.com/tidalcycles/strudel/pull/343 -- docs: tidal comparison + add global fx + add missing sampler fx by @felixroos in https://github.com/tidalcycles/strudel/pull/356 -- Fix .out(), renaming webaudio's out() to webaudio() by @yaxu in https://github.com/tidalcycles/strudel/pull/361 -- Support for multiple mininotation operators by @yaxu in https://github.com/tidalcycles/strudel/pull/350 -- doc structuring by @felixroos in https://github.com/tidalcycles/strudel/pull/360 -- add https to url by @urswilke in https://github.com/tidalcycles/strudel/pull/364 -- document more functions + change arp join by @felixroos in https://github.com/tidalcycles/strudel/pull/369 -- improve new draw logic by @felixroos in https://github.com/tidalcycles/strudel/pull/372 -- Draw fixes by @felixroos in https://github.com/tidalcycles/strudel/pull/377 -- update my-patterns instructions by @felixroos in https://github.com/tidalcycles/strudel/pull/384 -- docs: use note instead of n to mitigate confusion by @felixroos in https://github.com/tidalcycles/strudel/pull/385 -- add run + test + docs by @felixroos in https://github.com/tidalcycles/strudel/pull/386 -- Rename a to angle by @felixroos in https://github.com/tidalcycles/strudel/pull/387 -- document csound by @felixroos in https://github.com/tidalcycles/strudel/pull/391 -- Notes are not essential :) by @yaxu in https://github.com/tidalcycles/strudel/pull/393 -- add ribbon + test + docs by @felixroos in https://github.com/tidalcycles/strudel/pull/388 -- Add tidal-drum-patterns to examples by @urswilke in https://github.com/tidalcycles/strudel/pull/379 -- add pattern methods hurry, press and pressBy by @yaxu in https://github.com/tidalcycles/strudel/pull/397 -- proper builds + use pnpm workspaces by @felixroos in https://github.com/tidalcycles/strudel/pull/396 -- fix: minirepl styles by @felixroos in https://github.com/tidalcycles/strudel/pull/398 -- can now await initAudio + initAudioOnFirstClick by @felixroos in https://github.com/tidalcycles/strudel/pull/399 -- release webaudio by @felixroos in https://github.com/tidalcycles/strudel/pull/400 +- support freq in pianoroll by @felixroos in https://codeberg.org/uzu/strudel/pulls/308 +- ICLC2023 paper WIP by @yaxu in https://codeberg.org/uzu/strudel/pulls/306 +- fix: copy share link to clipboard was broken for some browsers by @felixroos in https://codeberg.org/uzu/strudel/pulls/311 +- Jsdoc component by @felixroos in https://codeberg.org/uzu/strudel/pulls/312 +- object support for .scale by @felixroos in https://codeberg.org/uzu/strudel/pulls/307 +- Astro build by @felixroos in https://codeberg.org/uzu/strudel/pulls/315 +- Reference tab sort by @felixroos in https://codeberg.org/uzu/strudel/pulls/318 +- tutorial updates by @jarmitage in https://codeberg.org/uzu/strudel/pulls/320 +- support notes without octave by @felixroos in https://codeberg.org/uzu/strudel/pulls/323 +- mini repl improvements by @felixroos in https://codeberg.org/uzu/strudel/pulls/324 +- fix: workaround Object.assign globalThis by @felixroos in https://codeberg.org/uzu/strudel/pulls/326 +- add examples route by @felixroos in https://codeberg.org/uzu/strudel/pulls/327 +- add my-patterns by @felixroos in https://codeberg.org/uzu/strudel/pulls/328 +- my-patterns build + deploy by @felixroos in https://codeberg.org/uzu/strudel/pulls/329 +- my-patterns: fix paths + update readme by @felixroos in https://codeberg.org/uzu/strudel/pulls/330 +- improve displaying 's' in pianoroll by @felixroos in https://codeberg.org/uzu/strudel/pulls/331 +- fix: can now multiply floats in mini notation by @felixroos in https://codeberg.org/uzu/strudel/pulls/332 +- Embed mode improvements by @felixroos in https://codeberg.org/uzu/strudel/pulls/333 +- testing + docs docs by @felixroos in https://codeberg.org/uzu/strudel/pulls/334 +- animate mvp by @felixroos in https://codeberg.org/uzu/strudel/pulls/335 +- Tidy parser, implement polymeters by @yaxu in https://codeberg.org/uzu/strudel/pulls/336 +- animation options by @felixroos in https://codeberg.org/uzu/strudel/pulls/337 +- move /my-patterns to /swatch by @yaxu in https://codeberg.org/uzu/strudel/pulls/338 +- more animate functions + mini repl fix by @felixroos in https://codeberg.org/uzu/strudel/pulls/340 +- Patternify euclid, fast, slow and polymeter step parameters in mininotation by @yaxu in https://codeberg.org/uzu/strudel/pulls/341 +- fixes #346 by @felixroos in https://codeberg.org/uzu/strudel/pulls/347 +- Fix prebake base path by @felixroos in https://codeberg.org/uzu/strudel/pulls/345 +- Fix Bjorklund by @yaxu in https://codeberg.org/uzu/strudel/pulls/343 +- docs: tidal comparison + add global fx + add missing sampler fx by @felixroos in https://codeberg.org/uzu/strudel/pulls/356 +- Fix .out(), renaming webaudio's out() to webaudio() by @yaxu in https://codeberg.org/uzu/strudel/pulls/361 +- Support for multiple mininotation operators by @yaxu in https://codeberg.org/uzu/strudel/pulls/350 +- doc structuring by @felixroos in https://codeberg.org/uzu/strudel/pulls/360 +- add https to url by @urswilke in https://codeberg.org/uzu/strudel/pulls/364 +- document more functions + change arp join by @felixroos in https://codeberg.org/uzu/strudel/pulls/369 +- improve new draw logic by @felixroos in https://codeberg.org/uzu/strudel/pulls/372 +- Draw fixes by @felixroos in https://codeberg.org/uzu/strudel/pulls/377 +- update my-patterns instructions by @felixroos in https://codeberg.org/uzu/strudel/pulls/384 +- docs: use note instead of n to mitigate confusion by @felixroos in https://codeberg.org/uzu/strudel/pulls/385 +- add run + test + docs by @felixroos in https://codeberg.org/uzu/strudel/pulls/386 +- Rename a to angle by @felixroos in https://codeberg.org/uzu/strudel/pulls/387 +- document csound by @felixroos in https://codeberg.org/uzu/strudel/pulls/391 +- Notes are not essential :) by @yaxu in https://codeberg.org/uzu/strudel/pulls/393 +- add ribbon + test + docs by @felixroos in https://codeberg.org/uzu/strudel/pulls/388 +- Add tidal-drum-patterns to examples by @urswilke in https://codeberg.org/uzu/strudel/pulls/379 +- add pattern methods hurry, press and pressBy by @yaxu in https://codeberg.org/uzu/strudel/pulls/397 +- proper builds + use pnpm workspaces by @felixroos in https://codeberg.org/uzu/strudel/pulls/396 +- fix: minirepl styles by @felixroos in https://codeberg.org/uzu/strudel/pulls/398 +- can now await initAudio + initAudioOnFirstClick by @felixroos in https://codeberg.org/uzu/strudel/pulls/399 +- release webaudio by @felixroos in https://codeberg.org/uzu/strudel/pulls/400 ## New Contributors -- @jarmitage made their first contribution in https://github.com/tidalcycles/strudel/pull/320 -- @urswilke made their first contribution in https://github.com/tidalcycles/strudel/pull/364 +- @jarmitage made their first contribution in https://codeberg.org/uzu/strudel/pulls/320 +- @urswilke made their first contribution in https://codeberg.org/uzu/strudel/pulls/364 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.5.0...v0.6.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.5.0...v0.6.0 diff --git a/website/src/content/blog/release-0.7.0-zuckerguss.mdx b/website/src/content/blog/release-0.7.0-zuckerguss.mdx index 0a3a4649b..f67deee73 100644 --- a/website/src/content/blog/release-0.7.0-zuckerguss.mdx +++ b/website/src/content/blog/release-0.7.0-zuckerguss.mdx @@ -23,66 +23,66 @@ author: froos ## What's Changed -- pin @csound/browser to 6.18.3 + bump by @felixroos in https://github.com/tidalcycles/strudel/pull/403 -- update csound + fix sound output by @felixroos in https://github.com/tidalcycles/strudel/pull/404 -- fix: share url on subpath by @felixroos in https://github.com/tidalcycles/strudel/pull/405 -- add shabda doc by @felixroos in https://github.com/tidalcycles/strudel/pull/407 -- Update effects.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/410 -- improve effects doc by @felixroos in https://github.com/tidalcycles/strudel/pull/409 -- google gtfo by @felixroos in https://github.com/tidalcycles/strudel/pull/413 -- improve samples doc by @felixroos in https://github.com/tidalcycles/strudel/pull/411 -- PWA with offline support by @felixroos in https://github.com/tidalcycles/strudel/pull/417 -- add caching strategy for missing file types + cache all samples loaded from github by @felixroos in https://github.com/tidalcycles/strudel/pull/419 -- add more offline caching by @felixroos in https://github.com/tidalcycles/strudel/pull/421 -- add cdn.freesound to cache list by @felixroos in https://github.com/tidalcycles/strudel/pull/425 -- minirepl: add keyboard shortcuts by @felixroos in https://github.com/tidalcycles/strudel/pull/429 -- Themes by @felixroos in https://github.com/tidalcycles/strudel/pull/431 -- autocomplete preparations by @felixroos in https://github.com/tidalcycles/strudel/pull/427 -- Fix anchors by @felixroos in https://github.com/tidalcycles/strudel/pull/433 -- Update code.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/436 -- Update mini-notation.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/437 -- Update synths.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/438 -- FIXES: Warning about jsxBracketSameLine deprecation by @bwagner in https://github.com/tidalcycles/strudel/pull/461 -- Composable functions by @yaxu in https://github.com/tidalcycles/strudel/pull/390 -- weave and weaveWith by @yaxu in https://github.com/tidalcycles/strudel/pull/465 -- slice and splice by @yaxu in https://github.com/tidalcycles/strudel/pull/466 -- fix: osc should not return a promise by @felixroos in https://github.com/tidalcycles/strudel/pull/472 -- FIXES: freqs instead of pitches by @bwagner in https://github.com/tidalcycles/strudel/pull/464 -- Update input-output.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/471 -- settings tab with vim / emacs modes + additional themes and fonts by @felixroos in https://github.com/tidalcycles/strudel/pull/467 -- fix: hash links by @felixroos in https://github.com/tidalcycles/strudel/pull/473 -- midi cc support by @felixroos in https://github.com/tidalcycles/strudel/pull/478 -- Fix array args by @felixroos in https://github.com/tidalcycles/strudel/pull/480 -- docs: packages + offline by @felixroos in https://github.com/tidalcycles/strudel/pull/482 -- Update mini-notation.mdx by @yaxu in https://github.com/tidalcycles/strudel/pull/365 -- Revert "Another attempt at composable functions - WIP (#390)" by @felixroos in https://github.com/tidalcycles/strudel/pull/484 -- fix app height by @felixroos in https://github.com/tidalcycles/strudel/pull/485 -- add algolia creds + optimize sidebar for crawling by @felixroos in https://github.com/tidalcycles/strudel/pull/488 -- refactor react package by @felixroos in https://github.com/tidalcycles/strudel/pull/490 -- react style fixes by @felixroos in https://github.com/tidalcycles/strudel/pull/491 -- implement cps in scheduler by @felixroos in https://github.com/tidalcycles/strudel/pull/493 -- Add control aliases by @yaxu in https://github.com/tidalcycles/strudel/pull/497 -- fix: nano-repl highlighting by @felixroos in https://github.com/tidalcycles/strudel/pull/501 -- Reinstate slice and splice by @yaxu in https://github.com/tidalcycles/strudel/pull/500 -- can now use : as a replacement for space in scales by @felixroos in https://github.com/tidalcycles/strudel/pull/502 -- Support list syntax in mininotation by @yaxu in https://github.com/tidalcycles/strudel/pull/512 -- update react to 18 by @felixroos in https://github.com/tidalcycles/strudel/pull/514 -- add arrange function by @felixroos in https://github.com/tidalcycles/strudel/pull/508 -- Update README.md by @bwagner in https://github.com/tidalcycles/strudel/pull/474 -- add 2 illegible fonts by @felixroos in https://github.com/tidalcycles/strudel/pull/518 -- registerSound API + improved sounds tab + regroup soundfonts by @felixroos in https://github.com/tidalcycles/strudel/pull/516 -- fix: envelopes in chrome by @felixroos in https://github.com/tidalcycles/strudel/pull/521 -- Update samples.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/524 -- Update intro.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/525 -- fix(footer): fix link to tidalcycles by @revolunet in https://github.com/tidalcycles/strudel/pull/529 -- FIXES: alias pm for polymeter by @bwagner in https://github.com/tidalcycles/strudel/pull/527 -- Maintain random seed state in parser, not globally by @ijc8 in https://github.com/tidalcycles/strudel/pull/531 -- feat: add freq support to gm soundfonts by @felixroos in https://github.com/tidalcycles/strudel/pull/534 -- Update lerna by @felixroos in https://github.com/tidalcycles/strudel/pull/535 +- pin @csound/browser to 6.18.3 + bump by @felixroos in https://codeberg.org/uzu/strudel/pulls/403 +- update csound + fix sound output by @felixroos in https://codeberg.org/uzu/strudel/pulls/404 +- fix: share url on subpath by @felixroos in https://codeberg.org/uzu/strudel/pulls/405 +- add shabda doc by @felixroos in https://codeberg.org/uzu/strudel/pulls/407 +- Update effects.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/410 +- improve effects doc by @felixroos in https://codeberg.org/uzu/strudel/pulls/409 +- google gtfo by @felixroos in https://codeberg.org/uzu/strudel/pulls/413 +- improve samples doc by @felixroos in https://codeberg.org/uzu/strudel/pulls/411 +- PWA with offline support by @felixroos in https://codeberg.org/uzu/strudel/pulls/417 +- add caching strategy for missing file types + cache all samples loaded from github by @felixroos in https://codeberg.org/uzu/strudel/pulls/419 +- add more offline caching by @felixroos in https://codeberg.org/uzu/strudel/pulls/421 +- add cdn.freesound to cache list by @felixroos in https://codeberg.org/uzu/strudel/pulls/425 +- minirepl: add keyboard shortcuts by @felixroos in https://codeberg.org/uzu/strudel/pulls/429 +- Themes by @felixroos in https://codeberg.org/uzu/strudel/pulls/431 +- autocomplete preparations by @felixroos in https://codeberg.org/uzu/strudel/pulls/427 +- Fix anchors by @felixroos in https://codeberg.org/uzu/strudel/pulls/433 +- Update code.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/436 +- Update mini-notation.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/437 +- Update synths.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/438 +- FIXES: Warning about jsxBracketSameLine deprecation by @bwagner in https://codeberg.org/uzu/strudel/pulls/461 +- Composable functions by @yaxu in https://codeberg.org/uzu/strudel/pulls/390 +- weave and weaveWith by @yaxu in https://codeberg.org/uzu/strudel/pulls/465 +- slice and splice by @yaxu in https://codeberg.org/uzu/strudel/pulls/466 +- fix: osc should not return a promise by @felixroos in https://codeberg.org/uzu/strudel/pulls/472 +- FIXES: freqs instead of pitches by @bwagner in https://codeberg.org/uzu/strudel/pulls/464 +- Update input-output.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/471 +- settings tab with vim / emacs modes + additional themes and fonts by @felixroos in https://codeberg.org/uzu/strudel/pulls/467 +- fix: hash links by @felixroos in https://codeberg.org/uzu/strudel/pulls/473 +- midi cc support by @felixroos in https://codeberg.org/uzu/strudel/pulls/478 +- Fix array args by @felixroos in https://codeberg.org/uzu/strudel/pulls/480 +- docs: packages + offline by @felixroos in https://codeberg.org/uzu/strudel/pulls/482 +- Update mini-notation.mdx by @yaxu in https://codeberg.org/uzu/strudel/pulls/365 +- Revert "Another attempt at composable functions - WIP (#390)" by @felixroos in https://codeberg.org/uzu/strudel/pulls/484 +- fix app height by @felixroos in https://codeberg.org/uzu/strudel/pulls/485 +- add algolia creds + optimize sidebar for crawling by @felixroos in https://codeberg.org/uzu/strudel/pulls/488 +- refactor react package by @felixroos in https://codeberg.org/uzu/strudel/pulls/490 +- react style fixes by @felixroos in https://codeberg.org/uzu/strudel/pulls/491 +- implement cps in scheduler by @felixroos in https://codeberg.org/uzu/strudel/pulls/493 +- Add control aliases by @yaxu in https://codeberg.org/uzu/strudel/pulls/497 +- fix: nano-repl highlighting by @felixroos in https://codeberg.org/uzu/strudel/pulls/501 +- Reinstate slice and splice by @yaxu in https://codeberg.org/uzu/strudel/pulls/500 +- can now use : as a replacement for space in scales by @felixroos in https://codeberg.org/uzu/strudel/pulls/502 +- Support list syntax in mininotation by @yaxu in https://codeberg.org/uzu/strudel/pulls/512 +- update react to 18 by @felixroos in https://codeberg.org/uzu/strudel/pulls/514 +- add arrange function by @felixroos in https://codeberg.org/uzu/strudel/pulls/508 +- Update README.md by @bwagner in https://codeberg.org/uzu/strudel/pulls/474 +- add 2 illegible fonts by @felixroos in https://codeberg.org/uzu/strudel/pulls/518 +- registerSound API + improved sounds tab + regroup soundfonts by @felixroos in https://codeberg.org/uzu/strudel/pulls/516 +- fix: envelopes in chrome by @felixroos in https://codeberg.org/uzu/strudel/pulls/521 +- Update samples.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/524 +- Update intro.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/525 +- fix(footer): fix link to tidalcycles by @revolunet in https://codeberg.org/uzu/strudel/pulls/529 +- FIXES: alias pm for polymeter by @bwagner in https://codeberg.org/uzu/strudel/pulls/527 +- Maintain random seed state in parser, not globally by @ijc8 in https://codeberg.org/uzu/strudel/pulls/531 +- feat: add freq support to gm soundfonts by @felixroos in https://codeberg.org/uzu/strudel/pulls/534 +- Update lerna by @felixroos in https://codeberg.org/uzu/strudel/pulls/535 ## New Contributors -- @revolunet made their first contribution in https://github.com/tidalcycles/strudel/pull/529 -- @ijc8 made their first contribution in https://github.com/tidalcycles/strudel/pull/531 +- @revolunet made their first contribution in https://codeberg.org/uzu/strudel/pulls/529 +- @ijc8 made their first contribution in https://codeberg.org/uzu/strudel/pulls/531 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.6.0...v0.7.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.6.0...v0.7.0 diff --git a/website/src/content/blog/release-0.8.0-himbeermuffin.mdx b/website/src/content/blog/release-0.8.0-himbeermuffin.mdx index 7620140b5..babab5954 100644 --- a/website/src/content/blog/release-0.8.0-himbeermuffin.mdx +++ b/website/src/content/blog/release-0.8.0-himbeermuffin.mdx @@ -82,49 +82,49 @@ A big thanks to all the contributors! ## What's Changed -- fix period key for dvorak + remove duplicated code by @felixroos in https://github.com/tidalcycles/strudel/pull/537 -- improve initial loading + wait before eval by @felixroos in https://github.com/tidalcycles/strudel/pull/538 -- do not reset cps before eval #517 by @felixroos in https://github.com/tidalcycles/strudel/pull/539 -- feat: add loader bar to animate loading state by @felixroos in https://github.com/tidalcycles/strudel/pull/542 -- add firacode font by @felixroos in https://github.com/tidalcycles/strudel/pull/544 -- fix: allow whitespace at the end of a mini pattern by @felixroos in https://github.com/tidalcycles/strudel/pull/547 -- fix: reset time on stop by @felixroos in https://github.com/tidalcycles/strudel/pull/548 -- fix: load soundfonts in prebake by @felixroos in https://github.com/tidalcycles/strudel/pull/550 -- fix: colorable highlighting by @felixroos in https://github.com/tidalcycles/strudel/pull/553 -- fix: make soundfonts import dynamic by @felixroos in https://github.com/tidalcycles/strudel/pull/556 -- add basic triads and guidetone voicings by @felixroos in https://github.com/tidalcycles/strudel/pull/557 -- Patchday by @felixroos in https://github.com/tidalcycles/strudel/pull/559 -- Vanilla JS Refactoring by @felixroos in https://github.com/tidalcycles/strudel/pull/563 -- repl: add option to display line numbers by @roipoussiere in https://github.com/tidalcycles/strudel/pull/582 -- learn/tonal: fix typo in "scaleTran[s]pose" by @srenatus in https://github.com/tidalcycles/strudel/pull/585 -- Music metadata by @roipoussiere in https://github.com/tidalcycles/strudel/pull/580 -- New Workshop by @felixroos in https://github.com/tidalcycles/strudel/pull/587 -- Fix option dot by @felixroos in https://github.com/tidalcycles/strudel/pull/596 -- fix: allow f for flat notes like tidal by @felixroos in https://github.com/tidalcycles/strudel/pull/593 -- fix: division by zero by @felixroos in https://github.com/tidalcycles/strudel/pull/591 -- Solmization added by @dariacotocu in https://github.com/tidalcycles/strudel/pull/570 -- improve cursor by @felixroos in https://github.com/tidalcycles/strudel/pull/597 -- enable auto-completion by @roipoussiere in https://github.com/tidalcycles/strudel/pull/588 -- add ratio function by @felixroos in https://github.com/tidalcycles/strudel/pull/602 -- editor: enable line wrapping by @roipoussiere in https://github.com/tidalcycles/strudel/pull/581 -- tonal fixes by @felixroos in https://github.com/tidalcycles/strudel/pull/607 -- fix: flatten scale lists by @felixroos in https://github.com/tidalcycles/strudel/pull/605 -- clip now works like legato in tidal by @felixroos in https://github.com/tidalcycles/strudel/pull/598 -- fix: doc links by @felixroos in https://github.com/tidalcycles/strudel/pull/612 -- tauri desktop app by @vasilymilovidov in https://github.com/tidalcycles/strudel/pull/613 -- add spiral viz by @felixroos in https://github.com/tidalcycles/strudel/pull/614 -- patterning ui settings by @felixroos in https://github.com/tidalcycles/strudel/pull/606 -- Fix typo on packages.mdx by @paikwiki in https://github.com/tidalcycles/strudel/pull/520 -- cps dependent functions by @felixroos in https://github.com/tidalcycles/strudel/pull/620 -- desktop: play samples from disk by @felixroos in https://github.com/tidalcycles/strudel/pull/621 -- fix: midi clock drift by @felixroos in https://github.com/tidalcycles/strudel/pull/627 +- fix period key for dvorak + remove duplicated code by @felixroos in https://codeberg.org/uzu/strudel/pulls/537 +- improve initial loading + wait before eval by @felixroos in https://codeberg.org/uzu/strudel/pulls/538 +- do not reset cps before eval #517 by @felixroos in https://codeberg.org/uzu/strudel/pulls/539 +- feat: add loader bar to animate loading state by @felixroos in https://codeberg.org/uzu/strudel/pulls/542 +- add firacode font by @felixroos in https://codeberg.org/uzu/strudel/pulls/544 +- fix: allow whitespace at the end of a mini pattern by @felixroos in https://codeberg.org/uzu/strudel/pulls/547 +- fix: reset time on stop by @felixroos in https://codeberg.org/uzu/strudel/pulls/548 +- fix: load soundfonts in prebake by @felixroos in https://codeberg.org/uzu/strudel/pulls/550 +- fix: colorable highlighting by @felixroos in https://codeberg.org/uzu/strudel/pulls/553 +- fix: make soundfonts import dynamic by @felixroos in https://codeberg.org/uzu/strudel/pulls/556 +- add basic triads and guidetone voicings by @felixroos in https://codeberg.org/uzu/strudel/pulls/557 +- Patchday by @felixroos in https://codeberg.org/uzu/strudel/pulls/559 +- Vanilla JS Refactoring by @felixroos in https://codeberg.org/uzu/strudel/pulls/563 +- repl: add option to display line numbers by @roipoussiere in https://codeberg.org/uzu/strudel/pulls/582 +- learn/tonal: fix typo in "scaleTran[s]pose" by @srenatus in https://codeberg.org/uzu/strudel/pulls/585 +- Music metadata by @roipoussiere in https://codeberg.org/uzu/strudel/pulls/580 +- New Workshop by @felixroos in https://codeberg.org/uzu/strudel/pulls/587 +- Fix option dot by @felixroos in https://codeberg.org/uzu/strudel/pulls/596 +- fix: allow f for flat notes like tidal by @felixroos in https://codeberg.org/uzu/strudel/pulls/593 +- fix: division by zero by @felixroos in https://codeberg.org/uzu/strudel/pulls/591 +- Solmization added by @dariacotocu in https://codeberg.org/uzu/strudel/pulls/570 +- improve cursor by @felixroos in https://codeberg.org/uzu/strudel/pulls/597 +- enable auto-completion by @roipoussiere in https://codeberg.org/uzu/strudel/pulls/588 +- add ratio function by @felixroos in https://codeberg.org/uzu/strudel/pulls/602 +- editor: enable line wrapping by @roipoussiere in https://codeberg.org/uzu/strudel/pulls/581 +- tonal fixes by @felixroos in https://codeberg.org/uzu/strudel/pulls/607 +- fix: flatten scale lists by @felixroos in https://codeberg.org/uzu/strudel/pulls/605 +- clip now works like legato in tidal by @felixroos in https://codeberg.org/uzu/strudel/pulls/598 +- fix: doc links by @felixroos in https://codeberg.org/uzu/strudel/pulls/612 +- tauri desktop app by @vasilymilovidov in https://codeberg.org/uzu/strudel/pulls/613 +- add spiral viz by @felixroos in https://codeberg.org/uzu/strudel/pulls/614 +- patterning ui settings by @felixroos in https://codeberg.org/uzu/strudel/pulls/606 +- Fix typo on packages.mdx by @paikwiki in https://codeberg.org/uzu/strudel/pulls/520 +- cps dependent functions by @felixroos in https://codeberg.org/uzu/strudel/pulls/620 +- desktop: play samples from disk by @felixroos in https://codeberg.org/uzu/strudel/pulls/621 +- fix: midi clock drift by @felixroos in https://codeberg.org/uzu/strudel/pulls/627 ## New Contributors -- @roipoussiere made their first contribution in https://github.com/tidalcycles/strudel/pull/582 -- @srenatus made their first contribution in https://github.com/tidalcycles/strudel/pull/585 -- @dariacotocu made their first contribution in https://github.com/tidalcycles/strudel/pull/570 -- @vasilymilovidov made their first contribution in https://github.com/tidalcycles/strudel/pull/613 -- @paikwiki made their first contribution in https://github.com/tidalcycles/strudel/pull/520 +- @roipoussiere made their first contribution in https://codeberg.org/uzu/strudel/pulls/582 +- @srenatus made their first contribution in https://codeberg.org/uzu/strudel/pulls/585 +- @dariacotocu made their first contribution in https://codeberg.org/uzu/strudel/pulls/570 +- @vasilymilovidov made their first contribution in https://codeberg.org/uzu/strudel/pulls/613 +- @paikwiki made their first contribution in https://codeberg.org/uzu/strudel/pulls/520 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.7.0...v0.8.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.7.0...v0.8.0 diff --git a/website/src/content/blog/release-0.9.0-bananenbrot.mdx b/website/src/content/blog/release-0.9.0-bananenbrot.mdx index ae77b4247..944675e38 100644 --- a/website/src/content/blog/release-0.9.0-bananenbrot.mdx +++ b/website/src/content/blog/release-0.9.0-bananenbrot.mdx @@ -29,14 +29,14 @@ Main new features include: Related PRs: -- superdough: encapsulates web audio output by @felixroos in https://github.com/tidalcycles/strudel/pull/664 -- basic fm by @felixroos in https://github.com/tidalcycles/strudel/pull/669 -- Wave Selection and Global Envelope on the FM Synth Modulator by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/683 -- control osc partial count with n by @felixroos in https://github.com/tidalcycles/strudel/pull/674 -- ZZFX Synth support by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/684 -- Adding filter envelopes and filter order selection by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/692 -- Adding loop points and thus wavetable synthesis by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/698 -- Adding vibrato to base oscillators by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/693 +- superdough: encapsulates web audio output by @felixroos in https://codeberg.org/uzu/strudel/pulls/664 +- basic fm by @felixroos in https://codeberg.org/uzu/strudel/pulls/669 +- Wave Selection and Global Envelope on the FM Synth Modulator by @Bubobubobubobubo in https://codeberg.org/uzu/strudel/pulls/683 +- control osc partial count with n by @felixroos in https://codeberg.org/uzu/strudel/pulls/674 +- ZZFX Synth support by @Bubobubobubobubo in https://codeberg.org/uzu/strudel/pulls/684 +- Adding filter envelopes and filter order selection by @Bubobubobubobubo in https://codeberg.org/uzu/strudel/pulls/692 +- Adding loop points and thus wavetable synthesis by @Bubobubobubobubo in https://codeberg.org/uzu/strudel/pulls/698 +- Adding vibrato to base oscillators by @Bubobubobubobubo in https://codeberg.org/uzu/strudel/pulls/693 ## Desktop App Improvements @@ -45,70 +45,70 @@ which do not depend on browser APIs! You can see superdough, superdirt via OSC + hardware synths via MIDI all together playing in harmony in this [awesome video](https://www.youtube.com/watch?v=lxQgBeLQBgk). These are the related PRs: -- Create Midi Integration for Tauri Desktop app by @daslyfe in https://github.com/tidalcycles/strudel/pull/685 -- add sleep timer + improve message iterating by @daslyfe in https://github.com/tidalcycles/strudel/pull/688 -- fix MIDI CC messages by @vasilymilovidov in https://github.com/tidalcycles/strudel/pull/690 -- Direct OSC Support in Tauri by @daslyfe in https://github.com/tidalcycles/strudel/pull/694 -- Add logging from tauri by @daslyfe in https://github.com/tidalcycles/strudel/pull/697 -- fix osc bundle timestamp glitches caused by drifting clock by @daslyfe in https://github.com/tidalcycles/strudel/pull/666 -- Midi time fixes by @daslyfe in https://github.com/tidalcycles/strudel/pull/668 -- [Bug Fix] Account for numeral notation when converting to midi by @daslyfe in https://github.com/tidalcycles/strudel/pull/656 -- [Bug Fix] Midi: Don't treat note 0 as false by @daslyfe in https://github.com/tidalcycles/strudel/pull/657 +- Create Midi Integration for Tauri Desktop app by @daslyfe in https://codeberg.org/uzu/strudel/pulls/685 +- add sleep timer + improve message iterating by @daslyfe in https://codeberg.org/uzu/strudel/pulls/688 +- fix MIDI CC messages by @vasilymilovidov in https://codeberg.org/uzu/strudel/pulls/690 +- Direct OSC Support in Tauri by @daslyfe in https://codeberg.org/uzu/strudel/pulls/694 +- Add logging from tauri by @daslyfe in https://codeberg.org/uzu/strudel/pulls/697 +- fix osc bundle timestamp glitches caused by drifting clock by @daslyfe in https://codeberg.org/uzu/strudel/pulls/666 +- Midi time fixes by @daslyfe in https://codeberg.org/uzu/strudel/pulls/668 +- [Bug Fix] Account for numeral notation when converting to midi by @daslyfe in https://codeberg.org/uzu/strudel/pulls/656 +- [Bug Fix] Midi: Don't treat note 0 as false by @daslyfe in https://codeberg.org/uzu/strudel/pulls/657 ## Visuals -- 2 new FFT based vizualisations have now landed: [scope and fscope](https://github.com/tidalcycles/strudel/pull/677) (featured in the video at the top). -- pianoroll has new options, see [PR](https://github.com/tidalcycles/strudel/pull/679) +- 2 new FFT based vizualisations have now landed: [scope and fscope](https://codeberg.org/uzu/strudel/pulls/677) (featured in the video at the top). +- pianoroll has new options, see [PR](https://codeberg.org/uzu/strudel/pulls/679) Related PRs: -- Scope by @felixroos in https://github.com/tidalcycles/strudel/pull/677 ([demo](https://strudel.tidalcycles.org/?hXVQF-KxMI8p)) -- Pianoroll improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/679 ([demo](https://strudel.tidalcycles.org/?aPMKqXGVMgSM)) +- Scope by @felixroos in https://codeberg.org/uzu/strudel/pulls/677 ([demo](https://strudel.tidalcycles.org/?hXVQF-KxMI8p)) +- Pianoroll improvements by @felixroos in https://codeberg.org/uzu/strudel/pulls/679 ([demo](https://strudel.tidalcycles.org/?aPMKqXGVMgSM)) ## Voicings There is now a new way to play chord voicings + a huge selection of chord voicings available. Find out more in these PRs: -- stateless voicings + tonleiter lib by @felixroos in https://github.com/tidalcycles/strudel/pull/647 ([demo](https://strudel.tidalcycles.org/?FoILM0Hs9y9f)) -- ireal voicings by @felixroos in https://github.com/tidalcycles/strudel/pull/653 ([demo](https://strudel.tidalcycles.org/?bv_TjY9hOC28)) +- stateless voicings + tonleiter lib by @felixroos in https://codeberg.org/uzu/strudel/pulls/647 ([demo](https://strudel.tidalcycles.org/?FoILM0Hs9y9f)) +- ireal voicings by @felixroos in https://codeberg.org/uzu/strudel/pulls/653 ([demo](https://strudel.tidalcycles.org/?bv_TjY9hOC28)) ## Adaptive Highlighting Thanks to @mindofmatthew , the highlighting will adapt to edits instantly! Related PRs: -- More work on highlight IDs by @mindofmatthew in https://github.com/tidalcycles/strudel/pull/636 -- Adaptive Highlighting by @felixroos in https://github.com/tidalcycles/strudel/pull/634 +- More work on highlight IDs by @mindofmatthew in https://codeberg.org/uzu/strudel/pulls/636 +- Adaptive Highlighting by @felixroos in https://codeberg.org/uzu/strudel/pulls/634 ## UI Changes -- teletext theme + fonts by @felixroos in https://github.com/tidalcycles/strudel/pull/681 (featured in video at the top) -- togglable panel position by @felixroos in https://github.com/tidalcycles/strudel/pull/667 +- teletext theme + fonts by @felixroos in https://codeberg.org/uzu/strudel/pulls/681 (featured in video at the top) +- togglable panel position by @felixroos in https://codeberg.org/uzu/strudel/pulls/667 ## Other New Features -- slice: list mode by @felixroos in https://github.com/tidalcycles/strudel/pull/645 ([demo](https://strudel.tidalcycles.org/?bAYIqz5NLjRr)) -- add emoji support by @felixroos in https://github.com/tidalcycles/strudel/pull/680 ([demo](https://strudel.tidalcycles.org/?a6FgLz475gN9)) +- slice: list mode by @felixroos in https://codeberg.org/uzu/strudel/pulls/645 ([demo](https://strudel.tidalcycles.org/?bAYIqz5NLjRr)) +- add emoji support by @felixroos in https://codeberg.org/uzu/strudel/pulls/680 ([demo](https://strudel.tidalcycles.org/?a6FgLz475gN9)) ## Articles -- Understand pitch by @felixroos in https://github.com/tidalcycles/strudel/pull/652 +- Understand pitch by @felixroos in https://codeberg.org/uzu/strudel/pulls/652 ## Other Fixes & Enhancements -- fix: out of range error by @felixroos in https://github.com/tidalcycles/strudel/pull/630 -- fix: update canvas size on window resize by @felixroos in https://github.com/tidalcycles/strudel/pull/631 -- FIXES: TODO in rotateChroma by @bwagner in https://github.com/tidalcycles/strudel/pull/650 -- snapshot tests: sort haps by part by @felixroos in https://github.com/tidalcycles/strudel/pull/637 -- Delete old packages by @felixroos in https://github.com/tidalcycles/strudel/pull/639 -- update vitest by @felixroos in https://github.com/tidalcycles/strudel/pull/651 -- fix: welcome message for latestCode by @felixroos in https://github.com/tidalcycles/strudel/pull/659 -- fix: always run previous trigger by @felixroos in https://github.com/tidalcycles/strudel/pull/660 +- fix: out of range error by @felixroos in https://codeberg.org/uzu/strudel/pulls/630 +- fix: update canvas size on window resize by @felixroos in https://codeberg.org/uzu/strudel/pulls/631 +- FIXES: TODO in rotateChroma by @bwagner in https://codeberg.org/uzu/strudel/pulls/650 +- snapshot tests: sort haps by part by @felixroos in https://codeberg.org/uzu/strudel/pulls/637 +- Delete old packages by @felixroos in https://codeberg.org/uzu/strudel/pulls/639 +- update vitest by @felixroos in https://codeberg.org/uzu/strudel/pulls/651 +- fix: welcome message for latestCode by @felixroos in https://codeberg.org/uzu/strudel/pulls/659 +- fix: always run previous trigger by @felixroos in https://codeberg.org/uzu/strudel/pulls/660 ## New Contributors -- @daslyfe made their first contribution in https://github.com/tidalcycles/strudel/pull/656 -- @Bubobubobubobubo made their first contribution in https://github.com/tidalcycles/strudel/pull/683 +- @daslyfe made their first contribution in https://codeberg.org/uzu/strudel/pulls/656 +- @Bubobubobubobubo made their first contribution in https://codeberg.org/uzu/strudel/pulls/683 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.8.0...v0.9.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.8.0...v0.9.0 A big thanks to all the contributors! diff --git a/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx b/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx index 5784fe52d..0ee149a20 100644 --- a/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx +++ b/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx @@ -27,13 +27,13 @@ Let me write up some of the highlights: This version changes the default cps value from 1 to 0.5 to give patterns a little bit more time by default. If you find your existing patterns to be suddenly half the speed, just add a `setcps(1)` to the top and it should sound as it did before! -- make 0.5hz cps the default by @yaxu in https://github.com/tidalcycles/strudel/pull/931 +- make 0.5hz cps the default by @yaxu in https://codeberg.org/uzu/strudel/pulls/931 ## New Domain Strudel is now available under [strudel.cc](https://strudel.cc/). The old domain still works but you might not get the most recent version. -- replace strudel.tidalcycles.org with strudel.cc by @felixroos in https://github.com/tidalcycles/strudel/pull/768 +- replace strudel.tidalcycles.org with strudel.cc by @felixroos in https://codeberg.org/uzu/strudel/pulls/768 ## Strudel on Mastodon @@ -43,52 +43,52 @@ Strudel now has a mastodon presence: https://social.toplap.org/@strudel superdough, the audio engine of strudel has gotten some new features: -- Create phaser effect by @daslyfe in https://github.com/tidalcycles/strudel/pull/798 -- Multichannel audio by @daslyfe in https://github.com/tidalcycles/strudel/pull/820 -- Audio device selection by @daslyfe in https://github.com/tidalcycles/strudel/pull/854 -- Better convolution reverb by generating impulse responses by @Bubobubobubobubo and @felixroos in https://github.com/tidalcycles/strudel/pull/718 -- Add 'white', 'pink' and 'brown' oscillators + refactor synth by @Bubobubobubobubo and @felixroos in https://github.com/tidalcycles/strudel/pull/713 -- New noise type: "crackle" by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/806 -- Add support for using samples as impulse response buffers for the reverb by @vasilymilovidov in https://github.com/tidalcycles/strudel/pull/717 -- Compressor by @felixroos in https://github.com/tidalcycles/strudel/pull/729 -- Adding vibrato to Superdough sampler by @Bubobubobubobubo and @felixroos in https://github.com/tidalcycles/strudel/pull/706 -- Further Envelope improvements by @daslyfe in https://github.com/tidalcycles/strudel/pull/868 -- Add more vowel qualities for the vowels function by @fnordomat in https://github.com/tidalcycles/strudel/pull/907 -- pitch envelope by @felixroos in https://github.com/tidalcycles/strudel/pull/913 +- Create phaser effect by @daslyfe in https://codeberg.org/uzu/strudel/pulls/798 +- Multichannel audio by @daslyfe in https://codeberg.org/uzu/strudel/pulls/820 +- Audio device selection by @daslyfe in https://codeberg.org/uzu/strudel/pulls/854 +- Better convolution reverb by generating impulse responses by @Bubobubobubobubo and @felixroos in https://codeberg.org/uzu/strudel/pulls/718 +- Add 'white', 'pink' and 'brown' oscillators + refactor synth by @Bubobubobubobubo and @felixroos in https://codeberg.org/uzu/strudel/pulls/713 +- New noise type: "crackle" by @Bubobubobubobubo in https://codeberg.org/uzu/strudel/pulls/806 +- Add support for using samples as impulse response buffers for the reverb by @vasilymilovidov in https://codeberg.org/uzu/strudel/pulls/717 +- Compressor by @felixroos in https://codeberg.org/uzu/strudel/pulls/729 +- Adding vibrato to Superdough sampler by @Bubobubobubobubo and @felixroos in https://codeberg.org/uzu/strudel/pulls/706 +- Further Envelope improvements by @daslyfe in https://codeberg.org/uzu/strudel/pulls/868 +- Add more vowel qualities for the vowels function by @fnordomat in https://codeberg.org/uzu/strudel/pulls/907 +- pitch envelope by @felixroos in https://codeberg.org/uzu/strudel/pulls/913 ## Slider Controls The new `slider` function inlines a draggable slider element into the code, bridging the gap between code and GUI. -- widgets by @felixroos in https://github.com/tidalcycles/strudel/pull/714 -- Slider afterthoughts by @felixroos in https://github.com/tidalcycles/strudel/pull/723 -- add xfade by @felixroos in https://github.com/tidalcycles/strudel/pull/780 +- widgets by @felixroos in https://codeberg.org/uzu/strudel/pulls/714 +- Slider afterthoughts by @felixroos in https://codeberg.org/uzu/strudel/pulls/723 +- add xfade by @felixroos in https://codeberg.org/uzu/strudel/pulls/780 ## Improved MIDI integration Pattern params [can now be controlled with cc messages](https://www.youtube.com/watch?v=e2-Sv_jjDQk) + you can now send a MIDI clock to sync your DAW with strudel. -- Midi in by @felixroos in https://github.com/tidalcycles/strudel/pull/699 -- add midi clock support by @felixroos in https://github.com/tidalcycles/strudel/pull/710 +- Midi in by @felixroos in https://codeberg.org/uzu/strudel/pulls/699 +- add midi clock support by @felixroos in https://codeberg.org/uzu/strudel/pulls/710 ## hydra [hydra](https://hydra.ojack.xyz), the live coding video synth [can now be used directly inside the strudel REPL](https://strudel.cc/learn/hydra/). -- Hydra integration by @felixroos in https://github.com/tidalcycles/strudel/pull/759 -- add options param to initHydra by @kasparsj in https://github.com/tidalcycles/strudel/pull/808 -- Hydra fixes and improvements by @atfornes in https://github.com/tidalcycles/strudel/pull/818 +- Hydra integration by @felixroos in https://codeberg.org/uzu/strudel/pulls/759 +- add options param to initHydra by @kasparsj in https://codeberg.org/uzu/strudel/pulls/808 +- Hydra fixes and improvements by @atfornes in https://codeberg.org/uzu/strudel/pulls/818 ## Vanilla REPL The codemirror editor and the repl abstraction have been refactored from react to vanilla JS! This should give some performance improvements and less dependency / maintenance burden: -- Vanilla repl 2 by @felixroos in https://github.com/tidalcycles/strudel/pull/863 -- Vanilla repl 3 by @felixroos in https://github.com/tidalcycles/strudel/pull/865 -- more work on vanilla repl: repl web component + package + MicroRepl by @felixroos in https://github.com/tidalcycles/strudel/pull/866 -- main repl vanillification by @felixroos in https://github.com/tidalcycles/strudel/pull/873 -- final vanillification by @felixroos in https://github.com/tidalcycles/strudel/pull/876 +- Vanilla repl 2 by @felixroos in https://codeberg.org/uzu/strudel/pulls/863 +- Vanilla repl 3 by @felixroos in https://codeberg.org/uzu/strudel/pulls/865 +- more work on vanilla repl: repl web component + package + MicroRepl by @felixroos in https://codeberg.org/uzu/strudel/pulls/866 +- main repl vanillification by @felixroos in https://codeberg.org/uzu/strudel/pulls/873 +- final vanillification by @felixroos in https://codeberg.org/uzu/strudel/pulls/876 ## Doc Changes @@ -97,25 +97,25 @@ Plenty of things have been added to the docs, including a [showcase of what peop
show PRs -- Showcase by @felixroos in https://github.com/tidalcycles/strudel/pull/885 -- Recipes by @felixroos in https://github.com/tidalcycles/strudel/pull/742 -- Document striate function by @ilesinge in https://github.com/tidalcycles/strudel/pull/766 -- Document adsr function by @ilesinge in https://github.com/tidalcycles/strudel/pull/767 -- Add function params in reference tab by @ilesinge in https://github.com/tidalcycles/strudel/pull/785 -- Update first-sounds.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/794 -- Update recap.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/797 -- Update pattern-effects.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/796 -- Update first-effects.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/795 -- Document pianoroll by @ilesinge in https://github.com/tidalcycles/strudel/pull/784 -- Add doc for euclidLegatoRot, wordfall and slider by @ilesinge in https://github.com/tidalcycles/strudel/pull/801 -- Improve documentation for synonym functions by @ilesinge in https://github.com/tidalcycles/strudel/pull/800 -- Add and style algolia search by @ilesinge in https://github.com/tidalcycles/strudel/pull/827 -- Fix a typo by @drewgbarnes in https://github.com/tidalcycles/strudel/pull/830 -- add mastodon link by @felixroos in https://github.com/tidalcycles/strudel/pull/884 -- adds a blog by @felixroos in https://github.com/tidalcycles/strudel/pull/911 -- community bakery by @felixroos in https://github.com/tidalcycles/strudel/pull/923 -- Blog improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/919 -- 2 years blog post by @felixroos in https://github.com/tidalcycles/strudel/pull/929 +- Showcase by @felixroos in https://codeberg.org/uzu/strudel/pulls/885 +- Recipes by @felixroos in https://codeberg.org/uzu/strudel/pulls/742 +- Document striate function by @ilesinge in https://codeberg.org/uzu/strudel/pulls/766 +- Document adsr function by @ilesinge in https://codeberg.org/uzu/strudel/pulls/767 +- Add function params in reference tab by @ilesinge in https://codeberg.org/uzu/strudel/pulls/785 +- Update first-sounds.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/794 +- Update recap.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/797 +- Update pattern-effects.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/796 +- Update first-effects.mdx by @bwagner in https://codeberg.org/uzu/strudel/pulls/795 +- Document pianoroll by @ilesinge in https://codeberg.org/uzu/strudel/pulls/784 +- Add doc for euclidLegatoRot, wordfall and slider by @ilesinge in https://codeberg.org/uzu/strudel/pulls/801 +- Improve documentation for synonym functions by @ilesinge in https://codeberg.org/uzu/strudel/pulls/800 +- Add and style algolia search by @ilesinge in https://codeberg.org/uzu/strudel/pulls/827 +- Fix a typo by @drewgbarnes in https://codeberg.org/uzu/strudel/pulls/830 +- add mastodon link by @felixroos in https://codeberg.org/uzu/strudel/pulls/884 +- adds a blog by @felixroos in https://codeberg.org/uzu/strudel/pulls/911 +- community bakery by @felixroos in https://codeberg.org/uzu/strudel/pulls/923 +- Blog improvements by @felixroos in https://codeberg.org/uzu/strudel/pulls/919 +- 2 years blog post by @felixroos in https://codeberg.org/uzu/strudel/pulls/929
@@ -124,34 +124,34 @@ Plenty of things have been added to the docs, including a [showcase of what peop
There is a lot more -- mini notation: international alphabets support by @ilesinge in https://github.com/tidalcycles/strudel/pull/751 -- Add shabda shortcut by @ilesinge in https://github.com/tidalcycles/strudel/pull/740 -- add play function by @felixroos in https://github.com/tidalcycles/strudel/pull/758 (superseded by next) -- tidal style d1 ... d9 functions + more by @felixroos in https://github.com/tidalcycles/strudel/pull/805 -- add vscode bindings by @Dsm0 in https://github.com/tidalcycles/strudel/pull/773 -- Implement optional hover tooltip with function documentation by @ilesinge in https://github.com/tidalcycles/strudel/pull/783 -- samples loading shortcuts: by @felixroos in https://github.com/tidalcycles/strudel/pull/788 -- add option to disable active line highlighting in Code Settings by @kasparsj in https://github.com/tidalcycles/strudel/pull/804 -- Color hsl by @felixroos in https://github.com/tidalcycles/strudel/pull/815 -- Patterns tab + Refactor Panel by @felixroos in https://github.com/tidalcycles/strudel/pull/769 -- patterns tab: import patterns + style by @felixroos in https://github.com/tidalcycles/strudel/pull/852 -- Export patterns + ui tweaks by @felixroos in https://github.com/tidalcycles/strudel/pull/855 -- Pattern organization by @felixroos in https://github.com/tidalcycles/strudel/pull/858 -- Sound Import from local file system by @daslyfe in https://github.com/tidalcycles/strudel/pull/839 -- bugfix: suspend and close existing audio context when changing interface by @daslyfe in https://github.com/tidalcycles/strudel/pull/882 -- add root mode for voicings by @felixroos in https://github.com/tidalcycles/strudel/pull/887 -- scales can now be anchored by @felixroos in https://github.com/tidalcycles/strudel/pull/888 -- add dough function for raw dsp by @felixroos in https://github.com/tidalcycles/strudel/pull/707 (experimental) -- support mininotation '..' range operator, fixes #715 by @yaxu in https://github.com/tidalcycles/strudel/pull/716 -- Add pick and squeeze functions by @daslyfe in https://github.com/tidalcycles/strudel/pull/771 -- support , in < > by @felixroos in https://github.com/tidalcycles/strudel/pull/886 -- public sharing by @felixroos in https://github.com/tidalcycles/strudel/pull/910 -- pick, pickmod, inhabit, inhabitmod by @yaxu in https://github.com/tidalcycles/strudel/pull/921 -- Mini-notation additions towards tidal compatibility by @yaxu in https://github.com/tidalcycles/strudel/pull/926 -- add pickF and pickmodF by @geikha in https://github.com/tidalcycles/strudel/pull/924 -- Make splice cps-aware by @yaxu in https://github.com/tidalcycles/strudel/pull/932 -- Refactor cps functions by @felixroos in https://github.com/tidalcycles/strudel/pull/933 -- Add useful pattern selection behavior for performing. by @daslyfe in https://github.com/tidalcycles/strudel/pull/897 +- mini notation: international alphabets support by @ilesinge in https://codeberg.org/uzu/strudel/pulls/751 +- Add shabda shortcut by @ilesinge in https://codeberg.org/uzu/strudel/pulls/740 +- add play function by @felixroos in https://codeberg.org/uzu/strudel/pulls/758 (superseded by next) +- tidal style d1 ... d9 functions + more by @felixroos in https://codeberg.org/uzu/strudel/pulls/805 +- add vscode bindings by @Dsm0 in https://codeberg.org/uzu/strudel/pulls/773 +- Implement optional hover tooltip with function documentation by @ilesinge in https://codeberg.org/uzu/strudel/pulls/783 +- samples loading shortcuts: by @felixroos in https://codeberg.org/uzu/strudel/pulls/788 +- add option to disable active line highlighting in Code Settings by @kasparsj in https://codeberg.org/uzu/strudel/pulls/804 +- Color hsl by @felixroos in https://codeberg.org/uzu/strudel/pulls/815 +- Patterns tab + Refactor Panel by @felixroos in https://codeberg.org/uzu/strudel/pulls/769 +- patterns tab: import patterns + style by @felixroos in https://codeberg.org/uzu/strudel/pulls/852 +- Export patterns + ui tweaks by @felixroos in https://codeberg.org/uzu/strudel/pulls/855 +- Pattern organization by @felixroos in https://codeberg.org/uzu/strudel/pulls/858 +- Sound Import from local file system by @daslyfe in https://codeberg.org/uzu/strudel/pulls/839 +- bugfix: suspend and close existing audio context when changing interface by @daslyfe in https://codeberg.org/uzu/strudel/pulls/882 +- add root mode for voicings by @felixroos in https://codeberg.org/uzu/strudel/pulls/887 +- scales can now be anchored by @felixroos in https://codeberg.org/uzu/strudel/pulls/888 +- add dough function for raw dsp by @felixroos in https://codeberg.org/uzu/strudel/pulls/707 (experimental) +- support mininotation '..' range operator, fixes #715 by @yaxu in https://codeberg.org/uzu/strudel/pulls/716 +- Add pick and squeeze functions by @daslyfe in https://codeberg.org/uzu/strudel/pulls/771 +- support , in < > by @felixroos in https://codeberg.org/uzu/strudel/pulls/886 +- public sharing by @felixroos in https://codeberg.org/uzu/strudel/pulls/910 +- pick, pickmod, inhabit, inhabitmod by @yaxu in https://codeberg.org/uzu/strudel/pulls/921 +- Mini-notation additions towards tidal compatibility by @yaxu in https://codeberg.org/uzu/strudel/pulls/926 +- add pickF and pickmodF by @geikha in https://codeberg.org/uzu/strudel/pulls/924 +- Make splice cps-aware by @yaxu in https://codeberg.org/uzu/strudel/pulls/932 +- Refactor cps functions by @felixroos in https://codeberg.org/uzu/strudel/pulls/933 +- Add useful pattern selection behavior for performing. by @daslyfe in https://codeberg.org/uzu/strudel/pulls/897
@@ -160,79 +160,79 @@ Plenty of things have been added to the docs, including a [showcase of what peop
show -- fix: finally repair envelopes by @felixroos in https://github.com/tidalcycles/strudel/pull/861 -- fix: reverb regenerate loophole by @felixroos in https://github.com/tidalcycles/strudel/pull/726 -- fix: reverb roomsize not required by @felixroos in https://github.com/tidalcycles/strudel/pull/731 -- fix: reverb sampleRate by @felixroos in https://github.com/tidalcycles/strudel/pull/732 -- consume n with scale by @felixroos in https://github.com/tidalcycles/strudel/pull/727 -- fix: hashes in urls by @felixroos in https://github.com/tidalcycles/strudel/pull/728 -- [Bug Fix] chooseWith: prevent pattern from stopping audio when selection is >= 1 or < 0 by @daslyfe in https://github.com/tidalcycles/strudel/pull/741 -- Fix addivite synthesis phases by @felixroos in https://github.com/tidalcycles/strudel/pull/762 -- fix: scale offset by @felixroos in https://github.com/tidalcycles/strudel/pull/764 -- fix zen mode logo overlap by @felixroos in https://github.com/tidalcycles/strudel/pull/760 -- fix: share copy to clipboard + alert by @felixroos in https://github.com/tidalcycles/strudel/pull/774 -- fix: style issues by @felixroos in https://github.com/tidalcycles/strudel/pull/781 -- Fix scope pos + document by @felixroos in https://github.com/tidalcycles/strudel/pull/786 -- don't use anchor links for reference by @felixroos in https://github.com/tidalcycles/strudel/pull/791 -- remove unwanted cm6 outline for strudelTheme by @kasparsj in https://github.com/tidalcycles/strudel/pull/802 -- FIXES: palindrome abc -> abccba by @bwagner in https://github.com/tidalcycles/strudel/pull/831 -- Bug Fix #119: Clock drift by @daslyfe in https://github.com/tidalcycles/strudel/pull/874 -- bugfix: sound select indexes out of bounds by @daslyfe in https://github.com/tidalcycles/strudel/pull/871 -- Error tolerance by @felixroos in https://github.com/tidalcycles/strudel/pull/880 -- fix: make sure n is never undefined before nanFallback by @felixroos in https://github.com/tidalcycles/strudel/pull/881 -- fix: invisible selection on vim + emacs mode by @felixroos in https://github.com/tidalcycles/strudel/pull/889 -- fix: autocomplete / tooltip code example bug by @felixroos in https://github.com/tidalcycles/strudel/pull/898 -- Fix examples page, piano() and a few workshop imgs by @shiyouganai in https://github.com/tidalcycles/strudel/pull/848 -- fix: trailing slash confusion by @felixroos in https://github.com/tidalcycles/strudel/pull/743 -- fix: try different trailing slash behavior by @felixroos in https://github.com/tidalcycles/strudel/pull/744 -- Fix krill build command in README by @ilesinge in https://github.com/tidalcycles/strudel/pull/748 -- Fix for #1. Enables named instruments for csoundm. by @gogins in https://github.com/tidalcycles/strudel/pull/662 -- fix: missing hash for links starting with / by @felixroos in https://github.com/tidalcycles/strudel/pull/845 -- fix: swatch png src by @felixroos in https://github.com/tidalcycles/strudel/pull/846 -- Fix edge case with rehype-urls and trailing slashes in image file paths by @shiyouganai in https://github.com/tidalcycles/strudel/pull/849 -- fix: multiple repls by @felixroos in https://github.com/tidalcycles/strudel/pull/813 -- Fix chunk, add fastChunk and repeatCycles by @yaxu in https://github.com/tidalcycles/strudel/pull/712 -- Update tauri.yml workflow file by @vasilymilovidov in https://github.com/tidalcycles/strudel/pull/705 -- vite-vanilla-repl readme fix by @felixroos in https://github.com/tidalcycles/strudel/pull/737 -- completely revert config mess by @felixroos in https://github.com/tidalcycles/strudel/pull/745 -- hopefully fix trailing slashes bug by @felixroos in https://github.com/tidalcycles/strudel/pull/753 -- Update vite pwa by @felixroos in https://github.com/tidalcycles/strudel/pull/772 -- Update to Astro 3 by @felixroos in https://github.com/tidalcycles/strudel/pull/775 -- support multiple named serial connections, change default baudrate by @yaxu in https://github.com/tidalcycles/strudel/pull/551 -- CHANGES: github action checkout v2 -> v4 by @bwagner in https://github.com/tidalcycles/strudel/pull/837 -- CHANGES: pin pnpm to version 8.3.1 by @bwagner in https://github.com/tidalcycles/strudel/pull/834 -- CHANGES: github action pnpm version from 7 to 8.3.1 by @bwagner in https://github.com/tidalcycles/strudel/pull/835 -- ADDS: JetBrains IDE files and directories to .gitignore by @bwagner in https://github.com/tidalcycles/strudel/pull/840 -- Prevent 404 on Algolia crawls by @ilesinge in https://github.com/tidalcycles/strudel/pull/838 -- Add in fixes from my fork to slashocalypse branch by @shiyouganai in https://github.com/tidalcycles/strudel/pull/843 -- improve slashing + base href behavior by @felixroos in https://github.com/tidalcycles/strudel/pull/842 -- CHANGES: pnpm 8.1.3 to 8.11.0 by @bwagner in https://github.com/tidalcycles/strudel/pull/850 -- add missing trailing slashes by @felixroos in https://github.com/tidalcycles/strudel/pull/860 -- move all examples to separate examples folder by @felixroos in https://github.com/tidalcycles/strudel/pull/878 -- Dependency update by @felixroos in https://github.com/tidalcycles/strudel/pull/879 -- Update Vite version so hot reload works properly with newest pnpm version by @daslyfe in https://github.com/tidalcycles/strudel/pull/892 -- prevent vite from complaining about additional exports in jsx files by @daslyfe in https://github.com/tidalcycles/strudel/pull/891 -- fix some build warnings by @felixroos in https://github.com/tidalcycles/strudel/pull/902 -- Remove hideHeader for better mobile UI and consistency by @rjulian in https://github.com/tidalcycles/strudel/pull/894 -- Fix: swatch/[name].png.js static path by @oscarbyrne in https://github.com/tidalcycles/strudel/pull/916 -- rename @strudel.cycles/_ packages to @strudel/_ by @felixroos in https://github.com/tidalcycles/strudel/pull/917 -- `pick` now accepts lookup tables, with alternate cycle squeezing behaviour as new `inhabit` function by @yaxu in https://github.com/tidalcycles/strudel/pull/918 -- Revert "`pick` now accepts lookup tables, with alternate cycle squeezing behaviour as new `inhabit` function" by @yaxu in https://github.com/tidalcycles/strudel/pull/920 -- Fix pattern tab not showing patterns without created date by @daslyfe in https://github.com/tidalcycles/strudel/pull/934 +- fix: finally repair envelopes by @felixroos in https://codeberg.org/uzu/strudel/pulls/861 +- fix: reverb regenerate loophole by @felixroos in https://codeberg.org/uzu/strudel/pulls/726 +- fix: reverb roomsize not required by @felixroos in https://codeberg.org/uzu/strudel/pulls/731 +- fix: reverb sampleRate by @felixroos in https://codeberg.org/uzu/strudel/pulls/732 +- consume n with scale by @felixroos in https://codeberg.org/uzu/strudel/pulls/727 +- fix: hashes in urls by @felixroos in https://codeberg.org/uzu/strudel/pulls/728 +- [Bug Fix] chooseWith: prevent pattern from stopping audio when selection is >= 1 or < 0 by @daslyfe in https://codeberg.org/uzu/strudel/pulls/741 +- Fix addivite synthesis phases by @felixroos in https://codeberg.org/uzu/strudel/pulls/762 +- fix: scale offset by @felixroos in https://codeberg.org/uzu/strudel/pulls/764 +- fix zen mode logo overlap by @felixroos in https://codeberg.org/uzu/strudel/pulls/760 +- fix: share copy to clipboard + alert by @felixroos in https://codeberg.org/uzu/strudel/pulls/774 +- fix: style issues by @felixroos in https://codeberg.org/uzu/strudel/pulls/781 +- Fix scope pos + document by @felixroos in https://codeberg.org/uzu/strudel/pulls/786 +- don't use anchor links for reference by @felixroos in https://codeberg.org/uzu/strudel/pulls/791 +- remove unwanted cm6 outline for strudelTheme by @kasparsj in https://codeberg.org/uzu/strudel/pulls/802 +- FIXES: palindrome abc -> abccba by @bwagner in https://codeberg.org/uzu/strudel/pulls/831 +- Bug Fix #119: Clock drift by @daslyfe in https://codeberg.org/uzu/strudel/pulls/874 +- bugfix: sound select indexes out of bounds by @daslyfe in https://codeberg.org/uzu/strudel/pulls/871 +- Error tolerance by @felixroos in https://codeberg.org/uzu/strudel/pulls/880 +- fix: make sure n is never undefined before nanFallback by @felixroos in https://codeberg.org/uzu/strudel/pulls/881 +- fix: invisible selection on vim + emacs mode by @felixroos in https://codeberg.org/uzu/strudel/pulls/889 +- fix: autocomplete / tooltip code example bug by @felixroos in https://codeberg.org/uzu/strudel/pulls/898 +- Fix examples page, piano() and a few workshop imgs by @shiyouganai in https://codeberg.org/uzu/strudel/pulls/848 +- fix: trailing slash confusion by @felixroos in https://codeberg.org/uzu/strudel/pulls/743 +- fix: try different trailing slash behavior by @felixroos in https://codeberg.org/uzu/strudel/pulls/744 +- Fix krill build command in README by @ilesinge in https://codeberg.org/uzu/strudel/pulls/748 +- Fix for #1. Enables named instruments for csoundm. by @gogins in https://codeberg.org/uzu/strudel/pulls/662 +- fix: missing hash for links starting with / by @felixroos in https://codeberg.org/uzu/strudel/pulls/845 +- fix: swatch png src by @felixroos in https://codeberg.org/uzu/strudel/pulls/846 +- Fix edge case with rehype-urls and trailing slashes in image file paths by @shiyouganai in https://codeberg.org/uzu/strudel/pulls/849 +- fix: multiple repls by @felixroos in https://codeberg.org/uzu/strudel/pulls/813 +- Fix chunk, add fastChunk and repeatCycles by @yaxu in https://codeberg.org/uzu/strudel/pulls/712 +- Update tauri.yml workflow file by @vasilymilovidov in https://codeberg.org/uzu/strudel/pulls/705 +- vite-vanilla-repl readme fix by @felixroos in https://codeberg.org/uzu/strudel/pulls/737 +- completely revert config mess by @felixroos in https://codeberg.org/uzu/strudel/pulls/745 +- hopefully fix trailing slashes bug by @felixroos in https://codeberg.org/uzu/strudel/pulls/753 +- Update vite pwa by @felixroos in https://codeberg.org/uzu/strudel/pulls/772 +- Update to Astro 3 by @felixroos in https://codeberg.org/uzu/strudel/pulls/775 +- support multiple named serial connections, change default baudrate by @yaxu in https://codeberg.org/uzu/strudel/pulls/551 +- CHANGES: github action checkout v2 -> v4 by @bwagner in https://codeberg.org/uzu/strudel/pulls/837 +- CHANGES: pin pnpm to version 8.3.1 by @bwagner in https://codeberg.org/uzu/strudel/pulls/834 +- CHANGES: github action pnpm version from 7 to 8.3.1 by @bwagner in https://codeberg.org/uzu/strudel/pulls/835 +- ADDS: JetBrains IDE files and directories to .gitignore by @bwagner in https://codeberg.org/uzu/strudel/pulls/840 +- Prevent 404 on Algolia crawls by @ilesinge in https://codeberg.org/uzu/strudel/pulls/838 +- Add in fixes from my fork to slashocalypse branch by @shiyouganai in https://codeberg.org/uzu/strudel/pulls/843 +- improve slashing + base href behavior by @felixroos in https://codeberg.org/uzu/strudel/pulls/842 +- CHANGES: pnpm 8.1.3 to 8.11.0 by @bwagner in https://codeberg.org/uzu/strudel/pulls/850 +- add missing trailing slashes by @felixroos in https://codeberg.org/uzu/strudel/pulls/860 +- move all examples to separate examples folder by @felixroos in https://codeberg.org/uzu/strudel/pulls/878 +- Dependency update by @felixroos in https://codeberg.org/uzu/strudel/pulls/879 +- Update Vite version so hot reload works properly with newest pnpm version by @daslyfe in https://codeberg.org/uzu/strudel/pulls/892 +- prevent vite from complaining about additional exports in jsx files by @daslyfe in https://codeberg.org/uzu/strudel/pulls/891 +- fix some build warnings by @felixroos in https://codeberg.org/uzu/strudel/pulls/902 +- Remove hideHeader for better mobile UI and consistency by @rjulian in https://codeberg.org/uzu/strudel/pulls/894 +- Fix: swatch/[name].png.js static path by @oscarbyrne in https://codeberg.org/uzu/strudel/pulls/916 +- rename @strudel.cycles/_ packages to @strudel/_ by @felixroos in https://codeberg.org/uzu/strudel/pulls/917 +- `pick` now accepts lookup tables, with alternate cycle squeezing behaviour as new `inhabit` function by @yaxu in https://codeberg.org/uzu/strudel/pulls/918 +- Revert "`pick` now accepts lookup tables, with alternate cycle squeezing behaviour as new `inhabit` function" by @yaxu in https://codeberg.org/uzu/strudel/pulls/920 +- Fix pattern tab not showing patterns without created date by @daslyfe in https://codeberg.org/uzu/strudel/pulls/934
## New Contributors -- @ilesinge made their first contribution in https://github.com/tidalcycles/strudel/pull/748 -- @Dsm0 made their first contribution in https://github.com/tidalcycles/strudel/pull/773 -- @kasparsj made their first contribution in https://github.com/tidalcycles/strudel/pull/802 -- @atfornes made their first contribution in https://github.com/tidalcycles/strudel/pull/818 -- @drewgbarnes made their first contribution in https://github.com/tidalcycles/strudel/pull/830 -- @shiyouganai made their first contribution in https://github.com/tidalcycles/strudel/pull/843 -- @rjulian made their first contribution in https://github.com/tidalcycles/strudel/pull/894 -- @fnordomat made their first contribution in https://github.com/tidalcycles/strudel/pull/907 -- @oscarbyrne made their first contribution in https://github.com/tidalcycles/strudel/pull/916 -- @geikha made their first contribution in https://github.com/tidalcycles/strudel/pull/924 +- @ilesinge made their first contribution in https://codeberg.org/uzu/strudel/pulls/748 +- @Dsm0 made their first contribution in https://codeberg.org/uzu/strudel/pulls/773 +- @kasparsj made their first contribution in https://codeberg.org/uzu/strudel/pulls/802 +- @atfornes made their first contribution in https://codeberg.org/uzu/strudel/pulls/818 +- @drewgbarnes made their first contribution in https://codeberg.org/uzu/strudel/pulls/830 +- @shiyouganai made their first contribution in https://codeberg.org/uzu/strudel/pulls/843 +- @rjulian made their first contribution in https://codeberg.org/uzu/strudel/pulls/894 +- @fnordomat made their first contribution in https://codeberg.org/uzu/strudel/pulls/907 +- @oscarbyrne made their first contribution in https://codeberg.org/uzu/strudel/pulls/916 +- @geikha made their first contribution in https://codeberg.org/uzu/strudel/pulls/924 -**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v0.9.0...v1.0.0 +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v0.9.0...v1.0.0 diff --git a/website/src/content/blog/year-2.mdx b/website/src/content/blog/year-2.mdx index 38e39bbfd..0ea4616a3 100644 --- a/website/src/content/blog/year-2.mdx +++ b/website/src/content/blog/year-2.mdx @@ -216,9 +216,9 @@ Here is a recording of the first session we organized over the discord server: The midi integration has gotten a few new features: -- [clock out](https://github.com/tidalcycles/strudel/pull/710) to sync your midi devices / DAW to the strudel clock (better doc coming soon) +- [clock out](https://codeberg.org/uzu/strudel/pulls/710) to sync your midi devices / DAW to the strudel clock (better doc coming soon) - [cc output](https://strudel.cc/learn/input-output/#ccn--ccv) to send cc values to your gear -- [cc input](https://github.com/tidalcycles/strudel/pull/699) to control strudel via MIDI (better doc coming soon) +- [cc input](https://codeberg.org/uzu/strudel/pulls/699) to control strudel via MIDI (better doc coming soon) Here is a little demo of me fiddling with a midi controller, changing a piano pattern: From 912b3270aaaf1a28f2444f130c179bf110edc048 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 14 Jun 2025 14:35:19 +0200 Subject: [PATCH 200/538] use yt for gh video assets + replace release asset links --- .../content/blog/release-0.8.0-himbeermuffin.mdx | 14 +++++++------- .../src/content/blog/release-0.9.0-bananenbrot.mdx | 6 +++--- .../blog/release-1.0.0-geburtstagskuchen.mdx | 2 -- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/website/src/content/blog/release-0.8.0-himbeermuffin.mdx b/website/src/content/blog/release-0.8.0-himbeermuffin.mdx index babab5954..1eb1888a7 100644 --- a/website/src/content/blog/release-0.8.0-himbeermuffin.mdx +++ b/website/src/content/blog/release-0.8.0-himbeermuffin.mdx @@ -6,7 +6,7 @@ tags: ['meta'] author: froos --- -import BlogVideo from '../../components/BlogVideo.astro'; +import { Youtube } from '@src/components/Youtube'; These are the release notes for Strudel 0.8.0 aka "Himbeermuffin"! @@ -18,11 +18,11 @@ Let me write up some of the highlights: Besides the REPL (https://strudel.tidalcycles.org/), Strudel is now also distributed as a Desktop App via https://tauri.app/! Thanks to [vasilymilovidov](https://github.com/vasilymilovidov)! -- [Linux: Debian based](https://github.com/tidalcycles/strudel/releases/download/v0.8.0/strudel_0.1.0_amd64.deb) -- [Linux: AppImage](https://github.com/tidalcycles/strudel/releases/download/v0.8.0/strudel_0.1.0_amd64.AppImage) -- [MacOS](https://github.com/tidalcycles/strudel/releases/download/v0.8.0/Strudel_0.1.0_x64.dmg) -- [Windows .exe](https://github.com/tidalcycles/strudel/releases/download/v0.8.0/Strudel_0.1.0_x64-setup.exe) -- [Windows .msi](https://github.com/tidalcycles/strudel/releases/download/v0.8.0/Strudel_0.1.0_x64_en-US.msi) +- [Linux: Debian based](https://codeberg.org/uzu/strudel/releases/download/v0.8.0/strudel_0.1.0_amd64.deb) +- [Linux: AppImage](https://codeberg.org/uzu/strudel/releases/download/v0.8.0/strudel_0.1.0_amd64.AppImage) +- [MacOS](https://codeberg.org/uzu/strudel/releases/download/v0.8.0/Strudel_0.1.0_x64.dmg) +- [Windows .exe](https://codeberg.org/uzu/strudel/releases/download/v0.8.0/Strudel_0.1.0_x64-setup.exe) +- [Windows .msi](https://codeberg.org/uzu/strudel/releases/download/v0.8.0/Strudel_0.1.0_x64_en-US.msi) edit: the desktop app performance on linux is currently not that great.. the web REPL runs much smoother (using firefox or chromium) @@ -43,7 +43,7 @@ I would be very happy to collect some feedback on how it works across different Also still undocumented, but you can now visualize patterns as a spiral via `.spiral()`: - + This is especially nice because strudel is not only the name of a dessert but also the german word for vortex! The spiral is very fitting to visualize cycles because you can align cycles vertically, while surfing along an infinite twisted timeline. diff --git a/website/src/content/blog/release-0.9.0-bananenbrot.mdx b/website/src/content/blog/release-0.9.0-bananenbrot.mdx index 944675e38..eb6894f69 100644 --- a/website/src/content/blog/release-0.9.0-bananenbrot.mdx +++ b/website/src/content/blog/release-0.9.0-bananenbrot.mdx @@ -6,8 +6,6 @@ tags: ['meta'] author: froos --- -import BlogVideo from '../../components/BlogVideo.astro'; - These are the release notes for Strudel 0.9.0 aka "Bananenbrot"! The last release was over 11 weeks ago, so a lot of things have happened! @@ -25,7 +23,9 @@ Main new features include: - [vibrato](https://strudel.tidalcycles.org/learn/synths#vibrato) - an integration of [ZZFX](https://strudel.tidalcycles.org/learn/synths#zzfx) - +import { Youtube } from '@src/components/Youtube'; + + Related PRs: diff --git a/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx b/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx index 0ee149a20..bfbf24a20 100644 --- a/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx +++ b/website/src/content/blog/release-1.0.0-geburtstagskuchen.mdx @@ -6,8 +6,6 @@ tags: ['meta'] author: froos --- -import BlogVideo from '../../components/BlogVideo.astro'; - These are the release notes for Strudel 1.0.0 aka "Geburtstagskuchen" This release marks the 2 year anniversary of the project, the first commit was on the 22nd January 2022 by Alex McLean. From 0163c8ba25712f56c0aa725a770d8c84a3b6552f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 14 Jun 2025 14:47:16 +0200 Subject: [PATCH 201/538] replace remaining links to old github repo --- website/src/content/blog/year-2.mdx | 2 +- website/src/pages/learn/getting-started.mdx | 2 +- website/src/pages/learn/input-output.mdx | 4 +-- website/src/pages/learn/metadata.mdx | 2 -- website/src/pages/learn/samples.mdx | 2 +- website/src/pages/learn/strudel-vs-tidal.mdx | 8 ++--- website/src/pages/technical-manual/docs.mdx | 4 +-- .../src/pages/technical-manual/packages.mdx | 30 +++++++++---------- .../pages/technical-manual/project-start.mdx | 6 ++-- website/src/pages/technical-manual/repl.mdx | 4 +-- 10 files changed, 31 insertions(+), 33 deletions(-) diff --git a/website/src/content/blog/year-2.mdx b/website/src/content/blog/year-2.mdx index 0ea4616a3..1a33c673b 100644 --- a/website/src/content/blog/year-2.mdx +++ b/website/src/content/blog/year-2.mdx @@ -228,7 +228,7 @@ Here is a little demo of me fiddling with a midi controller, changing a piano pa - You can now run [hydra inside strudel](https://strudel.cc/learn/hydra/) + you can even run strudel in [hydra](https://hydra.ojack.xyz), thanks to [Olivia Jack](https://ojack.xyz/) and [Ámbar Tenorio Fornés](https://atenor.io/)! Read more [here](https://alpaca.pubpub.org/pub/b7hwrjfk/release/2?readingCollection=1def0192) - There is now a [VSCode Plugin](https://marketplace.visualstudio.com/items?itemName=roipoussiere.tidal-strudel) thanks to [roipoussiere](https://github.com/roipoussiere)! It allows you run patterns from a `.strudel` file inside VSCode. -- Strudel can now be downloaded as a desktop app, thanks to [vasilymilovidov](https://github.com/vasilymilovidov) who has wrapped the REPL with [tauri](https://tauri.app/). You can download it [on the releases page](https://github.com/tidalcycles/strudel/releases) (scroll down to Assets). The performance is not optimal on MacOS and Linux, which is why it is still considered experimental +- Strudel can now be downloaded as a desktop app, thanks to [vasilymilovidov](https://github.com/vasilymilovidov) who has wrapped the REPL with [tauri](https://tauri.app/). You can download it [on the releases page](https://codeberg.org/uzu/strudel/releases) (scroll down to Assets). The performance is not optimal on MacOS and Linux, which is why it is still considered experimental ## Stats diff --git a/website/src/pages/learn/getting-started.mdx b/website/src/pages/learn/getting-started.mdx index d18a05607..2d056d3b4 100644 --- a/website/src/pages/learn/getting-started.mdx +++ b/website/src/pages/learn/getting-started.mdx @@ -81,7 +81,7 @@ s("bd,[~ ],hh*8") // drums Please note that this project is still in its experimental state. In the future, parts of it might change significantly. This tutorial is also far from complete. -You can contribute to it clicking 'Edit this page' in the top right, or by visiting the [Strudel GitHub page](https://github.com/tidalcycles/strudel/). +You can contribute to it clicking 'Edit this page' in the top right, or by visiting the [Strudel GitHub page](https://codeberg.org/uzu/strudel/). # What's next? diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index 92379aa6f..0dd48b7de 100644 --- a/website/src/pages/learn/input-output.mdx +++ b/website/src/pages/learn/input-output.mdx @@ -8,7 +8,7 @@ import { JsDoc } from '../../docs/JsDoc'; # MIDI, OSC and MQTT -Normally, Strudel is used to pattern sound, using its own '[web audio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)'-based synthesiser called [SuperDough](https://github.com/tidalcycles/strudel/tree/main/packages/superdough). +Normally, Strudel is used to pattern sound, using its own '[web audio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)'-based synthesiser called [SuperDough](https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough). It is also possible to pattern other things with Strudel, such as software and hardware synthesisers with MIDI, other software using Open Sound Control/OSC (including the [SuperDirt](https://github.com/musikinformatik/SuperDirt/) synthesiser commonly used with Strudel's sibling [TidalCycles](https://tidalcycles.org/)), or the MQTT 'internet of things' protocol. @@ -184,7 +184,7 @@ To get SuperDirt to work with Strudel, you need to 1. install SuperCollider + sc3 plugins, see [Tidal Docs](https://tidalcycles.org/docs/) (Install Tidal) for more info. 2. install SuperDirt, or the [StrudelDirt](https://github.com/daslyfe/StrudelDirt) fork which is optimised for use with Strudel 3. install [node.js](https://nodejs.org/en/) -4. download [Strudel Repo](https://github.com/tidalcycles/strudel/) (or git clone, if you have git installed) +4. download [Strudel Repo](https://codeberg.org/uzu/strudel/) (or git clone, if you have git installed) 5. run `pnpm i` in the strudel directory 6. run `pnpm run osc` to start the osc server, which forwards OSC messages from Strudel REPL to SuperCollider diff --git a/website/src/pages/learn/metadata.mdx b/website/src/pages/learn/metadata.mdx index e8bb86dfc..61db16f0e 100644 --- a/website/src/pages/learn/metadata.mdx +++ b/website/src/pages/learn/metadata.mdx @@ -18,8 +18,6 @@ You can optionally add some music metadata in your Strudel code, by using tags i Like other comments, those are ignored by Strudel, but it can be used by other tools to retrieve some information about the music. -It is for instance used by the [swatch tool](https://github.com/tidalcycles/strudel/tree/main/my-patterns) to display pattern titles in the [examples page](https://strudel.cc/examples/). - ## Alternative syntax You can also use comment blocks: diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index 890de3aae..cd7944caa 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -62,7 +62,7 @@ To see which sample names are available, open the `sounds` tab in the [REPL](htt 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. -[This might be fixed in the future](https://github.com/tidalcycles/strudel/issues/187) +[This might be fixed in the future](https://codeberg.org/uzu/strudel/issues/187) # Sound Banks diff --git a/website/src/pages/learn/strudel-vs-tidal.mdx b/website/src/pages/learn/strudel-vs-tidal.mdx index 40ea188fd..9c1cc6b73 100644 --- a/website/src/pages/learn/strudel-vs-tidal.mdx +++ b/website/src/pages/learn/strudel-vs-tidal.mdx @@ -79,7 +79,7 @@ Instead of `+` / `add`, you can use any of the available operators of the first ## Function Compatibility -[This issue](https://github.com/tidalcycles/strudel/issues/31) tracks which Tidal functions are implemented in Strudel. +[This issue](https://codeberg.org/uzu/strudel/issues/31) tracks which Tidal functions are implemented in Strudel. The list might not be 100% up to date and probably also misses some functions completely.. Feel encouraged to search the source code for a function you're looking for. If you find a function that's not on the list, please tell! @@ -89,7 +89,7 @@ If you find a function that's not on the list, please tell! As seen in the example, the `#` operator (shorthand for `|>`) is also just a function call in strudel. So `note "c5" # s "gtr"` becomes `note("c5").s('gtr')`. -[This file](https://github.com/tidalcycles/strudel/blob/main/packages/core/controls.mjs) lists all available control params. +[This file](https://codeberg.org/uzu/strudel/src/branch/main/packages/core/controls.mjs) lists all available control params. Note that not all of those work in the Webaudio Output of Strudel. If you find a tidal control that's not on the list, please tell! @@ -107,11 +107,11 @@ You can find a [list of available effects here](/learn/effects/). ### Sampler Strudel's sampler supports [a subset](/learn/samples) of Superdirt's sampler. -Also, samples are always loaded from a URL rather than from the disk, although [that might be possible in the future](https://github.com/tidalcycles/strudel/issues/118). +Also, samples are always loaded from a URL rather than from the disk, although [that might be possible in the future](https://codeberg.org/uzu/strudel/issues/118). ## Evaluation -The Strudel REPL does not support [block based evaluation](https://github.com/tidalcycles/strudel/issues/34) yet. +The Strudel REPL does not support [block based evaluation](https://codeberg.org/uzu/strudel/issues/34) yet. You can use labeled statements and `_` to mute: *8").scale('G4 minor') This will load the strudel website in an iframe, using the code provided within the HTML comments ``. The HTML comments are needed to make sure the browser won't interpret it as HTML. -For alternative ways to load this package, see the [@strudel/embed README](https://github.com/tidalcycles/strudel/tree/main/packages/embed#strudelembed). +For alternative ways to load this package, see the [@strudel/embed README](https://codeberg.org/uzu/strudel/src/branch/main/packages/embed#strudel-embed). ### @strudel/repl @@ -99,7 +99,7 @@ The upside of using the repl without an iframe is that you can pin the strudel v This will guarantee your pattern wont break due to changes to the strudel project in the future. -For more info on this package, see the [@strudel/repl README](https://github.com/tidalcycles/strudel/tree/main/packages/repl#strudelrepl). +For more info on this package, see the [@strudel/repl README](https://codeberg.org/uzu/strudel/src/branch/main/packages/repl#strudel-repl). ## With your own UI @@ -118,7 +118,7 @@ If you'd rather use your own UI, you can use the `@strudel/web` package: ``` -For more info on this package, see the [@strudel/web README](https://github.com/tidalcycles/strudel/tree/main/packages/web#strudelweb). +For more info on this package, see the [@strudel/web README]https://codeberg.org/uzu/strudel/src/branch/main/packages/web#strudel-web). ## Via npm diff --git a/website/src/pages/technical-manual/repl.mdx b/website/src/pages/technical-manual/repl.mdx index f336ce361..f53efac41 100644 --- a/website/src/pages/technical-manual/repl.mdx +++ b/website/src/pages/technical-manual/repl.mdx @@ -17,7 +17,7 @@ Besides a UI for playback control and meta information, the main part of the REP 2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span. 3. For each scheduling tick, all generated `Events` are triggered by calling their `onTrigger` method, which is set by the output. - + ## User Code @@ -75,7 +75,7 @@ The sum of both is passed to `withLoc` to tell each element its location, which Another important part of the user code is the mini notation, which allows to express rhythms in a short manner. -- the mini notation is [implemented as a PEG grammar](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), living in the [mini package](https://github.com/tidalcycles/strudel/tree/main/packages/mini) +- the mini notation is [implemented as a PEG grammar](https://codeberg.org/uzu/strudel/src/branch/talk/packages/mini/krill.pegjs), living in the [mini package](https://codeberg.org/uzu/strudel/src/branch/main/packages/mini) - it is based on [krill](https://github.com/Mdashdotdashn/krill) by Mdashdotdashn - the peg grammar is used to generate a parser with [peggyjs](https://peggyjs.org/) - the generated parser takes a mini notation string and outputs an AST From a7f044e1aae575355d8a92dea3ca05ef480a8c64 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 14 Jun 2025 16:17:22 +0200 Subject: [PATCH 202/538] more degithubbing --- packages/core/test/controls.test.mjs | 2 +- packages/core/test/drawLine.test.mjs | 2 +- packages/core/test/fraction.test.mjs | 2 +- packages/core/test/pattern.test.mjs | 2 +- packages/core/test/util.test.mjs | 2 +- packages/mini/test/mini.test.mjs | 2 +- packages/repl/README.md | 2 +- packages/tonal/test/tonal.test.mjs | 2 +- packages/tonal/test/tonleiter.test.mjs | 2 +- packages/transpiler/test/transpiler.test.mjs | 2 +- packages/xen/test/xen.test.mjs | 2 +- technical.manual.md | 6 +++--- website/src/cx.mjs | 2 +- website/src/repl/Repl.jsx | 2 +- website/src/repl/components/panel/WelcomeTab.jsx | 2 +- website/src/repl/tunes.mjs | 2 +- website/src/repl/useReplContext.jsx | 2 +- 17 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/core/test/controls.test.mjs b/packages/core/test/controls.test.mjs index f0af02eb6..667c3a4e5 100644 --- a/packages/core/test/controls.test.mjs +++ b/packages/core/test/controls.test.mjs @@ -1,6 +1,6 @@ /* controls.test.mjs - -Copyright (C) 2023 Strudel contributors - see +Copyright (C) 2023 Strudel contributors - see 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 . */ diff --git a/packages/core/test/drawLine.test.mjs b/packages/core/test/drawLine.test.mjs index 84359a7cf..2240080cd 100644 --- a/packages/core/test/drawLine.test.mjs +++ b/packages/core/test/drawLine.test.mjs @@ -1,6 +1,6 @@ /* drawLine.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/core/test/fraction.test.mjs b/packages/core/test/fraction.test.mjs index 6b710d043..b167c9483 100644 --- a/packages/core/test/fraction.test.mjs +++ b/packages/core/test/fraction.test.mjs @@ -1,6 +1,6 @@ /* fraction.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 08611032c..696f13fef 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1,6 +1,6 @@ /* pattern.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/core/test/util.test.mjs b/packages/core/test/util.test.mjs index a511e0fc2..d3033a82c 100644 --- a/packages/core/test/util.test.mjs +++ b/packages/core/test/util.test.mjs @@ -1,6 +1,6 @@ /* util.test.mjs - Tests for the core 'util' module -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/mini/test/mini.test.mjs b/packages/mini/test/mini.test.mjs index 15d501111..c38997308 100644 --- a/packages/mini/test/mini.test.mjs +++ b/packages/mini/test/mini.test.mjs @@ -1,6 +1,6 @@ /* mini.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/repl/README.md b/packages/repl/README.md index 46819712c..3db4cc3f7 100644 --- a/packages/repl/README.md +++ b/packages/repl/README.md @@ -91,7 +91,7 @@ or ``` -The `.editor` property on the `strudel-editor` web component gives you the instance of [StrudelMirror](https://github.com/tidalcycles/strudel/blob/a46bd9b36ea7d31c9f1d3fca484297c7da86893f/packages/codemirror/codemirror.mjs#L124) that runs the REPL. +The `.editor` property on the `strudel-editor` web component gives you the instance of [StrudelMirror](https://codeberg.org/uzu/strudel/src/branch/a46bd9b36ea7d31c9f1d3fca484297c7da86893f/packages/codemirror/codemirror.mjs#L124) that runs the REPL. For example, you could use `setCode` to change the code from the outside, `start` / `stop` to toggle playback or `evaluate` to evaluate the code. diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 8dd0e1861..c1155c238 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -1,6 +1,6 @@ /* tonal.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/tonal/test/tonleiter.test.mjs b/packages/tonal/test/tonleiter.test.mjs index e1a693579..b53a8f405 100644 --- a/packages/tonal/test/tonleiter.test.mjs +++ b/packages/tonal/test/tonleiter.test.mjs @@ -1,6 +1,6 @@ /* tonleiter.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/transpiler/test/transpiler.test.mjs b/packages/transpiler/test/transpiler.test.mjs index 988479b4f..02970cf43 100644 --- a/packages/transpiler/test/transpiler.test.mjs +++ b/packages/transpiler/test/transpiler.test.mjs @@ -1,6 +1,6 @@ /* transpiler.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/packages/xen/test/xen.test.mjs b/packages/xen/test/xen.test.mjs index 977b0f694..a0982208a 100644 --- a/packages/xen/test/xen.test.mjs +++ b/packages/xen/test/xen.test.mjs @@ -1,6 +1,6 @@ /* xen.test.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/technical.manual.md b/technical.manual.md index 58f7cd934..6068b48fe 100644 --- a/technical.manual.md +++ b/technical.manual.md @@ -17,7 +17,7 @@ More info in the [REPL README](https://codeberg.org/uzu/strudel/src/branch/main/ # High Level Overview - + ## 1. End User Code @@ -48,7 +48,7 @@ mini('c3 [e3 g3]') This is how it works: - + - The user code is parsed with a [shift parser](https://github.com/shapesecurity/shift-parser-js), generating an AST - The AST is transformed to resolve the syntax sugar @@ -171,7 +171,7 @@ Here is an example Hap value with different properties: ```js { note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 } ``` - +
diff --git a/website/src/cx.mjs b/website/src/cx.mjs index 4e4aea08d..d8ff9eda1 100644 --- a/website/src/cx.mjs +++ b/website/src/cx.mjs @@ -1,6 +1,6 @@ /* cx.js - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index e46eb7171..813f63897 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -1,6 +1,6 @@ /* Repl.jsx - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/website/src/repl/components/panel/WelcomeTab.jsx b/website/src/repl/components/panel/WelcomeTab.jsx index a43f02ad4..b3b167c74 100644 --- a/website/src/repl/components/panel/WelcomeTab.jsx +++ b/website/src/repl/components/panel/WelcomeTab.jsx @@ -38,7 +38,7 @@ export function WelcomeTab({ context }) { , which is a popular live coding language for music, written in Haskell. Strudel is free/open source software: you can redistribute and/or modify it under the terms of the{' '} - + GNU Affero General Public License . You can find the source code at{' '} diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index cffe0c0eb..774e04e49 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -1,6 +1,6 @@ /* tunes.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 36e8099cb..12621c50a 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -1,6 +1,6 @@ /* Repl.jsx - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2022 Strudel contributors - see 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 . */ From d856e567c17c04cad083850955d838e3e328e404 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 14 Jun 2025 15:22:27 +0100 Subject: [PATCH 203/538] link back to tech manual in wiki --- packages/README.md | 2 +- technical.manual.md | 193 -------------------------------------------- 2 files changed, 1 insertion(+), 194 deletions(-) delete mode 100644 technical.manual.md diff --git a/packages/README.md b/packages/README.md index a5b93d321..98938d6c3 100644 --- a/packages/README.md +++ b/packages/README.md @@ -2,4 +2,4 @@ Each folder represents one of the @strudel/* packages [published to npm](https://www.npmjs.com/org/strudel). -To understand how those pieces connect, refer to the [Technical Manual](https://codeberg.org/uzu/strudel/src/branch/main/technical-manual.md) or the individual READMEs. +To understand how those pieces connect, refer to the [Technical Manual](https://codeberg.org/uzu/strudel/wiki/Technical-Manual) or the individual READMEs. diff --git a/technical.manual.md b/technical.manual.md deleted file mode 100644 index 6068b48fe..000000000 --- a/technical.manual.md +++ /dev/null @@ -1,193 +0,0 @@ -This document introduces you to Strudel in a technical sense. If you just want to *use* Strudel, have a look at the [Tutorial](https://strudel.tidalcycles.org/tutorial/). - -## Strudel Packages - -There are different packages for different purposes. They.. - -- split up the code into smaller chunks -- can be selectively used to implement some sort of time based system - -Please refer to the individual README files in the [packages folder](https://codeberg.org/uzu/strudel/src/branch/main/packages) - -## REPL - -The [REPL](https://strudel.tidalcycles.org/) 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. - -More info in the [REPL README](https://codeberg.org/uzu/strudel/src/branch/main/packages/repl/README.md) - -# High Level Overview - - - -## 1. End User Code - -The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://codeberg.org/uzu/strudel/src/branch/main/packages/eval#strudelcycleseval) evaluates the user code -after a transpilation step, which resolves the syntax sugar. If you don't want the syntax sugar, you can omit the eval package and call the native javascript `eval` instead. - -### 🍭 Syntax Sugar - -JavaScript Transpilation = converting valid JavaScript to valid JavaScript: - -```js -"c3 [e3 g3]".fast(2) -``` - -becomes - -```js -mini('c3 [e3 g3]') - .withMiniLocation([1, 0, 0], [1, 11, 11]) // source location - .fast(2); -``` - -- double quoted strings and backtick strings are turned into `mini` calls (single quoted strings are left as is) -- The source location is added by chaining `withMiniLocation`, which enables the real time highlighting later -- (psuedo) variable names that look like notes (like `c4`, `bb2` or `fs3`) are turned into strings -- support for top level await -- operator overloading could be implemented in the future - -This is how it works: - - - -- The user code is parsed with a [shift parser](https://github.com/shapesecurity/shift-parser-js), generating an AST -- The AST is transformed to resolve the syntax sugar -- The AST is used to generate code again (shift-codegen) - -Shift will most likely be replaced with acorn in the future, see https://codeberg.org/uzu/strudel/issues/174 - -### Mini Notation - -Another important part of the user code is the mini notation, which allows to express rhythms in a short manner. - -- the mini notation is [implemented as a PEG grammar](https://codeberg.org/uzu/strudel/src/branch/main/packages/mini/krill.pegjs), living in the [mini package](https://codeberg.org/uzu/strudel/src/branch/main/packages/mini) -- it is based on [krill](https://github.com/Mdashdotdashn/krill) by Mdashdotdashn -- the peg grammar is used to generate a parser with [peggyjs](https://peggyjs.org/) -- the generated parser takes a mini notation string and outputs an AST -- the AST can then be used to construct a pattern using the regular Strudel API - -Here's an example AST: - -```json -{ - "type_": "pattern", - "arguments_": { "alignment": "h" }, - "source_": [ - { - "type_": "element", "source_": "c3", - "location_": { "start": { "offset": 1, "line": 1, "column": 2 }, "end": { "offset": 4, "line": 1, "column": 5 } } - }, - { - "type_": "element", - "location_": { "start": { "offset": 4, "line": 1, "column": 5 }, "end": { "offset": 11, "line": 1, "column": 12 } } - "source_": { - "type_": "pattern", "arguments_": { "alignment": "h" }, - "source_": [ - { - "type_": "element", "source_": "e3", - "location_": { "start": { "offset": 5, "line": 1, "column": 6 }, "end": { "offset": 8, "line": 1, "column": 9 } } - }, - { - "type_": "element", "source_": "g3", - "location_": { "start": { "offset": 8, "line": 1, "column": 9 }, "end": { "offset": 10, "line": 1, "column": 11 } } - } - ] - }, - } - ] -} -``` - -which translates to `seq(c3, seq(e3, g3))` - -## 2. Querying & Scheduling - -When the user code has been evaluated, we hopefully get a Pattern instance, which we can use to query events from. -These events can then be used to trigger side effects in the real world. On that note, Events are mostly called Hap(s) in the codebase, because JS already has a built in `Event` class. - -### Querying - -> Querying = Asking a Pattern for Events within a certain time span - -```js -seq('c3', ['e3', 'g3']) // <--- Pattern - .queryArc(0, 2) // query events within 0 and 2 cycles - .map((hap) => hap.showWhole()); // make readable -``` - -yields - -```js -[ - '0/1 -> 1/2: c3', // cycle 0 - '1/2 -> 3/4: e3', - '3/4 -> 1/1: g3', - '1/1 -> 3/2: c3', // cycle 1 - '3/2 -> 7/4: e3', - '7/4 -> 2/1: g3', -]; -``` - -### 🗓️ Scheduling - -The scheduler will query events repeatedly, creating a possibly endless loop of time slices. -Here is a simplified example of how it works - -```js -let step = 0.5; // query interval in seconds -let tick = 0; // how many intervals have passed -let pattern = seq('c3', ['e3', 'g3']); // pattern from user -setInterval(() => { - const events = pattern.queryArc(tick * step, ++tick * step); - events.forEach((event) => { - console.log(event.showWhole()); - const o = getAudioContext().createOscillator(); - o.frequency.value = getFreq(event.value); - o.start(event.whole.begin); - o.stop(event.whole.begin + event.duration); - o.connect(getAudioContext().destination); - }); -}, step * 1000); // query each "step" seconds -``` - -## 3. Sound Output - -The third and last step is to use the scheduled events to make sound. -Patterns are wrapped with param functions to compose different properties of the sound. - -```js -note("[c2(3,8) [ bb1]]") // sets frequency - .s("") // sound source - .gain(.5) // turn down volume - .cutoff(sine.range(200,1000).slow(4)) // modulated cutoff - .slow(2) - .out().logValues()`, - ]} -/> -``` - -Here is an example Hap value with different properties: - -```js -{ note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 } -``` - - -
- -- Patterns represent just values in time! -- Suitable for any time based output (music, visuals, movement, .. ?) - -### Supported Outputs - -At the time of writing this doc, the following outputs are supported: - -- Web Audio API `.out()` see [/webaudio](https://codeberg.org/uzu/strudel/src/branch/main/packages/webaudio) -- MIDI `.midi()` see [/midi](https://codeberg.org/uzu/strudel/src/branch/main/packages/midi) -- OSC `.osc()` see [/osc](https://codeberg.org/uzu/strudel/src/branch/main/packages/osc) -- Serial `.serial()` see [/serial](https://codeberg.org/uzu/strudel/src/branch/main/packages/serial) -- Tone.js `.tone()` (deprecated?) [/tone](https://codeberg.org/uzu/strudel/src/branch/main/packages/tone) -- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://codeberg.org/uzu/strudel/src/branch/main/packages/webdirt) -- Speech `.speak()` (experimental) part of [/core](https://codeberg.org/uzu/strudel/src/branch/main/packages/core) - -These could change, so make sure to check the [packages folder](https://codeberg.org/uzu/strudel/src/branch/main/packages). From 9738c90b82dbae40e2bf5ad2e477d464d644ccdc Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 14 Jun 2025 16:23:03 +0200 Subject: [PATCH 204/538] even more degithubbing --- packages/core/test/pattern.test.mjs | 2 +- packages/repl/README.md | 2 +- paper/demo-preprocessed.md | 2 +- paper/demo.md | 2 +- paper/iclc2023.html | 6 +- paper/iclc2023.md | 2 +- .../src/components/Footer/AvatarList.astro | 169 ------------------ website/src/components/Header/Search.tsx | 2 +- .../RightSidebar/RightSidebar.astro | 1 - .../src/repl/components/panel/WelcomeTab.jsx | 2 +- 10 files changed, 10 insertions(+), 180 deletions(-) delete mode 100644 website/src/components/Footer/AvatarList.astro diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 696f13fef..45a1e2a98 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -884,7 +884,7 @@ describe('Pattern', () => { ); }); it('Doesnt drop haps in the 9th cycle', () => { - // fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a + // fixed with https://codeberg.org/uzu/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a expect(sequence(1, 2, 3).ply(2).early(8).firstCycle().length).toBe(6); }); }); diff --git a/packages/repl/README.md b/packages/repl/README.md index 3db4cc3f7..5b43b5e79 100644 --- a/packages/repl/README.md +++ b/packages/repl/README.md @@ -17,7 +17,7 @@ You can also pin the version like this: ``` This has the advantage that your code will always work, regardless of potential breaking changes in the strudel codebase. -See [releases](https://github.com/tidalcycles/strudel/releases) for the latest versions. +See [releases](https://codeberg.org/uzu/strudel/releases) for the latest versions. ## Use Web Component diff --git a/paper/demo-preprocessed.md b/paper/demo-preprocessed.md index e22823d90..2f0871e7c 100644 --- a/paper/demo-preprocessed.md +++ b/paper/demo-preprocessed.md @@ -201,7 +201,7 @@ interfaces. The Strudel REPL is available at , including an interactive tutorial. The repository is at -, all the code is open source +, all the code is open source under the GPL-3.0 License. # Acknowledgments diff --git a/paper/demo.md b/paper/demo.md index 23fe27754..841fb6064 100644 --- a/paper/demo.md +++ b/paper/demo.md @@ -128,7 +128,7 @@ For the future, it is planned to integrate alternative sound engines such as Gli # Links The Strudel REPL is available at , including an interactive tutorial. -The repository is at , all the code is open source under the GPL-3.0 License. +The repository is at , all the code is open source under the GPL-3.0 License. # Acknowledgments diff --git a/paper/iclc2023.html b/paper/iclc2023.html index a83e075a2..3771f560b 100644 --- a/paper/iclc2023.html +++ b/paper/iclc2023.html @@ -374,7 +374,7 @@ by the output.
REPL control flow
@@ -720,8 +720,8 @@ class="header-section-number">11 Links href="https://strudel.cc" class="uri">https://strudel.cc, including an interactive tutorial. The repository is at https://github.com/tidalcycles/strudel, all the code is +href="https://codeberg.org/uzu/strudel" +class="uri">https://codeberg.org/uzu/strudel, all the code is open source under the AGPL-3.0 License.

12 Acknowledgments

diff --git a/paper/iclc2023.md b/paper/iclc2023.md index 3afb27828..fdc9f0ca4 100644 --- a/paper/iclc2023.md +++ b/paper/iclc2023.md @@ -451,7 +451,7 @@ While Haskell's type system makes it a great language for the ongoing developmen # Links The Strudel REPL is available at , including an interactive tutorial. -The repository is at , all the code is open source under the AGPL-3.0 License. +The repository is at , all the code is open source under the AGPL-3.0 License. # Acknowledgments diff --git a/website/src/components/Footer/AvatarList.astro b/website/src/components/Footer/AvatarList.astro deleted file mode 100644 index 86bbcc875..000000000 --- a/website/src/components/Footer/AvatarList.astro +++ /dev/null @@ -1,169 +0,0 @@ ---- -// fetch all commits for just this page's path -type Props = { - path: string; -}; -const { path } = Astro.props as Props; -const resolvedPath = `website/src/pages${path}.mdx`; -const url = `https://api.github.com/repos/tidalcycles/strudel/commits?path=${resolvedPath}`; -const commitsURL = `https://github.com/tidalcycles/strudel/commits/main/${resolvedPath}`; - -type Commit = { - author: { - id: string; - login: string; - }; -}; - -async function getCommits(url: string) { - try { - const token = import.meta.env.SNOWPACK_PUBLIC_GITHUB_TOKEN ?? 'hello'; - if (!token) { - throw new Error('Cannot find "SNOWPACK_PUBLIC_GITHUB_TOKEN" used for escaping rate-limiting.'); - } - - const auth = `Basic ${Buffer.from(token, 'binary').toString('base64')}`; - - const res = await fetch(url, { - method: 'GET', - headers: { - Authorization: auth, - 'User-Agent': 'astro-docs/1.0', - }, - }); - - const data = await res.json(); - - if (!res.ok) { - throw new Error( - `Request to fetch commits failed. Reason: ${res.statusText} - Message: ${data.message}`, - ); - } - - return data as Commit[]; - } catch (e) { - console.warn(`[error] /src/components/AvatarList.astro - ${(e as any)?.message ?? e}`); - return [] as Commit[]; - } -} - -function removeDups(arr: Commit[]) { - const map = new Map(); - for (let item of arr) { - const author = item.author; - // Deduplicate based on author.id - //map.set(author.id, { login: author.login, id: author.id }); - author && map.set(author.id, { login: author.login, id: author.id }); - } - - return [...map.values()]; -} - -const data = await getCommits(url); -const unique = removeDups(data); -const recentContributors = unique.slice(0, 3); // only show avatars for the 3 most recent contributors -const additionalContributors = unique.length - recentContributors.length; // list the rest of them as # of extra contributors ---- - - -
-
    - { - recentContributors.map((item) => ( -
  • - - {`Contributor - -
  • - )) - } -
- { - additionalContributors > 0 && ( - - {`and ${additionalContributors} additional contributor${ - additionalContributors > 1 ? 's' : '' - }.`} - - ) - } - {unique.length === 0 && Contributors} -
- - diff --git a/website/src/components/Header/Search.tsx b/website/src/components/Header/Search.tsx index c982d80b6..f200dc79f 100644 --- a/website/src/components/Header/Search.tsx +++ b/website/src/components/Header/Search.tsx @@ -82,7 +82,7 @@ export default function Search() { appId={ALGOLIA.appId} apiKey={ALGOLIA.apiKey} getMissingResultsUrl={({ query }) => { - return `https://github.com/tidalcycles/strudel/issues/new?title=Missing doc for ${query}`; + return `https://codeberg.org/uzu/strudel/issues/new?title=Missing%20doc%20for${encodeURIComponent(query)}`; }} transformItems={(items) => { return items.map((item) => { diff --git a/website/src/components/RightSidebar/RightSidebar.astro b/website/src/components/RightSidebar/RightSidebar.astro index cd501b1a2..28c713022 100644 --- a/website/src/components/RightSidebar/RightSidebar.astro +++ b/website/src/components/RightSidebar/RightSidebar.astro @@ -18,5 +18,4 @@ currentPage = currentPage.endsWith('/') ? currentPage.slice(0, -1) : currentPage diff --git a/website/src/repl/components/panel/WelcomeTab.jsx b/website/src/repl/components/panel/WelcomeTab.jsx index b3b167c74..cd5fe030a 100644 --- a/website/src/repl/components/panel/WelcomeTab.jsx +++ b/website/src/repl/components/panel/WelcomeTab.jsx @@ -42,7 +42,7 @@ export function WelcomeTab({ context }) { GNU Affero General Public License . You can find the source code at{' '} - + github . You can also find licensing info{' '} From ee7d7830992ee521dac2d511158c0a0f3c133ed0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 14 Jun 2025 16:27:54 +0200 Subject: [PATCH 205/538] fix homepage link --- website/src/pages/learn/getting-started.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/getting-started.mdx b/website/src/pages/learn/getting-started.mdx index 2d056d3b4..b3348ccda 100644 --- a/website/src/pages/learn/getting-started.mdx +++ b/website/src/pages/learn/getting-started.mdx @@ -14,7 +14,7 @@ These pages will introduce you to [Strudel](https://strudel.cc/), a web-based [l # What is Strudel? -[Strudel](https://strudel.cc/) is a version of [Tidal Cycles](https://tidalcycles.org) written in [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript), initiated by [Alex McLean](https://slab.org) and [Felix Roos](https://github.com/felixroos) in 2022. +[Strudel](https://strudel.cc/) is a version of [Tidal Cycles](https://tidalcycles.org) written in [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript), initiated by [Alex McLean](https://slab.org) and [Felix Roos](https://froos.cc/) in 2022. Tidal Cycles, also known as Tidal, is a language for [algorithmic pattern](https://algorithmicpattern.org), and though it is most commonly used for [making music](https://tidalcycles.org/docs/showcase), it can be used for any kind of pattern making activity, including [weaving](https://www.youtube.com/watch?v=TfEmEsusXjU). Tidal was first implemented as a library written in the [Haskell](https://www.haskell.org/) functional programming language, and by itself it does not make any sound. From 2cb22e94b56bde24a461e6c216381c8c3242de11 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 14 Jun 2025 23:26:59 +0100 Subject: [PATCH 206/538] move iclc paper under docs/ --- {paper => docs/iclc2023-paper}/Makefile | 0 {paper => docs/iclc2023-paper}/README.md | 0 {paper => docs/iclc2023-paper}/bin/code-filter.py | 0 {paper => docs/iclc2023-paper}/citations.json | 0 {paper => docs/iclc2023-paper}/demo-preprocessed.md | 0 {paper => docs/iclc2023-paper}/demo.md | 0 {paper => docs/iclc2023-paper}/demo.pdf | Bin {paper => docs/iclc2023-paper}/iclc-reviews.md | 0 {paper => docs/iclc2023-paper}/iclc2023.html | 0 {paper => docs/iclc2023-paper}/iclc2023.md | 0 {paper => docs/iclc2023-paper}/iclc2023.pdf | Bin {paper => docs/iclc2023-paper}/iclc2023x.pdf | Bin {paper => docs/iclc2023-paper}/images/cc.png | Bin .../iclc2023-paper}/images/strudel-screenshot.png | Bin .../iclc2023-paper}/images/strudel-screenshot2.png | Bin .../iclc2023-paper}/images/strudelflow.png | Bin {paper => docs/iclc2023-paper}/inconsolata.sty | 0 {paper => docs/iclc2023-paper}/make.sh | 0 {paper => docs/iclc2023-paper}/pandoc/iclc.html | 0 {paper => docs/iclc2023-paper}/pandoc/iclc.latex | 0 {paper => docs/iclc2023-paper}/pandoc/iclc.sty | 0 .../iclc2023-paper}/paper-preprocessed.md | 0 {paper => docs/iclc2023-paper}/paper.md | 0 {paper => docs/iclc2023-paper}/paper.pdf | Bin .../iclc2023-paper}/tex/latex-template.tex | 0 .../iclc2023-paper}/tex/sig-alternate.cls | 0 {paper => docs/iclc2023-paper}/tex/waccopyright.sty | 0 27 files changed, 0 insertions(+), 0 deletions(-) rename {paper => docs/iclc2023-paper}/Makefile (100%) rename {paper => docs/iclc2023-paper}/README.md (100%) rename {paper => docs/iclc2023-paper}/bin/code-filter.py (100%) rename {paper => docs/iclc2023-paper}/citations.json (100%) rename {paper => docs/iclc2023-paper}/demo-preprocessed.md (100%) rename {paper => docs/iclc2023-paper}/demo.md (100%) rename {paper => docs/iclc2023-paper}/demo.pdf (100%) rename {paper => docs/iclc2023-paper}/iclc-reviews.md (100%) rename {paper => docs/iclc2023-paper}/iclc2023.html (100%) rename {paper => docs/iclc2023-paper}/iclc2023.md (100%) rename {paper => docs/iclc2023-paper}/iclc2023.pdf (100%) rename {paper => docs/iclc2023-paper}/iclc2023x.pdf (100%) rename {paper => docs/iclc2023-paper}/images/cc.png (100%) rename {paper => docs/iclc2023-paper}/images/strudel-screenshot.png (100%) rename {paper => docs/iclc2023-paper}/images/strudel-screenshot2.png (100%) rename {paper => docs/iclc2023-paper}/images/strudelflow.png (100%) rename {paper => docs/iclc2023-paper}/inconsolata.sty (100%) rename {paper => docs/iclc2023-paper}/make.sh (100%) rename {paper => docs/iclc2023-paper}/pandoc/iclc.html (100%) rename {paper => docs/iclc2023-paper}/pandoc/iclc.latex (100%) rename {paper => docs/iclc2023-paper}/pandoc/iclc.sty (100%) rename {paper => docs/iclc2023-paper}/paper-preprocessed.md (100%) rename {paper => docs/iclc2023-paper}/paper.md (100%) rename {paper => docs/iclc2023-paper}/paper.pdf (100%) rename {paper => docs/iclc2023-paper}/tex/latex-template.tex (100%) rename {paper => docs/iclc2023-paper}/tex/sig-alternate.cls (100%) rename {paper => docs/iclc2023-paper}/tex/waccopyright.sty (100%) diff --git a/paper/Makefile b/docs/iclc2023-paper/Makefile similarity index 100% rename from paper/Makefile rename to docs/iclc2023-paper/Makefile diff --git a/paper/README.md b/docs/iclc2023-paper/README.md similarity index 100% rename from paper/README.md rename to docs/iclc2023-paper/README.md diff --git a/paper/bin/code-filter.py b/docs/iclc2023-paper/bin/code-filter.py similarity index 100% rename from paper/bin/code-filter.py rename to docs/iclc2023-paper/bin/code-filter.py diff --git a/paper/citations.json b/docs/iclc2023-paper/citations.json similarity index 100% rename from paper/citations.json rename to docs/iclc2023-paper/citations.json diff --git a/paper/demo-preprocessed.md b/docs/iclc2023-paper/demo-preprocessed.md similarity index 100% rename from paper/demo-preprocessed.md rename to docs/iclc2023-paper/demo-preprocessed.md diff --git a/paper/demo.md b/docs/iclc2023-paper/demo.md similarity index 100% rename from paper/demo.md rename to docs/iclc2023-paper/demo.md diff --git a/paper/demo.pdf b/docs/iclc2023-paper/demo.pdf similarity index 100% rename from paper/demo.pdf rename to docs/iclc2023-paper/demo.pdf diff --git a/paper/iclc-reviews.md b/docs/iclc2023-paper/iclc-reviews.md similarity index 100% rename from paper/iclc-reviews.md rename to docs/iclc2023-paper/iclc-reviews.md diff --git a/paper/iclc2023.html b/docs/iclc2023-paper/iclc2023.html similarity index 100% rename from paper/iclc2023.html rename to docs/iclc2023-paper/iclc2023.html diff --git a/paper/iclc2023.md b/docs/iclc2023-paper/iclc2023.md similarity index 100% rename from paper/iclc2023.md rename to docs/iclc2023-paper/iclc2023.md diff --git a/paper/iclc2023.pdf b/docs/iclc2023-paper/iclc2023.pdf similarity index 100% rename from paper/iclc2023.pdf rename to docs/iclc2023-paper/iclc2023.pdf diff --git a/paper/iclc2023x.pdf b/docs/iclc2023-paper/iclc2023x.pdf similarity index 100% rename from paper/iclc2023x.pdf rename to docs/iclc2023-paper/iclc2023x.pdf diff --git a/paper/images/cc.png b/docs/iclc2023-paper/images/cc.png similarity index 100% rename from paper/images/cc.png rename to docs/iclc2023-paper/images/cc.png diff --git a/paper/images/strudel-screenshot.png b/docs/iclc2023-paper/images/strudel-screenshot.png similarity index 100% rename from paper/images/strudel-screenshot.png rename to docs/iclc2023-paper/images/strudel-screenshot.png diff --git a/paper/images/strudel-screenshot2.png b/docs/iclc2023-paper/images/strudel-screenshot2.png similarity index 100% rename from paper/images/strudel-screenshot2.png rename to docs/iclc2023-paper/images/strudel-screenshot2.png diff --git a/paper/images/strudelflow.png b/docs/iclc2023-paper/images/strudelflow.png similarity index 100% rename from paper/images/strudelflow.png rename to docs/iclc2023-paper/images/strudelflow.png diff --git a/paper/inconsolata.sty b/docs/iclc2023-paper/inconsolata.sty similarity index 100% rename from paper/inconsolata.sty rename to docs/iclc2023-paper/inconsolata.sty diff --git a/paper/make.sh b/docs/iclc2023-paper/make.sh similarity index 100% rename from paper/make.sh rename to docs/iclc2023-paper/make.sh diff --git a/paper/pandoc/iclc.html b/docs/iclc2023-paper/pandoc/iclc.html similarity index 100% rename from paper/pandoc/iclc.html rename to docs/iclc2023-paper/pandoc/iclc.html diff --git a/paper/pandoc/iclc.latex b/docs/iclc2023-paper/pandoc/iclc.latex similarity index 100% rename from paper/pandoc/iclc.latex rename to docs/iclc2023-paper/pandoc/iclc.latex diff --git a/paper/pandoc/iclc.sty b/docs/iclc2023-paper/pandoc/iclc.sty similarity index 100% rename from paper/pandoc/iclc.sty rename to docs/iclc2023-paper/pandoc/iclc.sty diff --git a/paper/paper-preprocessed.md b/docs/iclc2023-paper/paper-preprocessed.md similarity index 100% rename from paper/paper-preprocessed.md rename to docs/iclc2023-paper/paper-preprocessed.md diff --git a/paper/paper.md b/docs/iclc2023-paper/paper.md similarity index 100% rename from paper/paper.md rename to docs/iclc2023-paper/paper.md diff --git a/paper/paper.pdf b/docs/iclc2023-paper/paper.pdf similarity index 100% rename from paper/paper.pdf rename to docs/iclc2023-paper/paper.pdf diff --git a/paper/tex/latex-template.tex b/docs/iclc2023-paper/tex/latex-template.tex similarity index 100% rename from paper/tex/latex-template.tex rename to docs/iclc2023-paper/tex/latex-template.tex diff --git a/paper/tex/sig-alternate.cls b/docs/iclc2023-paper/tex/sig-alternate.cls similarity index 100% rename from paper/tex/sig-alternate.cls rename to docs/iclc2023-paper/tex/sig-alternate.cls diff --git a/paper/tex/waccopyright.sty b/docs/iclc2023-paper/tex/waccopyright.sty similarity index 100% rename from paper/tex/waccopyright.sty rename to docs/iclc2023-paper/tex/waccopyright.sty From cc4cad05b2f7fce4f50155a4799a67ea35bd5b2e Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 14 Jun 2025 23:33:52 +0100 Subject: [PATCH 207/538] add the technical manual again --- docs/technical-manual/index.md | 197 ++++++++++++++++++++++++++ docs/technical-manual/shiftflow.png | Bin 0 -> 90584 bytes docs/technical-manual/strudelflow.png | Bin 0 -> 84696 bytes docs/technical-manual/waa-nodes.png | Bin 0 -> 50450 bytes 4 files changed, 197 insertions(+) create mode 100644 docs/technical-manual/index.md create mode 100644 docs/technical-manual/shiftflow.png create mode 100644 docs/technical-manual/strudelflow.png create mode 100644 docs/technical-manual/waa-nodes.png diff --git a/docs/technical-manual/index.md b/docs/technical-manual/index.md new file mode 100644 index 000000000..16c9a5533 --- /dev/null +++ b/docs/technical-manual/index.md @@ -0,0 +1,197 @@ +This document introduces you to Strudel in a technical sense. + +It is rather out of date, but there might still be useful info below. + +If you just want to *use* Strudel, have a look at the [Tutorial](https://strudel.tidalcycles.org/tutorial/). + +## Strudel Packages + +There are different packages for different purposes. They.. + +- split up the code into smaller chunks +- can be selectively used to implement some sort of time based system + +Please refer to the individual README files in the [packages folder](https://codeberg.org/uzu/strudel/src/branch/main/packages) + +## REPL + +The [REPL](https://strudel.tidalcycles.org/) 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. + +More info in the [REPL README](https://codeberg.org/uzu/strudel/src/branch/main/packages/repl/README.md) + +# High Level Overview + + + +## 1. End User Code + +The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://github.com/tidalcycles/strudel/tree/main/packages/eval#strudelcycleseval) evaluates the user code +after a transpilation step, which resolves the syntax sugar. If you don't want the syntax sugar, you can omit the eval package and call the native javascript `eval` instead. + +### 🍭 Syntax Sugar + +JavaScript Transpilation = converting valid JavaScript to valid JavaScript: + +```js +"c3 [e3 g3]".fast(2) +``` + +becomes + +```js +mini('c3 [e3 g3]') + .withMiniLocation([1, 0, 0], [1, 11, 11]) // source location + .fast(2); +``` + +- double quoted strings and backtick strings are turned into `mini` calls (single quoted strings are left as is) +- The source location is added by chaining `withMiniLocation`, which enables the real time highlighting later +- (psuedo) variable names that look like notes (like `c4`, `bb2` or `fs3`) are turned into strings +- support for top level await +- operator overloading could be implemented in the future + +This is how it works: + + + +- The user code is parsed with a [shift parser](https://github.com/shapesecurity/shift-parser-js), generating an AST +- The AST is transformed to resolve the syntax sugar +- The AST is used to generate code again (shift-codegen) + +Shift will most likely be replaced with acorn in the future, see https://github.com/tidalcycles/strudel/issues/174 + +### Mini Notation + +Another important part of the user code is the mini notation, which allows to express rhythms in a short manner. + +- the mini notation is [implemented as a PEG grammar](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), living in the [mini package](https://github.com/tidalcycles/strudel/tree/main/packages/mini) +- it is based on [krill](https://github.com/Mdashdotdashn/krill) by Mdashdotdashn +- the peg grammar is used to generate a parser with [peggyjs](https://peggyjs.org/) +- the generated parser takes a mini notation string and outputs an AST +- the AST can then be used to construct a pattern using the regular Strudel API + +Here's an example AST: + +```json +{ + "type_": "pattern", + "arguments_": { "alignment": "h" }, + "source_": [ + { + "type_": "element", "source_": "c3", + "location_": { "start": { "offset": 1, "line": 1, "column": 2 }, "end": { "offset": 4, "line": 1, "column": 5 } } + }, + { + "type_": "element", + "location_": { "start": { "offset": 4, "line": 1, "column": 5 }, "end": { "offset": 11, "line": 1, "column": 12 } } + "source_": { + "type_": "pattern", "arguments_": { "alignment": "h" }, + "source_": [ + { + "type_": "element", "source_": "e3", + "location_": { "start": { "offset": 5, "line": 1, "column": 6 }, "end": { "offset": 8, "line": 1, "column": 9 } } + }, + { + "type_": "element", "source_": "g3", + "location_": { "start": { "offset": 8, "line": 1, "column": 9 }, "end": { "offset": 10, "line": 1, "column": 11 } } + } + ] + }, + } + ] +} +``` + +which translates to `seq(c3, seq(e3, g3))` + +## 2. Querying & Scheduling + +When the user code has been evaluated, we hopefully get a Pattern instance, which we can use to query events from. +These events can then be used to trigger side effects in the real world. On that note, Events are mostly called Hap(s) in the codebase, because JS already has a built in `Event` class. + +### Querying + +> Querying = Asking a Pattern for Events within a certain time span + +```js +seq('c3', ['e3', 'g3']) // <--- Pattern + .queryArc(0, 2) // query events within 0 and 2 cycles + .map((hap) => hap.showWhole()); // make readable +``` + +yields + +```js +[ + '0/1 -> 1/2: c3', // cycle 0 + '1/2 -> 3/4: e3', + '3/4 -> 1/1: g3', + '1/1 -> 3/2: c3', // cycle 1 + '3/2 -> 7/4: e3', + '7/4 -> 2/1: g3', +]; +``` + +### 🗓️ Scheduling + +The scheduler will query events repeatedly, creating a possibly endless loop of time slices. +Here is a simplified example of how it works + +```js +let step = 0.5; // query interval in seconds +let tick = 0; // how many intervals have passed +let pattern = seq('c3', ['e3', 'g3']); // pattern from user +setInterval(() => { + const events = pattern.queryArc(tick * step, ++tick * step); + events.forEach((event) => { + console.log(event.showWhole()); + const o = getAudioContext().createOscillator(); + o.frequency.value = getFreq(event.value); + o.start(event.whole.begin); + o.stop(event.whole.begin + event.duration); + o.connect(getAudioContext().destination); + }); +}, step * 1000); // query each "step" seconds +``` + +## 3. Sound Output + +The third and last step is to use the scheduled events to make sound. +Patterns are wrapped with param functions to compose different properties of the sound. + +```js +note("[c2(3,8) [ bb1]]") // sets frequency + .s("") // sound source + .gain(.5) // turn down volume + .cutoff(sine.range(200,1000).slow(4)) // modulated cutoff + .slow(2) + .out().logValues()`, + ]} +/> +``` + +Here is an example Hap value with different properties: + +```js +{ note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 } +``` + + +
+ +- Patterns represent just values in time! +- Suitable for any time based output (music, visuals, movement, .. ?) + +### Supported Outputs + +At the time of writing this doc, the following outputs are supported: + +- Web Audio API `.out()` see [/webaudio](https://github.com/tidalcycles/strudel/tree/main/packages/webaudio) +- MIDI `.midi()` see [/midi](https://github.com/tidalcycles/strudel/tree/main/packages/midi) +- OSC `.osc()` see [/osc](https://github.com/tidalcycles/strudel/tree/main/packages/osc) +- Serial `.serial()` see [/serial](https://github.com/tidalcycles/strudel/tree/main/packages/serial) +- Tone.js `.tone()` (deprecated?) [/tone](https://github.com/tidalcycles/strudel/tree/main/packages/tone) +- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://github.com/tidalcycles/strudel/tree/main/packages/webdirt) +- Speech `.speak()` (experimental) part of [/core](https://github.com/tidalcycles/strudel/tree/main/packages/core) + +These could change, so make sure to check the [packages folder](https://github.com/tidalcycles/strudel/tree/main/packages). diff --git a/docs/technical-manual/shiftflow.png b/docs/technical-manual/shiftflow.png new file mode 100644 index 0000000000000000000000000000000000000000..8d91658dcb1d5db29f5c8009dcfadc0d9200b19f GIT binary patch literal 90584 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 *sd!B@`9#?etzVvn33C3{X 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 literal 0 HcmV?d00001 diff --git a/docs/technical-manual/strudelflow.png b/docs/technical-manual/strudelflow.png new file mode 100644 index 0000000000000000000000000000000000000000..72cdf494bdb28e181df263387e412fe95698c795 GIT binary patch literal 84696 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 z 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%0Dl0)fVK4U6MUl5WZb0& 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 DG-OT) literal 0 HcmV?d00001 diff --git a/docs/technical-manual/waa-nodes.png b/docs/technical-manual/waa-nodes.png new file mode 100644 index 0000000000000000000000000000000000000000..cece1aa91ad50241c23557b6d2acda4787dbbb5d GIT binary patch literal 50450 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 literal 0 HcmV?d00001 From bebbe30b50fcb0925be20437a6cc6acca3d69f3a Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 15 Jun 2025 08:03:23 +0100 Subject: [PATCH 208/538] codeformat --- docs/iclc2023-paper/iclc2023.html | 1696 ++++++++++++++------------ docs/iclc2023-paper/pandoc/iclc.html | 135 +- 2 files changed, 975 insertions(+), 856 deletions(-) diff --git a/docs/iclc2023-paper/iclc2023.html b/docs/iclc2023-paper/iclc2023.html index a83e075a2..add08377f 100644 --- a/docs/iclc2023-paper/iclc2023.html +++ b/docs/iclc2023-paper/iclc2023.html @@ -1,458 +1,598 @@ - - - - - - Strudel: live coding patterns on the Web - - - - - -

+ + + + + + Strudel: live coding patterns on the Web + + + + + + -

Abstract

-
-

This paper introduces Strudel, which brings the TidalCycles approach -to live coding algorithmic patterns to native JavaScript and the web. We -begin by giving a little background of the first year of development, -before sharing some detail about its implementation and examples of use. -We go on to outline the wide range of synthesis and other outputs -available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), -WebSerial and CSound, and introduce Strudel’s REPL live editor, -including its built-in visualisations. We then compare Strudel with -Tidal, the trade-offs involved between JavaScript and Haskell, and the -unique capabilities offered by Strudel for aligning patterns.

-
+

Abstract

+
+

+ This paper introduces Strudel, which brings the TidalCycles approach to live coding algorithmic patterns to + native JavaScript and the web. We begin by giving a little background of the first year of development, before + sharing some detail about its implementation and examples of use. We go on to outline the wide range of + synthesis and other outputs available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), WebSerial and + CSound, and introduce Strudel’s REPL live editor, including its built-in visualisations. We then compare Strudel + with Tidal, the trade-offs involved between JavaScript and Haskell, and the unique capabilities offered by + Strudel for aligning patterns. +

+
-

1 Introduction

-

In the following paper, we introduce Strudel, an alternative -implementation of the TidalCycles (or ‘Tidal’ for short) live coding -system, using the JavaScript programming language. Strudel is an attempt -to make live coding more accessible, by creating a system that runs -entirely in the browser, while opening Tidal’s approach to algorithmic -patterns (Mclean 2020) up to -modern audio/visual web technologies. The Strudel REPL is a live code -editor dedicated to manipulating patterns while they play, with builtin -visual feedback. While Strudel is written in JavaScript, the API is -optimized for simplicity and readability by applying code -transformations on the syntax tree level, allowing language operations -that would otherwise be impossible. The application supports multiple -ways to output sound, including Tone.js, Web Audio Nodes, OSC (Open -Sound Control) messages, Web Serial, Web MIDI and Csound. The project is -split into multiple packages, allowing granular reuse in other -applications. Apart from TidalCycles, Strudel draws inspiration from -many prior existing projects like TidalVortex (McLean et al. 2022), -Gibber (Roberts and Kuchera-morin -2012), Estuary (Ogborn et al. -2017), Hydra (Jack [2022] 2022), Ocarina (Solomon -[2021] 2022) and Feedforward (McLean 2020). This paper -expands the Strudel Demo paper for the Web Audio Conference 2022 (Roos and McLean -2022).

-

The first tentative commit to the Strudel project was on 22nd January -2022 by Alex McLean, with the core representation implemented over the -following few days. Although this was his first attempt at a -JavaScript-based application, by 27th January, Alex had managed to -upload the initial version to the ‘npm’ javascript package database, -sharing with the wider community for comment. By 4th February, Felix -Roos had discovered Strudel and contributed a ‘REPL’ user interface to -it, and then contributed a scheduler the next day, so that Strudel could -already make sound. At this point, Alex and Felix shared ownership to -the repository, and the project has since proved to be a productive -confluence of Felix’s own work into music representation and -visualisation, with Alex’s experience with making Tidal. Felix has since -become the primary contributor to Strudel, with Alex continuing to jump -between developing both Strudel and Tidal. Aspects of Strudel’s -development have therefore fed back into TidalCycles, and both systems -have maintained a shared conceptual underpinning. We plan to continue -working towards feature parity between these systems, although within -the syntactical trade-offs and library ecosystems of JavaScript and -Haskell, some divergence is inevitable and healthy.

-

Over the first year of its life, Strudel is now a fully-fledged live -coding environment, porting Tidal’s core represention of patterns, -pattern transformations, and mininotation for polymetric sequences, -combined with a wealth of features for synthesising and visualising -those patterns.

-

2 From Tidal to Strudel and -back

-

As mentioned above, the original Tidal is implemented as a domain -specific language (DSL) embedded in the Haskell pure functional -programming language, and takes advantage of Haskell’s terse syntax and -advanced, ‘strong’ type system. JavaScript on the other hand, is a -multi-paradigm programming language, with a dynamic type system. Because -Tidal leans heavily on many of Haskell’s more unique features, it was -not always clear that it could meaningfully be ported to a -multi-paradigm scripting language. However, this possibility was already -demonstrated with an earlier port to Python [TidalVortex; McLean et al. -(2022)], and we have now successfully implemented Tidal’s pure -functional representation of patterns in Strudel, including partial -application, currying, and the functor, applicative and monadic -structures that underlie Tidal’s expressive pattern transformations. The -result is a terse and highly composable system, where everything is -either a pattern, or a function for combining and manipulating patterns, -offering a rich creative ground for exploration.

-

This development process has been far from a one-way port, however. -The process of porting Tidal’s concepts has also opened up new -possibilities, some just from revisiting every design decision, and some -from the particular affordances and constraints offered by JavaScript. -This has lead to new features (and indeed bugfixes) that have found -their way back to Tidal where appropriate, and ongoing work that we will -return to in the conclusion of this paper.

-

3 Representing Patterns

-

Patterns are the essence of Tidal. Its patterns are abstract entities -that represent flows of time as functions, adapting a technique called -pure functional reactive programming. Taking a time span as its input, a -Pattern can output a set of events that happen within that time span. It -depends on the structure of the Pattern how the events are located in -time. From now on, this process of generating events from a time span -will be called querying. Example:

-
const pattern = sequence(c3, [e3, g3])
+    

1 Introduction

+

+ In the following paper, we introduce Strudel, an alternative implementation of the TidalCycles (or + ‘Tidal’ for short) live coding system, using the JavaScript programming language. Strudel is an attempt to make + live coding more accessible, by creating a system that runs entirely in the browser, while opening Tidal’s + approach to algorithmic patterns + (Mclean 2020) up to modern audio/visual + web technologies. The Strudel REPL is a live code editor dedicated to manipulating patterns while they play, with + builtin visual feedback. While Strudel is written in JavaScript, the API is optimized for simplicity and + readability by applying code transformations on the syntax tree level, allowing language operations that would + otherwise be impossible. The application supports multiple ways to output sound, including Tone.js, Web Audio + Nodes, OSC (Open Sound Control) messages, Web Serial, Web MIDI and Csound. The project is split into multiple + packages, allowing granular reuse in other applications. Apart from TidalCycles, Strudel draws inspiration from + many prior existing projects like TidalVortex + (McLean et al. 2022), Gibber + (Roberts and Kuchera-morin 2012), Estuary + (Ogborn et al. 2017), Hydra + (Jack [2022] 2022), Ocarina + (Solomon [2021] 2022) and Feedforward + (McLean 2020). This paper expands the Strudel + Demo paper for the Web Audio Conference 2022 + (Roos and McLean 2022). +

+

+ The first tentative commit to the Strudel project was on 22nd January 2022 by Alex McLean, with the core + representation implemented over the following few days. Although this was his first attempt at a JavaScript-based + application, by 27th January, Alex had managed to upload the initial version to the ‘npm’ javascript package + database, sharing with the wider community for comment. By 4th February, Felix Roos had discovered Strudel and + contributed a ‘REPL’ user interface to it, and then contributed a scheduler the next day, so that Strudel could + already make sound. At this point, Alex and Felix shared ownership to the repository, and the project has since + proved to be a productive confluence of Felix’s own work into music representation and visualisation, with Alex’s + experience with making Tidal. Felix has since become the primary contributor to Strudel, with Alex continuing to + jump between developing both Strudel and Tidal. Aspects of Strudel’s development have therefore fed back into + TidalCycles, and both systems have maintained a shared conceptual underpinning. We plan to continue working + towards feature parity between these systems, although within the syntactical trade-offs and library ecosystems of + JavaScript and Haskell, some divergence is inevitable and healthy. +

+

+ Over the first year of its life, Strudel is now a fully-fledged live coding environment, porting Tidal’s core + represention of patterns, pattern transformations, and mininotation for polymetric sequences, combined with a + wealth of features for synthesising and visualising those patterns. +

+

+ 2 From Tidal to Strudel and back +

+

+ As mentioned above, the original Tidal is implemented as a domain specific language (DSL) embedded in the Haskell + pure functional programming language, and takes advantage of Haskell’s terse syntax and advanced, ‘strong’ type + system. JavaScript on the other hand, is a multi-paradigm programming language, with a dynamic type system. + Because Tidal leans heavily on many of Haskell’s more unique features, it was not always clear that it could + meaningfully be ported to a multi-paradigm scripting language. However, this possibility was already demonstrated + with an earlier port to Python [TidalVortex; + McLean et al. (2022)], and we have now + successfully implemented Tidal’s pure functional representation of patterns in Strudel, including partial + application, currying, and the functor, applicative and monadic structures that underlie Tidal’s expressive + pattern transformations. The result is a terse and highly composable system, where everything is either a pattern, + or a function for combining and manipulating patterns, offering a rich creative ground for exploration. +

+

+ This development process has been far from a one-way port, however. The process of porting Tidal’s concepts has + also opened up new possibilities, some just from revisiting every design decision, and some from the particular + affordances and constraints offered by JavaScript. This has lead to new features (and indeed bugfixes) that have + found their way back to Tidal where appropriate, and ongoing work that we will return to in the conclusion of this + paper. +

+

+ 3 Representing Patterns +

+

+ Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, + adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern can + output a set of events that happen within that time span. It depends on the structure of the Pattern how the + events are located in time. From now on, this process of generating events from a time span will be called + querying. Example: +

+
+
const pattern = sequence(c3, [e3, g3])
 const events = pattern.queryArc(0, 1)
-console.log(events.map(e => e.show()))
-

In this example, we create a pattern using the sequence -function and query it for the time span from -0 to 1. Those numbers represent units of time -called cycles. The length of one cycle depends on the -tempo, which defaults to one cycle per second. The resulting events -are:

-
[{ value: 'c3', begin: 0, end: 1/2 },
+console.log(events.map(e => e.show()))
+
+

+ In this example, we create a pattern using the sequence function and query it for + the time span from 0 to 1. Those numbers represent units of time called + cycles. The length of one cycle depends on the tempo, which defaults to one cycle per second. The + resulting events are: +

+
+
[{ value: 'c3', begin: 0, end: 1/2 },
 { value: 'e3', begin: 1/2, end: 3/4 },
-{ value: 'g3', begin: 3/4, end: 1 }]
-

Each event has a value, a begin time and an end time, where time is -represented as a fraction. In the above case, the events are placed in -sequential order, where c3 takes the first half, and e3 and g3 together -take the second half. This temporal placement is the result of the -sequence function, which divides its arguments equally over -one cycle. If an argument is an array, the same rule applies to that -part of the cycle. In the example, e3 and g3 are divided equally over -the second half of the whole cycle.

-

The above examples do not represent how Strudel is used in practice. -In the live coding editor, the user only has to type in the pattern -itself, the querying will be handled by the scheduler. The scheduler -will repeatedly query the pattern for events, which are then scheduled -as sound synthesis or other event triggers. Also, the above event data -structure has been simplified for readability.

-
- - -
-

4 Making Patterns

-

In practice, the end-user live coder will not deal with constructing -patterns directly, but will rather build patterns using Strudel’s -extensive combinator library to create, combine and transform -patterns.

-

The live coder will rarely use the sequence function as -seen above, as sequencing is implicit in many functions. For example in -the following, the note function constructs a pattern of -notes, sequencing its arguments in the same manner as the previous -example.

-
note(c3, [e3, g3])
-

Perhaps more often, they will use the mini-notation for even terser -notation of rhythmic sequences: [^This last example is also valid Tidal -code, albeit the parenthesis is not required in its Haskell syntax in -this case. Tidal does not support passing sequences as lists directly to -the note function, however.].

-
note("c3 [e3 g3]")
-

Such sequences are often treated only a starting point for -manipulation, where they then undergo pattern transformations such as -repetition, symmetry, interference/combination or randomisation, -potentially at multiple timescales. Because Strudel patterns are -represented as pure functions of time rather than as data structures, -very long and complex generative results can be represented and -manipulated without having to store the resulting sequences in -memory.

-

5 Pattern Example

-

The following example showcases how patterns can be utilized to -create musical complexity from simple parts, using repetition and -interference:

-
"<0 2 [4 6](3,4,1) 3>"
+{ value: 'g3', begin: 3/4, end: 1 }]
+
+

+ Each event has a value, a begin time and an end time, where time is represented as a fraction. In the above case, + the events are placed in sequential order, where c3 takes the first half, and e3 and g3 together take the second + half. This temporal placement is the result of the sequence function, which divides its arguments + equally over one cycle. If an argument is an array, the same rule applies to that part of the cycle. In the + example, e3 and g3 are divided equally over the second half of the whole cycle. +

+

+ The above examples do not represent how Strudel is used in practice. In the live coding editor, the user only has + to type in the pattern itself, the querying will be handled by the scheduler. The scheduler will repeatedly query + the pattern for events, which are then scheduled as sound synthesis or other event triggers. Also, the above event + data structure has been simplified for readability. +

+
+ Screenshot of the Strudel ‘REPL’ live coding editor, including piano-roll visualisation. + +
+

4 Making Patterns

+

+ In practice, the end-user live coder will not deal with constructing patterns directly, but will rather build + patterns using Strudel’s extensive combinator library to create, combine and transform patterns. +

+

+ The live coder will rarely use the sequence function as seen above, as sequencing is implicit in many + functions. For example in the following, the note function constructs a pattern of notes, sequencing + its arguments in the same manner as the previous example. +

+
+
note(c3, [e3, g3])
+
+

+ Perhaps more often, they will use the mini-notation for even terser notation of rhythmic sequences: [^This last + example is also valid Tidal code, albeit the parenthesis is not required in its Haskell syntax in this case. Tidal + does not support passing sequences as lists directly to the note function, however.]. +

+
+
note("c3 [e3 g3]")
+
+

+ Such sequences are often treated only a starting point for manipulation, where they then undergo pattern + transformations such as repetition, symmetry, interference/combination or randomisation, potentially at multiple + timescales. Because Strudel patterns are represented as pure functions of time rather than as data structures, + very long and complex generative results can be represented and manipulated without having to store the resulting + sequences in memory. +

+

5 Pattern Example

+

+ The following example showcases how patterns can be utilized to create musical complexity from simple parts, using + repetition and interference: +

+
+
"<0 2 [4 6](3,4,1) 3>"
 .off(1/4, add(2))
 .off(1/2, add(6))
 .scale('D minor')
 .legato(.25)
 .note().s("sawtooth square")
-.delay(.8).delaytime(.125)
-

The pattern starts with a rhythm of numbers in mini notation, which -are later interpreted inside the scale of D minor. The first line could -also be expressed without mini notation:

-
cat(0, 2, [4, 6].euclid(3, 4, 1), 3)
-

These numbers then undergo various pattern transformations. Here is a -short description of all the functions used:

-
    -
  • cat: play elements sequentially, where each lasts one -cycle
  • -
  • brackets: elements inside brackets are divided equally -over the time of their parent
  • -
  • .euclid(p, s, o): place p pulses evenly over s steps, -with offset o (Toussaint -2005)
  • -
  • .off(n, f): layers a pattern on top of itself, with the -new layer offset by n cycles, and with function f applied
  • -
  • .legato(n): multiply the duration of all events in a -pattern by a factor of n
  • -
  • .echo(t, n, v): copy each event t times, with n cycles -in between each copy, decreasing velocity by v
  • -
  • .note(): interpretes values as notes
  • -
  • .s(name): play back each event with the given -sound
  • -
  • .delay(wet): add delay
  • -
  • .delaytime(t): set delay time
  • -
-

Much of the above will be familiar to Tidal users.

- -

6 Ways to make Sound (and other -events)

-

To generate sound, Strudel supports bindings for different -outputs:

-
    -
  • Tone.js (deprecated)
  • -
  • Web Audio API
  • -
  • WebDirt, a js recreation of Tidal’s Dirt sample engine -(deprecated)
  • -
  • OSC via osc-js, compatible with superdirt
  • -
  • Csound via the Csound WebAssembly build
  • -
  • MIDI via WebMIDI
  • -
  • Serial via WebSerial
  • -
-

At first, we used Tone.js as sound output, but it proved to be -limited for the use case of Strudel, where each individual event could -potentially have a completely different audio graph. While the Web Audio -API takes a fire-and-forget approach, creating a lot of Tone.js -instruments and effects causes performance issues quickly. For that -reason, we chose to search for alternatives.

-

Strudel’s new default output uses the Web Audio API to create a new -audio graph for each event. It currently supports basic oscillators, -sample playback, various effects and an experimental support for -soundfonts.

-

WebDirt (Ogborn [2016] 2022) was -created as part of the Estuary Live Coding System (Ogborn et al. -2017), and proved to be a solid choice for handling samples in -Strudel as well. We are however focused on working more directly with -the Web Audio API to be able to integrate new features more tightly.

-

Using the OSC protocol via Strudel’s provided Node.js-based OSC proxy -server, it is possible to send network messages to trigger events. This -is mainly used to render sound using SuperDirt (SuperDirt [2015] 2022), -which is the well-developed Supercollider-based synthesis framework that -Tidal live coders generally use as standard.

-

Recently, the experimental integration of Csound proved to bring a -new dimension of sound design capabilities to Strudel. Thanks to the -WebAssembly distribution of this classic system (Yi, Lazzarini, and Costello -2018), Csound ‘orchestra’ synthesisers can be embedded in and -then patterned with Strudel code.

-

MIDI output can also be used to send MIDI messages to either external -instruments or to other programs on the same device. Unlike OSC, Strudel -is able to send MIDI directly without requiring additional proxy -software, but only from web browsers that support it (at the time of -writing, this means Chromium-based browsers).

-

Finally, Strudel supports Serial output, for example to trigger -events via microcontrollers. This has already been explored for robot -choreography by Kate Sicchio and Alex McLean, via a performance -presented at the International Conference on Live Interfaces 2022.

-

7 The Strudel REPL

-

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, which highlights 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.

-

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:

-
    -
  1. The user writes and updates code. Each update transpiles and -evaluates it to create a Pattern instance
  2. -
  3. While the REPL is running, the Scheduler queries the -active Pattern by a regular interval, generating -Events (also known as Haps in Strudel) for the -next time span.
  4. -
  5. For each scheduling tick, all generated Events are -triggered by calling their onTrigger method, which is set -by the output.
  6. -
-
-REPL control flow - -
-

7.1 User Code

-

To create a Pattern from the user code, two steps are -needed:

-
    -
  1. Transpile the JS input code to make it functional
  2. -
  3. Evaluate the transpiled code
  4. -
-

7.1.1 Transpilation & -Evaluation

-

In the JavaScript world, using transpilation is a common practise to -be able to use language features that are not supported by the base -language. Tools like babel will transpile code that -contains unsupported language features into a version of the code -without those features.

-

In the same tradition, Strudel can add a transpilation step to -simplify the user code in the context of live coding. For example, the -Strudel REPL lets the user create mini notation patterns using just -double quoted strings, while single quoted strings remain what they -are:

-
"c3 [e3 g3]*2"
-

is transpiled to:

-
mini("c3 [e3 g3]*2").withMiniLocation([1,0,0],[1,14,14])
-

Here, the string is wrapped in mini, which will create a -pattern from a mini notation string. Additionally, the -withMiniLocation method passes the original source code -location of the string to the pattern, which enables highlighting active -events.

-

Other convenient features like pseudo variables, operator overloading -and top level await are possible with transpilation.

-

After the transpilation, the code is ready to be evaluated into a -Pattern.

-

Behind the scenes, the user code string is parsed with -acorn, turning it into an Abstract Syntax Tree (AST). The -AST allows changing the structure of the code before generating the -transpiled version using escodegen.

-

7.1.2 Mini Notation

-

While the transpilation allows JavaScript to express Patterns in a -less verbose way, it is still preferable to use the Mini Notation as a -more compact way to express rhythm. Strudel aims to provide the same -Mini Notation features and syntax as used in Tidal.

-

The Mini Notation parser is implemented using peggy, -which allows generating performant parsers for Domain Specific Languages -(DSLs) using a concise grammar notation. The generated parser turns the -Mini Notation string into an AST which is used to call the respective -Strudel functions with the given structure. For example, -"c3 [e3 g3]*2" will result in the following calls:

-
seq(
+.delay(.8).delaytime(.125)
+
+

+ The pattern starts with a rhythm of numbers in mini notation, which are later interpreted inside the scale of D + minor. The first line could also be expressed without mini notation: +

+
+
cat(0, 2, [4, 6].euclid(3, 4, 1), 3)
+
+

+ These numbers then undergo various pattern transformations. Here is a short description of all the functions used: +

+
    +
  • cat: play elements sequentially, where each lasts one cycle
  • +
  • brackets: elements inside brackets are divided equally over the time of their parent
  • +
  • + .euclid(p, s, o): place p pulses evenly over s steps, with offset o + (Toussaint 2005) +
  • +
  • + .off(n, f): layers a pattern on top of itself, with the new layer offset by n cycles, and with + function f applied +
  • +
  • .legato(n): multiply the duration of all events in a pattern by a factor of n
  • +
  • + .echo(t, n, v): copy each event t times, with n cycles in between each copy, decreasing velocity by + v +
  • +
  • .note(): interpretes values as notes
  • +
  • .s(name): play back each event with the given sound
  • +
  • .delay(wet): add delay
  • +
  • .delaytime(t): set delay time
  • +
+

Much of the above will be familiar to Tidal users.

+ +

+ 6 Ways to make Sound (and other events) +

+

To generate sound, Strudel supports bindings for different outputs:

+
    +
  • Tone.js (deprecated)
  • +
  • Web Audio API
  • +
  • WebDirt, a js recreation of Tidal’s Dirt sample engine (deprecated)
  • +
  • OSC via osc-js, compatible with superdirt
  • +
  • Csound via the Csound WebAssembly build
  • +
  • MIDI via WebMIDI
  • +
  • Serial via WebSerial
  • +
+

+ At first, we used Tone.js as sound output, but it proved to be limited for the use case of Strudel, where each + individual event could potentially have a completely different audio graph. While the Web Audio API takes a + fire-and-forget approach, creating a lot of Tone.js instruments and effects causes performance issues + quickly. For that reason, we chose to search for alternatives. +

+

+ Strudel’s new default output uses the Web Audio API to create a new audio graph for each event. It currently + supports basic oscillators, sample playback, various effects and an experimental support for soundfonts. +

+

+ WebDirt (Ogborn [2016] 2022) was created as part + of the Estuary Live Coding System + (Ogborn et al. 2017), and + proved to be a solid choice for handling samples in Strudel as well. We are however focused on working more + directly with the Web Audio API to be able to integrate new features more tightly. +

+

+ Using the OSC protocol via Strudel’s provided Node.js-based OSC proxy server, it is possible to send network + messages to trigger events. This is mainly used to render sound using SuperDirt + (SuperDirt [2015] 2022), which is the + well-developed Supercollider-based synthesis framework that Tidal live coders generally use as standard. +

+

+ Recently, the experimental integration of Csound proved to bring a new dimension of sound design capabilities to + Strudel. Thanks to the WebAssembly distribution of this classic system + (Yi, Lazzarini, and Costello 2018), Csound + ‘orchestra’ synthesisers can be embedded in and then patterned with Strudel code. +

+

+ MIDI output can also be used to send MIDI messages to either external instruments or to other programs on the same + device. Unlike OSC, Strudel is able to send MIDI directly without requiring additional proxy software, but only + from web browsers that support it (at the time of writing, this means Chromium-based browsers). +

+

+ Finally, Strudel supports Serial output, for example to trigger events via microcontrollers. This has already been + explored for robot choreography by Kate Sicchio and Alex McLean, via a performance presented at the International + Conference on Live Interfaces 2022. +

+

7 The Strudel REPL

+

+ 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, which highlights 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. +

+

+ 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: +

+
    +
  1. + The user writes and updates code. Each update transpiles and evaluates it to create a + Pattern instance +
  2. +
  3. + While the REPL is running, the Scheduler queries the active Pattern by a regular + interval, generating Events (also known as Haps in Strudel) for the next time span. +
  4. +
  5. + For each scheduling tick, all generated Events are triggered by calling their + onTrigger method, which is set by the output. +
  6. +
+
+ REPL control flow + +
+

7.1 User Code

+

To create a Pattern from the user code, two steps are needed:

+
    +
  1. Transpile the JS input code to make it functional
  2. +
  3. Evaluate the transpiled code
  4. +
+

+ 7.1.1 Transpilation & Evaluation +

+

+ In the JavaScript world, using transpilation is a common practise to be able to use language features that are not + supported by the base language. Tools like babel will transpile code that contains unsupported + language features into a version of the code without those features. +

+

+ In the same tradition, Strudel can add a transpilation step to simplify the user code in the context of live + coding. For example, the Strudel REPL lets the user create mini notation patterns using just double quoted + strings, while single quoted strings remain what they are: +

+
+
"c3 [e3 g3]*2"
+
+

is transpiled to:

+
+
mini("c3 [e3 g3]*2").withMiniLocation([1,0,0],[1,14,14])
+
+

+ Here, the string is wrapped in mini, which will create a pattern from a mini notation string. + Additionally, the withMiniLocation method passes the original source code location of the string to + the pattern, which enables highlighting active events. +

+

+ Other convenient features like pseudo variables, operator overloading and top level await are possible with + transpilation. +

+

After the transpilation, the code is ready to be evaluated into a Pattern.

+

+ Behind the scenes, the user code string is parsed with acorn, turning it into an Abstract Syntax Tree + (AST). The AST allows changing the structure of the code before generating the transpiled version using + escodegen. +

+

7.1.2 Mini Notation

+

+ While the transpilation allows JavaScript to express Patterns in a less verbose way, it is still preferable to use + the Mini Notation as a more compact way to express rhythm. Strudel aims to provide the same Mini Notation features + and syntax as used in Tidal. +

+

+ The Mini Notation parser is implemented using peggy, which allows generating performant parsers for + Domain Specific Languages (DSLs) using a concise grammar notation. The generated parser turns the Mini Notation + string into an AST which is used to call the respective Strudel functions with the given structure. For example, + "c3 [e3 g3]*2" will result in the following calls: +

+
+
seq(
   reify('c3').withLocation([1,1,1], [1,4,4]),
   seq(
     reify('e3').withLocation([1,5,5], [1,8,8]),
     reify('g3').withLocation([1,8,8], [1,10,10]),
   ).fast(2)
-)
-

7.1.3 Highlighting Locations

-

As seen in the examples above, both the JS and the Mini Notation -parser add source code locations using withMiniLocation and -withLocation methods. While the JS parser adds locations -relative to the user code as a whole, the Mini Notation adds locations -relative to the position of the mini notation string. The absolute -location of elements within Mini Notation can be calculated by simply -adding both locations together. This absolute location can be used to -highlight active events in real time.

-

7.2 Scheduling Events

-

After an instance of Pattern is obtained from the user -code, it is used by the scheduler to get queried for events. Once -started, the scheduler runs at a fixed interval to query active pattern -for events withing the current interval’s time span. A simplified -implementation looks like this:

-
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
+)
+
+

+ 7.1.3 Highlighting Locations +

+

+ As seen in the examples above, both the JS and the Mini Notation parser add source code locations using + withMiniLocation and withLocation methods. While the JS parser adds locations relative + to the user code as a whole, the Mini Notation adds locations relative to the position of the mini notation + string. The absolute location of elements within Mini Notation can be calculated by simply adding both locations + together. This absolute location can be used to highlight active events in real time. +

+

7.2 Scheduling Events

+

+ After an instance of Pattern is obtained from the user code, it is used by the scheduler to get + queried for events. Once started, the scheduler runs at a fixed interval to query active pattern for events + withing the current interval’s time span. A simplified implementation looks like this: +

+
+
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
 let interval = 0.5; // query interval in seconds
 let time = 0; // beginning of current time span
 let minLatency = .1; // min time before a hap should trigger
@@ -463,70 +603,80 @@ class="sourceCode js">    const deadline = hap.whole.begin - time + minLatency;
     onTrigger(hap, deadline, duration);
   });
-}, interval * 1000); // query each "interval" seconds
-

Note that the above code is simplified for illustrative purposes. The -actual implementation has to work around imprecise callbacks of -setInterval. More about the implementation details can be -read in this -blog post.

-

The fact that Pattern.queryArc is a pure function that -maps a time span to a set of events allows us to choose any interval we -like without changing the resulting output. It also means that when the -pattern is changed from outside, the next scheduling callback will work -with the new pattern, keeping its clock running.

-

The latency between the time the pattern is evaluated and the change -is heard is between minLatency and -interval + minLatency, in our example between 100ms and -600ms. In Strudel, the current query interval is 50ms with a minLatency -of 100ms, meaning the latency is between 50ms and 150ms.

-

7.3 Output

-

The last step is to trigger each event in the chosen output. This is -where the given time and value of each event is used to generate audio -or any other form of time based output. The default output of the -Strudel REPL is the WebAudio output. To understand what an output does, -we first have to understand what control parameters are.

-

7.3.1 Control Parameters

-

To be able to manipulate multiple aspects of sound in parallel, so -called control parameters are used to shape the value of each event. -Example:

-
note("c3 e3").cutoff(1000).s('sawtooth')
+}, interval * 1000); // query each "interval" seconds
+
+

+ Note that the above code is simplified for illustrative purposes. The actual implementation has to work around + imprecise callbacks of setInterval. More about the implementation details can be read in + this blog post. +

+

+ The fact that Pattern.queryArc is a pure function that maps a time span to a set of events allows us + to choose any interval we like without changing the resulting output. It also means that when the pattern is + changed from outside, the next scheduling callback will work with the new pattern, keeping its clock running. +

+

+ The latency between the time the pattern is evaluated and the change is heard is between + minLatency and interval + minLatency, in our example between 100ms and 600ms. In + Strudel, the current query interval is 50ms with a minLatency of 100ms, meaning the latency is between 50ms and + 150ms. +

+

7.3 Output

+

+ The last step is to trigger each event in the chosen output. This is where the given time and value of each event + is used to generate audio or any other form of time based output. The default output of the Strudel REPL is the + WebAudio output. To understand what an output does, we first have to understand what control parameters are. +

+

+ 7.3.1 Control Parameters +

+

+ To be able to manipulate multiple aspects of sound in parallel, so called control parameters are used to shape the + value of each event. Example: +

+
+
note("c3 e3").cutoff(1000).s('sawtooth')
   .queryArc(0, 1).map(hap => hap.value)
 /* [
   { note: 'c3', cutoff: 1000, s: 'sawtooth' }
   { note: 'e3', cutoff: 1000, s: 'sawtooth' }
-] */
-

Here, the control parameter functions note, -cutoff and s are used, where each controls a -different property in the value object. Each control parameter function -accepts a primitive value, a list of values to be sequenced into a -Pattern, or a Pattern. In the example, -note gets a Pattern from a Mini Notation -expression (double quoted), while cutoff and s -are given a Number and a (single quoted) -String respectively.

-

Strudel comes with a large default set of control parameter functions -that are based on the ones used by Tidal and SuperDirt, focusing on -music and audio terminology. It is however possible to create custom -control paramters for any purpose:

-
const { x, y } = createParams('x', 'y')
-x(sine.range(0, 200)).y(cosine.range(0,200))
-

This example creates the custom control parameters x and -y which are then used to form a pattern that descibes the -coordinates of a circle.

-

7.3.2 Outputs

-

Now that we know how the value of an event is manipulated using -control parameters, we can look at how outputs can use that value to -generate anything. The scheduler above was calling the -onTrigger function which is used to implement the output. A -very simple version of the web audio output could look like this:

-
function onTrigger(hap, deadline, duration) {
+] */
+
+

+ Here, the control parameter functions note, cutoff and s are used, where + each controls a different property in the value object. Each control parameter function accepts a primitive value, + a list of values to be sequenced into a Pattern, or a Pattern. In the example, + note gets a Pattern from a Mini Notation expression (double quoted), while + cutoff and s are given a Number and a (single quoted) + String respectively. +

+

+ Strudel comes with a large default set of control parameter functions that are based on the ones used by Tidal and + SuperDirt, focusing on music and audio terminology. It is however possible to create custom control paramters for + any purpose: +

+
+
const { x, y } = createParams('x', 'y')
+x(sine.range(0, 200)).y(cosine.range(0,200))
+
+

+ This example creates the custom control parameters x and y which are then used to form a + pattern that descibes the coordinates of a circle. +

+

7.3.2 Outputs

+

+ Now that we know how the value of an event is manipulated using control parameters, we can look at how outputs can + use that value to generate anything. The scheduler above was calling the onTrigger function which is + used to implement the output. A very simple version of the web audio output could look like this: +

+
+
function onTrigger(hap, deadline, duration) {
   const { note } = hap.value;
   const time = getAudioContext().currentTime + deadline;
   const o = getAudioContext().createOscillator();
@@ -534,285 +684,271 @@ class="sourceCode js">  o.start(time);
   o.stop(time + event.duration);
   o.connect(getAudioContext().destination);
-}
-

The above example will create an OscillatorNode for each -event, where the frequency is controlled by the note param. -In essence, this is how the WebAudio API output of Strudel works, only -with many more parameters to control synths, samples and effects.

-

8 Pattern alignment and -combination

-

One core aspect of Strudel, inherited from Tidal, is the flexible way -that patterns can be combined, irrespective of their structure. Its -declarative approach means a live coder does not have to think about the -details of how this is done, only what is to be -done.

-

As a simple example, consider two number patterns -"0 [1 2] 3", and "10 20". The first has three -contiguous steps of equal lengths, with the second step broken down into -two substeps, giving four events in total. There are a very large number -of ways in which the structure of these two patterns could be combined, -but the default method in both Strudel and Tidal is to line up the -cycles of the two patterns, and then take events from the first pattern -and match them with those in the second pattern. Therefore, the -following two lines are equivalent:

-
"0 [1 2] 3".add("10 20")
-"10 [11 22] 23"
-

Where the events only partially overlap, they are treated as -fragments of the event in the first pattern. This is a little difficult -to conceptualise, but lets start by comparing the two patterns in the -following example:

-
"0 1 2".add("10 20")
-"10 [11 21] 20"
-

They are similar to the previous example in that the number -1 is split in two, with its two halves added to -10 and 20 respectively. However, the -11 ‘remembers’ that it is a fragment of that original -1 event, and so is treated as having a duration of a third -of a cycle, despite only being active for a sixth of a cycle. Likewise, -the 21 is also a fragment of that original 1 -event, but a fragment of its second half. Because the start of its event -is missing, it wouldn’t actually trigger a sound (unless it underwent -further pattern transformations/combinations).

-

In practice, the effect of this default, implicit method for -combining two patterns is that the second pattern is added in -to the first one, and indeed this can be made explicit:

-
"0 1 2".add.in("10 20")
-

This makes way for other ways to align the pattern, and several are -already defined, in particular:

-
    -
  • in - as explained above, aligns cycles, and applies -values from the pattern on the right in to the pattern on the -left.
  • -
  • out - as with in, but values are applied -out of the pattern on the left (i.e. in to the one on -the right).
  • -
  • mix - structures from both patterns are combined, so -that the new events are not fragments but are created at intersections -of events from both sides.
  • -
  • squeeze - cycles from the pattern on the right are -squeezed into events on the left. So that -e.g. "0 1 2".add.squeeze("10 20") is equivalent to -"[10 20] [11 21] [12 22]".
  • -
  • squeezeout - as with squeeze, but cycles -from the left are squeezed into events on the right. So, -"0 1 2".add.squeezeout("10 20") is equivalent to -[10 11 12] [20 21 22].
  • -
  • trig is similar to squeezeout in that -cycles from the right are aligned with events on the left. However those -cycles are not ‘squeezed’, rather they are truncated to fit the event. -So "0 1 2 3 4 5 6 7".add.trig("10 [20 30]") would be -equivalent to 10 11 12 13 20 21 30 31. In effect, events on -the right ‘trigger’ cycles on the left.
  • -
  • trigzero is similar to trig, but the -pattern is ‘triggered’ from its very first cycle, rather than from the -current cycle. trig and trigzero therefore -only give different results where the leftmost pattern differs from one -cycle to the next.
  • -
-

We will save going deeper into the background, design and -practicalities of these alignment functions for future publications. -However in the next section, we take them as a case study for looking at -the different design affordances offered by Haskell to Tidal, and -JavaScript to Strudel.

-

9 Comparing Strudel and Haskell in -use

-

Unlike Haskell, JavaScript lacks the ability to define custom infix -operators, or change the meaning of existing ones. So the above Strudel -example of "0 1 2".add.out("10 20") is equivalent to the -Tidal expression "0 1 2" +| "10 20", where the vertical bar -in the operator +| stands for out (where -a |+ b would be equivalent of -a.add.in(b)).

-

From this we can already see that Tidal tends towards brevity through -mixing infix operators with functions, and Strudel tends towards -spelling out operations which are joined together with the -. operator. This then is the design trade-off of Tidal’s -tersity, versus Strudel’s simplicity.

-

To demonstrate this, consider the following Tidal pattern:

-
iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4
-

This can be directly translated to the Strudel equivalent:

-
iter(4, every(3, add.squeeze("10 20"), n("0 1 3").s("triangle").crush(4)))
-

Although for a more canonical Strudel expression, we would reorder it -as:

-
n("0 1 3").every(3, add.squeeze("10 20")).iter(4).s("triangle").crush(4)
-

The Strudel example uses the . method call operator for -all operations and combinations, whereas the Tidal example has -# for the default method for combining patterns and uses -infix operators for other methods. The lack of parenthesis in the Tidal -example is partly due to the way that arguments are applied to Haskell’s -functions, and partly due to the use of the $ operator as -an alternative way to establish precedence and control the order of -evaluation.

-

Considering the above, we argue that the Haskell syntax is a little -cleaner, but that the Strudel syntax is easier to learn. Our informal -observation is that while Haskell’s dollar $ operator is -very useful in making code easier to work with, it is one of the most -difficult aspects of Tidal use for beginners to learn. On the other -hand, the deeper levels of parenthesis in Strudel code can be difficult -to keep track of, especially while coding under pressure of live musical -performance. However this difficulty can be largely be mitigated by -reordering expressions, and further mitigated by supporting editor -features.

-

With Strudel, we have little choice but to embrace the affordances -and constraints offered by JavaScript, and while designing a -domain-specific language entirely based on method calls is a challenge, -through creative adoption of functional programming techniques like -partial application, we are so far very happy with the results. Tidal’s -functional reactive approach to pattern-making has in general translated -well to JavaScript, and opportunities and constraints have overall -traded off to create a very approachable and useable live coding -environment.

-

9.1 The trade-off of flexible -typing

-

We have identified one problem with porting Tidal to JavaScript where -we have missed Haskell’s strict typing and type inference. In both Tidal -and Strudel, time is rational, where any point in time is represented as -the ratio of two integers. This allows representation of musical ratios -such that are impossible to represent accurately using the more common -floating point numbers. However while libraries are available that -support rational numbers in JavaScript, the lack of strict typing means -that it is easy to implement pattern methods where computationally -expensive conversion from floating point to rational numbers are -performed late, and therefore often enough to overload the CPUs, due to -the large number of iterative calculations required to estimate a ratio -for a given floating point number. To mitigate this problem, we might -consider moving to TypeScript in the future.

-

10 Future Outlook

-

The project is still young, with many features on the horizon. As -general guiding principles, Strudel aims to be

-
    -
  1. accessible
  2. -
  3. consistent with Tidal’s approach to pattern
  4. -
  5. modular and extensible
  6. -
-

While Haskell’s type system makes it a great language for the ongoing -development of Tidal’s inner representation of pattern, JavaScript’s -vibrant ecosystem, flexibility and accessibility makes it a great host -for more ad-hoc experiments, including interface design. For the future, -it is planned to integrate additional alternative sound engines such as -Glicol (Lan -[2020] 2022) and Faust (Faust - Programming -Language for Audio Applications and Plugins [2016] 2022). -Strudel is already approaching feature parity with Tidal, but there are -more Tidal functions to be ported, and work to be done to improve -compatibility with Tidal’s mininotation. Tidal version 2.0 is under -development, which brings a new representation for sequences to its -patterns, which will then be brought to Strudel. Besides sound, other -ways to render events are being explored, such as graphical, and -choreographic output. We are also looking into alternative ways of -editing patterns, including multi-user editing for network music, -parsing a novel syntax to escape the constraints of javascript, and -developing hardware/e-textile interfaces. In summary, there is a lot of -fun ahead.

-

11 Links

-

The Strudel REPL is available at https://strudel.cc, including an -interactive tutorial. The repository is at https://github.com/tidalcycles/strudel, all the code is -open source under the AGPL-3.0 License.

-

12 Acknowledgments

-

Thanks to the Strudel and wider Tidal, live coding, WebAudio and -free/open source software communities for inspiration and support. Alex -McLean’s work on this project is supported by a UKRI Future Leaders -Fellowship [grant number MR/V025260/1].

-

References

-
-
-Faust - Programming Language for Audio Applications and -Plugins. (2016) 2022. C++. GRAME. https://github.com/grame-cncm/faust. -
-
-Jack, Olivia. (2022) 2022. Hydra. https://github.com/ojack/hydra. -
-
-Lan, Qichao. (2020) 2022. Chaosprint/Glicol. Rust. https://github.com/chaosprint/glicol. -
-
-Mclean, Alex. 2020. “Algorithmic Pattern.” In -Proceedings of the International Conference on New Interfaces for -Musical Expression, 265--270. Birmingham, UK. https://zenodo.org/record/4813352. -
-
-McLean, Alex. 2020. “Feedforward.” In Proceedings of -New Interfaces for Musical Expression. Birmingham. https://zenodo.org/record/6353969. -
-
-McLean, Alex, Raphaël Forment, Sylvain Le Beux, and Damián Silvani. -2022. “TidalVortex Zero.” In Proceedings of the 7th -International Conference on Live Coding. Limerick, Ireland: Zenodo. -https://doi.org/10.5281/zenodo.6456380. -
-
-Ogborn, David. (2016) 2022. Dktr0/WebDirt. JavaScript. https://github.com/dktr0/WebDirt. -
-
-Ogborn, David, Jamie Beverley, Luis Navarro del Angel, Eldad Tsabary, -and Alex McLean. 2017. “Estuary: Browser-Based Collaborative -Projectional Live Coding of Musical Patterns.” In Proceedings -of the International Conference on Live Coding, 11. Morelia. -
-
-Roberts, Charles, and Joann Kuchera-morin. 2012. “Gibber: Live -Coding Audio in the Browser.” In In Proceedings of the 2012 -International Computer Music Conference. -
-
-Roos, Felix, and Alex McLean. 2022. “Strudel: Algorithmic Patterns -for the Web.” In. Zenodo. https://doi.org/10.5281/zenodo.6768844. -
-
-Solomon, Mike. (2021) 2022. Purescript-Ocarina. PureScript. https://github.com/mikesol/purescript-ocarina. -
-
-SuperDirt. (2015) 2022. SuperCollider. musikinformatik. https://github.com/musikinformatik/SuperDirt. -
-
-Toussaint, Godfried. 2005. “The Euclidean Algorithm Generates -Traditional Musical Rhythms.” In In Proceedings of BRIDGES: -Mathematical Connections in Art, Music and Science, 47–56. http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231. -
-
-Yi, Steven, Victor Lazzarini, and Edward Costello. 2018. -“WebAssembly AudioWorklet Csound.” In. Berlin, Germany. https://mural.maynoothuniversity.ie/16018/. -
-
- +}
+
+

+ The above example will create an OscillatorNode for each event, where the frequency is controlled by + the note param. In essence, this is how the WebAudio API output of Strudel works, only with many more + parameters to control synths, samples and effects. +

+

+ 8 Pattern alignment and combination +

+

+ One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective + of their structure. Its declarative approach means a live coder does not have to think about the details of + how this is done, only what is to be done. +

+

+ As a simple example, consider two number patterns "0 [1 2] 3", and "10 20". The first + has three contiguous steps of equal lengths, with the second step broken down into two substeps, giving four + events in total. There are a very large number of ways in which the structure of these two patterns could be + combined, but the default method in both Strudel and Tidal is to line up the cycles of the two patterns, and then + take events from the first pattern and match them with those in the second pattern. Therefore, the following two + lines are equivalent: +

+
+
"0 [1 2] 3".add("10 20")
+"10 [11 22] 23"
+
+

+ Where the events only partially overlap, they are treated as fragments of the event in the first pattern. This is + a little difficult to conceptualise, but lets start by comparing the two patterns in the following example: +

+
+
"0 1 2".add("10 20")
+"10 [11 21] 20"
+
+

+ They are similar to the previous example in that the number 1 is split in two, with its two halves + added to 10 and 20 respectively. However, the 11 ‘remembers’ that it is a + fragment of that original 1 event, and so is treated as having a duration of a third of a cycle, + despite only being active for a sixth of a cycle. Likewise, the 21 is also a fragment of that + original 1 event, but a fragment of its second half. Because the start of its event is missing, it + wouldn’t actually trigger a sound (unless it underwent further pattern transformations/combinations). +

+

+ In practice, the effect of this default, implicit method for combining two patterns is that the second pattern is + added in to the first one, and indeed this can be made explicit: +

+
+
"0 1 2".add.in("10 20")
+
+

This makes way for other ways to align the pattern, and several are already defined, in particular:

+
    +
  • + in - as explained above, aligns cycles, and applies values from the pattern on the right + in to the pattern on the left. +
  • +
  • + out - as with in, but values are applied out of the pattern on the left + (i.e. in to the one on the right). +
  • +
  • + mix - structures from both patterns are combined, so that the new events are not fragments but are + created at intersections of events from both sides. +
  • +
  • + squeeze - cycles from the pattern on the right are squeezed into events on the left. So that + e.g. "0 1 2".add.squeeze("10 20") is equivalent to "[10 20] [11 21] [12 22]". +
  • +
  • + squeezeout - as with squeeze, but cycles from the left are squeezed into events on the + right. So, "0 1 2".add.squeezeout("10 20") is equivalent to [10 11 12] [20 21 22]. +
  • +
  • + trig is similar to squeezeout in that cycles from the right are aligned with events on + the left. However those cycles are not ‘squeezed’, rather they are truncated to fit the event. So + "0 1 2 3 4 5 6 7".add.trig("10 [20 30]") would be equivalent to + 10 11 12 13 20 21 30 31. In effect, events on the right ‘trigger’ cycles on the left. +
  • +
  • + trigzero is similar to trig, but the pattern is ‘triggered’ from its very first cycle, + rather than from the current cycle. trig and trigzero therefore only give different + results where the leftmost pattern differs from one cycle to the next. +
  • +
+

+ We will save going deeper into the background, design and practicalities of these alignment functions for future + publications. However in the next section, we take them as a case study for looking at the different design + affordances offered by Haskell to Tidal, and JavaScript to Strudel. +

+

+ 9 Comparing Strudel and Haskell in use +

+

+ Unlike Haskell, JavaScript lacks the ability to define custom infix operators, or change the meaning of existing + ones. So the above Strudel example of "0 1 2".add.out("10 20") is equivalent to the Tidal expression + "0 1 2" +| "10 20", where the vertical bar in the operator +| stands for + out (where a |+ b would be equivalent of a.add.in(b)). +

+

+ From this we can already see that Tidal tends towards brevity through mixing infix operators with functions, and + Strudel tends towards spelling out operations which are joined together with the . operator. This + then is the design trade-off of Tidal’s tersity, versus Strudel’s simplicity. +

+

To demonstrate this, consider the following Tidal pattern:

+
iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4
+

This can be directly translated to the Strudel equivalent:

+
+
iter(4, every(3, add.squeeze("10 20"), n("0 1 3").s("triangle").crush(4)))
+
+

Although for a more canonical Strudel expression, we would reorder it as:

+
+
n("0 1 3").every(3, add.squeeze("10 20")).iter(4).s("triangle").crush(4)
+
+

+ The Strudel example uses the . method call operator for all operations and combinations, whereas the + Tidal example has # for the default method for combining patterns and uses infix operators for other + methods. The lack of parenthesis in the Tidal example is partly due to the way that arguments are applied to + Haskell’s functions, and partly due to the use of the $ operator as an alternative way to establish + precedence and control the order of evaluation. +

+

+ Considering the above, we argue that the Haskell syntax is a little cleaner, but that the Strudel syntax is easier + to learn. Our informal observation is that while Haskell’s dollar $ operator is very useful in making + code easier to work with, it is one of the most difficult aspects of Tidal use for beginners to learn. On the + other hand, the deeper levels of parenthesis in Strudel code can be difficult to keep track of, especially while + coding under pressure of live musical performance. However this difficulty can be largely be mitigated by + reordering expressions, and further mitigated by supporting editor features. +

+

+ With Strudel, we have little choice but to embrace the affordances and constraints offered by JavaScript, and + while designing a domain-specific language entirely based on method calls is a challenge, through creative + adoption of functional programming techniques like partial application, we are so far very happy with the results. + Tidal’s functional reactive approach to pattern-making has in general translated well to JavaScript, and + opportunities and constraints have overall traded off to create a very approachable and useable live coding + environment. +

+

+ 9.1 The trade-off of flexible typing +

+

+ We have identified one problem with porting Tidal to JavaScript where we have missed Haskell’s strict typing and + type inference. In both Tidal and Strudel, time is rational, where any point in time is represented as the ratio + of two integers. This allows representation of musical ratios such that are impossible to represent accurately + using the more common floating point numbers. However while libraries are available that support rational numbers + in JavaScript, the lack of strict typing means that it is easy to implement pattern methods where computationally + expensive conversion from floating point to rational numbers are performed late, and therefore often enough to + overload the CPUs, due to the large number of iterative calculations required to estimate a ratio for a given + floating point number. To mitigate this problem, we might consider moving to TypeScript in the future. +

+

10 Future Outlook

+

+ The project is still young, with many features on the horizon. As general guiding principles, Strudel aims to be +

+
    +
  1. accessible
  2. +
  3. consistent with Tidal’s approach to pattern
  4. +
  5. modular and extensible
  6. +
+

+ While Haskell’s type system makes it a great language for the ongoing development of Tidal’s inner representation + of pattern, JavaScript’s vibrant ecosystem, flexibility and accessibility makes it a great host for more ad-hoc + experiments, including interface design. For the future, it is planned to integrate additional alternative sound + engines such as Glicol (Lan [2020] 2022) and + Faust + (Faust - Programming Language for Audio Applications and Plugins [2016] 2022). Strudel is already approaching feature parity with Tidal, but there are more Tidal functions to be ported, and + work to be done to improve compatibility with Tidal’s mininotation. Tidal version 2.0 is under development, which + brings a new representation for sequences to its patterns, which will then be brought to Strudel. Besides sound, + other ways to render events are being explored, such as graphical, and choreographic output. We are also looking + into alternative ways of editing patterns, including multi-user editing for network music, parsing a novel syntax + to escape the constraints of javascript, and developing hardware/e-textile interfaces. In summary, there is a lot + of fun ahead. +

+

11 Links

+

+ The Strudel REPL is available at https://strudel.cc, including an + interactive tutorial. The repository is at + https://github.com/tidalcycles/strudel, all the + code is open source under the AGPL-3.0 License. +

+

12 Acknowledgments

+

+ Thanks to the Strudel and wider Tidal, live coding, WebAudio and free/open source software communities for + inspiration and support. Alex McLean’s work on this project is supported by a UKRI Future Leaders Fellowship + [grant number MR/V025260/1]. +

+

References

+
+
+ Faust - Programming Language for Audio Applications and Plugins. (2016) 2022. C++. GRAME. + https://github.com/grame-cncm/faust. +
+
+ Jack, Olivia. (2022) 2022. Hydra. + https://github.com/ojack/hydra. +
+
+ Lan, Qichao. (2020) 2022. Chaosprint/Glicol. Rust. + https://github.com/chaosprint/glicol. +
+
+ Mclean, Alex. 2020. “Algorithmic Pattern.” In + Proceedings of the International Conference on New Interfaces for Musical Expression, 265--270. + Birmingham, UK. https://zenodo.org/record/4813352. +
+
+ McLean, Alex. 2020. “Feedforward.” In + Proceedings of New Interfaces for Musical Expression. Birmingham. + https://zenodo.org/record/6353969. +
+
+ McLean, Alex, Raphaël Forment, Sylvain Le Beux, and Damián Silvani. 2022. “TidalVortex Zero.” In + Proceedings of the 7th International Conference on Live Coding. Limerick, Ireland: Zenodo. + https://doi.org/10.5281/zenodo.6456380. +
+
+ Ogborn, David. (2016) 2022. Dktr0/WebDirt. JavaScript. + https://github.com/dktr0/WebDirt. +
+
+ Ogborn, David, Jamie Beverley, Luis Navarro del Angel, Eldad Tsabary, and Alex McLean. 2017. + “Estuary: Browser-Based Collaborative Projectional Live Coding of Musical Patterns.” In + Proceedings of the International Conference on Live Coding, 11. Morelia. +
+
+ Roberts, Charles, and Joann Kuchera-morin. 2012. “Gibber: Live Coding Audio in the Browser.” In + In Proceedings of the 2012 International Computer Music Conference. +
+
+ Roos, Felix, and Alex McLean. 2022. “Strudel: Algorithmic Patterns for the Web.” In. Zenodo. + https://doi.org/10.5281/zenodo.6768844. +
+
+ Solomon, Mike. (2021) 2022. Purescript-Ocarina. PureScript. + https://github.com/mikesol/purescript-ocarina. +
+
+ SuperDirt. (2015) 2022. SuperCollider. musikinformatik. + https://github.com/musikinformatik/SuperDirt. +
+
+ Toussaint, Godfried. 2005. “The Euclidean Algorithm Generates Traditional Musical Rhythms.” In + In Proceedings of BRIDGES: Mathematical Connections in Art, Music and Science, 47–56. + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231. +
+
+ Yi, Steven, Victor Lazzarini, and Edward Costello. 2018. + “WebAssembly AudioWorklet Csound.” In. Berlin, Germany. + https://mural.maynoothuniversity.ie/16018/. +
+
+ diff --git a/docs/iclc2023-paper/pandoc/iclc.html b/docs/iclc2023-paper/pandoc/iclc.html index 90406b16e..c47d6595c 100755 --- a/docs/iclc2023-paper/pandoc/iclc.html +++ b/docs/iclc2023-paper/pandoc/iclc.html @@ -1,80 +1,63 @@ -$if(false)$ - -This is a pandoc template and should not be edited. - -$endif$ +$if(false)$ This is a pandoc template and should not be edited. $endif$ - - - - - -$for(author-meta)$ - -$endfor$ -$if(date-meta)$ - -$endif$ - $if(title-prefix)$$title-prefix$ - $endif$$pagetitle$ - -$if(quotes)$ - -$endif$ -$if(highlighting-css)$ - -$endif$ - -$for(css)$ - -$endfor$ -$if(math)$ - $math$ -$endif$ -$for(header-includes)$ - $header-includes$ -$endfor$ - - -$for(include-before)$ -$include-before$ -$endfor$ -$if(title)$ -
-

$title$

-$if(subtitle)$ -

$subtitle$

-$endif$ -
    -$for(author)$ -
  • $author$
  • -$endfor$ -
-$if(date)$ -

$date$

-$endif$ -
-$endif$ -$if(toc)$ -
-$toc$ -
-$endif$ + + + + + + $for(author-meta)$ + + $endfor$ $if(date-meta)$ + + $endif$ + $if(title-prefix)$$title-prefix$ - $endif$$pagetitle$ + + $if(quotes)$ + + $endif$ $if(highlighting-css)$ + + $endif$ + + $for(css)$ + + $endfor$ $if(math)$ $math$ $endif$ $for(header-includes)$ $header-includes$ $endfor$ + + + $for(include-before)$ $include-before$ $endfor$ $if(title)$ +
+

$title$

+ $if(subtitle)$ +

$subtitle$

+ $endif$ +
    + $for(author)$ +
  • $author$
  • + $endfor$ +
+ $if(date)$ +

$date$

+ $endif$ +
+ $endif$ $if(toc)$ +
$toc$
+ $endif$ -

Abstract

-
-$if(abstract)$ -$abstract$ -$else$ -Please provide an abstract in the metadata block at the top of your -markdown document. Refer to template.txt for details. -$endif$ -
+

Abstract

+
+ $if(abstract)$ $abstract$ $else$ Please provide an abstract in the metadata block at the top of your markdown + document. Refer to template.txt for details. $endif$ +
-$body$ -$for(include-after)$ -$include-after$ -$endfor$ - + $body$ $for(include-after)$ $include-after$ $endfor$ + From e52017c0d8605a82c4475de82a9ce9e169ee0319 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 15 Jun 2025 10:12:17 +0200 Subject: [PATCH 209/538] fix: reference tab critical fail --- website/src/repl/components/panel/Reference.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index 18721eb43..ab925eb91 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -70,7 +70,7 @@ export function Reference() {
    {entry.params?.map(({ name, type, description }, i) => (
  • - {name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)} : ''} + {name} : {type?.names?.join(' | ')} {description ? <> - {getInnerText(description)} : ''}
  • ))}
From 351314ccdf8a52831393724b107c26d1f9a76d13 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 15 Jun 2025 09:23:29 +0100 Subject: [PATCH 210/538] rename .github to .forgejo --- {.github => .forgejo}/FUNDING.yml | 0 {.github => .forgejo}/workflows/deploy.yml | 0 {.github => .forgejo}/workflows/test.yml | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {.github => .forgejo}/FUNDING.yml (100%) rename {.github => .forgejo}/workflows/deploy.yml (100%) rename {.github => .forgejo}/workflows/test.yml (100%) diff --git a/.github/FUNDING.yml b/.forgejo/FUNDING.yml similarity index 100% rename from .github/FUNDING.yml rename to .forgejo/FUNDING.yml diff --git a/.github/workflows/deploy.yml b/.forgejo/workflows/deploy.yml similarity index 100% rename from .github/workflows/deploy.yml rename to .forgejo/workflows/deploy.yml diff --git a/.github/workflows/test.yml b/.forgejo/workflows/test.yml similarity index 100% rename from .github/workflows/test.yml rename to .forgejo/workflows/test.yml From 5efcd14f4084295e287cf4a97364e7e932db994e Mon Sep 17 00:00:00 2001 From: anecondev Date: Wed, 18 Jun 2025 17:07:13 +0200 Subject: [PATCH 211/538] 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 184cb746f331c16800ac97664dfbfd0e997efc03 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 18 Jun 2025 19:55:30 +0200 Subject: [PATCH 212/538] arch --- packages/codemirror/themes.mjs | 8 ++++ packages/codemirror/themes/archBtw.mjs | 39 ++++++++++++++++++ .../codemirror/themes/bluescreenlight.mjs | 40 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 packages/codemirror/themes/archBtw.mjs create mode 100644 packages/codemirror/themes/bluescreenlight.mjs diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs index 6aa70471d..c3763a643 100644 --- a/packages/codemirror/themes.mjs +++ b/packages/codemirror/themes.mjs @@ -8,6 +8,9 @@ import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs'; import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs'; import redText, { settings as redTextSettings } from './themes/red-text.mjs'; import greenText, { settings as greenTextSettings } from './themes/green-text.mjs'; +import archBtw, { settings as archBtwSettings } from './themes/archBtw.mjs'; +import bluescreenlight, { settings as bluescreenlightsettings } from './themes/bluescreenlight.mjs'; + import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs'; import atomone, { settings as atomOneSettings } from './themes/atomone.mjs'; import aura, { settings as auraSettings } from './themes/aura.mjs'; @@ -39,12 +42,14 @@ import { setTheme } from '@strudel/draw'; export const themes = { strudelTheme, algoboy, + archBtw, androidstudio, atomone, aura, bbedit, blackscreen, bluescreen, + bluescreenlight, CutiePi, darcula, dracula, @@ -78,10 +83,12 @@ export const themes = { export const settings = { strudelTheme: strudelThemeSettings, bluescreen: bluescreenSettings, + bluescreenlight: bluescreenlightsettings, blackscreen: blackscreenSettings, whitescreen: whitescreenSettings, teletext: teletextSettings, algoboy: algoboySettings, + archBtw: archBtwSettings, androidstudio: androidstudioSettings, atomone: atomOneSettings, aura: auraSettings, @@ -95,6 +102,7 @@ export const settings = { githubLight: githubLightSettings, githubDark: githubDarkSettings, greenText: greenTextSettings, + gruvboxDark: gruvboxDarkSettings, gruvboxLight: gruvboxLightSettings, materialDark: materialDarkSettings, diff --git a/packages/codemirror/themes/archBtw.mjs b/packages/codemirror/themes/archBtw.mjs new file mode 100644 index 000000000..7f4c1b5e3 --- /dev/null +++ b/packages/codemirror/themes/archBtw.mjs @@ -0,0 +1,39 @@ +/* + * Atom One + * Atom One dark syntax theme + * + * https://github.com/atom/one-dark-syntax + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['rgb(0, 0, 0)', 'rgb(113, 208, 250)', 'rgba(113, 208, 250, .4)', 'rgba(113, 208, 250, .15)']; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[1], + selection: hex[2], + selectionMatch: hex[0], + gutterBackground: hex[0], + gutterForeground: hex[2], + gutterBorder: 'transparent', + lineHighlight: hex[0], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[1], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] }, + { tag:[ t.comment, t.brace, t.bracket, ], color: hex[2] }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[1] }, + { tag: [t.attributeName, t.number], color: hex[1] }, + { tag: t.keyword, color: hex[1] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] }, + ], +}); diff --git a/packages/codemirror/themes/bluescreenlight.mjs b/packages/codemirror/themes/bluescreenlight.mjs new file mode 100644 index 000000000..e78bf707c --- /dev/null +++ b/packages/codemirror/themes/bluescreenlight.mjs @@ -0,0 +1,40 @@ +/* + * Atom One + * Atom One dark syntax theme + * + * https://github.com/atom/one-dark-syntax + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['rgb(75, 130, 247)', 'rgb(47, 108, 246)', 'rgb(255, 255, 255)', 'rgba(255, 255, 255,.3)']; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[2], + selection: hex[3], + selectionMatch: hex[0], + gutterBackground: hex[0], + gutterForeground: hex[2], + gutterBorder: 'transparent', + lineHighlight: hex[1], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[2], + + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[2] }, + { tag: [t.comment, t.bracket, t.brace, t.compareOperator], color: hex[3] }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] }, + { tag: [t.attributeName, t.number], color: hex[2] }, + { tag: t.keyword, color: hex[2] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] }, + ], +}); From 011139eff8049abf81af37d169a19cfdbff8e4ed Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 18 Jun 2025 20:12:36 +0200 Subject: [PATCH 213/538] arch2 --- packages/codemirror/themes/archBtw.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/themes/archBtw.mjs b/packages/codemirror/themes/archBtw.mjs index 7f4c1b5e3..1ed6a0c6a 100644 --- a/packages/codemirror/themes/archBtw.mjs +++ b/packages/codemirror/themes/archBtw.mjs @@ -7,7 +7,7 @@ import { tags as t } from '@lezer/highlight'; import { createTheme } from './theme-helper.mjs'; -const hex = ['rgb(0, 0, 0)', 'rgb(113, 208, 250)', 'rgba(113, 208, 250, .4)', 'rgba(113, 208, 250, .15)']; +const hex = ['rgb(0, 0, 0)', 'rgb(82, 208, 250)', 'rgba(113, 208, 250, .4)', 'rgba(113, 208, 250, .15)']; export const settings = { background: hex[0], From c751bb7e5f06a63f47aa134d671943efef952de1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 18 Jun 2025 21:36:34 +0200 Subject: [PATCH 214/538] allow _ as silence --- packages/mondo/mondo.mjs | 3 ++- packages/mondough/mondough.mjs | 1 + packages/superdough/superdough.mjs | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 73dbf1c7a..e3f62846b 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -20,7 +20,8 @@ export class MondoParser { open_curly: /^\{/, close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. + // "+" and "-" might be added here, but then "-" won't work as silence anymore.. + op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, pipe: /^#/, stack: /^[,$]/, diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 3731e8cee..cd0521bad 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -28,6 +28,7 @@ let nope = (...args) => args[args.length - 1]; let lib = {}; lib['nope'] = nope; lib['-'] = silence; +lib['_'] = silence; lib['~'] = silence; lib.curly = stepcat; lib.square = (...args) => stepcat(...args).setSteps(1); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 61be0b4e8..90bb5e701 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -575,7 +575,7 @@ export const superdough = async (value, t, hapDuration, cps) => { let audioNodes = []; - if (['-', '~'].includes(s)) { + if (['-', '~', '_'].includes(s)) { return; } if (bank && s) { From 9412bf60bfa63d7c4c1cbb1db4507e84af352123 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 18 Jun 2025 22:06:18 +0200 Subject: [PATCH 215/538] add e as euclid that accepts a list --- packages/core/euclid.mjs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 2ee9962da..25e12b07c 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -139,6 +139,14 @@ export const euclid = register('euclid', function (pulses, steps, pat) { return pat.struct(_euclidRot(pulses, steps, 0)); }); +export const e = register('e', function (euc, pat) { + if (!Array.isArray(euc)) { + euc = [euc]; + } + const [pulses, steps = pulses, rot = 0] = euc; + return pat.struct(_euclidRot(pulses, steps, rot)); +}); + export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) { return pat.struct(_euclidRot(pulses, steps, rotation)); }); From 10ac7c353cb16034bce8da900c60702e431e4c9f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 18 Jun 2025 22:07:16 +0200 Subject: [PATCH 216/538] use + and - as late / early + add some extra error handling --- packages/mondo/mondo.mjs | 16 ++++++++++++++-- packages/mondough/mondough.mjs | 3 ++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index e3f62846b..b879f24bf 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -20,8 +20,8 @@ export class MondoParser { open_curly: /^\{/, close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! - // "+" and "-" might be added here, but then "-" won't work as silence anymore.. - op: /^[*/:!@%?]|^\.{2}/, // * / : ! @ % ? .. + // TODO: better error handling when "-" is used as rest, e.g "s [- bd]" + op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, pipe: /^#/, stack: /^[,$]/, @@ -173,6 +173,18 @@ export class MondoParser { children[opIndex] = op; continue; } + // some careful error handling + if (left.type === 'op') { + throw new Error(`got 2 ops in a row: "${left.value}${op.value}"`); + } + if (right.type === 'op') { + let err = `got 2 ops in a row: "${op.value}${right.value}"`; + if (op.value === '-') { + // yes i know this file is not supposed to know about rests x.X + err += '. you probably want a rest, which is "_" in mondo!'; + } + throw new Error(err); + } const call = { type: 'list', children: [op, right, left] }; // insert call while keeping other siblings children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index cd0521bad..a4e7c93b3 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -27,7 +27,8 @@ let nope = (...args) => args[args.length - 1]; let lib = {}; lib['nope'] = nope; -lib['-'] = silence; +lib['-'] = (a, b) => b.early(a); +lib['+'] = (a, b) => b.late(a); lib['_'] = silence; lib['~'] = silence; lib.curly = stepcat; From 1cc15c7e8dd6e20e9aeff072b827c96f3df80fd4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 18 Jun 2025 22:25:05 +0200 Subject: [PATCH 217/538] fix: fall back to silence when empty removes annoying error --- packages/transpiler/transpiler.mjs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index b0061c600..fea6bad4d 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -113,8 +113,18 @@ export function transpiler(input, options = {}) { leave(node, parent, prop, index) {}, }); - const { body } = ast; - if (!body?.[body.length - 1]?.expression) { + let { body } = ast; + + if (!body.length) { + console.warn('empty body -> fallback to silence'); + body.push({ + type: 'ExpressionStatement', + expression: { + type: 'Identifier', + name: 'silence', + }, + }); + } else if (!body?.[body.length - 1]?.expression) { throw new Error('unexpected ast format without body expression'); } From 7e6876c2824071b2603b63e80675d9be9a2e4bc9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 18 Jun 2025 20:44:09 -0400 Subject: [PATCH 218/538] comment --- packages/codemirror/themes.mjs | 4 ++ packages/codemirror/themes/archBtw.mjs | 9 ++-- .../codemirror/themes/bluescreenlight.mjs | 11 ++-- packages/codemirror/themes/fruitDaw.mjs | 50 +++++++++++++++++++ 4 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 packages/codemirror/themes/fruitDaw.mjs diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs index c3763a643..1a523b391 100644 --- a/packages/codemirror/themes.mjs +++ b/packages/codemirror/themes.mjs @@ -9,6 +9,8 @@ import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mj import redText, { settings as redTextSettings } from './themes/red-text.mjs'; import greenText, { settings as greenTextSettings } from './themes/green-text.mjs'; import archBtw, { settings as archBtwSettings } from './themes/archBtw.mjs'; +import fruitDaw, { settings as fruitDawSettings } from './themes/fruitDaw.mjs'; + import bluescreenlight, { settings as bluescreenlightsettings } from './themes/bluescreenlight.mjs'; import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs'; @@ -55,6 +57,7 @@ export const themes = { dracula, duotoneDark, eclipse, + fruitDaw, githubDark, githubLight, greenText, @@ -99,6 +102,7 @@ export const settings = { eclipse: eclipseSettings, CutiePi: CutiePiSettings, sonicPink: sonicPinkSettings, + fruitDaw: fruitDawSettings, githubLight: githubLightSettings, githubDark: githubDarkSettings, greenText: greenTextSettings, diff --git a/packages/codemirror/themes/archBtw.mjs b/packages/codemirror/themes/archBtw.mjs index 1ed6a0c6a..87546569c 100644 --- a/packages/codemirror/themes/archBtw.mjs +++ b/packages/codemirror/themes/archBtw.mjs @@ -1,8 +1,7 @@ /* - * Atom One - * Atom One dark syntax theme - * - * https://github.com/atom/one-dark-syntax + * Arch Btw + * Modern terminal inspired theme + * made by Jade */ import { tags as t } from '@lezer/highlight'; import { createTheme } from './theme-helper.mjs'; @@ -30,7 +29,7 @@ export default createTheme({ color: hex[1], }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] }, - { tag:[ t.comment, t.brace, t.bracket, ], color: hex[2] }, + { tag: [t.comment, t.brace, t.bracket], color: hex[2] }, { tag: [t.variableName, t.propertyName, t.labelName], color: hex[1] }, { tag: [t.attributeName, t.number], color: hex[1] }, { tag: t.keyword, color: hex[1] }, diff --git a/packages/codemirror/themes/bluescreenlight.mjs b/packages/codemirror/themes/bluescreenlight.mjs index e78bf707c..031a3da33 100644 --- a/packages/codemirror/themes/bluescreenlight.mjs +++ b/packages/codemirror/themes/bluescreenlight.mjs @@ -1,13 +1,11 @@ /* - * Atom One - * Atom One dark syntax theme - * - * https://github.com/atom/one-dark-syntax + * A lighter blue screen theme + * made by Jade */ import { tags as t } from '@lezer/highlight'; import { createTheme } from './theme-helper.mjs'; -const hex = ['rgb(75, 130, 247)', 'rgb(47, 108, 246)', 'rgb(255, 255, 255)', 'rgba(255, 255, 255,.3)']; +const hex = ['rgb(75, 130, 247)', 'rgb(47, 108, 246)', 'rgb(255, 255, 255)', 'rgba(255, 255, 255,.3)']; export const settings = { background: hex[0], @@ -28,9 +26,8 @@ export default createTheme({ { tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], color: hex[2], - }, - { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[2] }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[2] }, { tag: [t.comment, t.bracket, t.brace, t.compareOperator], color: hex[3] }, { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] }, { tag: [t.attributeName, t.number], color: hex[2] }, diff --git a/packages/codemirror/themes/fruitDaw.mjs b/packages/codemirror/themes/fruitDaw.mjs new file mode 100644 index 000000000..c9e557796 --- /dev/null +++ b/packages/codemirror/themes/fruitDaw.mjs @@ -0,0 +1,50 @@ +/* + * Fruit Daw + * made by Jade + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = [ + 'rgb(84, 93, 98)', + 'rgb(255, 255, 255)', + 'rgba(255, 255, 255, .25)', + 'rgb(67, 76, 81)', + 'rgb(186, 230, 115)', + 'rgb(252, 184, 67)', + 'rgb(124, 206, 254)', + 'rgb(83, 101, 102)', + 'rgba(46, 62, 72,.5)', + 'rgb(94, 100, 108)', + 'rgb(167, 216, 177)', +]; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[10], + selection: hex[8], + selectionMatch: hex[0], + gutterBackground: hex[3], + gutterForeground: hex[2], + gutterBorder: 'transparent', + lineHighlight: hex[3], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[1], + }, + { tag: [t.bool, t.special(t.variableName)], color: hex[1] }, + { tag: [t.comment, t.brace, t.bracket], color: hex[2] }, + { tag: [t.variableName], color: hex[1] }, + { tag: [t.labelName, t.propertyName, t.self, t.atom], color: hex[5] }, + { tag: [t.attributeName, t.number], color: hex[6] }, + { tag: t.keyword, color: hex[5] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[4] }, + ], +}); From 6c4aa12c6045fc7a6e0fa086e124124ce194bf0c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 19 Jun 2025 09:09:45 +0200 Subject: [PATCH 219/538] refactor: replace .cpm with setcpm calls + doc setcpm --- packages/core/pattern.mjs | 1 + packages/core/repl.mjs | 11 ++++ packages/gamepad/docs/gamepad.mdx | 15 ++--- .../src/pages/de/workshop/first-sounds.mdx | 56 ++++++++++++------- website/src/pages/de/workshop/recap.mdx | 20 +++---- website/src/pages/learn/samples.mdx | 10 ++-- website/src/pages/understand/cycles.mdx | 49 +++++++++++----- website/src/pages/workshop/first-notes.mdx | 22 +++++--- website/src/pages/workshop/first-sounds.mdx | 44 ++++++++++----- .../src/pages/workshop/pattern-effects.mdx | 10 ++-- website/src/pages/workshop/recap.mdx | 20 +++---- website/src/repl/tunes.mjs | 20 ------- 12 files changed, 169 insertions(+), 109 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 76b02c21e..41b09ea20 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1981,6 +1981,7 @@ export const apply = register('apply', function (func, pat) { /** * Plays the pattern at the given cycles per minute. + * @deprecated * @example * s(",hh*2").cpm(90) // = 90 bpm */ diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 7a8cbbab2..8acd2bbea 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -85,6 +85,17 @@ export function repl({ const pause = () => scheduler.pause(); const toggle = () => scheduler.toggle(); const setCps = (cps) => scheduler.setCps(cps); + + /** + * Changes the global tempo to the given cycles per minute + * + * @name setcpm + * @alias setCpm + * @param {number} cpm cycles per minute + * @example + * setcpm(140/4) // =140 bpm in 4/4 + * $: s("bd*4,[- sd]*2").bank('tr707') + */ const setCpm = (cpm) => scheduler.setCps(cpm / 60); // TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`.. diff --git a/packages/gamepad/docs/gamepad.mdx b/packages/gamepad/docs/gamepad.mdx index 219d8ae0b..e2da594ea 100644 --- a/packages/gamepad/docs/gamepad.mdx +++ b/packages/gamepad/docs/gamepad.mdx @@ -56,14 +56,15 @@ You can use button inputs to control different aspects of your music, such as ga @@ -74,14 +75,13 @@ Analog sticks can be used for continuous control, such as pitch shifting or pann ### Button Sequences @@ -89,6 +89,7 @@ $: note("c4 d3 a3 e3").sound("sawtooth") You can define button sequences to trigger specific actions, like playing a sound when a sequence is detected. -**Tempo ändern mit `cpm`** +**Tempo ändern mit `setcpm`** -*8").cpm(90/4)`} punchcard /> +*8")`} + punchcard +/> @@ -233,8 +238,9 @@ Bolero: @@ -313,18 +319,23 @@ Das haben wir bisher gelernt: Die mit Apostrophen umgebene Mini-Notation benutzt man normalerweise in einer sogenannten Funktion. Die folgenden Funktionen haben wir bereits gesehen: -| Name | Description | Example | -| ----- | -------------------------------------- | ----------------------------------------------------------------------- | -| sound | Spielt den Sound mit dem Namen | | -| bank | Wählt die Soundbank / Drum Machine | | -| cpm | Tempo in **C**ycles **p**ro **M**inute | | -| n | Sample Nummer | | +| Name | Description | Example | +| ------ | -------------------------------------- | ----------------------------------------------------------------------- | +| sound | Spielt den Sound mit dem Namen | | +| bank | Wählt die Soundbank / Drum Machine | | +| setcpm | Tempo in **C**ycles **p**ro **M**inute | | +| n | Sample Nummer | | ## Beispiele **Einfacher Rock Beat** - + **Klassischer House** @@ -339,7 +350,12 @@ Bestimmte Drum Patterns werden oft genreübergreifend wiederverwendet. We Will Rock you - + **Yellow Magic Orchestra - Firecracker** @@ -354,12 +370,13 @@ We Will Rock you @@ -367,12 +384,13 @@ We Will Rock you @@ -380,11 +398,11 @@ We Will Rock you diff --git a/website/src/pages/de/workshop/recap.mdx b/website/src/pages/de/workshop/recap.mdx index 14ef6b252..ec279c084 100644 --- a/website/src/pages/de/workshop/recap.mdx +++ b/website/src/pages/de/workshop/recap.mdx @@ -56,13 +56,13 @@ Diese Seite ist eine Auflistung aller im Workshop vorgestellten Funktionen. ## Pattern-Effekte -| Name | Beschreibung | Beispiel | -| ---- | --------------------------------- | ----------------------------------------------------------------------------------- | -| cpm | Tempo in Cycles pro Minute | | -| fast | schneller | | -| slow | langsamer | | -| rev | rückwärts | | -| jux | einen Stereo-Kanal modifizieren | | -| add | addiert Zahlen oder Noten | ")).scale("C:minor")`} /> | -| ply | jedes Element schneller machen | ")`} /> | -| off | verzögert eine modifizierte Kopie | x.speed(2))`} /> | +| Name | Beschreibung | Beispiel | +| ------ | --------------------------------- | ----------------------------------------------------------------------------------- | +| setcpm | Tempo in Cycles pro Minute | | +| fast | schneller | | +| slow | langsamer | | +| rev | rückwärts | | +| jux | einen Stereo-Kanal modifizieren | | +| add | addiert Zahlen oder Noten | ")).scale("C:minor")`} /> | +| ply | jedes Element schneller machen | ")`} /> | +| off | verzögert eine modifizierte Kopie | x.speed(2))`} /> | diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index cd7944caa..ef97ec0ce 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -247,15 +247,17 @@ We can also declare different samples for different regions of the keyboard: !2, g4 f4]>") - .s('moog').clip(1) - .gain(.5).cpm(60)`} +.s('moog').clip(1) +.gain(.5)`} /> The sampler will always pick the closest matching sample for the current note! diff --git a/website/src/pages/understand/cycles.mdx b/website/src/pages/understand/cycles.mdx index 4694850c3..1c0a778a6 100644 --- a/website/src/pages/understand/cycles.mdx +++ b/website/src/pages/understand/cycles.mdx @@ -43,21 +43,33 @@ This is why the same CPS can produce different perceived tempos. ## Setting CPM -If you're familiar with BPM, you can use the `cpm` method to set the tempo in cycles per minute: +If you're familiar with BPM, you can use the `setcpm` method to set the global tempo in cycles per minute: - + If you want to add more beats per cycle, you might want to divide the cpm: - + Or using 2 beats per cycle: - + -To set a specific bpm, use `.cpm(bpm/bpc)` +To set a specific bpm, use `setcpm(bpm/bpc)` - bpm: the target beats per minute - bpc: the number of perceived beats per cycle @@ -74,22 +86,31 @@ Many music programs use it as a default. Strudel does not a have concept of bars or measures, there are only cycles. How you use them is up to you. Above, we've had this example: - + This could be interpreted as 4/4 time with a tempo of 110bpm. We could write out multiple bars like this: \`).cpm(110/4)`} +>\`)`} /> Instead of writing out each bar separately, we could express this much shorter: ->,hh*4").cpm(110/2)`} /> +>,hh*4")`} +/> Here we can see that thinking in cycles rather than bars simplifies things a lot! These types of simplifications work because of the repetitive nature of rhythm. @@ -109,10 +130,11 @@ We could also write multiple bars with different time signatures: \`).cpm(110*2)`} +>\`)`} /> Here we switch between 3/4 and 4/4, keeping the same tempo. @@ -121,10 +143,11 @@ If we don't specify the length, we get what's called a metric modulation: \`).cpm(110/2)`} +>\`)`} /> Now the 3 elements get the same time as the 4 elements, which is why the tempo changes. diff --git a/website/src/pages/workshop/first-notes.mdx b/website/src/pages/workshop/first-notes.mdx index f3d74ddce..56312a355 100644 --- a/website/src/pages/workshop/first-notes.mdx +++ b/website/src/pages/workshop/first-notes.mdx @@ -216,8 +216,9 @@ Finding the right notes can be difficult.. Scales are here to help: ") -.scale("C:minor").sound("piano").cpm(60)`} + tune={`setcpm(60) +n("0 2 4 <[6,8] [7,9]>") +.scale("C:minor").sound("piano")`} punchcard /> @@ -242,9 +243,10 @@ Just like anything, we can automate the scale with a pattern: , 2 4 <[6,8] [7,9]>") + tune={`setcpm(60) +n("<0 -3>, 2 4 <[6,8] [7,9]>") .scale("/4") -.sound("piano").cpm(60)`} +.sound("piano")`} punchcard /> @@ -275,9 +277,10 @@ Try changing that number! *2") + tune={`setcpm(60) +n("<[4@2 4] [5@2 5] [6@2 6] [5@2 5]>*2") .scale("/4") -.sound("gm_acoustic_bass").cpm(60)`} +.sound("gm_acoustic_bass")`} punchcard /> @@ -291,7 +294,12 @@ This is also sometimes called triplet swing. You'll often find it in blues and j **Replicate** -]").sound("piano").cpm(60)`} punchcard /> +]").sound("piano")`} + punchcard +/> diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx index 9c33cdc3c..083a55390 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -166,9 +166,14 @@ Try also changing the number at the end to change the tempo! -**changing the tempo with cpm** +**changing the tempo with setcpm** -*8").cpm(90/4)`} punchcard /> +*8")`} + punchcard +/> @@ -287,7 +292,7 @@ The Mini-Notation is usually used inside some function. These are the functions | ----- | ----------------------------------- | --------------------------------------------------------------------------------- | | sound | plays the sound of the given name | | | bank | selects the sound bank | | -| cpm | sets the tempo in cycles per minute | | +| cpm | sets the tempo in cycles per minute | | | n | select sample number | | ## Examples @@ -296,8 +301,8 @@ The Mini-Notation is usually used inside some function. These are the functions @@ -314,14 +319,20 @@ Certain drum patterns are reused across genres. We Will Rock you - + **Yellow Magic Orchestra - Firecracker** @@ -329,12 +340,14 @@ We Will Rock you @@ -342,12 +355,13 @@ We Will Rock you @@ -355,11 +369,11 @@ We Will Rock you diff --git a/website/src/pages/workshop/pattern-effects.mdx b/website/src/pages/workshop/pattern-effects.mdx index c9cb7b462..ad84a9ab3 100644 --- a/website/src/pages/workshop/pattern-effects.mdx +++ b/website/src/pages/workshop/pattern-effects.mdx @@ -68,9 +68,10 @@ Try commenting out one or more by adding `//` before a line >")) + tune={`setcpm(60) +note("c2 [eb3,g3] ".add("<0 <1 -1>>")) .color(">").adsr("[.1 0]:.2:[1 0]") -.sound("gm_acoustic_bass").room(.5).cpm(60)`} +.sound("gm_acoustic_bass").room(.5)`} punchcard /> @@ -84,9 +85,10 @@ We can add as often as we like: >").add("0,7")) + tune={`setcpm(60) +note("c2 [eb3,g3]".add("<0 <1 -1>>").add("0,7")) .color(">").adsr("[.1 0]:.2:[1 0]") -.sound("gm_acoustic_bass").room(.5).cpm(60)`} +.sound("gm_acoustic_bass").room(.5)`} punchcard /> diff --git a/website/src/pages/workshop/recap.mdx b/website/src/pages/workshop/recap.mdx index e2b661f56..08e243cef 100644 --- a/website/src/pages/workshop/recap.mdx +++ b/website/src/pages/workshop/recap.mdx @@ -56,13 +56,13 @@ This page is just a listing of all functions covered in the workshop! ## Pattern Effects -| name | description | example | -| ---- | ----------------------------------- | ----------------------------------------------------------------------------------- | -| cpm | sets the tempo in cycles per minute | | -| fast | speed up | | -| slow | slow down | | -| rev | reverse | | -| jux | split left/right, modify right | | -| add | add numbers / notes | ")).scale("C:minor")`} /> | -| ply | speed up each event n times | ")`} /> | -| off | copy, shift time & modify | x.speed(2))`} /> | +| name | description | example | +| ------ | ----------------------------------- | ----------------------------------------------------------------------------------- | +| setcpm | sets the tempo in cycles per minute | | +| fast | speed up | | +| slow | slow down | | +| rev | reverse | | +| jux | split left/right, modify right | | +| add | add numbers / notes | ")).scale("C:minor")`} /> | +| ply | speed up each event n times | ")`} /> | +| off | copy, shift time & modify | x.speed(2))`} /> | diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index 774e04e49..afaf937df 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -313,26 +313,6 @@ stack( ) .fast(2/3) .pianoroll()`; -/* -export const bridgeIsOver = `// "Bridge is over" -// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// @by Felix Roos, bassline by BDP - The Bridge Is Over - -samples({mad:'https://freesound.org/data/previews/22/22274_109943-lq.mp3'}) -stack( - stack( - note("c3*2 [[c3@1.4 bb2] ab2] gb2*2 <[[gb2@1.4 ab2] bb2] gb2>") - .gain(.8).clip("[.5 1]*2"), - n("<0 1 2 3 4 3 2 1>") - .clip(.5) - .echoWith(8, 1/32, (x,i)=>x.add(n(i)).velocity(Math.pow(.7,i))) - .scale('c4 whole tone') - .echo(3, 1/8, .5) - ).piano(), - s("mad").slow(2) -).cpm(78).slow(4) - .pianoroll() -`; */ export const goodTimes = `// "Good times" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ From 64ab3c9ba73fddfe3d554b376a72a8ac6dfb23f5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 19 Jun 2025 09:12:05 +0200 Subject: [PATCH 220/538] fix: tests --- test/__snapshots__/examples.test.mjs.snap | 29 +++++++++++++++++++++++ test/runtime.mjs | 2 ++ 2 files changed, 31 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 85948f0c0..f3607cb48 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -8450,6 +8450,35 @@ exports[`runs examples > example "seqPLoop" example index 0 1`] = ` ] `; +exports[`runs examples > example "setcpm" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd bank:tr707 ]", + "[ 1/4 → 1/2 | s:bd bank:tr707 ]", + "[ 1/4 → 1/2 | s:sd bank:tr707 ]", + "[ 1/2 → 3/4 | s:bd bank:tr707 ]", + "[ 3/4 → 1/1 | s:bd bank:tr707 ]", + "[ 3/4 → 1/1 | s:sd bank:tr707 ]", + "[ 1/1 → 5/4 | s:bd bank:tr707 ]", + "[ 5/4 → 3/2 | s:bd bank:tr707 ]", + "[ 5/4 → 3/2 | s:sd bank:tr707 ]", + "[ 3/2 → 7/4 | s:bd bank:tr707 ]", + "[ 7/4 → 2/1 | s:bd bank:tr707 ]", + "[ 7/4 → 2/1 | s:sd bank:tr707 ]", + "[ 2/1 → 9/4 | s:bd bank:tr707 ]", + "[ 9/4 → 5/2 | s:bd bank:tr707 ]", + "[ 9/4 → 5/2 | s:sd bank:tr707 ]", + "[ 5/2 → 11/4 | s:bd bank:tr707 ]", + "[ 11/4 → 3/1 | s:bd bank:tr707 ]", + "[ 11/4 → 3/1 | s:sd bank:tr707 ]", + "[ 3/1 → 13/4 | s:bd bank:tr707 ]", + "[ 13/4 → 7/2 | s:bd bank:tr707 ]", + "[ 13/4 → 7/2 | s:sd bank:tr707 ]", + "[ 7/2 → 15/4 | s:bd bank:tr707 ]", + "[ 15/4 → 4/1 | s:bd bank:tr707 ]", + "[ 15/4 → 4/1 | s:sd bank:tr707 ]", +] +`; + exports[`runs examples > example "shape" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh shape:0 ]", diff --git a/test/runtime.mjs b/test/runtime.mjs index 6b75fb3be..2a29de3f5 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -97,6 +97,7 @@ const toneHelpersMocked = { '_pianoroll', '_spectrum', 'markcss', + 'p', ].forEach((mock) => { strudel.Pattern.prototype[mock] = function () { return this; @@ -163,6 +164,7 @@ evalScope( loadCsound, loadcsound, setcps: id, + setcpm: id, Clock: {}, // whatever }, ); From 66673d211ea60716d83d483b80fdb353c0ba9538 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 19 Jun 2025 09:13:48 +0200 Subject: [PATCH 221/538] fix: cpm -> setcpm --- 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 083a55390..74daf4bab 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -288,12 +288,12 @@ This is what we've learned so far: The Mini-Notation is usually used inside some function. These are the functions we've seen so far: -| Name | Description | Example | -| ----- | ----------------------------------- | --------------------------------------------------------------------------------- | -| sound | plays the sound of the given name | | -| bank | selects the sound bank | | -| cpm | sets the tempo in cycles per minute | | -| n | select sample number | | +| Name | Description | Example | +| ------ | ----------------------------------- | --------------------------------------------------------------------------------- | +| sound | plays the sound of the given name | | +| bank | selects the sound bank | | +| setcpm | sets the tempo in cycles per minute | | +| n | select sample number | | ## Examples From 95193b0bfd4b4e2c2e0c6ef216b4331e2f037cf4 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 19 Jun 2025 08:18:20 +0100 Subject: [PATCH 222/538] a test for #1396 --- packages/core/test/pattern.test.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 45a1e2a98..b93d80f25 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -52,6 +52,7 @@ import { stackCentre, stepcat, sometimes, + expand } from '../index.mjs'; import { steady } from '../signal.mjs'; @@ -1179,6 +1180,9 @@ describe('Pattern', () => { it('calculates undefined steps as the average', () => { expect(sameFirst(stepcat(pure(1), pure(2), pure(3).setSteps(undefined)), fastcat(1, 2, 3))); }); + it('works with auto-reified values', () => { + expect(sameFirst(stepcat(expand(3, 'bd'), 'rim'), stepcat(expand(3, 'bd'), pure('rim')))) + }); }); describe('shrink', () => { it('can shrink', () => { From 440f1cb081c0947ba3ba8b00b33f4dcc74bafe98 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 19 Jun 2025 08:27:23 +0100 Subject: [PATCH 223/538] reify arguments of stepcat --- packages/core/pattern.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 76b02c21e..59833aea7 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2720,6 +2720,7 @@ export function stepcat(...timepats) { return nothing; } const findsteps = (x) => (Array.isArray(x) ? x : [x._steps, x]); + timepats = timepats.map(reify); timepats = timepats.map(findsteps); if (timepats.find((x) => x[0] === undefined)) { const times = timepats.map((a) => a[0]).filter((x) => x !== undefined); From a1d181d6097057542ca7dd943a2a8105330f52d5 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 19 Jun 2025 08:27:41 +0100 Subject: [PATCH 224/538] format --- packages/core/test/pattern.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index b93d80f25..07bdcdda7 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -52,7 +52,7 @@ import { stackCentre, stepcat, sometimes, - expand + expand, } from '../index.mjs'; import { steady } from '../signal.mjs'; @@ -1181,7 +1181,7 @@ describe('Pattern', () => { expect(sameFirst(stepcat(pure(1), pure(2), pure(3).setSteps(undefined)), fastcat(1, 2, 3))); }); it('works with auto-reified values', () => { - expect(sameFirst(stepcat(expand(3, 'bd'), 'rim'), stepcat(expand(3, 'bd'), pure('rim')))) + expect(sameFirst(stepcat(expand(3, 'bd'), 'rim'), stepcat(expand(3, 'bd'), pure('rim')))); }); }); describe('shrink', () => { From a8757fecc8554b10274ca27e378fe9bdb2e216fc Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 19 Jun 2025 08:39:05 +0100 Subject: [PATCH 225/538] avoid double action for PRs, try turning on pnpm cache --- .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 765f5958d..1266d2ddf 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -1,6 +1,6 @@ name: Strudel tests -on: [push, pull_request] +on: [push] jobs: build: @@ -17,7 +17,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 f3c4afaa54d0c7fa2428ec883052e6643ec725ea Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 19 Jun 2025 08:42:33 +0100 Subject: [PATCH 226/538] apt install zstd for caching --- .forgejo/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index 1266d2ddf..90cb7e258 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -11,6 +11,8 @@ jobs: steps: - uses: actions/checkout@v4 + - name: apt install ztd + run: apt update && apt install -y zstd - uses: pnpm/action-setup@v4 with: version: 9.12.2 From c01b4c651c54603fd295fc4750e77d9858123b1e Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 19 Jun 2025 09:06:53 +0100 Subject: [PATCH 227/538] don't trash lists passed to timecat --- packages/core/pattern.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 59833aea7..6441b7568 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2719,8 +2719,7 @@ export function stepcat(...timepats) { if (timepats.length === 0) { return nothing; } - const findsteps = (x) => (Array.isArray(x) ? x : [x._steps, x]); - timepats = timepats.map(reify); + const findsteps = (x) => (Array.isArray(x) ? x : [x._steps ?? 1, x]); timepats = timepats.map(findsteps); if (timepats.find((x) => x[0] === undefined)) { const times = timepats.map((a) => a[0]).filter((x) => x !== undefined); From 15f3321b04a7ace75294517d6d05605052921afa Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 19 Jun 2025 11:33:22 +0200 Subject: [PATCH 228/538] fix: extend in some situations, like [[bd]@3 bd] --- 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 a4e7c93b3..6e8278939 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -84,7 +84,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'].includes(value)) { + if (['!', 'extend', '@', 'expand', 'square', 'angle'].includes(value)) { return variable; } pat = reify(variable); From 4d3bbd5057b81a95f19b75762b2dcf01971d5ba1 Mon Sep 17 00:00:00 2001 From: yaxu Date: Thu, 19 Jun 2025 17:44:45 +0200 Subject: [PATCH 229/538] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3644d53d3..d54302ac6 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ This project is organized into many [packages](./packages), which are also avail Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start). -You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE.md). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details. +You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details. Licensing info for the default sound banks can be found over on the [dough-samples](https://github.com/felixroos/dough-samples/blob/main/README.md) repository. From e68df9e789bd3d2e1543234777b87f7c2edbfa71 Mon Sep 17 00:00:00 2001 From: Sergei Trofimovich Date: Fri, 20 Jun 2025 18:33:58 +0100 Subject: [PATCH 230/538] website intro: fix whitespace and code hosting name --- website/src/repl/components/panel/WelcomeTab.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/repl/components/panel/WelcomeTab.jsx b/website/src/repl/components/panel/WelcomeTab.jsx index cd5fe030a..7f0ee70ab 100644 --- a/website/src/repl/components/panel/WelcomeTab.jsx +++ b/website/src/repl/components/panel/WelcomeTab.jsx @@ -20,7 +20,7 @@ export function WelcomeTab({ context }) {

{/* To learn more about what this all means, check out the{' '} */} - To get started, check out the + To get started, check out the{' '} interactive tutorial @@ -43,7 +43,7 @@ export function WelcomeTab({ context }) { . You can find the source code at{' '} - github + codeberg . You can also find licensing info{' '} for the default sound banks there. Please consider to{' '} From a750764f8847973b9107e72c174c082646d54219 Mon Sep 17 00:00:00 2001 From: yaxu Date: Mon, 23 Jun 2025 11:13:00 +0200 Subject: [PATCH 231/538] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aa84cbce7..f455741f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ git remote set-url origin git@codeberg.org:uzu/strudel.git To get in touch with the contributors, either -- [join the Tidal Discord Channel](https://discord.gg/remJ6gQA) and go to the #strudel channel +- [join the Tidal Discord Channel](https://discord.com/invite/HGEdXmRkzT) and go to the #strudel channel - Find related discussions on the [tidal club forum](https://club.tidalcycles.org/) ## Ask a Question From e2a29914c73e7d8c576116e8b5daebb61dcc1773 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 24 Jun 2025 05:43:06 +0200 Subject: [PATCH 232/538] don't interpret note as n --- packages/tonal/test/tonal.test.mjs | 12 ------------ packages/tonal/tonal.mjs | 9 ++------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index a6cdae391..ec944b339 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -30,18 +30,6 @@ describe('tonal', () => { .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); }); - it('scale with n and note values', () => { - expect( - n(0, 1, 2) - .note(3, 4, 0) - .scale('C major') - .firstCycleValues.map((h) => [h.n, h.note]), - ).toEqual([ - [0, 'F3'], - [1, 'G3'], - [2, 'C3'], - ]); - }); it('scale with colon', () => { expect( n(0, 1, 2) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 2a7f64a8f..27f993bae 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -199,14 +199,9 @@ export const scale = register( pat .fmap((value) => { const isObject = typeof value === 'object'; - let step = value; + let step = isObject ? value.n : value; if (isObject) { - if (typeof value.note !== 'undefined') { - step = value.note; - } else { - step = value.n; - delete value.n; // remove n so it won't cause trouble - } + delete value.n; } if (isNote(step)) { // legacy.. From 71e60ed8defe184c76d398622240c0fddcac5221 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 24 Jun 2025 05:45:36 +0200 Subject: [PATCH 233/538] add back comment --- 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 27f993bae..a425782d2 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -201,7 +201,7 @@ export const scale = register( const isObject = typeof value === 'object'; let step = isObject ? value.n : value; if (isObject) { - delete value.n; + delete value.n; // remove n so it won't cause trouble } if (isNote(step)) { // legacy.. From 0003ce174e612b9ffc689d93c3e918f501981726 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 24 Jun 2025 06:06:37 +0200 Subject: [PATCH 234/538] allow calling `all` multiple times --- packages/core/repl.mjs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 8acd2bbea..e3d8f6640 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -113,8 +113,9 @@ export function repl({ * all(x => x.pianoroll()) * ``` */ + let allTransforms = []; const all = function (transform) { - allTransform = transform; + allTransforms.push(transform); return silence; }; /** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern. @@ -202,8 +203,10 @@ export function repl({ } else if (eachTransform) { pattern = eachTransform(pattern); } - if (allTransform) { - pattern = allTransform(pattern); + if (allTransforms.length) { + for (let i in allTransforms) { + pattern = allTransforms[i](pattern); + } } if (!isPattern(pattern)) { const message = `got "${typeof evaluated}" instead of pattern`; From d58b3e490b20f08cd5d41fd48c493f13de425939 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 24 Jun 2025 06:12:21 +0200 Subject: [PATCH 235/538] fix: reset all transforms before eval --- packages/core/repl.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index e3d8f6640..7329979c1 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -191,6 +191,7 @@ export function repl({ await injectPatternMethods(); setTime(() => scheduler.now()); // TODO: refactor? await beforeEval?.({ code }); + allTransforms = []; // reset all transforms shouldHush && hush(); let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { From fe9a91d1e4d9933d21f9aa028851a62f348ebe1e Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 24 Jun 2025 23:22:42 +0100 Subject: [PATCH 236/538] add unjoin, into and chunkinto --- packages/core/pattern.mjs | 41 ++++++++++++++ test/__snapshots__/examples.test.mjs.snap | 69 +++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index beefc5971..0ad1f77c9 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -869,6 +869,47 @@ export class Pattern { console.log(drawLine(this)); return this; } + + ////////////////////////////////////////////////////////////////////// + // methods relating to breaking patterns into subcycles + + // Breaks a pattern into a pattern of patterns, according to the structure of the given binary pattern. + unjoin(pieces, func = id) { + return pieces.withHap((hap) => + hap.withValue((v) => (v ? func(this.ribbon(hap.whole.begin, hap.whole.duration)) : this)), + ); + } + + /** + * Breaks a pattern into pieces according to the structure of a given pattern. + * True values in the given pattern cause the corresponding subcycle of the + * source pattern to be looped, and for an (optional) given function to be + * applied. False values result in the corresponding part of the source pattern + * to be played unchanged. + * @name into + * @memberof Pattern + * @example + * sound("bd sd ht lt").into("1 0", hurry(2)) + */ + into(pieces, func) { + return this.unjoin(pieces, func).innerJoin(); + } + + /** + * Like `chunk`, but the function is applied to a looped subcycle of the source pattern. + * @name chunkInto + * @synonym chunkinto + * @memberof Pattern + * @example + * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) + * .bank("tr909") + */ + chunkInto(n, func) { + return this.into(fastcat(true, ...Array(n - 1).fill(false)).iterback(4), func); + } + chunkinto(n, func) { + return this.chunkInto(n, func); + } } ////////////////////////////////////////////////////////////////////// diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index f3607cb48..d0a622940 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1889,6 +1889,46 @@ exports[`runs examples > example "chunkBack" example index 0 1`] = ` ] `; +exports[`runs examples > example "chunkInto" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd speed:2 bank:tr909 ]", + "[ 1/16 → 1/8 | s:sd speed:2 bank:tr909 ]", + "[ 1/8 → 3/16 | s:bd speed:2 bank:tr909 ]", + "[ 3/16 → 1/4 | s:sd speed:2 bank:tr909 ]", + "[ 1/4 → 3/8 | s:ht bank:tr909 ]", + "[ 3/8 → 1/2 | s:lt bank:tr909 ]", + "[ 1/2 → 5/8 | s:bd bank:tr909 ]", + "[ 3/4 → 7/8 | s:cp bank:tr909 ]", + "[ 7/8 → 1/1 | s:lt bank:tr909 ]", + "[ 1/1 → 9/8 | s:bd bank:tr909 ]", + "[ 9/8 → 5/4 | s:sd bank:tr909 ]", + "[ 5/4 → 21/16 | s:ht speed:2 bank:tr909 ]", + "[ 21/16 → 11/8 | s:lt speed:2 bank:tr909 ]", + "[ 11/8 → 23/16 | s:ht speed:2 bank:tr909 ]", + "[ 23/16 → 3/2 | s:lt speed:2 bank:tr909 ]", + "[ 3/2 → 13/8 | s:bd bank:tr909 ]", + "[ 7/4 → 15/8 | s:cp bank:tr909 ]", + "[ 15/8 → 2/1 | s:lt bank:tr909 ]", + "[ 2/1 → 17/8 | s:bd bank:tr909 ]", + "[ 17/8 → 9/4 | s:sd bank:tr909 ]", + "[ 9/4 → 19/8 | s:ht bank:tr909 ]", + "[ 19/8 → 5/2 | s:lt bank:tr909 ]", + "[ 5/2 → 41/16 | s:bd speed:2 bank:tr909 ]", + "[ 21/8 → 43/16 | s:bd speed:2 bank:tr909 ]", + "[ 11/4 → 23/8 | s:cp bank:tr909 ]", + "[ 23/8 → 3/1 | s:lt bank:tr909 ]", + "[ 3/1 → 25/8 | s:bd bank:tr909 ]", + "[ 25/8 → 13/4 | s:sd bank:tr909 ]", + "[ 13/4 → 27/8 | s:ht bank:tr909 ]", + "[ 27/8 → 7/2 | s:lt bank:tr909 ]", + "[ 7/2 → 29/8 | s:bd bank:tr909 ]", + "[ 15/4 → 61/16 | s:cp speed:2 bank:tr909 ]", + "[ 61/16 → 31/8 | s:lt speed:2 bank:tr909 ]", + "[ 31/8 → 63/16 | s:cp speed:2 bank:tr909 ]", + "[ 63/16 → 4/1 | s:lt speed:2 bank:tr909 ]", +] +`; + exports[`runs examples > example "clip" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano clip:0.5 ]", @@ -4391,6 +4431,35 @@ exports[`runs examples > example "inside" example index 0 1`] = ` ] `; +exports[`runs examples > example "into" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:bd speed:2 ]", + "[ 1/8 → 1/4 | s:sd speed:2 ]", + "[ 1/4 → 3/8 | s:bd speed:2 ]", + "[ 3/8 → 1/2 | s:sd speed:2 ]", + "[ 1/2 → 3/4 | s:ht ]", + "[ 3/4 → 1/1 | s:lt ]", + "[ 1/1 → 9/8 | s:bd speed:2 ]", + "[ 9/8 → 5/4 | s:sd speed:2 ]", + "[ 5/4 → 11/8 | s:bd speed:2 ]", + "[ 11/8 → 3/2 | s:sd speed:2 ]", + "[ 3/2 → 7/4 | s:ht ]", + "[ 7/4 → 2/1 | s:lt ]", + "[ 2/1 → 17/8 | s:bd speed:2 ]", + "[ 17/8 → 9/4 | s:sd speed:2 ]", + "[ 9/4 → 19/8 | s:bd speed:2 ]", + "[ 19/8 → 5/2 | s:sd speed:2 ]", + "[ 5/2 → 11/4 | s:ht ]", + "[ 11/4 → 3/1 | s:lt ]", + "[ 3/1 → 25/8 | s:bd speed:2 ]", + "[ 25/8 → 13/4 | s:sd speed:2 ]", + "[ 13/4 → 27/8 | s:bd speed:2 ]", + "[ 27/8 → 7/2 | s:sd speed:2 ]", + "[ 7/2 → 15/4 | s:ht ]", + "[ 15/4 → 4/1 | s:lt ]", +] +`; + exports[`runs examples > example "invert" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:bd ]", From e19fd2444704b1767f415d68c897d3da0058eb6a Mon Sep 17 00:00:00 2001 From: Khalid Date: Wed, 25 Jun 2025 08:44:12 -0400 Subject: [PATCH 237/538] Case insensitive search in the reference tab --- website/src/repl/components/panel/Reference.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index ab925eb91..42a03a02e 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -21,7 +21,8 @@ export function Reference() { return true; } - return entry.name.includes(search) || (entry.synonyms?.some((s) => s.includes(search)) ?? false); + const lowCaseSearch = search.toLowerCase(); + return entry.name.toLowerCase().includes(lowCaseSearch) || (entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false); }); }, [search]); From 82e3f9b837d300e51360181f5c9c5d9a11bc5651 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 26 Jun 2025 14:59:24 +0200 Subject: [PATCH 238/538] fix: js example --- website/src/pages/learn/mondo-notation.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index ccf5b72b6..3b00b9902 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -117,10 +117,10 @@ Here's the same written in JS: *4") - # scale("C4:minor") - # jux(rev) - # dec(.2) - # delay(.5)`} + .scale("C4:minor") + .jux(rev) + .dec(.2) + .delay(.5)`} /> ### Chaining Functions Locally From b1aba294ad66bd6e8da6147cb474367fe2a187c4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 26 Jun 2025 15:39:35 +0200 Subject: [PATCH 239/538] hotfix: fix upstream test --- packages/tonal/test/tonal.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 226522690..cd8b3c88b 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -39,7 +39,7 @@ describe('tonal', () => { }); it('scale without tonic', () => { expect( - n(0, 1, 2) + n(seq(0, 1, 2)) .scale('major') .firstCycleValues.map((h) => h.note), ).toEqual(['C3', 'D3', 'E3']); From fef50e654c0ef9827a137ba1229790a1c4c67de1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 27 Jun 2025 02:38:43 -0400 Subject: [PATCH 240/538] working --- website/public/EmuSP12.json | 69 ++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/website/public/EmuSP12.json b/website/public/EmuSP12.json index b129ee57a..102baf0b8 100644 --- a/website/public/EmuSP12.json +++ b/website/public/EmuSP12.json @@ -1,17 +1,54 @@ { -"_base": "https://raw.githubusercontent.com/ritchse/tidal-drum-machines/main/machines/EmuSP12/", -"bd": ["emusp12-bd/Bassdrum-01.wav","emusp12-bd/Bassdrum-02.wav","emusp12-bd/Bassdrum-03.wav","emusp12-bd/Bassdrum-04.wav","emusp12-bd/Bassdrum-05.wav","emusp12-bd/Bassdrum-06.wav","emusp12-bd/Bassdrum-07.wav","emusp12-bd/Bassdrum-08.wav","emusp12-bd/Bassdrum-09.wav","emusp12-bd/Bassdrum-10.wav","emusp12-bd/Bassdrum-11.wav","emusp12-bd/Bassdrum-12.wav","emusp12-bd/Bassdrum-13.wav","emusp12-bd/Bassdrum-14.wav"], -"cb": ["emusp12-cb/Cowbell.wav"], -"cp": ["emusp12-cp/Clap.wav"], -"cr": ["emusp12-cr/Crash.wav"], -"hh": ["emusp12-hh/Hat Closed-01.wav","emusp12-hh/Hat Closed-02.wav"], -"ht": ["emusp12-ht/Tom H-01.wav","emusp12-ht/Tom H-02.wav","emusp12-ht/Tom H-03.wav","emusp12-ht/Tom H-04.wav","emusp12-ht/Tom H-05.wav","emusp12-ht/Tom H-06.wav"], -"lt": ["emusp12-lt/Tom L-01.wav","emusp12-lt/Tom L-02.wav","emusp12-lt/Tom L-03.wav","emusp12-lt/Tom L-04.wav","emusp12-lt/Tom L-05.wav","emusp12-lt/Tom L-06.wav"], -"misc": ["emusp12-misc/Metal-01.wav","emusp12-misc/Metal-02.wav","emusp12-misc/Metal-03.wav","emusp12-misc/Scratch.wav","emusp12-misc/Shot-01.wav","emusp12-misc/Shot-02.wav","emusp12-misc/Shot-03.wav"], -"mt": ["emusp12-mt/Tom M-01.wav","emusp12-mt/Tom M-02.wav","emusp12-mt/Tom M-03.wav","emusp12-mt/Tom M-05.wav"], -"oh": ["emusp12-oh/Hhopen1.wav"], -"perc": ["emusp12-perc/Blow1.wav"], -"rd": ["emusp12-rd/Ride.wav"], -"rim": ["emusp12-rim/zRim Shot-01.wav","emusp12-rim/zRim Shot-02.wav"], -"sd": ["emusp12-sd/Snaredrum-01.wav","emusp12-sd/Snaredrum-02.wav","emusp12-sd/Snaredrum-03.wav","emusp12-sd/Snaredrum-04.wav","emusp12-sd/Snaredrum-05.wav","emusp12-sd/Snaredrum-06.wav","emusp12-sd/Snaredrum-07.wav","emusp12-sd/Snaredrum-08.wav","emusp12-sd/Snaredrum-09.wav","emusp12-sd/Snaredrum-10.wav","emusp12-sd/Snaredrum-11.wav","emusp12-sd/Snaredrum-12.wav","emusp12-sd/Snaredrum-13.wav","emusp12-sd/Snaredrum-14.wav","emusp12-sd/Snaredrum-15.wav","emusp12-sd/Snaredrum-16.wav","emusp12-sd/Snaredrum-17.wav","emusp12-sd/Snaredrum-18.wav","emusp12-sd/Snaredrum-19.wav","emusp12-sd/Snaredrum-20.wav","emusp12-sd/Snaredrum-21.wav"] -} + "_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" + ], + "breaks": [ + "breaks/10_break_amen_pprocessed.wav" + ], + "cp": [ + "cp/10_cp_switchangel.wav", + "cp/11_cp_mot4i.wav" + ], + "cr": [ + "cr/10_cr_mot4i.wav" + ], + "hh": [ + "hh/10_hh_mot4i.wav", + "hh/11_hh_switchangel.wav", + "hh/12_hh_switchangel.wav", + "hh/13_hh_mot4i.wav" + ], + "ht": [ + "ht/10_ht_mot4i.wav" + ], + "lt": [ + "lt/10_lt_mot4i.wav" + ], + "mt": [ + "mt/10_mt_mot4i.wav" + ], + "oh": [ + "oh/10_oh_switchangel.wav", + "oh/11_oh_switchangel.wav", + "oh/12_oh_switchangel.wav" + ], + "perc": [ + "perc/10_perc_switchangel.wav" + ], + "rim": [ + "rim/10_rim_switch_angel.wav" + ], + "sd": [ + "sd/10_sd_switchangel_3.wav", + "sd/11_sd_switchangel_2.wav", + "sd/12_sd_switchangel_2.wav", + "sd/13_sd.wav" + ] + } \ No newline at end of file From 2dd528134a37662f7655721b8b7f69adaa0b9095 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 27 Jun 2025 02:52:04 -0400 Subject: [PATCH 241/538] fix json --- packages/repl/prebake.mjs | 2 +- website/public/{EmuSP12.json => uzudrumkit.json} | 0 website/src/repl/prebake.mjs | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename website/public/{EmuSP12.json => uzudrumkit.json} (100%) diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index dd0023fb5..5e524bbc8 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(`${ds}/EmuSP12.json`), + samples(`${ds}/uzudrumkit.json`), samples(`${ds}/vcsl.json`), samples(`${ds}/mridangam.json`), ]); diff --git a/website/public/EmuSP12.json b/website/public/uzudrumkit.json similarity index 100% rename from website/public/EmuSP12.json rename to website/public/uzudrumkit.json diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 68befdd96..21034dc66 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(`${baseNoTrailing}/EmuSP12.json`, undefined, { prebake: true, tag: 'drum-machines' }), + samples(`${baseNoTrailing}/uzudrumkit.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples( { From 09d05abd707fefb77bdceb3f0d7dee35227e87ba Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 27 Jun 2025 15:54:39 -0400 Subject: [PATCH 242/538] delaysync --- packages/core/controls.mjs | 18 +++++++++++++++++- packages/superdough/superdough.mjs | 2 +- packages/webaudio/webaudio.mjs | 1 - 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 3a7faf081..c37960d41 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -985,15 +985,31 @@ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', * */ export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt'); -/* // TODO: test + +/** + * Sets the time of the delay effect in cycles. + * + * @name delaysync + * @param {number | Pattern} cycles delay length in cycles + * @synonyms delayt, dt + * @example + * s("bd bd").delay(.25).delaysync("<1 2 3 5>".div(8)) + * + */ +export const { delaysync } = registerControl('delaysync'); + +/** * Specifies whether delaytime is calculated relative to cps. * * @name lock * @param {number | Pattern} enable When set to 1, delaytime is a direct multiple of a cycle. + * @superdirtOnly * @example * s("sd").delay().lock(1).osc() * + * */ + export const { lock } = registerControl('lock'); /** * Set detune for stacked voices of supported oscillators diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fc75276a8..14f46711b 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -548,7 +548,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { compressorRelease, } = value; - delaytime = delaytime ?? cycleToSeconds(delaysync) + delaytime = delaytime ?? cycleToSeconds(delaysync, cps); const orbitChannels = mapChannelNumbers( multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'), diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 67490cd23..91e28defd 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -21,7 +21,6 @@ export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => { return superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration, cps); }; - Pattern.prototype.webaudio = function () { return this.onTrigger(webaudioOutputTrigger); }; From 7a53f6c021b89d53664a222b0f46b0a17c58a4f4 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 27 Jun 2025 15:56:37 -0400 Subject: [PATCH 243/538] add test --- test/__snapshots__/examples.test.mjs.snap | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index f3607cb48..ed3b5f793 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2505,6 +2505,19 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = ` ] `; +exports[`runs examples > example "delaysync" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd delay:0.25 delaysync:0.125 ]", + "[ 1/2 → 1/1 | s:bd delay:0.25 delaysync:0.125 ]", + "[ 1/1 → 3/2 | s:bd delay:0.25 delaysync:0.25 ]", + "[ 3/2 → 2/1 | s:bd delay:0.25 delaysync:0.25 ]", + "[ 2/1 → 5/2 | s:bd delay:0.25 delaysync:0.375 ]", + "[ 5/2 → 3/1 | s:bd delay:0.25 delaysync:0.375 ]", + "[ 3/1 → 7/2 | s:bd delay:0.25 delaysync:0.625 ]", + "[ 7/2 → 4/1 | s:bd delay:0.25 delaysync:0.625 ]", +] +`; + exports[`runs examples > example "delaytime" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:bd delay:0.25 delaytime:0.125 ]", @@ -5016,6 +5029,15 @@ exports[`runs examples > example "linger" example index 0 1`] = ` ] `; +exports[`runs examples > example "lock" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | delay:{s:sd} lock:1 ]", + "[ 1/1 → 2/1 | delay:{s:sd} lock:1 ]", + "[ 2/1 → 3/1 | delay:{s:sd} lock:1 ]", + "[ 3/1 → 4/1 | delay:{s:sd} lock:1 ]", +] +`; + exports[`runs examples > example "loop" example index 0 1`] = ` [ "[ 0/1 → 1/1 | s:casio loop:1 ]", From ff5b11f5edee691e5cb126f6a88d0fc4471bc4d0 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 27 Jun 2025 22:44:15 +0100 Subject: [PATCH 244/538] test + bugfix for chunkinto --- packages/core/pattern.mjs | 36 ++++++++++++++--------------- packages/core/test/pattern.test.mjs | 23 ++++++++++++++++++ 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 10527a1a8..c8c744628 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -894,22 +894,6 @@ export class Pattern { into(pieces, func) { return this.unjoin(pieces, func).innerJoin(); } - - /** - * Like `chunk`, but the function is applied to a looped subcycle of the source pattern. - * @name chunkInto - * @synonym chunkinto - * @memberof Pattern - * @example - * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) - * .bank("tr909") - */ - chunkInto(n, func) { - return this.into(fastcat(true, ...Array(n - 1).fill(false)).iterback(4), func); - } - chunkinto(n, func) { - return this.chunkInto(n, func); - } } ////////////////////////////////////////////////////////////////////// @@ -1858,9 +1842,9 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f const newWhole = !hap.whole ? undefined : new TimeSpan( - newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), - newPart.end.add(hap.whole.end.sub(end).div(factor)), - ); + newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), + newPart.end.add(hap.whole.end.sub(end).div(factor)), + ); return new Hap(newWhole, newPart, hap.value, hap.context); }; return pat.withQuerySpanMaybe(qf).withHap(ef).splitQueries(); @@ -2535,6 +2519,20 @@ export const { fastchunk, fastChunk } = register( true, ); +/** + * Like `chunk`, but the function is applied to a looped subcycle of the source pattern. + * @name chunkInto + * @synonym chunkinto + * @memberof Pattern + * @example + * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) + * .bank("tr909") + */ +export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], + function (n, func, pat) { + return pat.into(fastcat(true, ...Array(n - 1).fill(false)).iterback(n), func); + }); + // TODO - redefine elsewhere in terms of mask export const bypass = register( 'bypass', diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 874d45132..8d26a990c 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1271,4 +1271,27 @@ describe('Pattern', () => { ); }); }); + describe('unjoin', () => { + it('destructures a pattern into subcycles', () => { + sameFirst( + fastcat('a', 'b', 'c', 'd').unjoin(fastcat(true, fastcat(true, true))).fmap(fast(2)).join(), + fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd') + ) + }) + }) + describe('into', () => { + it('applies a function to subcycles of a pattern', () => { + sameFirst( + fastcat('a', 'b', 'c', 'd').into(fastcat(fastcat('true', 'true'), 'true'), fast(2)), + fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd') + ) + }) + }), + describe('chunkinto', () => { + it('chunks into subcycles', () => { + sameFirst(fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3), + fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')) + ) + }) + }) }); From f44caf90966e34271f155f366721a64b060c1f97 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 27 Jun 2025 22:51:47 +0100 Subject: [PATCH 245/538] tests and tweaks, and add chunkBackInto --- packages/core/pattern.mjs | 16 +++++++++++++++- packages/core/test/pattern.test.mjs | 23 +++++++++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index c8c744628..6d42c51fd 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2530,7 +2530,21 @@ export const { fastchunk, fastChunk } = register( */ export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], function (n, func, pat) { - return pat.into(fastcat(true, ...Array(n - 1).fill(false)).iterback(n), func); + return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iterback(n), func); + }); + +/** + * Like `chunkInto`, but moves backwards through the chunks. + * @name chunkBackInto + * @synonym chunkbackinto + * @memberof Pattern + * @example + * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) + * .bank("tr909") + */ +export const { chunkbackinto, chunkBackInto } = register(['chunkbackinto', 'chunkBackInto'], + function (n, func, pat) { + return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iter(n)._early(1), func); }); // TODO - redefine elsewhere in terms of mask diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 8d26a990c..4ab519a75 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1278,7 +1278,7 @@ describe('Pattern', () => { fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd') ) }) - }) + }); describe('into', () => { it('applies a function to subcycles of a pattern', () => { sameFirst( @@ -1286,12 +1286,19 @@ describe('Pattern', () => { fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd') ) }) - }), - describe('chunkinto', () => { - it('chunks into subcycles', () => { - sameFirst(fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3), - fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')) - ) - }) + }); + describe('chunkinto', () => { + it('chunks into subcycles', () => { + sameFirst(fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3), + fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')) + ) }) + }); + describe('chunkbackinto', () => { + it('chunks into subcycles backwards', () => { + sameFirst(fastcat('a', 'b', 'c').chunkBackInto(3, fast(2)).fast(3), + fastcat('a', 'b', fastcat('c', 'c'), 'a', fastcat('b', 'b'), 'c', fastcat('a', 'a'), 'b', 'c') + ) + }) + }); }); From 27073d6c17264dd3218102bd72bcb96d6a0186d6 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 27 Jun 2025 22:56:19 +0100 Subject: [PATCH 246/538] format --- packages/core/pattern.mjs | 25 ++++++++++++--------- packages/core/test/pattern.test.mjs | 35 ++++++++++++++++------------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 6d42c51fd..cbaf8a8a2 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1842,9 +1842,9 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f const newWhole = !hap.whole ? undefined : new TimeSpan( - newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), - newPart.end.add(hap.whole.end.sub(end).div(factor)), - ); + newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), + newPart.end.add(hap.whole.end.sub(end).div(factor)), + ); return new Hap(newWhole, newPart, hap.value, hap.context); }; return pat.withQuerySpanMaybe(qf).withHap(ef).splitQueries(); @@ -2528,10 +2528,9 @@ export const { fastchunk, fastChunk } = register( * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) * .bank("tr909") */ -export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], - function (n, func, pat) { - return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iterback(n), func); - }); +export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], function (n, func, pat) { + return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iterback(n), func); +}); /** * Like `chunkInto`, but moves backwards through the chunks. @@ -2542,10 +2541,14 @@ export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) * .bank("tr909") */ -export const { chunkbackinto, chunkBackInto } = register(['chunkbackinto', 'chunkBackInto'], - function (n, func, pat) { - return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iter(n)._early(1), func); - }); +export const { chunkbackinto, chunkBackInto } = register(['chunkbackinto', 'chunkBackInto'], function (n, func, pat) { + return pat.into( + fastcat(true, ...Array(n - 1).fill(false)) + ._iter(n) + ._early(1), + func, + ); +}); // TODO - redefine elsewhere in terms of mask export const bypass = register( diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 4ab519a75..93c4168c9 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1274,31 +1274,36 @@ describe('Pattern', () => { describe('unjoin', () => { it('destructures a pattern into subcycles', () => { sameFirst( - fastcat('a', 'b', 'c', 'd').unjoin(fastcat(true, fastcat(true, true))).fmap(fast(2)).join(), - fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd') - ) - }) + fastcat('a', 'b', 'c', 'd') + .unjoin(fastcat(true, fastcat(true, true))) + .fmap(fast(2)) + .join(), + fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd'), + ); + }); }); describe('into', () => { it('applies a function to subcycles of a pattern', () => { sameFirst( fastcat('a', 'b', 'c', 'd').into(fastcat(fastcat('true', 'true'), 'true'), fast(2)), - fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd') - ) - }) + fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd'), + ); + }); }); describe('chunkinto', () => { it('chunks into subcycles', () => { - sameFirst(fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3), - fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')) - ) - }) + sameFirst( + fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3), + fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')), + ); + }); }); describe('chunkbackinto', () => { it('chunks into subcycles backwards', () => { - sameFirst(fastcat('a', 'b', 'c').chunkBackInto(3, fast(2)).fast(3), - fastcat('a', 'b', fastcat('c', 'c'), 'a', fastcat('b', 'b'), 'c', fastcat('a', 'a'), 'b', 'c') - ) - }) + sameFirst( + fastcat('a', 'b', 'c').chunkBackInto(3, fast(2)).fast(3), + fastcat('a', 'b', fastcat('c', 'c'), 'a', fastcat('b', 'b'), 'c', fastcat('a', 'a'), 'b', 'c'), + ); + }); }); }); From df06248c54709e68d39d4f4ae607af4509742842 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 27 Jun 2025 22:59:01 +0100 Subject: [PATCH 247/538] snapshot --- test/__snapshots__/examples.test.mjs.snap | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index d0a622940..2d20cff4c 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1889,6 +1889,46 @@ exports[`runs examples > example "chunkBack" example index 0 1`] = ` ] `; +exports[`runs examples > example "chunkBackInto" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd speed:2 bank:tr909 ]", + "[ 1/16 → 1/8 | s:sd speed:2 bank:tr909 ]", + "[ 1/8 → 3/16 | s:bd speed:2 bank:tr909 ]", + "[ 3/16 → 1/4 | s:sd speed:2 bank:tr909 ]", + "[ 1/4 → 3/8 | s:ht bank:tr909 ]", + "[ 3/8 → 1/2 | s:lt bank:tr909 ]", + "[ 1/2 → 5/8 | s:bd bank:tr909 ]", + "[ 3/4 → 7/8 | s:cp bank:tr909 ]", + "[ 7/8 → 1/1 | s:lt bank:tr909 ]", + "[ 1/1 → 9/8 | s:bd bank:tr909 ]", + "[ 9/8 → 5/4 | s:sd bank:tr909 ]", + "[ 5/4 → 21/16 | s:ht speed:2 bank:tr909 ]", + "[ 21/16 → 11/8 | s:lt speed:2 bank:tr909 ]", + "[ 11/8 → 23/16 | s:ht speed:2 bank:tr909 ]", + "[ 23/16 → 3/2 | s:lt speed:2 bank:tr909 ]", + "[ 3/2 → 13/8 | s:bd bank:tr909 ]", + "[ 7/4 → 15/8 | s:cp bank:tr909 ]", + "[ 15/8 → 2/1 | s:lt bank:tr909 ]", + "[ 2/1 → 17/8 | s:bd bank:tr909 ]", + "[ 17/8 → 9/4 | s:sd bank:tr909 ]", + "[ 9/4 → 19/8 | s:ht bank:tr909 ]", + "[ 19/8 → 5/2 | s:lt bank:tr909 ]", + "[ 5/2 → 41/16 | s:bd speed:2 bank:tr909 ]", + "[ 21/8 → 43/16 | s:bd speed:2 bank:tr909 ]", + "[ 11/4 → 23/8 | s:cp bank:tr909 ]", + "[ 23/8 → 3/1 | s:lt bank:tr909 ]", + "[ 3/1 → 25/8 | s:bd bank:tr909 ]", + "[ 25/8 → 13/4 | s:sd bank:tr909 ]", + "[ 13/4 → 27/8 | s:ht bank:tr909 ]", + "[ 27/8 → 7/2 | s:lt bank:tr909 ]", + "[ 7/2 → 29/8 | s:bd bank:tr909 ]", + "[ 15/4 → 61/16 | s:cp speed:2 bank:tr909 ]", + "[ 61/16 → 31/8 | s:lt speed:2 bank:tr909 ]", + "[ 31/8 → 63/16 | s:cp speed:2 bank:tr909 ]", + "[ 63/16 → 4/1 | s:lt speed:2 bank:tr909 ]", +] +`; + exports[`runs examples > example "chunkInto" example index 0 1`] = ` [ "[ 0/1 → 1/16 | s:bd speed:2 bank:tr909 ]", From 77bab433e0c897048e874ad97f0721733a55948e Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 28 Jun 2025 14:06:16 -0400 Subject: [PATCH 248/538] cf --- website/src/repl/components/panel/Reference.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index 42a03a02e..1b617341b 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -21,8 +21,11 @@ export function Reference() { return true; } - const lowCaseSearch = search.toLowerCase(); - return entry.name.toLowerCase().includes(lowCaseSearch) || (entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false); + const lowCaseSearch = search.toLowerCase(); + return ( + entry.name.toLowerCase().includes(lowCaseSearch) || + (entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false) + ); }); }, [search]); From f2330a2307fcdba42b55fe976635dce3e941cfcc Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 29 Jun 2025 20:18:15 +0200 Subject: [PATCH 249/538] add 1.1.0 release notes --- .../blog/release-1.1.0-bananensplit.mdx | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 website/src/content/blog/release-1.1.0-bananensplit.mdx diff --git a/website/src/content/blog/release-1.1.0-bananensplit.mdx b/website/src/content/blog/release-1.1.0-bananensplit.mdx new file mode 100644 index 000000000..dcc547682 --- /dev/null +++ b/website/src/content/blog/release-1.1.0-bananensplit.mdx @@ -0,0 +1,317 @@ +--- +title: 'Release Notes v1.1.0' +description: '' +date: '2024-06-02' +tags: ['meta'] +author: froos +--- + +These are the release notes for Strudel 1.1.0 aka "Bananensplit". + +The last release was over 19 weeks ago, so a lot of things have happened! + +First, here's a little demo, teasing some of the new features: + +import { Youtube } from '@src/components/Youtube'; + + + +Let's write up some of the highlights: + +## New DSP Features + +### Stereo Supersaw + +with spread, unison, and detune parameters + +```plaintext +note("d f a a# a d3").fast(2) +.s("supersaw").spread(".8").detune(.3).unison("2 7") +``` + +### Analog "ladder" filter type + +works great for acid basslines and vibey tones + +```plaintext +note("{d d d a a# d3 f4}%16".sub(12)).gain(1).s("sawtooth") +.lpf(200).lpenv(slider(1.36,0,8)).lpq(7).distort("1.5:.7")` +.ftype('ladder') +``` + +### stereo distortion effect + +```plaintext +note("{g g a# g g4}%8".add("{0 7 12 0}%8")).lpf(500) +.s("supersaw").dist("4:.2") +``` + +## Editor Features + +### inline viz + +The editor now supports multiple visuals within the code, using the `_` prefix for viz functions: + +Screenshot 2024-06-01 at 01 23 51 + +- `._pianoroll()`: inline pianoroll +- `._punchcard()`: inline punchcard +- `._scope()`: inline scope +- `._pitchwheel()`: inline pitchwheel + +For more info, check out the new [Visual Feedback Page](https://strudel.cc/learn/visual-feedback/) + +### label notation + +This new notation simplifies writing patterns at the top level: + +```plaintext +d1: s("bd*4") +d2: s("[- hh]*4") +``` + +This is equivalent to: + +```plaintext +stack( + s("bd*4"), + s("[- hh]*4") +) +``` + +The labels you choose are arbitrary, the above `d1` and `d2` are a typical thing you'd write in tidal, for example `d1 $ s "bd*4"`. +If the same label is used multiple times, the last one wins: + +```plaintext +d1: s("bd*4") +d1: s("[- hh]*4") // <-- only this plays +``` + +There is a special label anonymous label `$`, which can appear multiple times without overriding itself: + +```plaintext +// both of these will play: +$: s("bd*4") +$: s("[- hh]*4") +``` + +You can mute a pattern by prefixing `_`: + +```plaintext +_$: s("bd*4") // <-- this one is muted +$: s("[- hh]*4") +``` + +To run a transformation on all patterns, you can use `all`: + +```plaintext +$: s("bd*4") +$: s("[- hh]*4") +all(x=>x.room(.5)) +``` + +This notation is now the recommended way to [play patterns in parallel](https://strudel.cc/workshop/first-notes/#playing-multiple-patterns) + +### Clock sync between multiple instances + +timing has received a major overhaul, and is now much more accurate on all browsers. Additionally, you can now sync timing across multiple windows. +![Screenshot 2024-06-02 at 11 24 40 PM](https://codeberg.org/uzu/strudel/assets/47068718/840be744-a13e-4d7b-ab09-50d3a70b1f85) + +### Better sample upload support + +you can now upload large amounts of samples much faster across all browsers including on IOS devices. supported filetypes now include: ogg flac mp3 wav aac m4a + +### experimental tidal syntax + +The new `tidal` function allows you to write strudel patterns in tidal syntax: + +```plaintext +await initTidal() + +tidal` +d1 $ s "bd*4" + +d2 $ s "[- hh]*4" +` +``` + +As we're looking to improve compatibility with tidal, we're happy to hear feedback. + +## breaking changes + +This release comes with a bunch of breaking changes. If you find your patterns to sound different, check out the PRs below for guidance on how to update them. Most of these changes shouldn't affect a lot of patterns. +In case of doubt, add the line `// @version 1.0` to your old pattern. +If you're having problems, please let us know! + +- remove legacy legato + duration implementations by @felixroos in https://codeberg.org/uzu/strudel/pull/965 +- Velocity in value by @felixroos in https://codeberg.org/uzu/strudel/pull/974 +- use ireal as default voicing dict by @felixroos https://codeberg.org/uzu/strudel/pull/967 +- Color in hap value by @felixroos https://codeberg.org/uzu/strudel/pull/1007 +- rename trig -> reset, trigzero -> restart by @felixroos https://codeberg.org/uzu/strudel/pull/1010 +- remove dangerous arithmetic feature by @felixroos https://codeberg.org/uzu/strudel/pull/1030 +- change fanchor to 0 by @daslyfe https://codeberg.org/uzu/strudel/pull/1107 + +## superdough features + +- replace shape with distort in learn doc by @daslyfe https://codeberg.org/uzu/strudel/pull/982 +- Worklet Improvents / fixes by @daslyfe https://codeberg.org/uzu/strudel/pull/963 +- supersaw oscillator by @daslyfe https://codeberg.org/uzu/strudel/pull/978 +- Add analog-style ladder filter by @daslyfe https://codeberg.org/uzu/strudel/pull/1103 +- Calculate phaser modulation phase based on time by @daslyfe https://codeberg.org/uzu/strudel/pull/1110 +- rollback phaser by @daslyfe https://codeberg.org/uzu/strudel/pull/1113 + +## editor / ui features + +- 'Enable Bracket Matching' option in Codemirror by @eefano https://codeberg.org/uzu/strudel/pull/956 +- REPL sync between windows by @daslyfe https://codeberg.org/uzu/strudel/pull/900 +- inline viz / widgets package by @felixroos https://codeberg.org/uzu/strudel/pull/989 +- Inline punchcard + spiral by @felixroos https://codeberg.org/uzu/strudel/pull/1008 +- More fonts by @felixroos https://codeberg.org/uzu/strudel/pull/1023 +- better theme integration for visuals + various fixes by @felixroos https://codeberg.org/uzu/strudel/pull/1024 +- add setting for sync flag by @felixroos https://codeberg.org/uzu/strudel/pull/1025 +- add closeBrackets setting by @felixroos https://codeberg.org/uzu/strudel/pull/1031 +- add font file types to offline cache by @felixroos https://codeberg.org/uzu/strudel/pull/1032 +- pitchwheel visual by @felixroos https://codeberg.org/uzu/strudel/pull/1041 +- repl: set document.title from @title by @kasparsj https://codeberg.org/uzu/strudel/pull/1090 +- Samples tab improvements by @daslyfe https://codeberg.org/uzu/strudel/pull/1102 + +## language features + +- pickOut(), pickRestart(), pickReset() by @eefano https://codeberg.org/uzu/strudel/pull/950 +- Auto await samples by @felixroos https://codeberg.org/uzu/strudel/pull/955 +- feat: can now invert euclid pulses with negative numbers by @felixroos https://codeberg.org/uzu/strudel/pull/959 +- Nested controls by @felixroos https://codeberg.org/uzu/strudel/pull/973 +- alias - for ~ by @yaxu https://codeberg.org/uzu/strudel/pull/981 +- Beat-oriented functionality by @yaxu https://codeberg.org/uzu/strudel/pull/976 +- Labeled statements by @felixroos https://codeberg.org/uzu/strudel/pull/991 +- accidentals in scale degrees by @eefano https://codeberg.org/uzu/strudel/pull/1000 +- Feature: tactus marking by @yaxu https://codeberg.org/uzu/strudel/pull/1021 +- Tactus tidy by @yaxu https://codeberg.org/uzu/strudel/pull/1027 +- Wax, wane, taper and taperlist by @yaxu https://codeberg.org/uzu/strudel/pull/1042 +- transpose: support all combinations of numbers and strings for notes and intervals by @felixroos https://codeberg.org/uzu/strudel/pull/1048 +- anonymous patterns + muting by @felixroos https://codeberg.org/uzu/strudel/pull/1059 +- add swing + swingBy by @felixroos https://codeberg.org/uzu/strudel/pull/1038 +- Stepwise functions from Tidal by @yaxu https://codeberg.org/uzu/strudel/pull/1060 +- Tactus tweaks - fixes for maintaining tactus and highlight locations by @yaxu https://codeberg.org/uzu/strudel/pull/1065 +- Fix stepjoin by @yaxu https://codeberg.org/uzu/strudel/pull/1067 +- More tactus tidying by @yaxu https://codeberg.org/uzu/strudel/pull/1071 +- Tactus calculation toggle and breaking change to tactus calculation in fast/slow/hurry by @yaxu https://codeberg.org/uzu/strudel/pull/1081 +- hs2js package / tidal parser by @felixroos https://codeberg.org/uzu/strudel/pull/870 +- Add the mousex and mousey signal by @Enelg52 https://codeberg.org/uzu/strudel/pull/1112 +- can now access strudelMirror from repl by @felixroos https://codeberg.org/uzu/strudel/pull/1117 + +## sampler + +If you have nodejs installed on your system, you can now use [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler) to serve samples from disk to the REPL or flok. + +- local sample server cli by @felixroos https://codeberg.org/uzu/strudel/pull/1033 +- Fix sampler paths by @felixroos https://codeberg.org/uzu/strudel/pull/1034 +- Fix sampler windows by @felixroos https://codeberg.org/uzu/strudel/pull/1108 +- fix sampler on windows by @geikha https://codeberg.org/uzu/strudel/pull/1109 + +## docs + +- V1 release notes by @felixroos https://codeberg.org/uzu/strudel/pull/935 +- Minor documentation error: Update first-sounds.mdx by @mhetrick https://codeberg.org/uzu/strudel/pull/941 +- Update synths.mdx by @andresgottlieb https://codeberg.org/uzu/strudel/pull/984 +- using strudel in your project guide + cleanup examples by @felixroos https://codeberg.org/uzu/strudel/pull/1006 +- Document signals by @ilesinge https://codeberg.org/uzu/strudel/pull/1015 +- improve tutorial + custom samples doc by @felixroos https://codeberg.org/uzu/strudel/pull/1053 +- fix cr typo on first-sounds.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1068 +- fix first sounds typo by @cleary https://codeberg.org/uzu/strudel/pull/1069 +- add `<...>` to first-sounds.mdx recap by @cleary https://codeberg.org/uzu/strudel/pull/1070 +- add nesting to `off` example variation in pattern-effects.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1075 +- fix translation issue in first-effects.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1072 +- add signals to recap in first-effects.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1073 +- fix docs on alignment.mdx by @diegodorado https://codeberg.org/uzu/strudel/pull/1076 +- fix little dub tune example by @lukad https://codeberg.org/uzu/strudel/pull/1104 +- clarify `off` in pattern-effects.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1074 +- Fixes drawPianoroll import in codemirror example by @giohappy https://codeberg.org/uzu/strudel/pull/1116 +- Migrate tutorial fanchor by @felixroos https://codeberg.org/uzu/strudel/pull/1122 + +## internals + +- remove cjs builds by @felixroos https://codeberg.org/uzu/strudel/pull/945 +- controls refactoring: simplify exports by @felixroos https://codeberg.org/uzu/strudel/pull/962 +- move canvas related helpers from core to new draw package by @felixroos https://codeberg.org/uzu/strudel/pull/971 +- remove canvas, externalize samples, delete junk by @felixroos https://codeberg.org/uzu/strudel/pull/1003 +- Improve performance of ! (replicate) by @yaxu https://codeberg.org/uzu/strudel/pull/1084 +- Benchmarks by @yaxu https://codeberg.org/uzu/strudel/pull/1079 + +## fixes + +- fix midi issue on firefox and added quote error by @Enelg52 https://codeberg.org/uzu/strudel/pull/936 +- fix: pianoroll sorting by @felixroos https://codeberg.org/uzu/strudel/pull/938 +- account for cps in midi time duration by @daslyfe https://codeberg.org/uzu/strudel/pull/954 +- fix script importable packages (web + repl) by @felixroos https://codeberg.org/uzu/strudel/pull/957 +- fix: reset global fx on pattern change by @felixroos https://codeberg.org/uzu/strudel/pull/960 +- add debounce to logger by @felixroos https://codeberg.org/uzu/strudel/pull/968 +- fix for transpose(): preserve hap value object structure by @eefano https://codeberg.org/uzu/strudel/pull/966 +- fix: clear hydra on reset by @felixroos https://codeberg.org/uzu/strudel/pull/983 +- little fix for withVal by @eefano https://codeberg.org/uzu/strudel/pull/980 +- fix: share now shares what's visible instead of active by @felixroos https://codeberg.org/uzu/strudel/pull/985 +- Fix pure mini highlight by @yaxu https://codeberg.org/uzu/strudel/pull/994 +- fix: await injectPatternMethods by @felixroos https://codeberg.org/uzu/strudel/pull/1012 +- update undocumented script by @felixroos https://codeberg.org/uzu/strudel/pull/1013 +- eliminate chromium clock jitter by @felixroos https://codeberg.org/uzu/strudel/pull/1004 +- Repl sync fixes by @daslyfe https://codeberg.org/uzu/strudel/pull/1014 +- hotfix for 1017 by @daslyfe https://codeberg.org/uzu/strudel/pull/1020 +- fix cyclist fizzling out by @felixroos https://codeberg.org/uzu/strudel/pull/1046 +- Midi Time hotfix for scheduler updates by @daslyfe https://codeberg.org/uzu/strudel/pull/1047 +- fix: do not reset cc input values on each eval by @felixroos https://codeberg.org/uzu/strudel/pull/1054 +- Fix wchooseCycles not picking the whole pattern by @ilesinge https://codeberg.org/uzu/strudel/pull/1061 +- fix OSC timing for recent scheduler updates by @daslyfe https://codeberg.org/uzu/strudel/pull/1062 +- clarify license by @yaxu https://codeberg.org/uzu/strudel/pull/1064 +- fix failing format test by @daslyfe https://codeberg.org/uzu/strudel/pull/1077 +- fix: url parsing with extra params by @felixroos https://codeberg.org/uzu/strudel/pull/1083 +- fix: csound + dough timing by @felixroos https://codeberg.org/uzu/strudel/pull/1086 +- fix: missing events due to premature worklet cleanup by @felixroos https://codeberg.org/uzu/strudel/pull/1089 +- Use sessionStorage for viewingPatternData and activePattern by @kasparsj https://codeberg.org/uzu/strudel/pull/1091 +- osc: couple of fixes by @kasparsj https://codeberg.org/uzu/strudel/pull/1093 +- web package fixes by @felixroos https://codeberg.org/uzu/strudel/pull/1044 +- Fix audio worklets by @daslyfe https://codeberg.org/uzu/strudel/pull/1114 +- fix: use full repl in web package by @felixroos https://codeberg.org/uzu/strudel/pull/1119 +- [BUG FIX] Audio worklets sometimes dont load by @daslyfe https://codeberg.org/uzu/strudel/pull/1121 + +## New Contributors + +- @mhetrick made their first contribution https://codeberg.org/uzu/strudel/pull/941 +- @eefano made their first contribution https://codeberg.org/uzu/strudel/pull/956 +- @Enelg52 made their first contribution https://codeberg.org/uzu/strudel/pull/936 +- @andresgottlieb made their first contribution https://codeberg.org/uzu/strudel/pull/984 +- @cleary made their first contribution https://codeberg.org/uzu/strudel/pull/1068 +- @diegodorado made their first contribution https://codeberg.org/uzu/strudel/pull/1076 +- @lukad made their first contribution https://codeberg.org/uzu/strudel/pull/1104 +- @giohappy made their first contribution https://codeberg.org/uzu/strudel/pull/1116 + +A huge thanks to all contributors!!! + +## Packages + +- @strudel/codemirror@1.1.0 +- @strudel/core@1.1.0 +- @strudel/csound@1.1.0 +- @strudel/draw@1.1.0 +- @strudel/embed@1.1.0 +- hs2js@0.1.0 +- @strudel/hydra@1.1.0 +- @strudel/midi@1.1.0 +- @strudel/mini@1.1.0 +- @strudel/osc@1.1.0 +- @strudel/repl@1.1.0 +- @strudel/sampler@0.1.0 +- @strudel/serial@1.1.0 +- @strudel/soundfonts@1.1.0 +- superdough@1.1.0 +- @strudel/tidal@0.1.0 +- @strudel/tonal@1.1.0 +- @strudel/transpiler@1.1.0 +- @strudel/web@1.1.0 +- @strudel/webaudio@1.1.0 +- @strudel/xen@1.1.0 + +**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v1.0.0...v1.1.0 From eac6f27ca4e79a0b21c2c48e6cc3bdf3331e1be5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 29 Jun 2025 21:29:11 +0200 Subject: [PATCH 250/538] add 1.2.0 release notes --- .../blog/release-1.2.0-kardinalschnitten.mdx | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 website/src/content/blog/release-1.2.0-kardinalschnitten.mdx diff --git a/website/src/content/blog/release-1.2.0-kardinalschnitten.mdx b/website/src/content/blog/release-1.2.0-kardinalschnitten.mdx new file mode 100644 index 000000000..e58e27aec --- /dev/null +++ b/website/src/content/blog/release-1.2.0-kardinalschnitten.mdx @@ -0,0 +1,187 @@ +--- +title: 'Release Notes v1.2.0' +description: '' +date: '2025-05-01' +tags: ['meta'] +author: froos +--- + +## What's Changed + +## highlights + +- [stepwise functions](https://strudel.cc/learn/stepwise/) ([PR](https://github.com/tidalcycles/strudel/pull/1262)) +- [midimaps](https://strudel.cc/learn/input-output/#midimaps) ([PR](https://github.com/tidalcycles/strudel/pull/1274)) +- [spectrum](https://strudel.cc/learn/visual-feedback/#spectrum) ([PR](https://github.com/tidalcycles/strudel/pull/1213)) +- [mqtt](https://strudel.cc/learn/input-output/#mqtt) ([PR](https://github.com/tidalcycles/strudel/pull/1224)) +- pulse oscillator (todo: https://github.com/tidalcycles/strudel/issues/1336) ([PR](https://github.com/tidalcycles/strudel/pull/1304)) +- theme improvements + +## breaking changes + +- [breaking change] Sample signals from query onset, rather than midpoint by @yaxu in https://github.com/tidalcycles/strudel/pull/1278 +- change behaviour of polymeter, and remove polymeterSteps by @yaxu in https://github.com/tidalcycles/strudel/pull/1302 +- Polish, rename, and document stepwise functions by @yaxu in https://github.com/tidalcycles/strudel/pull/1262 + +### superdough + +- feat: Create Pulse Oscillator with variable PWM by @daslyfe in https://github.com/tidalcycles/strudel/pull/1304 +- add num samples (edited numbers) by @yaxu in https://github.com/tidalcycles/strudel/pull/1309 +- Add num samples from 0 up to 20 by @yaxu in https://github.com/tidalcycles/strudel/pull/1310 +- feat: add max polyphony feature for superdough by @daslyfe in https://github.com/tidalcycles/strudel/pull/1317 + +### docs + +- doc: visual functions + refactor onPaint by @felixroos in https://github.com/tidalcycles/strudel/pull/1125 +- Labeled statements doc by @felixroos in https://github.com/tidalcycles/strudel/pull/1126 +- Correct spelling mistakes by @EdwardBetts in https://github.com/tidalcycles/strudel/pull/1183 +- remove redundant example for cat, update snapshot by @kdiab in https://github.com/tidalcycles/strudel/pull/1189 +- chore: Edit run locally instructions in README.md by @ChinoUkaegbu in https://github.com/tidalcycles/strudel/pull/1206 +- suggested changes to voicings.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/1231 +- Documentation for all/each, and bugfix for each by @yaxu in https://github.com/tidalcycles/strudel/pull/1233 +- Update documentation for param value modification by @gillespi314 in https://github.com/tidalcycles/strudel/pull/1238 +- fix docs for beat function by @daslyfe in https://github.com/tidalcycles/strudel/pull/1248 +- understand voicings page by @felixroos in https://github.com/tidalcycles/strudel/pull/1230 +- add reference package by @felixroos in https://github.com/tidalcycles/strudel/pull/1252 +- Stepwise documentation tweaks, with mridangam samples by @yaxu in https://github.com/tidalcycles/strudel/pull/1275 +- showcase tweaks by @yaxu in https://github.com/tidalcycles/strudel/pull/1291 +- Signpost licenses for source code and samples a bit more, ref #1277 by @yaxu in https://github.com/tidalcycles/strudel/pull/1289 +- Fix misplaced ending sentence by @makmanalp in https://github.com/tidalcycles/strudel/pull/1296 +- Fix typo pattnr by @ReneNyffenegger in https://github.com/tidalcycles/strudel/pull/1316 +- update docs to reflect import sounds tab change by @hpunq in https://github.com/tidalcycles/strudel/pull/1332 + +### ui improvements + +- Udels (MultiFrame Strudel) Revisited by @daslyfe in https://github.com/tidalcycles/strudel/pull/1132 +- Create audio target selector for OSC/Superdirt by @daslyfe in https://github.com/tidalcycles/strudel/pull/1160 +- Add a search bar to the REPL Reference tab by @netux in https://github.com/tidalcycles/strudel/pull/1165 +- Adding search bar (soundtab.jsx) by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/1185 +- add 2 new ui settings by @felixroos in https://github.com/tidalcycles/strudel/pull/1200 +- Theme glowup by @felixroos in https://github.com/tidalcycles/strudel/pull/1268 +- Create Pattern Page Pagination by @daslyfe in https://github.com/tidalcycles/strudel/pull/1287 +- feat: Theme improvements by @daslyfe in https://github.com/tidalcycles/strudel/pull/1295 +- feat: new themes + theme improvements by @daslyfe in https://github.com/tidalcycles/strudel/pull/1326 +- Add new "import-sounds" tab with explanation on folder import by @hpunq in https://github.com/tidalcycles/strudel/pull/1329 +- Add Icon to import sample button by @daslyfe in https://github.com/tidalcycles/strudel/pull/1331 +- better spacing in zen mode by @felixroos in https://github.com/tidalcycles/strudel/pull/1147 +- Screenreader improvements by @yaxu in https://github.com/tidalcycles/strudel/pull/1158 +- colorize console + tweak header by @felixroos in https://github.com/tidalcycles/strudel/pull/1203 +- Menu Panel Improvements! by @daslyfe in https://github.com/tidalcycles/strudel/pull/1193 +- Make panel hover behavior optional by @daslyfe in https://github.com/tidalcycles/strudel/pull/1199 +- REPL: solo and sync configuration by @bthj in https://github.com/tidalcycles/strudel/pull/1214 +- enhancement: make error messages easier to read by @daslyfe in https://github.com/tidalcycles/strudel/pull/1315 + +### mqtt + +- MQTT support by @yaxu in https://github.com/tidalcycles/strudel/pull/1224 +- MQTT - if password isn't provided, prompt for one by @yaxu in https://github.com/tidalcycles/strudel/pull/1249 +- MQTT - support adding hap duration and cps metadata to JSON messages by @yaxu in https://github.com/tidalcycles/strudel/pull/1279 +- make mqtt topic patternable by @yaxu in https://github.com/tidalcycles/strudel/pull/1280 +- Bugfix: update mqtt connections dictionary by @yaxu in https://github.com/tidalcycles/strudel/pull/1281 +- mqtt bugfix - connection check by @yaxu in https://github.com/tidalcycles/strudel/pull/1282 + +### new functions + +- Add scramble and shuffle by @yaxu in https://github.com/tidalcycles/strudel/pull/1167 +- polyJoin by @yaxu in https://github.com/tidalcycles/strudel/pull/1168 +- Add seqPLoop from Tidal by @yaxu in https://github.com/tidalcycles/strudel/pull/1182 +- add filter + filterWhen + within by @felixroos in https://github.com/tidalcycles/strudel/pull/1039 +- Add bite function by @yaxu in https://github.com/tidalcycles/strudel/pull/1187 +- markcss by @felixroos in https://github.com/tidalcycles/strudel/pull/1202 +- "beat" function for "step sequencer" style rhythm notation by @daslyfe in https://github.com/tidalcycles/strudel/pull/1237 +- Add s_zip for 'cat'-ing patterns together step-by-step, bugfix `steps` by @yaxu in https://github.com/tidalcycles/strudel/pull/1208 +- "stretch" function (phase vocoder) by @daslyfe in https://github.com/tidalcycles/strudel/pull/1130 +- add basic spectrum function by @felixroos in https://github.com/tidalcycles/strudel/pull/1213 +- Add onKey function for custom key commands for patterns by @daslyfe in https://github.com/tidalcycles/strudel/pull/1235 +- Add binary and binaryN by @heerman in https://github.com/tidalcycles/strudel/pull/1226 +- midimaps by @felixroos in https://github.com/tidalcycles/strudel/pull/1274 +- small feat: Add alias for segment and ribbon by @daslyfe in https://github.com/tidalcycles/strudel/pull/1314 +- feat: Create scrub function for scrubbing an audio file by @daslyfe in https://github.com/tidalcycles/strudel/pull/1321 +- feat: Improve gain curve by @daslyfe in https://github.com/tidalcycles/strudel/pull/1318 +- Chop chop by @yaxu in https://github.com/tidalcycles/strudel/pull/1078 + +### more + +- Make `all()` post-stack again, and add `each()` for pre-stack by @yaxu in https://github.com/tidalcycles/strudel/pull/1229 +- Add stepBind, and some toplevel aliases for binds and withValue by @yaxu in https://github.com/tidalcycles/strudel/pull/1241 +- Make cps patternable by @eefano in https://github.com/tidalcycles/strudel/pull/1001 +- Allow wchooseCycles probabilities to be patterned by @yaxu in https://github.com/tidalcycles/strudel/pull/1292 +- @strudel/sampler improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/1288 + +### refactor + +- export comment commands by @felixroos in https://github.com/tidalcycles/strudel/pull/113 + 6 +- containerize/seperate out boolean checks for repl types/Repl logic into bespoke components. by @daslyfe in https://github.com/tidalcycles/strudel/pull/1163 +- Improve + simplify neocyclist timing by @daslyfe in https://github.com/tidalcycles/strudel/pull/1164 +- Make phaser control consistent with superdirt by @daslyfe in https://github.com/tidalcycles/strudel/pull/1178 +- Revert "Make phaser control consistent with superdirt" by @daslyfe in https://github.com/tidalcycles/strudel/pull/1179 +- make phaser control match superdirt by @daslyfe in https://github.com/tidalcycles/strudel/pull/1180 +- refactor sampler by @felixroos in https://github.com/tidalcycles/strudel/pull/1101 +- update lockfile + minor versions by @felixroos in https://github.com/tidalcycles/strudel/pull/1198 +- Preserve tactus for 'degrade' and friends, and tidy up 'pick' and friends by @yaxu in https://github.com/tidalcycles/strudel/pull/1205 +- Apply `all` function to individual patterns rather than final stack by @yaxu in https://github.com/tidalcycles/strudel/pull/1209 +- Revert "Fix sometimes" by @yaxu in https://github.com/tidalcycles/strudel/pull/1267 +- patchday by @felixroos in https://github.com/tidalcycles/strudel/pull/1264 +- Rename repeat back to extend by @yaxu in https://github.com/tidalcycles/strudel/pull/1285 +- Send delta in OSC message in seconds, to match tidal/superdirt by @yaxu in https://github.com/tidalcycles/strudel/pull/1323 + +### fixes + +- Fix clock worker dependency path in module builds by @matthewkaney in https://github.com/tidalcycles/strudel/pull/1129 +- Fix bug in Fraction.lcm by @yaxu in https://github.com/tidalcycles/strudel/pull/1133 +- Fix tactus marking in mininotation by @yaxu in https://github.com/tidalcycles/strudel/pull/1144 +- Fix loopAt tactus by @yaxu in https://github.com/tidalcycles/strudel/pull/1145 +- Fix OSC clock jitter by @daslyfe in https://github.com/tidalcycles/strudel/pull/1157 +- [CORS HOTFIX] by @daslyfe in https://github.com/tidalcycles/strudel/pull/1162 +- Fixes fit so it works after a chop or slice by @yaxu in https://github.com/tidalcycles/strudel/pull/1171 +- fix sample speed when using splice and fit with superdirt by @daslyfe in https://github.com/tidalcycles/strudel/pull/1172 +- handle midin device not found error by @felixroos in https://github.com/tidalcycles/strudel/pull/1146 +- Fix serial timing by @yaxu in https://github.com/tidalcycles/strudel/pull/1188 +- Fix regression for d1, p1, p(n) by @yaxu in https://github.com/tidalcycles/strudel/pull/1227 +- Fix sometimes by @yaxu in https://github.com/tidalcycles/strudel/pull/1243 +- Fix sf2 timing by @felixroos in https://github.com/tidalcycles/strudel/pull/1272 +- Fix "squeezejoin" and functions using it, including "bite" by @yaxu in https://github.com/tidalcycles/strudel/pull/1286 +- Fixes inverted triangle wave by renaming it to "itri", making non-inverted "tri" by @yaxu in https://github.com/tidalcycles/strudel/pull/1283 +- Hotfix: prevent undefined pattern code from crashing strudel on load by @daslyfe in https://github.com/tidalcycles/strudel/pull/1297 +- Fix test error #1297 by @nkymut in https://github.com/tidalcycles/strudel/pull/1298 +- bugfix zoom stepcount by @yaxu in https://github.com/tidalcycles/strudel/pull/1301 +- bugfix: Allow single param to be used in the as function by @daslyfe in https://github.com/tidalcycles/strudel/pull/1312 +- fix: replace empty spaces in registered sound keys by @daslyfe in https://github.com/tidalcycles/strudel/pull/1319 +- FIX: Multichannel Audio by @daslyfe in https://github.com/tidalcycles/strudel/pull/1322 +- fix: udels header by @daslyfe in https://github.com/tidalcycles/strudel/pull/1325 +- fix: disable astro toolbar by default by @daslyfe in https://github.com/tidalcycles/strudel/pull/1324 +- FIX: sound import order by @daslyfe in https://github.com/tidalcycles/strudel/pull/1333` + +## New Contributors + +- @EdwardBetts made their first contribution in https://github.com/tidalcycles/strudel/pull/1183 +- @netux made their first contribution in https://github.com/tidalcycles/strudel/pull/1165 +- @kdiab made their first contribution in https://github.com/tidalcycles/strudel/pull/1189 + +**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v1.1.0...v1.1.1 + +## packages + +- @strudel/codemirror@1.2.0 +- @strudel/core@1.2.0 +- @strudel/csound@1.2.0 +- @strudel/draw@1.2.0 +- @strudel/gamepad@1.2.0 +- @strudel/hydra@1.2.0 +- @strudel/midi@1.2.0 +- @strudel/mini@1.2.0 +- @strudel/motion@1.2.0 +- @strudel/mqtt@1.2.0 +- @strudel/osc@1.2.0 +- @strudel/reference@1.2.0 +- @strudel/repl@1.2.0 +- @strudel/sampler@0.2.0 +- @strudel/serial@1.2.0 +- @strudel/soundfonts@1.2.0 +- superdough@1.2.0 +- @strudel/tonal@1.2.0 +- @strudel/transpiler@1.2.0 +- @strudel/web@1.2.0 +- @strudel/webaudio@1.2.0 +- @strudel/xen@1.2.0 From 5a72ea94b15e1905e82dd210ac18f63ebae9b5cc Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 30 Jun 2025 05:15:30 +0200 Subject: [PATCH 251/538] fix: format --- website/src/content/blog/release-1.2.0-kardinalschnitten.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/src/content/blog/release-1.2.0-kardinalschnitten.mdx b/website/src/content/blog/release-1.2.0-kardinalschnitten.mdx index e58e27aec..e215d4532 100644 --- a/website/src/content/blog/release-1.2.0-kardinalschnitten.mdx +++ b/website/src/content/blog/release-1.2.0-kardinalschnitten.mdx @@ -110,8 +110,7 @@ author: froos ### refactor -- export comment commands by @felixroos in https://github.com/tidalcycles/strudel/pull/113 - 6 +- expose comment commands by @felixroos in https://github.com/tidalcycles/strudel/pull/1136 - containerize/seperate out boolean checks for repl types/Repl logic into bespoke components. by @daslyfe in https://github.com/tidalcycles/strudel/pull/1163 - Improve + simplify neocyclist timing by @daslyfe in https://github.com/tidalcycles/strudel/pull/1164 - Make phaser control consistent with superdirt by @daslyfe in https://github.com/tidalcycles/strudel/pull/1178 From 2e3356978610400136a983740d013cb7305809c4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 30 Jun 2025 05:48:08 +0200 Subject: [PATCH 252/538] add setDefault + resetDefaults to superdough --- packages/superdough/superdough.mjs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 14f46711b..0db5e2b99 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -150,6 +150,17 @@ const defaultDefaultValues = { fft: 8, }; +const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues }); + +export function setDefault(control, value) { + const main = getControlName(control); + defaultDefaultValues[main] = value; +} + +export function resetDefaults() { + defaultDefaultValues = { ...defaultDefaultDefaultValues }; +} + let defaultControls = new Map(Object.entries(defaultDefaultValues)); export function setDefaultValue(key, value) { From e3680b96deab28811e2c8c63be09839242493002 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 30 Jun 2025 10:18:54 +0200 Subject: [PATCH 253/538] refactor: remove first param of all onTrigger calls --- packages/core/pattern.mjs | 2 +- packages/core/repl.mjs | 2 +- packages/core/speak.mjs | 2 +- packages/csound/index.mjs | 6 +++--- packages/desktopbridge/midibridge.mjs | 2 +- packages/desktopbridge/oscbridge.mjs | 2 +- packages/midi/midi.mjs | 2 +- packages/osc/osc.mjs | 2 +- packages/soundfonts/sfumato.mjs | 2 +- packages/superdough/dspworklet.mjs | 2 +- packages/superdough/superdough.mjs | 2 +- packages/webaudio/webaudio.mjs | 8 ++------ test/testtunes.mjs | 22 ---------------------- 13 files changed, 15 insertions(+), 41 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index cbaf8a8a2..d9bb94075 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3263,7 +3263,7 @@ export const slice = register( * s("bd!8").onTriggerTime((hap) => {console.info(hap)}) */ Pattern.prototype.onTriggerTime = function (func) { - return this.onTrigger((t_deprecate, hap, currentTime, cps = 1, targetTime) => { + return this.onTrigger((hap, currentTime, _cps, targetTime) => { const diff = targetTime - currentTime; window.setTimeout(() => { func(hap); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index dcc5fcba4..117b1d046 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -252,7 +252,7 @@ export const getTrigger = } if (hap.context.onTrigger) { // call signature of output / onTrigger is different... - await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t); + await hap.context.onTrigger(hap, getTime(), cps, t); } } catch (err) { logger(`[cyclist] error: ${err.message}`, 'error'); diff --git a/packages/core/speak.mjs b/packages/core/speak.mjs index 7e548a73b..e0f60184e 100644 --- a/packages/core/speak.mjs +++ b/packages/core/speak.mjs @@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) { } export const speak = register('speak', function (lang, voice, pat) { - return pat.onTrigger((_, hap) => { + return pat.onTrigger((hap) => { triggerSpeech(hap.value, lang, voice); }); }); diff --git a/packages/csound/index.mjs b/packages/csound/index.mjs index 1eeaf6fa5..e44765227 100644 --- a/packages/csound/index.mjs +++ b/packages/csound/index.mjs @@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => { instrument = instrument || 'triangle'; init(); // not async to support csound inside other patterns + to be able to call pattern methods after it // TODO: find a alternative way to wait for csound to load (to wait with first time playback) - return pat.onTrigger((time_deprecate, hap, currentTime, _cps, targetTime) => { + return pat.onTrigger((hap, currentTime, _cps, targetTime) => { if (!_csound) { logger('[csound] not loaded yet', 'warning'); return; @@ -142,7 +142,7 @@ export const csoundm = register('csoundm', (instrument, pat) => { p1 = `"${instrument}"`; } init(); // not async to support csound inside other patterns + to be able to call pattern methods after it - return pat.onTrigger((tidal_time, hap) => { + return pat.onTrigger((hap, currentTime, _cps, targetTime) => { if (!_csound) { logger('[csound] not loaded yet', 'warning'); return; @@ -151,7 +151,7 @@ export const csoundm = register('csoundm', (instrument, pat) => { throw new Error('csound only support objects as hap values'); } // Time in seconds counting from now. - const p2 = tidal_time - getAudioContext().currentTime; + const p2 = targetTime - currentTime; const p3 = hap.duration.valueOf() + 0; const frequency = getFrequency(hap); let { gain = 1, velocity = 0.9 } = hap.value; diff --git a/packages/desktopbridge/midibridge.mjs b/packages/desktopbridge/midibridge.mjs index 471c826ea..2436d8ad7 100644 --- a/packages/desktopbridge/midibridge.mjs +++ b/packages/desktopbridge/midibridge.mjs @@ -6,7 +6,7 @@ const OFF_MESSAGE = 0x80; const CC_MESSAGE = 0xb0; Pattern.prototype.midi = function (output) { - return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { + return this.onTrigger((hap, currentTime, cps, targetTime) => { let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value; //magic number to get audio engine to line up, can probably be calculated somehow const latencyMs = 34; diff --git a/packages/desktopbridge/oscbridge.mjs b/packages/desktopbridge/oscbridge.mjs index 9bead6d19..2568c78e5 100644 --- a/packages/desktopbridge/oscbridge.mjs +++ b/packages/desktopbridge/oscbridge.mjs @@ -4,7 +4,7 @@ import { Invoke } from './utils.mjs'; const collator = new ClockCollator({}); -export async function oscTriggerTauri(t_deprecate, hap, currentTime, cps = 1, targetTime) { +export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) { const controls = parseControlsFromHap(hap, cps); const params = []; const timestamp = collator.calculateTimestamp(currentTime, targetTime); diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index af2dd3a62..d0e092938 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -333,7 +333,7 @@ Pattern.prototype.midi = function (midiport, options = {}) { logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`), }); - return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { + return this.onTrigger((hap, currentTime, cps, targetTime) => { if (!WebMidi.enabled) { logger('Midi not enabled'); return; diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index ac70b9e73..8262e5569 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -60,7 +60,7 @@ export function parseControlsFromHap(hap, cps) { const collator = new ClockCollator({}); -export async function oscTrigger(t_deprecate, hap, currentTime, cps = 1, targetTime) { +export async function oscTrigger(hap, currentTime, cps = 1, targetTime) { const osc = await connect(); const controls = parseControlsFromHap(hap, cps); const keyvals = Object.entries(controls).flat(); diff --git a/packages/soundfonts/sfumato.mjs b/packages/soundfonts/sfumato.mjs index 6d60f80dd..4d5e7ef45 100644 --- a/packages/soundfonts/sfumato.mjs +++ b/packages/soundfonts/sfumato.mjs @@ -3,7 +3,7 @@ import { getAudioContext, registerSound } from '@strudel/webaudio'; import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato'; Pattern.prototype.soundfont = function (sf, n = 0) { - return this.onTrigger((time_deprecate, h, ct, cps, targetTime) => { + return this.onTrigger((h, ct, cps, targetTime) => { const ctx = getAudioContext(); const note = getPlayableNoteValue(h); const preset = sf.presets[n % sf.presets.length]; diff --git a/packages/superdough/dspworklet.mjs b/packages/superdough/dspworklet.mjs index ed5c1e7e0..5b02ad298 100644 --- a/packages/superdough/dspworklet.mjs +++ b/packages/superdough/dspworklet.mjs @@ -74,6 +74,6 @@ export const dough = async (code) => { worklet.node.connect(ac.destination); }; -export function doughTrigger(time_deprecate, hap, currentTime, cps, targetTime) { +export function doughTrigger(hap, currentTime, cps, targetTime) { window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps }); } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 14f46711b..003f58bbe 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -451,8 +451,8 @@ function mapChannelNumbers(channels) { } export const superdough = async (value, t, hapDuration, cps = 0.5) => { + // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); - t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; let { stretch } = value; if (stretch != null) { //account for phase vocoder latency diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 91e28defd..01ad9fb8b 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -15,14 +15,10 @@ const hap2value = (hap) => { return hap.value; }; -export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps); // uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 +// TODO: refactor output callbacks to eliminate deadline export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => { - return superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration, cps); -}; - -Pattern.prototype.webaudio = function () { - return this.onTrigger(webaudioOutputTrigger); + return superdough(hap2value(hap), t, hapDuration, cps); }; export function webaudioRepl(options = {}) { diff --git a/test/testtunes.mjs b/test/testtunes.mjs index fc887610e..3690d315b 100644 --- a/test/testtunes.mjs +++ b/test/testtunes.mjs @@ -359,28 +359,6 @@ stack( "[~ [0 ~]] 0 [~ [4 ~]] 4".sub(7).restart(scales).scale(scales).early(.25) ).note().piano().slow(2)`; -/* -export const customTrigger = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos -stack( - freq("55 [110,165] 110 [220,275]".mul("<1 <3/4 2/3>>").struct("x(3,8)").layer(x=>x.mul("1.006,.995"))), - freq("440(5,8)".clip(.18).mul("<1 3/4 2 2/3>")).gain(perlin.range(.2,.8)) -).s("/2") - .onTrigger((t,hap,ct)=>{ - const ac = Tone.getContext().rawContext; - t = ac.currentTime + t - ct; - const { freq, s, gain = 1 } = hap.value; - const master = ac.createGain(); - master.gain.value = 0.1 * gain; - master.connect(ac.destination); - const o = ac.createOscillator(); - o.type = s || 'triangle'; - o.frequency.value = Number(freq); - o.connect(master); - o.start(t); - o.stop(t + hap.duration); -}).stack(s("bd(3,8),hh*4,~ sd").webdirt())`; */ - export const swimmingWithSoundfonts = `// Koji Kondo - Swimming (Super Mario World) stack( n( From c063fb6b1ca7f2cd32fe626b9545702e8b65ec24 Mon Sep 17 00:00:00 2001 From: tj-mueller Date: Mon, 30 Jun 2025 11:51:39 +0200 Subject: [PATCH 254/538] correct rd to cr --- website/src/pages/de/workshop/first-sounds.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/de/workshop/first-sounds.mdx b/website/src/pages/de/workshop/first-sounds.mdx index 3695843b0..64a58db02 100644 --- a/website/src/pages/de/workshop/first-sounds.mdx +++ b/website/src/pages/de/workshop/first-sounds.mdx @@ -95,7 +95,7 @@ Diese Kombinationen von Buchstaben stehen für verschiedene Teile eines Schlagze - `mt` = **m**iddle tom - `ht` = **h**igh tom - `rd` = **r**i**d**e cymbal -- `rd` = **cr**ash cymbal +- `cr` = **cr**ash cymbal Probier verschiedene Sounds aus! From 504fe9ed4029438ba0214a5b730ab869a4eaaa60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aurel=20B=C3=ADl=C3=BD?= Date: Mon, 30 Jun 2025 12:17:24 +0200 Subject: [PATCH 255/538] document setcps in cycles reference --- website/src/pages/understand/cycles.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/src/pages/understand/cycles.mdx b/website/src/pages/understand/cycles.mdx index 1c0a778a6..bcf987c4d 100644 --- a/website/src/pages/understand/cycles.mdx +++ b/website/src/pages/understand/cycles.mdx @@ -67,6 +67,8 @@ Or using 2 beats per cycle: s("bd sd, hh*4")`} /> +You can use the `setcps` method to set the global tempo in cycles per second. `setcpm(x)` is the same as `setcps(x / 60)`. + To set a specific bpm, use `setcpm(bpm/bpc)` From 34771f03d424b021bdc683a8227dfffdab050222 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 4 Jul 2025 22:17:23 +0200 Subject: [PATCH 256/538] some clarification comments --- packages/core/cyclist.mjs | 1 + packages/core/repl.mjs | 1 + undocumented.json | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index f28dc604c..fec819548 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -67,6 +67,7 @@ export class Cyclist { // the following line is dumb and only here for backwards compatibility // see https://codeberg.org/uzu/strudel/pulls/1004 const deadline = targetTime - phase; + // this onTrigger has another signature onTrigger?.(hap, deadline, duration, this.cps, targetTime); if (hap.value.cps !== undefined && this.cps != hap.value.cps) { this.cps = hap.value.cps; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 117b1d046..aeaa6418e 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -245,6 +245,7 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 try { if (!hap.context.onTrigger || !hap.context.dominantTrigger) { diff --git a/undocumented.json b/undocumented.json index 8a38f205f..3f5a3be30 100644 --- a/undocumented.json +++ b/undocumented.json @@ -647,7 +647,6 @@ "evaluate" ], "/packages/webaudio/webaudio.mjs": [ - "webaudioOutputTrigger", "webaudioOutput", "webaudioRepl" ], From 168269a839f54ca95dab38097f9ecbb87b90259c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 4 Jul 2025 22:29:18 +0200 Subject: [PATCH 257/538] disable fm for supersaw --- packages/superdough/synth.mjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 13cfa13a5..88e14e5ab 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -121,7 +121,10 @@ export function registerSynthSounds() { const gainAdjustment = 1 / Math.sqrt(voices); getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); - const fm = applyFM(o.parameters.get('frequency'), value, begin); + // const fm = applyFM(o.parameters.get('frequency'), value, begin); + // https://codeberg.org/uzu/strudel/issues/1428 + // if you think about re-enabling this, please test with fm > 1 first + // it's like 10x gain, so it's really dangerous let envGain = gainNode(1); envGain = o.connect(envGain); @@ -133,7 +136,7 @@ export function registerSynthSounds() { destroyAudioWorkletNode(o); envGain.disconnect(); onended(); - fm?.stop(); + // fm?.stop(); vibratoOscillator?.stop(); }, begin, From cbb4207bab08eb1a791d8e77f9508ddd37431324 Mon Sep 17 00:00:00 2001 From: dudymas Date: Fri, 4 Jul 2025 18:50:21 -0400 Subject: [PATCH 258/538] 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 259/538] 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 4cc2de9b7efd37d941625a9805cb7117c2b4a2ef Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 5 Jul 2025 00:39:23 -0400 Subject: [PATCH 260/538] wip --- packages/core/cyclist.mjs | 62 +++++++++++------------------- packages/core/repl.mjs | 4 +- packages/superdough/superdough.mjs | 43 ++++++++++++++------- packages/superdough/util.mjs | 4 ++ 4 files changed, 59 insertions(+), 54 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 072e97abb..f28dc604c 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -22,44 +22,38 @@ export class Cyclist { this.started = false; this.beforeStart = beforeStart; this.cps = 0.5; - this.time_at_last_tick_message = 0; - this.cycle = 0; + this.num_ticks_since_cps_change = 0; + this.lastTick = 0; // absolute time when last tick (clock callback) happened + this.lastBegin = 0; // query begin of last tick + this.lastEnd = 0; // query end of last tick this.getTime = getTime; // get absolute time this.num_cycles_at_cps_change = 0; this.seconds_at_cps_change; // clock phase when cps was changed - this.num_ticks_since_cps_change = 0; this.onToggle = onToggle; this.latency = latency; // fixed trigger time offset - - this.interval = interval; - this.clock = createClock( getTime, // called slightly before each cycle - (phase, duration, _, time) => { - if (this.started === false) { - return; + (phase, duration, _, t) => { + if (this.num_ticks_since_cps_change === 0) { + this.num_cycles_at_cps_change = this.lastEnd; + this.seconds_at_cps_change = phase; } - const num_seconds_since_cps_change = this.num_ticks_since_cps_change * duration; - const tickdeadline = phase - time; - const lastTick = time + tickdeadline; - const num_cycles_since_cps_change = num_seconds_since_cps_change * this.cps; - const begin = this.num_cycles_at_cps_change + num_cycles_since_cps_change; - const secondsSinceLastTick = time - lastTick - duration; - const eventLength = duration * this.cps; - const end = begin + eventLength; - this.cycle = begin + secondsSinceLastTick * this.cps; + this.num_ticks_since_cps_change++; + const seconds_since_cps_change = this.num_ticks_since_cps_change * duration; + const num_cycles_since_cps_change = seconds_since_cps_change * this.cps; - //account for latency and tick duration when using cycle calculations for audio downstream - const cycle_gap = (this.latency - duration) * this.cps; + try { + const begin = this.lastEnd; + this.lastBegin = begin; + const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; + this.lastEnd = end; + this.lastTick = phase; - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); - haps.forEach((hap) => { - if (hap.hasOnset()) { - let targetTime = (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps; - targetTime = targetTime + this.latency + tickdeadline + time - num_seconds_since_cps_change; - const duration = hap.duration / this.cps; - onTrigger?.(hap, tickdeadline, duration, this.cps, targetTime, this.cycle - cycle_gap); + if (phase < t) { + // avoid querying haps that are in the past anyway + console.log(`skip query: too late`); + return; } // query the pattern for events @@ -96,25 +90,17 @@ export class Cyclist { if (!this.started) { return 0; } - const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps; - return this.cycle + gap; + const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; + return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; } - - setCycle = (cycle) => { - this.num_ticks_since_cps_change = 0; - this.num_cycles_at_cps_change = cycle; - }; setStarted(v) { this.started = v; - - this.setCycle(0); this.onToggle?.(v); } async start() { await this.beforeStart?.(); this.num_ticks_since_cps_change = 0; this.num_cycles_at_cps_change = 0; - if (!this.pattern) { throw new Error('Scheduler: no pattern set! call .setPattern first.'); } @@ -143,8 +129,6 @@ export class Cyclist { if (this.cps === cps) { return; } - const num_seconds_since_cps_change = this.num_ticks_since_cps_change * this.interval; - this.num_cycles_at_cps_change = this.num_cycles_at_cps_change + num_seconds_since_cps_change * cps; this.cps = cps; this.num_ticks_since_cps_change = 0; } diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index d5cab857d..dcc5fcba4 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -248,11 +248,11 @@ export const getTrigger = // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 try { if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t, cycle); + await defaultOutput(hap, deadline, duration, cps, t); } if (hap.context.onTrigger) { // call signature of output / onTrigger is different... - await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t, cycle); + await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t); } } catch (err) { logger(`[cyclist] error: ${err.message}`, 'error'); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c59a5c0d2..e4296c6b7 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -7,7 +7,7 @@ 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 } from './util.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 { map } from 'nanostores'; @@ -482,6 +482,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { amdepth = 1, amskew = 0.5, amphase = 0, + amshape = 1, s = getDefaultValue('s'), bank, source, @@ -707,19 +708,35 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); - am !== undefined && - chain.push( - getWorklet(ac, 'am-processor', { - speed: am, - depth: amdepth, - skew: amskew, - phaseoffset: amphase, - // shape: amshape, + // am !== undefined && + // chain.push( + // getWorklet(ac, 'am-processor', { + // speed: am, + // depth: amdepth, + // skew: amskew, + // phaseoffset: amphase, + // // shape: amshape, - cps, - cycle, - }), - ); + // cps, + // cycle, + // }), + // ); + + if (am !== undefined) { + const amGain = new GainNode(ac, { gain: 1 }); + const frequency = cycleToSeconds(am, cps) + const phaseoffset = cycleToSeconds(amphase, cps) + const lfo = getLfo(ac, t, t + hapDuration, { + skew: amskew, + frequency: am * cps, + depth: amdepth, + // dcoffset: 0, + shape: amshape, + phaseoffset + }) + lfo.connect(amGain.gain) + chain.push(amGain) + } compressorThreshold !== undefined && chain.push( diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index ea63a16f0..f4d59024e 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -72,3 +72,7 @@ export const getSoundIndex = (n, numSounds) => { export function cycleToSeconds(cycle, cps) { return cycle / cps; } + +export function secondsToCycle(t, cps) { + return t * cps; +} From 05bcd5e32fd706969aa9c9c343ed7bf43cc528e3 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 5 Jul 2025 09:01:48 +0100 Subject: [PATCH 261/538] Fix randrun and deps including shuffle. Fixes #1441 --- packages/core/signal.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index c933e7fe2..6bd6fde67 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -264,7 +264,7 @@ export const randrun = (n) => { const rands = timeToRands(t.floor().add(0.5), n); const nums = rands .map((n, i) => [n, i]) - .sort((a, b) => a[0] > b[0] - a[0] < b[0]) + .sort((a, b) => (a[0] > b[0]) - (a[0] < b[0])) .map((x) => x[1]); const i = t.cyclePos().mul(n).floor() % n; return nums[i]; From 82b14b529201f72b35e9603da43a62de22bb19ad Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 5 Jul 2025 11:13:40 +0200 Subject: [PATCH 262/538] fix: lint --- packages/superdough/superdough.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 0db5e2b99..8ff64ca8c 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -126,7 +126,7 @@ export const getAudioDevices = async () => { return devicesMap; }; -const defaultDefaultValues = { +let defaultDefaultValues = { s: 'triangle', gain: 0.8, postgain: 1, @@ -153,8 +153,8 @@ const defaultDefaultValues = { const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues }); export function setDefault(control, value) { - const main = getControlName(control); - defaultDefaultValues[main] = value; + // const main = getControlName(control); // we cant do this because superdough is independent of strudel/core + defaultDefaultValues[control] = value; } export function resetDefaults() { From a901e2e3875288626ab2136b85ab969ec60847e6 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 5 Jul 2025 10:28:20 +0100 Subject: [PATCH 263/538] snapshot --- test/__snapshots__/examples.test.mjs.snap | 48 +++++++++++------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index cc45848bf..36c7ca58b 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -8813,46 +8813,46 @@ exports[`runs examples > example "shrink" example index 3 1`] = ` exports[`runs examples > example "shuffle" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c s:piano ]", + "[ 0/1 → 1/4 | note:e s:piano ]", "[ 1/4 → 1/2 | note:d s:piano ]", - "[ 1/2 → 3/4 | note:e s:piano ]", - "[ 3/4 → 1/1 | note:f s:piano ]", - "[ 1/1 → 5/4 | note:c s:piano ]", - "[ 5/4 → 3/2 | note:d s:piano ]", - "[ 3/2 → 7/4 | note:e s:piano ]", - "[ 7/4 → 2/1 | note:f s:piano ]", - "[ 2/1 → 9/4 | note:c s:piano ]", - "[ 9/4 → 5/2 | note:d s:piano ]", + "[ 1/2 → 3/4 | note:f s:piano ]", + "[ 3/4 → 1/1 | note:c s:piano ]", + "[ 1/1 → 5/4 | note:e s:piano ]", + "[ 5/4 → 3/2 | note:c s:piano ]", + "[ 3/2 → 7/4 | note:f s:piano ]", + "[ 7/4 → 2/1 | note:d s:piano ]", + "[ 2/1 → 9/4 | note:d s:piano ]", + "[ 9/4 → 5/2 | note:c s:piano ]", "[ 5/2 → 11/4 | note:e s:piano ]", "[ 11/4 → 3/1 | note:f s:piano ]", "[ 3/1 → 13/4 | note:c s:piano ]", - "[ 13/4 → 7/2 | note:d s:piano ]", - "[ 7/2 → 15/4 | note:e s:piano ]", - "[ 15/4 → 4/1 | note:f s:piano ]", + "[ 13/4 → 7/2 | note:e s:piano ]", + "[ 7/2 → 15/4 | note:f s:piano ]", + "[ 15/4 → 4/1 | note:d s:piano ]", ] `; exports[`runs examples > example "shuffle" example index 1 1`] = ` [ - "[ 0/1 → 1/8 | note:c s:piano ]", + "[ 0/1 → 1/8 | note:e s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]", - "[ 1/4 → 3/8 | note:e s:piano ]", - "[ 3/8 → 1/2 | note:f s:piano ]", + "[ 1/4 → 3/8 | note:f s:piano ]", + "[ 3/8 → 1/2 | note:c s:piano ]", "[ 1/2 → 1/1 | note:g s:piano ]", - "[ 1/1 → 9/8 | note:c s:piano ]", - "[ 9/8 → 5/4 | note:d s:piano ]", - "[ 5/4 → 11/8 | note:e s:piano ]", - "[ 11/8 → 3/2 | note:f s:piano ]", + "[ 1/1 → 9/8 | note:e s:piano ]", + "[ 9/8 → 5/4 | note:c s:piano ]", + "[ 5/4 → 11/8 | note:f s:piano ]", + "[ 11/8 → 3/2 | note:d s:piano ]", "[ 3/2 → 2/1 | note:g s:piano ]", - "[ 2/1 → 17/8 | note:c s:piano ]", - "[ 17/8 → 9/4 | note:d s:piano ]", + "[ 2/1 → 17/8 | note:d s:piano ]", + "[ 17/8 → 9/4 | note:c s:piano ]", "[ 9/4 → 19/8 | note:e s:piano ]", "[ 19/8 → 5/2 | note:f s:piano ]", "[ 5/2 → 3/1 | note:g s:piano ]", "[ 3/1 → 25/8 | note:c s:piano ]", - "[ 25/8 → 13/4 | note:d s:piano ]", - "[ 13/4 → 27/8 | note:e s:piano ]", - "[ 27/8 → 7/2 | note:f s:piano ]", + "[ 25/8 → 13/4 | note:e s:piano ]", + "[ 13/4 → 27/8 | note:f s:piano ]", + "[ 27/8 → 7/2 | note:d s:piano ]", "[ 7/2 → 4/1 | note:g s:piano ]", ] `; From 664d76cf1b1756f1cd01155c3120642793297f62 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 5 Jul 2025 11:32:16 +0200 Subject: [PATCH 264/538] resetDefaults when editor is reset --- website/src/repl/useReplContext.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 12621c50a..ac30fe342 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -13,6 +13,7 @@ import { resetGlobalEffects, resetLoadedSounds, initAudioOnFirstClick, + resetDefaults, } from '@strudel/webaudio'; import { setVersionDefaultsFrom } from './util.mjs'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; @@ -181,6 +182,7 @@ export function useReplContext() { const resetEditor = async () => { (await getModule('@strudel/tonal'))?.resetVoicings(); + resetDefaults(); resetGlobalEffects(); clearCanvas(); clearHydra(); From 37fde8f3e807a64289701ed67dd14d9d30713cc7 Mon Sep 17 00:00:00 2001 From: dudymas Date: Sat, 5 Jul 2025 13:43:09 -0400 Subject: [PATCH 265/538] 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 266/538] 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 d9e44dcda61d724d26585e6a537eb54b636b51ce Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 6 Jul 2025 00:59:39 -0400 Subject: [PATCH 267/538] sync without cyclist changes --- packages/core/controls.mjs | 16 +++- packages/core/cyclist copy.mjs | 140 +++++++++++++++++++++++++++++ packages/core/cyclist.mjs | 114 ++++++++++------------- packages/core/neocyclist.mjs | 2 +- packages/core/repl.mjs | 6 +- packages/superdough/superdough.mjs | 42 +++++++-- packages/superdough/worklets.mjs | 15 +++- packages/webaudio/webaudio.mjs | 8 +- 8 files changed, 261 insertions(+), 82 deletions(-) create mode 100644 packages/core/cyclist copy.mjs diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 66dee4f1c..12bcc3565 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -448,13 +448,23 @@ export const { coarse } = registerControl('coarse'); * modulate the amplitude of a sound with a continuous waveform * * @name am - * @synonyms tremelo - * @param {number | Pattern} speed modulation speed in cycles + * @param {number | Pattern} speed modulation speed in HZ * @example * s("triangle").am("2").amshape("").amdepth(.5) * */ -export const { am, tremolo } = registerControl(['am', 'amdepth', 'amskew', 'amphase'], 'tremolo'); +export const { am, } = registerControl(['am', 'amdepth', 'amskew', 'amphase'],); + +/** + * modulate the amplitude of a sound with a continuous waveform + * + * @name amsync + * @param {number | Pattern} cycles modulation speed in cycles + * @example + * s("triangle").am("2").amshape("").amdepth(.5) + * + */ +export const { amsync } = registerControl(['amsync', 'amdepth', 'amskew', 'amphase']); /** * depth of amplitude modulation diff --git a/packages/core/cyclist copy.mjs b/packages/core/cyclist copy.mjs new file mode 100644 index 000000000..fec819548 --- /dev/null +++ b/packages/core/cyclist copy.mjs @@ -0,0 +1,140 @@ +/* +cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see +Copyright (C) 2022 Strudel contributors - see +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 createClock from './zyklus.mjs'; +import { logger } from './logger.mjs'; + +export class Cyclist { + constructor({ + interval, + onTrigger, + onToggle, + onError, + getTime, + latency = 0.1, + setInterval, + clearInterval, + beforeStart, + }) { + this.started = false; + this.beforeStart = beforeStart; + this.cps = 0.5; + this.num_ticks_since_cps_change = 0; + this.lastTick = 0; // absolute time when last tick (clock callback) happened + this.lastBegin = 0; // query begin of last tick + this.lastEnd = 0; // query end of last tick + this.getTime = getTime; // get absolute time + this.num_cycles_at_cps_change = 0; + this.seconds_at_cps_change; // clock phase when cps was changed + this.onToggle = onToggle; + this.latency = latency; // fixed trigger time offset + this.clock = createClock( + getTime, + // called slightly before each cycle + (phase, duration, _, t) => { + if (this.num_ticks_since_cps_change === 0) { + this.num_cycles_at_cps_change = this.lastEnd; + this.seconds_at_cps_change = phase; + } + this.num_ticks_since_cps_change++; + const seconds_since_cps_change = this.num_ticks_since_cps_change * duration; + const num_cycles_since_cps_change = seconds_since_cps_change * this.cps; + + try { + const begin = this.lastEnd; + this.lastBegin = begin; + const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; + this.lastEnd = end; + this.lastTick = phase; + + if (phase < t) { + // avoid querying haps that are in the past anyway + console.log(`skip query: too late`); + return; + } + + // query the pattern for events + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + + haps.forEach((hap) => { + if (hap.hasOnset()) { + const targetTime = + (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency; + const duration = hap.duration / this.cps; + // the following line is dumb and only here for backwards compatibility + // see https://codeberg.org/uzu/strudel/pulls/1004 + const deadline = targetTime - phase; + // this onTrigger has another signature + onTrigger?.(hap, deadline, duration, this.cps, targetTime); + if (hap.value.cps !== undefined && this.cps != hap.value.cps) { + this.cps = hap.value.cps; + this.num_ticks_since_cps_change = 0; + } + } + }); + } catch (e) { + logger(`[cyclist] error: ${e.message}`); + onError?.(e); + } + }, + interval, // duration of each cycle + 0.1, + 0.1, + setInterval, + clearInterval, + ); + } + now() { + if (!this.started) { + return 0; + } + const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; + return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; + } + setStarted(v) { + this.started = v; + this.onToggle?.(v); + } + async start() { + await this.beforeStart?.(); + this.num_ticks_since_cps_change = 0; + this.num_cycles_at_cps_change = 0; + if (!this.pattern) { + throw new Error('Scheduler: no pattern set! call .setPattern first.'); + } + logger('[cyclist] start'); + this.clock.start(); + this.setStarted(true); + } + pause() { + logger('[cyclist] pause'); + this.clock.pause(); + this.setStarted(false); + } + stop() { + logger('[cyclist] stop'); + this.clock.stop(); + this.lastEnd = 0; + this.setStarted(false); + } + async setPattern(pat, autostart = false) { + this.pattern = pat; + if (autostart && !this.started) { + await this.start(); + } + } + setCps(cps = 0.5) { + if (this.cps === cps) { + return; + } + this.cps = cps; + this.num_ticks_since_cps_change = 0; + } + log(begin, end, haps) { + const onsets = haps.filter((h) => h.hasOnset()); + console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); + } +} diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index fec819548..43e1f508e 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -1,6 +1,6 @@ /* -cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see -Copyright (C) 2022 Strudel contributors - see +cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see +Copyright (C) 2022 Strudel contributors - see 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 . */ @@ -8,77 +8,50 @@ import createClock from './zyklus.mjs'; import { logger } from './logger.mjs'; export class Cyclist { - constructor({ - interval, - onTrigger, - onToggle, - onError, - getTime, - latency = 0.1, - setInterval, - clearInterval, - beforeStart, - }) { + constructor({ interval = 0.05, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) { this.started = false; - this.beforeStart = beforeStart; this.cps = 0.5; - this.num_ticks_since_cps_change = 0; - this.lastTick = 0; // absolute time when last tick (clock callback) happened - this.lastBegin = 0; // query begin of last tick - this.lastEnd = 0; // query end of last tick + this.time_at_last_tick_message = 0; + this.cycle = 0; this.getTime = getTime; // get absolute time this.num_cycles_at_cps_change = 0; this.seconds_at_cps_change; // clock phase when cps was changed + this.num_ticks_since_cps_change = 0; this.onToggle = onToggle; this.latency = latency; // fixed trigger time offset + + this.interval = interval; + this.clock = createClock( getTime, // called slightly before each cycle - (phase, duration, _, t) => { - if (this.num_ticks_since_cps_change === 0) { - this.num_cycles_at_cps_change = this.lastEnd; - this.seconds_at_cps_change = phase; + (lastTick, duration, _, time) => { + if (this.started === false) { + return; } - this.num_ticks_since_cps_change++; - const seconds_since_cps_change = this.num_ticks_since_cps_change * duration; - const num_cycles_since_cps_change = seconds_since_cps_change * this.cps; + const num_seconds_since_cps_change = this.num_ticks_since_cps_change * duration; + const tickdeadline = lastTick - time; + const num_cycles_since_cps_change = num_seconds_since_cps_change * this.cps; + const begin = this.num_cycles_at_cps_change + num_cycles_since_cps_change; + const secondsSinceLastTick = time - lastTick - duration; + const eventLength = duration * this.cps; + const end = begin + eventLength; + this.cycle = begin + secondsSinceLastTick * this.cps; - try { - const begin = this.lastEnd; - this.lastBegin = begin; - const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; - this.lastEnd = end; - this.lastTick = phase; + //account for latency and tick duration when using cycle calculations for audio downstream + const cycle_gap = (this.latency - duration) * this.cps; - if (phase < t) { - // avoid querying haps that are in the past anyway - console.log(`skip query: too late`); - return; + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + haps.forEach((hap) => { + if (hap.hasOnset()) { + let targetTime = (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps; + targetTime = targetTime + this.latency + tickdeadline + time - num_seconds_since_cps_change; + const duration = hap.duration / this.cps; + onTrigger?.(hap, tickdeadline, duration, this.cps, targetTime, this.cycle - cycle_gap); } - - // query the pattern for events - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); - - haps.forEach((hap) => { - if (hap.hasOnset()) { - const targetTime = - (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency; - const duration = hap.duration / this.cps; - // the following line is dumb and only here for backwards compatibility - // see https://codeberg.org/uzu/strudel/pulls/1004 - const deadline = targetTime - phase; - // this onTrigger has another signature - onTrigger?.(hap, deadline, duration, this.cps, targetTime); - if (hap.value.cps !== undefined && this.cps != hap.value.cps) { - this.cps = hap.value.cps; - this.num_ticks_since_cps_change = 0; - } - } - }); - } catch (e) { - logger(`[cyclist] error: ${e.message}`); - onError?.(e); - } + }); + this.time_at_last_tick_message = time; + this.num_ticks_since_cps_change++; }, interval, // duration of each cycle 0.1, @@ -91,17 +64,24 @@ export class Cyclist { if (!this.started) { return 0; } - const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; - return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; + const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps; + return this.cycle + gap; } + + setCycle = (cycle) => { + this.num_ticks_since_cps_change = 0; + this.num_cycles_at_cps_change = cycle; + }; setStarted(v) { this.started = v; + + this.setCycle(0); this.onToggle?.(v); } - async start() { - await this.beforeStart?.(); + start() { this.num_ticks_since_cps_change = 0; this.num_cycles_at_cps_change = 0; + if (!this.pattern) { throw new Error('Scheduler: no pattern set! call .setPattern first.'); } @@ -120,16 +100,18 @@ export class Cyclist { this.lastEnd = 0; this.setStarted(false); } - async setPattern(pat, autostart = false) { + setPattern(pat, autostart = false) { this.pattern = pat; if (autostart && !this.started) { - await this.start(); + this.start(); } } setCps(cps = 0.5) { if (this.cps === cps) { return; } + const num_seconds_since_cps_change = this.num_ticks_since_cps_change * this.interval; + this.num_cycles_at_cps_change = this.num_cycles_at_cps_change + num_seconds_since_cps_change * cps; this.cps = cps; this.num_ticks_since_cps_change = 0; } @@ -137,4 +119,4 @@ export class Cyclist { const onsets = haps.filter((h) => h.hasOnset()); console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); } -} +} \ No newline at end of file diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 261f08acb..4e6d385d8 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -45,7 +45,7 @@ export class NeoCyclist { const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps); const targetTime = timeUntilTrigger + currentTime + this.latency; const duration = cycleToSeconds(hap.duration, this.cps); - onTrigger?.(hap, 0, duration, this.cps, targetTime); + onTrigger?.(hap, 0, duration, this.cps, targetTime, this.cycle); } }); }; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index aeaa6418e..5e696c85f 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -244,16 +244,16 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { + async (hap, deadline, duration, cps, t, cycle = 0) => { // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 try { if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); + await defaultOutput(hap, deadline, duration, cps, t, cycle); } if (hap.context.onTrigger) { // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); + await hap.context.onTrigger(hap, getTime(), cps, t, cycle); } } catch (err) { logger(`[cyclist] error: ${err.message}`, 'error'); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index bbc7d8a1c..79c1ff117 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -45,6 +45,8 @@ export function setGainCurve(newGainCurveFunc) { gainCurveFunc = newGainCurveFunc; } + + function aliasBankMap(aliasMap) { // Make all bank keys lower case for case insensitivity for (const key in aliasMap) { @@ -325,12 +327,29 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) { return delays[orbit]; } -export function getLfo(audioContext, time, end, properties = {}) { +export function getLfo(audioContext, begin, end, properties = {}) { return getWorklet(audioContext, 'lfo-processor', { frequency: 1, depth: 1, skew: 0, phaseoffset: 0, + time: begin, + begin, + end, + shape: 1, + dcoffset: -0.5, + ...properties, + }); +} + +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, @@ -450,9 +469,10 @@ function mapChannelNumbers(channels) { return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); } -export const superdough = async (value, t, hapDuration, cps = 0.5) => { +export const superdough = async (value, t, hapDuration, cps = 0.5, cycle) => { // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); + let { stretch } = value; if (stretch != null) { //account for phase vocoder latency @@ -707,11 +727,12 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse })); crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); - distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); + // distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); // am !== undefined && // chain.push( // getWorklet(ac, 'am-processor', { // speed: am, + // begin: t, // depth: amdepth, // skew: amskew, // phaseoffset: amphase, @@ -724,12 +745,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { if (am !== undefined) { const amGain = new GainNode(ac, { gain: 1 }); - const frequency = cycleToSeconds(am, cps) + const frequency = cps / am const phaseoffset = cycleToSeconds(amphase, cps) + + + const time = cycle / cps + + // console.info(cycle, time, frequency) + + + const lfo = getLfo(ac, t, t + hapDuration, { skew: amskew, - frequency: am * cps, - depth: amdepth, + frequency, + depth: amdepth, + time, // dcoffset: 0, shape: amshape, phaseoffset diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index ffa2165b4..366be8a5e 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -85,6 +85,7 @@ const waveShapeNames = Object.keys(waveshapes); class LFOProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ + { name: 'begin', defaultValue: 0 }, { name: 'time', defaultValue: 0 }, { name: 'end', defaultValue: 0 }, { name: 'frequency', defaultValue: 0.5 }, @@ -109,10 +110,14 @@ class LFOProcessor extends AudioWorkletProcessor { } process(inputs, outputs, parameters) { + const begin = parameters['begin'][0] // eslint-disable-next-line no-undef if (currentTime >= parameters.end[0]) { return false; } + if (currentTime <= begin) { + return true; + } const output = outputs[0]; const frequency = parameters['frequency'][0]; @@ -159,6 +164,8 @@ class CoarseProcessor extends AudioWorkletProcessor { const input = inputs[0]; const output = outputs[0]; + + const hasInput = !(input[0] === undefined); if (this.started && !hasInput) { return false; @@ -899,6 +906,7 @@ registerProcessor('byte-beat-processor', ByteBeatProcessor); class AMProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ + { name: 'begin', defaultValue: 0 }, { name: 'cps', defaultValue: 0.5 }, { name: 'speed', defaultValue: 0.5 }, { name: 'cycle', defaultValue: 0 }, @@ -925,10 +933,15 @@ class AMProcessor extends AudioWorkletProcessor { const input = inputs[0]; const output = outputs[0]; const hasInput = !(input[0] === undefined); + + if (this.started && !hasInput) { return false; } this.started = hasInput; + if (currentTime <= parameters.begin[0]) { + return true; + } const speed = parameters['speed'][0]; const cps = parameters['cps'][0]; @@ -937,7 +950,7 @@ class AMProcessor extends AudioWorkletProcessor { const skew = parameters['skew'][0]; const phaseoffset = parameters['phaseoffset'][0]; - const frequency = speed * cps; + const frequency = cps / speed if (this.phase == null) { const secondsPassed = cycle / cps; this.phase = _mod(secondsPassed * frequency + phaseoffset, 1); diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 01ad9fb8b..78441ee55 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -17,8 +17,12 @@ const hap2value = (hap) => { // uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 // TODO: refactor output callbacks to eliminate deadline -export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => { - return superdough(hap2value(hap), t, hapDuration, cps); +export const webaudioOutput = (hap, deadline, hapDuration, cps, t, cycle) => { + + return superdough(hap2value(hap), t, hapDuration, cps, + hap.whole.begin.valueOf() + // cycle +); }; export function webaudioRepl(options = {}) { From a7a283a99e1505005e45ccf0e7df0f5be83391e2 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 6 Jul 2025 17:00:33 +0200 Subject: [PATCH 268/538] doc: add version tagging guide to contributing.md --- CONTRIBUTING.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f455741f5..a7f65b5bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -150,6 +150,7 @@ Important: Always publish with `pnpm`, as `npm` does not support overriding main ## useful commands + ```sh #regenerate the test snapshots (ex: when updating or creating new pattern functions) pnpm snapshot @@ -160,6 +161,81 @@ pnpm run osc #build the standalone version pnpm tauri build ``` + +## version tag patching + +here's a little guide on how to patch patterns in the database to prevent breaking old patterns due to breaking changes in newer versions. + +the general tactic is to use `// @version x.y` to tag a pattern with a specific strudel version. when a pattern is evaluated, this metadata will de-activate any breaking changes that came after the specified version. +for example, in version 1.1, the default value for `fanchor` was changed from `0.5` to `0`. +if play a pattern that was made before that change, sounds that use filter evenlopes can sound very different, so by adding `// @version 1.0` will make it sound like it used to. +before releasing a new version with breaking changes, we can edit all patterns in the database, inserting the version tag they were created under: + +as an example, to release version 1.2, do the following: + +1. get date range + +```sh +# get date of last version: +git log -1 --format=%aI @strudel/core@1.1.0 +# 2024-05-31T23:07:26+02:00 + +# get date of current version: +git log -1 --format=%aI @strudel/core@1.2.0 +# 2025-05-01T12:39:24+02:00 +# might also use todays timestamp if version is not yet released +``` + +now we know, all patterns between these 2 dates have to receive a version tag (unless they already have one). + +2. get patterns in question + +```sql +SELECT * +FROM code_v1 +WHERE code NOT LIKE '%@version%' +AND created_at > '2024-05-31T23:07:26+02:00' +AND created_at < '2025-05-01T12:39:24+02:00' +ORDER BY created_at ASC; +``` + +this gives us all unversioned patterns that were saved between 1.1.0 and 1.2.0. in this case, it's 9373 patterns! + +3. insert version tags + +we are now ready to insert the version tag to these patterns. +before updating thousands of patterns, it's probably a good idea to test if a single one gets udpated: + +```sql +UPDATE code_v1 +SET code = code || E'\n// @version 1.1' +WHERE hash = 'Ns2sMB40yIw4'; +``` + +after [verifying](https://strudel.cc/?Ns2sMB40yIw4) that the version tag has been added, let's insert it everywhere: + +```sql +UPDATE code_v1 +SET code = code || E'\n// @version 1.1' +WHERE code NOT LIKE '%@version%' +AND created_at > '2024-05-31T23:07:26+02:00' +AND created_at < '2025-05-01T12:39:24+02:00' +``` + +4. verify + +we can verify that the edits worked by querying all patterns that contain the new version tag: + +```sql +SELECT * +FROM code_v1 +WHERE code LIKE '%@version 1.1%' +AND created_at > '2024-05-31T23:07:26+02:00' +AND created_at < '2025-05-01T12:39:24+02:00' +ORDER BY created_at ASC; +``` + + ## Have Fun Remember to have fun, and that this project is driven by the passion of volunteers! From 72b582c605bd68beae398877a9c8f41e8dffc716 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 7 Jul 2025 22:20:04 +0200 Subject: [PATCH 269/538] fix: remove github contributor avatars --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index d54302ac6..854e2c317 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,6 @@ Licensing info for the default sound banks can be found over on the [dough-sampl There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). - - - - -Made with [contrib.rocks](https://contrib.rocks). - ## Community There is a #strudel channel on the TidalCycles discord: From 81a9811803bfdbe89f721da1bbf5a327173d267a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 7 Jul 2025 22:23:16 +0200 Subject: [PATCH 270/538] add logo --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 854e2c317..640fc24c9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # strudel + + Live coding patterns on the web https://strudel.cc/ From 681eed3d0e544062099a78089912aaad82f8670e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 7 Jul 2025 22:23:40 +0200 Subject: [PATCH 271/538] revert add logo --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 640fc24c9..854e2c317 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # strudel - - Live coding patterns on the web https://strudel.cc/ From a6ad58a3755cda9e049c1378002b735311c3be11 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 7 Jul 2025 23:12:15 +0200 Subject: [PATCH 272/538] fix: remove first gm_synth_bass_1, as it doesn't work in safari --- packages/soundfonts/gm.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/soundfonts/gm.mjs b/packages/soundfonts/gm.mjs index c0e57dcb0..d8b7687d7 100644 --- a/packages/soundfonts/gm.mjs +++ b/packages/soundfonts/gm.mjs @@ -537,7 +537,7 @@ export default { ], gm_synth_bass_1: [ // Synth Bass 1: Bass - '0380_Aspirin_sf2_file', + // '0380_Aspirin_sf2_file', // broken in safari https://codeberg.org/uzu/strudel/issues/1384 '0380_Chaos_sf2_file', '0380_FluidR3_GM_sf2_file', // 0380_GeneralUserGS_sf2_file // laut From 671a22fc221e920e9550f8c8ee13abda1b3d6f96 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 7 Jul 2025 23:28:41 +0200 Subject: [PATCH 273/538] chore: add link to contributors --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 854e2c317..4aa90fd38 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Licensing info for the default sound banks can be found over on the [dough-sampl ## Contributing -There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). +There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). You can find the full list of contributors [here](https://codeberg.org/uzu/strudel/activity/contributors). ## Community From ce5f425142852573a68349371870ed1165e2d125 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 8 Jul 2025 00:26:15 -0400 Subject: [PATCH 274/538] fully implemented --- packages/core/controls.mjs | 33 ++- packages/core/cyclist.mjs | 7 +- packages/core/repl.mjs | 6 +- packages/superdough/superdough.mjs | 97 +++---- packages/superdough/worklets.mjs | 79 +----- packages/webaudio/webaudio.mjs | 15 +- test/__snapshots__/examples.test.mjs.snap | 296 ++++++++++------------ 7 files changed, 227 insertions(+), 306 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 12bcc3565..a0f5d10e3 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -448,12 +448,13 @@ export const { coarse } = registerControl('coarse'); * modulate the amplitude of a sound with a continuous waveform * * @name am + * @synonym tremolo * @param {number | Pattern} speed modulation speed in HZ * @example * s("triangle").am("2").amshape("").amdepth(.5) * */ -export const { am, } = registerControl(['am', 'amdepth', 'amskew', 'amphase'],); +export const { am, tremolo } = registerControl(['am', 'amdepth', 'amskew', 'amphase'], 'tremolo'); /** * modulate the amplitude of a sound with a continuous waveform @@ -461,7 +462,7 @@ export const { am, } = registerControl(['am', 'amdepth', 'amskew', 'amphase'],); * @name amsync * @param {number | Pattern} cycles modulation speed in cycles * @example - * s("triangle").am("2").amshape("").amdepth(.5) + * s("supersaw").amsync(1/4).amskew("<0 .5 1>").amdepth(2) * */ export const { amsync } = registerControl(['amsync', 'amdepth', 'amskew', 'amphase']); @@ -482,7 +483,7 @@ export const { amdepth } = registerControl('amdepth'); * @name amskew * @param {number | Pattern} amount between 0 & 1, the shape of the waveform * @example - * note("{f a c e}%16").am(4).amskew("<.5 0 1>") + * note("{f a c e}%16").am(5).amskew(.5).amdepth("<1 0.5 2>").att(.01).rel(.03) * */ export const { amskew } = registerControl('amskew'); @@ -493,7 +494,7 @@ export const { amskew } = registerControl('amskew'); * @name amphase * @param {number | Pattern} offset the offset in cycles of the modulation * @example - * note("{f a c e}%16").am(4).amphase("<0 .25 .66>") + * note("{f a c e}%16").s("sawtooth").am(4).amphase("<0 .25 .66>") * */ export const { amphase } = registerControl('amphase'); @@ -517,6 +518,30 @@ export const { amshape } = registerControl('amshape'); * note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>") * */ + +// 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 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'); + export const { drive } = registerControl('drive'); /** diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 43e1f508e..6864301db 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -38,16 +38,13 @@ export class Cyclist { const end = begin + eventLength; this.cycle = begin + secondsSinceLastTick * this.cps; - //account for latency and tick duration when using cycle calculations for audio downstream - const cycle_gap = (this.latency - duration) * this.cps; - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); haps.forEach((hap) => { if (hap.hasOnset()) { let targetTime = (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps; targetTime = targetTime + this.latency + tickdeadline + time - num_seconds_since_cps_change; const duration = hap.duration / this.cps; - onTrigger?.(hap, tickdeadline, duration, this.cps, targetTime, this.cycle - cycle_gap); + onTrigger?.(hap, tickdeadline, duration, this.cps, targetTime); } }); this.time_at_last_tick_message = time; @@ -119,4 +116,4 @@ export class Cyclist { const onsets = haps.filter((h) => h.hasOnset()); console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); } -} \ No newline at end of file +} diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 5e696c85f..aeaa6418e 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -244,16 +244,16 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t, cycle = 0) => { + async (hap, deadline, duration, cps, t) => { // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 try { if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t, cycle); + await defaultOutput(hap, deadline, duration, cps, t); } if (hap.context.onTrigger) { // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t, cycle); + await hap.context.onTrigger(hap, getTime(), cps, t); } } catch (err) { logger(`[cyclist] error: ${err.message}`, 'error'); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 79c1ff117..ccb69cceb 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -45,8 +45,6 @@ export function setGainCurve(newGainCurveFunc) { gainCurveFunc = newGainCurveFunc; } - - function aliasBankMap(aliasMap) { // Make all bank keys lower case for case insensitivity for (const key in aliasMap) { @@ -328,22 +326,26 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) { } export function getLfo(audioContext, begin, end, properties = {}) { + const { dcoffset = -0.5, depth = 1 } = properties; return getWorklet(audioContext, 'lfo-processor', { frequency: 1, - depth: 1, + depth, skew: 0, phaseoffset: 0, time: begin, begin, end, shape: 1, - dcoffset: -0.5, + dcoffset, + min: dcoffset - depth * 0.5, + max: dcoffset + depth * 0.5, + curve: 1, ...properties, }); } export function getSyncedLfo(audioContext, time, end, cps, cycle, properties = {}) { - const frequency = cycle/cps + const frequency = cycle / cps; return getWorklet(audioContext, 'lfo-processor', { frequency, @@ -499,10 +501,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle) => { // destructure let { am, + amsync, amdepth = 1, - amskew = 0.5, + amskew = 1, amphase = 0, - amshape = 1, + amshape = 0, s = getDefaultValue('s'), bank, source, @@ -512,6 +515,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle) => { // filters fanchor = getDefaultValue('fanchor'), drive = 0.69, + release = 0, // low pass cutoff, lpenv, @@ -587,8 +591,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle) => { distortvol = applyGainCurve(distortvol); delay = applyGainCurve(delay); velocity = applyGainCurve(velocity); + amdepth = applyGainCurve(amdepth); gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future + const end = t + hapDuration; + const endWithRelease = end + release; const chainID = Math.round(Math.random() * 1000000); // oldest audio nodes will be destroyed if maximum polyphony is exceeded @@ -663,7 +670,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle) => { lprelease, lpenv, t, - t + hapDuration, + end, fanchor, ftype, drive, @@ -687,7 +694,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle) => { hprelease, hpenv, t, - t + hapDuration, + end, fanchor, ); chain.push(hp()); @@ -698,20 +705,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle) => { if (bandf !== undefined) { let bp = () => - createFilter( - ac, - 'bandpass', - bandf, - bandq, - bpattack, - bpdecay, - bpsustain, - bprelease, - bpenv, - t, - t + hapDuration, - fanchor, - ); + createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor); chain.push(bp()); if (ftype === '24db') { chain.push(bp()); @@ -727,45 +721,32 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle) => { coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse })); crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); - // distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); - // am !== undefined && - // chain.push( - // getWorklet(ac, 'am-processor', { - // speed: am, - // begin: t, - // depth: amdepth, - // skew: amskew, - // phaseoffset: amphase, - // // shape: amshape, - - // cps, - // cycle, - // }), - // ); + distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); + if (amsync != null) { + am = cps / amsync; + } if (am !== undefined) { - const amGain = new GainNode(ac, { gain: 1 }); - const frequency = cps / am - const phaseoffset = cycleToSeconds(amphase, cps) + // Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload + // EX: a triangle waveform will clip like this /-\ when the depth is above 1 + const gain = Math.max(1 - amdepth, 0); + const amGain = new GainNode(ac, { gain }); - - const time = cycle / cps - - // console.info(cycle, time, frequency) - - - - const lfo = getLfo(ac, t, t + hapDuration, { - skew: amskew, - frequency, + const time = cycle / cps; + const lfo = getLfo(ac, t, endWithRelease, { + skew: amskew, + frequency: am, depth: amdepth, time, - // dcoffset: 0, - shape: amshape, - phaseoffset - }) - lfo.connect(amGain.gain) - chain.push(amGain) + dcoffset: 0, + shape: amshape, + phaseoffset: amphase, + min: 0, + max: 1, + curve: 1.5, + }); + lfo.connect(amGain.gain); + chain.push(amGain); } compressorThreshold !== undefined && @@ -781,7 +762,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle) => { } // phaser if (phaser !== undefined && phaserdepth > 0) { - const phaserFX = getPhaser(t, t + hapDuration, phaser, phaserdepth, phasercenter, phasersweep); + const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep); chain.push(phaserFX); } diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 366be8a5e..466be558a 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -93,7 +93,10 @@ class LFOProcessor extends AudioWorkletProcessor { { name: 'depth', defaultValue: 1 }, { name: 'phaseoffset', defaultValue: 0 }, { name: 'shape', defaultValue: 0 }, + { name: 'curve', defaultValue: 1 }, { name: 'dcoffset', defaultValue: 0 }, + { name: 'min', defaultValue: 0 }, + { name: 'max', defaultValue: 1 }, ]; } @@ -110,7 +113,7 @@ class LFOProcessor extends AudioWorkletProcessor { } process(inputs, outputs, parameters) { - const begin = parameters['begin'][0] + const begin = parameters['begin'][0]; // eslint-disable-next-line no-undef if (currentTime >= parameters.end[0]) { return false; @@ -126,6 +129,9 @@ class LFOProcessor extends AudioWorkletProcessor { const depth = parameters['depth'][0]; const skew = parameters['skew'][0]; const phaseoffset = parameters['phaseoffset'][0]; + const min = parameters['min'][0]; + const max = parameters['max'][0]; + const curve = parameters['curve'][0]; const dcoffset = parameters['dcoffset'][0]; const shape = waveShapeNames[parameters['shape'][0]]; @@ -140,7 +146,7 @@ class LFOProcessor extends AudioWorkletProcessor { for (let n = 0; n < blockSize; n++) { for (let i = 0; i < output.length; i++) { const modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth; - output[i][n] = modval; + output[i][n] = clamp(Math.pow(modval, curve), min, max); } this.incrementPhase(dt); } @@ -164,8 +170,6 @@ class CoarseProcessor extends AudioWorkletProcessor { const input = inputs[0]; const output = outputs[0]; - - const hasInput = !(input[0] === undefined); if (this.started && !hasInput) { return false; @@ -901,70 +905,3 @@ class ByteBeatProcessor extends AudioWorkletProcessor { } registerProcessor('byte-beat-processor', ByteBeatProcessor); - - -class AMProcessor extends AudioWorkletProcessor { - static get parameterDescriptors() { - return [ - { name: 'begin', defaultValue: 0 }, - { name: 'cps', defaultValue: 0.5 }, - { name: 'speed', defaultValue: 0.5 }, - { name: 'cycle', defaultValue: 0 }, - { name: 'skew', defaultValue: 0.5 }, - { name: 'depth', defaultValue: 1 }, - { name: 'phaseoffset', defaultValue: 0 }, - ]; - } - - constructor() { - super(); - this.phase; - this.started = false; - } - - incrementPhase(dt) { - this.phase += dt; - if (this.phase > 1.0) { - this.phase = this.phase - 1; - } - } - - process(inputs, outputs, parameters) { - const input = inputs[0]; - const output = outputs[0]; - const hasInput = !(input[0] === undefined); - - - if (this.started && !hasInput) { - return false; - } - this.started = hasInput; - if (currentTime <= parameters.begin[0]) { - return true; - } - - const speed = parameters['speed'][0]; - const cps = parameters['cps'][0]; - const cycle = parameters['cycle'][0]; - const depth = parameters['depth'][0]; - const skew = parameters['skew'][0]; - const phaseoffset = parameters['phaseoffset'][0]; - - const frequency = cps / speed - if (this.phase == null) { - const secondsPassed = cycle / cps; - this.phase = _mod(secondsPassed * 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 < input.length; i++) { - const modval = clamp(waveshapes.tri(this.phase, skew) * depth + (1 - depth), 0, 1); - output[i][n] = input[i][n] * modval; - } - this.incrementPhase(dt); - } - return true; - } -} -registerProcessor('am-processor', AMProcessor); diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 78441ee55..1e8455b3f 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -17,12 +17,15 @@ const hap2value = (hap) => { // uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 // TODO: refactor output callbacks to eliminate deadline -export const webaudioOutput = (hap, deadline, hapDuration, cps, t, cycle) => { - - return superdough(hap2value(hap), t, hapDuration, cps, - hap.whole.begin.valueOf() - // cycle -); +export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { + return superdough( + hap2value(hap), + t, + hapDuration, + cps, + hap.whole.begin.valueOf(), + // cycle + ); }; export function webaudioRepl(options = {}) { diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 4ddf79eff..50d4f2095 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -860,70 +860,70 @@ exports[`runs examples > example "amp" example index 0 1`] = ` exports[`runs examples > example "amphase" example index 0 1`] = ` [ - "[ 0/1 → 1/16 | note:f am:4 amphase:0 ]", - "[ 1/16 → 1/8 | note:a am:4 amphase:0 ]", - "[ 1/8 → 3/16 | note:c am:4 amphase:0 ]", - "[ 3/16 → 1/4 | note:e am:4 amphase:0 ]", - "[ 1/4 → 5/16 | note:f am:4 amphase:0 ]", - "[ 5/16 → 3/8 | note:a am:4 amphase:0 ]", - "[ 3/8 → 7/16 | note:c am:4 amphase:0 ]", - "[ 7/16 → 1/2 | note:e am:4 amphase:0 ]", - "[ 1/2 → 9/16 | note:f am:4 amphase:0 ]", - "[ 9/16 → 5/8 | note:a am:4 amphase:0 ]", - "[ 5/8 → 11/16 | note:c am:4 amphase:0 ]", - "[ 11/16 → 3/4 | note:e am:4 amphase:0 ]", - "[ 3/4 → 13/16 | note:f am:4 amphase:0 ]", - "[ 13/16 → 7/8 | note:a am:4 amphase:0 ]", - "[ 7/8 → 15/16 | note:c am:4 amphase:0 ]", - "[ 15/16 → 1/1 | note:e am:4 amphase:0 ]", - "[ 1/1 → 17/16 | note:f am:4 amphase:0.25 ]", - "[ 17/16 → 9/8 | note:a am:4 amphase:0.25 ]", - "[ 9/8 → 19/16 | note:c am:4 amphase:0.25 ]", - "[ 19/16 → 5/4 | note:e am:4 amphase:0.25 ]", - "[ 5/4 → 21/16 | note:f am:4 amphase:0.25 ]", - "[ 21/16 → 11/8 | note:a am:4 amphase:0.25 ]", - "[ 11/8 → 23/16 | note:c am:4 amphase:0.25 ]", - "[ 23/16 → 3/2 | note:e am:4 amphase:0.25 ]", - "[ 3/2 → 25/16 | note:f am:4 amphase:0.25 ]", - "[ 25/16 → 13/8 | note:a am:4 amphase:0.25 ]", - "[ 13/8 → 27/16 | note:c am:4 amphase:0.25 ]", - "[ 27/16 → 7/4 | note:e am:4 amphase:0.25 ]", - "[ 7/4 → 29/16 | note:f am:4 amphase:0.25 ]", - "[ 29/16 → 15/8 | note:a am:4 amphase:0.25 ]", - "[ 15/8 → 31/16 | note:c am:4 amphase:0.25 ]", - "[ 31/16 → 2/1 | note:e am:4 amphase:0.25 ]", - "[ 2/1 → 33/16 | note:f am:4 amphase:0.66 ]", - "[ 33/16 → 17/8 | note:a am:4 amphase:0.66 ]", - "[ 17/8 → 35/16 | note:c am:4 amphase:0.66 ]", - "[ 35/16 → 9/4 | note:e am:4 amphase:0.66 ]", - "[ 9/4 → 37/16 | note:f am:4 amphase:0.66 ]", - "[ 37/16 → 19/8 | note:a am:4 amphase:0.66 ]", - "[ 19/8 → 39/16 | note:c am:4 amphase:0.66 ]", - "[ 39/16 → 5/2 | note:e am:4 amphase:0.66 ]", - "[ 5/2 → 41/16 | note:f am:4 amphase:0.66 ]", - "[ 41/16 → 21/8 | note:a am:4 amphase:0.66 ]", - "[ 21/8 → 43/16 | note:c am:4 amphase:0.66 ]", - "[ 43/16 → 11/4 | note:e am:4 amphase:0.66 ]", - "[ 11/4 → 45/16 | note:f am:4 amphase:0.66 ]", - "[ 45/16 → 23/8 | note:a am:4 amphase:0.66 ]", - "[ 23/8 → 47/16 | note:c am:4 amphase:0.66 ]", - "[ 47/16 → 3/1 | note:e am:4 amphase:0.66 ]", - "[ 3/1 → 49/16 | note:f am:4 amphase:0 ]", - "[ 49/16 → 25/8 | note:a am:4 amphase:0 ]", - "[ 25/8 → 51/16 | note:c am:4 amphase:0 ]", - "[ 51/16 → 13/4 | note:e am:4 amphase:0 ]", - "[ 13/4 → 53/16 | note:f am:4 amphase:0 ]", - "[ 53/16 → 27/8 | note:a am:4 amphase:0 ]", - "[ 27/8 → 55/16 | note:c am:4 amphase:0 ]", - "[ 55/16 → 7/2 | note:e am:4 amphase:0 ]", - "[ 7/2 → 57/16 | note:f am:4 amphase:0 ]", - "[ 57/16 → 29/8 | note:a am:4 amphase:0 ]", - "[ 29/8 → 59/16 | note:c am:4 amphase:0 ]", - "[ 59/16 → 15/4 | note:e am:4 amphase:0 ]", - "[ 15/4 → 61/16 | note:f am:4 amphase:0 ]", - "[ 61/16 → 31/8 | note:a am:4 amphase:0 ]", - "[ 31/8 → 63/16 | note:c am:4 amphase:0 ]", - "[ 63/16 → 4/1 | note:e am:4 amphase:0 ]", + "[ 0/1 → 1/16 | note:f s:sawtooth am:4 amphase:0 ]", + "[ 1/16 → 1/8 | note:a s:sawtooth am:4 amphase:0 ]", + "[ 1/8 → 3/16 | note:c s:sawtooth am:4 amphase:0 ]", + "[ 3/16 → 1/4 | note:e s:sawtooth am:4 amphase:0 ]", + "[ 1/4 → 5/16 | note:f s:sawtooth am:4 amphase:0 ]", + "[ 5/16 → 3/8 | note:a s:sawtooth am:4 amphase:0 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth am:4 amphase:0 ]", + "[ 7/16 → 1/2 | note:e s:sawtooth am:4 amphase:0 ]", + "[ 1/2 → 9/16 | note:f s:sawtooth am:4 amphase:0 ]", + "[ 9/16 → 5/8 | note:a s:sawtooth am:4 amphase:0 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth am:4 amphase:0 ]", + "[ 11/16 → 3/4 | note:e s:sawtooth am:4 amphase:0 ]", + "[ 3/4 → 13/16 | note:f s:sawtooth am:4 amphase:0 ]", + "[ 13/16 → 7/8 | note:a s:sawtooth am:4 amphase:0 ]", + "[ 7/8 → 15/16 | note:c s:sawtooth am:4 amphase:0 ]", + "[ 15/16 → 1/1 | note:e s:sawtooth am:4 amphase:0 ]", + "[ 1/1 → 17/16 | note:f s:sawtooth am:4 amphase:0.25 ]", + "[ 17/16 → 9/8 | note:a s:sawtooth am:4 amphase:0.25 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth am:4 amphase:0.25 ]", + "[ 19/16 → 5/4 | note:e s:sawtooth am:4 amphase:0.25 ]", + "[ 5/4 → 21/16 | note:f s:sawtooth am:4 amphase:0.25 ]", + "[ 21/16 → 11/8 | note:a s:sawtooth am:4 amphase:0.25 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth am:4 amphase:0.25 ]", + "[ 23/16 → 3/2 | note:e s:sawtooth am:4 amphase:0.25 ]", + "[ 3/2 → 25/16 | note:f s:sawtooth am:4 amphase:0.25 ]", + "[ 25/16 → 13/8 | note:a s:sawtooth am:4 amphase:0.25 ]", + "[ 13/8 → 27/16 | note:c s:sawtooth am:4 amphase:0.25 ]", + "[ 27/16 → 7/4 | note:e s:sawtooth am:4 amphase:0.25 ]", + "[ 7/4 → 29/16 | note:f s:sawtooth am:4 amphase:0.25 ]", + "[ 29/16 → 15/8 | note:a s:sawtooth am:4 amphase:0.25 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth am:4 amphase:0.25 ]", + "[ 31/16 → 2/1 | note:e s:sawtooth am:4 amphase:0.25 ]", + "[ 2/1 → 33/16 | note:f s:sawtooth am:4 amphase:0.66 ]", + "[ 33/16 → 17/8 | note:a s:sawtooth am:4 amphase:0.66 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth am:4 amphase:0.66 ]", + "[ 35/16 → 9/4 | note:e s:sawtooth am:4 amphase:0.66 ]", + "[ 9/4 → 37/16 | note:f s:sawtooth am:4 amphase:0.66 ]", + "[ 37/16 → 19/8 | note:a s:sawtooth am:4 amphase:0.66 ]", + "[ 19/8 → 39/16 | note:c s:sawtooth am:4 amphase:0.66 ]", + "[ 39/16 → 5/2 | note:e s:sawtooth am:4 amphase:0.66 ]", + "[ 5/2 → 41/16 | note:f s:sawtooth am:4 amphase:0.66 ]", + "[ 41/16 → 21/8 | note:a s:sawtooth am:4 amphase:0.66 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth am:4 amphase:0.66 ]", + "[ 43/16 → 11/4 | note:e s:sawtooth am:4 amphase:0.66 ]", + "[ 11/4 → 45/16 | note:f s:sawtooth am:4 amphase:0.66 ]", + "[ 45/16 → 23/8 | note:a s:sawtooth am:4 amphase:0.66 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth am:4 amphase:0.66 ]", + "[ 47/16 → 3/1 | note:e s:sawtooth am:4 amphase:0.66 ]", + "[ 3/1 → 49/16 | note:f s:sawtooth am:4 amphase:0 ]", + "[ 49/16 → 25/8 | note:a s:sawtooth am:4 amphase:0 ]", + "[ 25/8 → 51/16 | note:c s:sawtooth am:4 amphase:0 ]", + "[ 51/16 → 13/4 | note:e s:sawtooth am:4 amphase:0 ]", + "[ 13/4 → 53/16 | note:f s:sawtooth am:4 amphase:0 ]", + "[ 53/16 → 27/8 | note:a s:sawtooth am:4 amphase:0 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth am:4 amphase:0 ]", + "[ 55/16 → 7/2 | note:e s:sawtooth am:4 amphase:0 ]", + "[ 7/2 → 57/16 | note:f s:sawtooth am:4 amphase:0 ]", + "[ 57/16 → 29/8 | note:a s:sawtooth am:4 amphase:0 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth am:4 amphase:0 ]", + "[ 59/16 → 15/4 | note:e s:sawtooth am:4 amphase:0 ]", + "[ 15/4 → 61/16 | note:f s:sawtooth am:4 amphase:0 ]", + "[ 61/16 → 31/8 | note:a s:sawtooth am:4 amphase:0 ]", + "[ 31/8 → 63/16 | note:c s:sawtooth am:4 amphase:0 ]", + "[ 63/16 → 4/1 | note:e s:sawtooth am:4 amphase:0 ]", ] `; @@ -998,70 +998,79 @@ exports[`runs examples > example "amshape" example index 0 1`] = ` exports[`runs examples > example "amskew" example index 0 1`] = ` [ - "[ 0/1 → 1/16 | note:f am:4 amskew:0.5 ]", - "[ 1/16 → 1/8 | note:a am:4 amskew:0.5 ]", - "[ 1/8 → 3/16 | note:c am:4 amskew:0.5 ]", - "[ 3/16 → 1/4 | note:e am:4 amskew:0.5 ]", - "[ 1/4 → 5/16 | note:f am:4 amskew:0.5 ]", - "[ 5/16 → 3/8 | note:a am:4 amskew:0.5 ]", - "[ 3/8 → 7/16 | note:c am:4 amskew:0.5 ]", - "[ 7/16 → 1/2 | note:e am:4 amskew:0.5 ]", - "[ 1/2 → 9/16 | note:f am:4 amskew:0.5 ]", - "[ 9/16 → 5/8 | note:a am:4 amskew:0.5 ]", - "[ 5/8 → 11/16 | note:c am:4 amskew:0.5 ]", - "[ 11/16 → 3/4 | note:e am:4 amskew:0.5 ]", - "[ 3/4 → 13/16 | note:f am:4 amskew:0.5 ]", - "[ 13/16 → 7/8 | note:a am:4 amskew:0.5 ]", - "[ 7/8 → 15/16 | note:c am:4 amskew:0.5 ]", - "[ 15/16 → 1/1 | note:e am:4 amskew:0.5 ]", - "[ 1/1 → 17/16 | note:f am:4 amskew:0 ]", - "[ 17/16 → 9/8 | note:a am:4 amskew:0 ]", - "[ 9/8 → 19/16 | note:c am:4 amskew:0 ]", - "[ 19/16 → 5/4 | note:e am:4 amskew:0 ]", - "[ 5/4 → 21/16 | note:f am:4 amskew:0 ]", - "[ 21/16 → 11/8 | note:a am:4 amskew:0 ]", - "[ 11/8 → 23/16 | note:c am:4 amskew:0 ]", - "[ 23/16 → 3/2 | note:e am:4 amskew:0 ]", - "[ 3/2 → 25/16 | note:f am:4 amskew:0 ]", - "[ 25/16 → 13/8 | note:a am:4 amskew:0 ]", - "[ 13/8 → 27/16 | note:c am:4 amskew:0 ]", - "[ 27/16 → 7/4 | note:e am:4 amskew:0 ]", - "[ 7/4 → 29/16 | note:f am:4 amskew:0 ]", - "[ 29/16 → 15/8 | note:a am:4 amskew:0 ]", - "[ 15/8 → 31/16 | note:c am:4 amskew:0 ]", - "[ 31/16 → 2/1 | note:e am:4 amskew:0 ]", - "[ 2/1 → 33/16 | note:f am:4 amskew:1 ]", - "[ 33/16 → 17/8 | note:a am:4 amskew:1 ]", - "[ 17/8 → 35/16 | note:c am:4 amskew:1 ]", - "[ 35/16 → 9/4 | note:e am:4 amskew:1 ]", - "[ 9/4 → 37/16 | note:f am:4 amskew:1 ]", - "[ 37/16 → 19/8 | note:a am:4 amskew:1 ]", - "[ 19/8 → 39/16 | note:c am:4 amskew:1 ]", - "[ 39/16 → 5/2 | note:e am:4 amskew:1 ]", - "[ 5/2 → 41/16 | note:f am:4 amskew:1 ]", - "[ 41/16 → 21/8 | note:a am:4 amskew:1 ]", - "[ 21/8 → 43/16 | note:c am:4 amskew:1 ]", - "[ 43/16 → 11/4 | note:e am:4 amskew:1 ]", - "[ 11/4 → 45/16 | note:f am:4 amskew:1 ]", - "[ 45/16 → 23/8 | note:a am:4 amskew:1 ]", - "[ 23/8 → 47/16 | note:c am:4 amskew:1 ]", - "[ 47/16 → 3/1 | note:e am:4 amskew:1 ]", - "[ 3/1 → 49/16 | note:f am:4 amskew:0.5 ]", - "[ 49/16 → 25/8 | note:a am:4 amskew:0.5 ]", - "[ 25/8 → 51/16 | note:c am:4 amskew:0.5 ]", - "[ 51/16 → 13/4 | note:e am:4 amskew:0.5 ]", - "[ 13/4 → 53/16 | note:f am:4 amskew:0.5 ]", - "[ 53/16 → 27/8 | note:a am:4 amskew:0.5 ]", - "[ 27/8 → 55/16 | note:c am:4 amskew:0.5 ]", - "[ 55/16 → 7/2 | note:e am:4 amskew:0.5 ]", - "[ 7/2 → 57/16 | note:f am:4 amskew:0.5 ]", - "[ 57/16 → 29/8 | note:a am:4 amskew:0.5 ]", - "[ 29/8 → 59/16 | note:c am:4 amskew:0.5 ]", - "[ 59/16 → 15/4 | note:e am:4 amskew:0.5 ]", - "[ 15/4 → 61/16 | note:f am:4 amskew:0.5 ]", - "[ 61/16 → 31/8 | note:a am:4 amskew:0.5 ]", - "[ 31/8 → 63/16 | note:c am:4 amskew:0.5 ]", - "[ 63/16 → 4/1 | note:e am:4 amskew:0.5 ]", + "[ 0/1 → 1/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 1/16 → 1/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 1/8 → 3/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 3/16 → 1/4 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 1/4 → 5/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 5/16 → 3/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 3/8 → 7/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 7/16 → 1/2 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 1/2 → 9/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 9/16 → 5/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 5/8 → 11/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 11/16 → 3/4 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 3/4 → 13/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 13/16 → 7/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 7/8 → 15/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 15/16 → 1/1 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 1/1 → 17/16 | note:f am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 17/16 → 9/8 | note:a am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 9/8 → 19/16 | note:c am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 19/16 → 5/4 | note:e am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 5/4 → 21/16 | note:f am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 21/16 → 11/8 | note:a am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 11/8 → 23/16 | note:c am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 23/16 → 3/2 | note:e am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 3/2 → 25/16 | note:f am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 25/16 → 13/8 | note:a am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 13/8 → 27/16 | note:c am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 27/16 → 7/4 | note:e am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 7/4 → 29/16 | note:f am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 29/16 → 15/8 | note:a am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 15/8 → 31/16 | note:c am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 31/16 → 2/1 | note:e am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", + "[ 2/1 → 33/16 | note:f am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 33/16 → 17/8 | note:a am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 17/8 → 35/16 | note:c am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 35/16 → 9/4 | note:e am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 9/4 → 37/16 | note:f am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 37/16 → 19/8 | note:a am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 19/8 → 39/16 | note:c am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 39/16 → 5/2 | note:e am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 5/2 → 41/16 | note:f am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 41/16 → 21/8 | note:a am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 21/8 → 43/16 | note:c am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 43/16 → 11/4 | note:e am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 11/4 → 45/16 | note:f am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 45/16 → 23/8 | note:a am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 23/8 → 47/16 | note:c am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 47/16 → 3/1 | note:e am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", + "[ 3/1 → 49/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 49/16 → 25/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 25/8 → 51/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 51/16 → 13/4 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 13/4 → 53/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 53/16 → 27/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 27/8 → 55/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 55/16 → 7/2 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 7/2 → 57/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 57/16 → 29/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 29/8 → 59/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 59/16 → 15/4 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 15/4 → 61/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 61/16 → 31/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 31/8 → 63/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 63/16 → 4/1 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", +] +`; + +exports[`runs examples > example "amsync" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:supersaw amsync:0.25 amskew:0 amdepth:2 ]", + "[ 1/1 → 2/1 | s:supersaw amsync:0.25 amskew:0.5 amdepth:2 ]", + "[ 2/1 → 3/1 | s:supersaw amsync:0.25 amskew:1 amdepth:2 ]", + "[ 3/1 → 4/1 | s:supersaw amsync:0.25 amskew:0 amdepth:2 ]", ] `; @@ -7076,8 +7085,6 @@ exports[`runs examples > example "ply" example index 0 1`] = ` ] `; -<<<<<<< HEAD -======= exports[`runs examples > example "polymeter" example index 0 1`] = ` [ "[ 0/1 → 1/6 | note:c ]", @@ -7131,7 +7138,6 @@ exports[`runs examples > example "polymeter" example index 0 1`] = ` ] `; ->>>>>>> main exports[`runs examples > example "postgain" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]", @@ -9785,30 +9791,6 @@ exports[`runs examples > example "stack" example index 1 1`] = ` ] `; -<<<<<<< HEAD -exports[`runs examples > example "stack" example index 0 2`] = ` -[ - "[ 0/1 → 1/2 | note:e4 ]", - "[ 0/1 → 1/1 | note:g3 ]", - "[ 0/1 → 1/1 | note:b3 ]", - "[ 1/2 → 1/1 | note:d4 ]", - "[ 1/1 → 3/2 | note:e4 ]", - "[ 1/1 → 2/1 | note:g3 ]", - "[ 1/1 → 2/1 | note:b3 ]", - "[ 3/2 → 2/1 | note:d4 ]", - "[ 2/1 → 5/2 | note:e4 ]", - "[ 2/1 → 3/1 | note:g3 ]", - "[ 2/1 → 3/1 | note:b3 ]", - "[ 5/2 → 3/1 | note:d4 ]", - "[ 3/1 → 7/2 | note:e4 ]", - "[ 3/1 → 4/1 | note:g3 ]", - "[ 3/1 → 4/1 | note:b3 ]", - "[ 7/2 → 4/1 | note:d4 ]", -] -`; - -exports[`runs examples > example "steps" example index 0 1`] = ` -======= exports[`runs examples > example "stepalt" example index 0 1`] = ` [ "[ 0/1 → 1/5 | s:bd ]", @@ -9835,7 +9817,6 @@ exports[`runs examples > example "stepalt" example index 0 1`] = ` `; exports[`runs examples > example "stepcat" example index 0 1`] = ` ->>>>>>> main [ "[ 0/1 → 3/4 | note:e3 ]", "[ 3/4 → 1/1 | note:g3 ]", @@ -10182,8 +10163,6 @@ exports[`runs examples > example "swingBy" example index 0 1`] = ` ] `; -<<<<<<< HEAD -======= exports[`runs examples > example "sysex" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", @@ -10323,7 +10302,6 @@ exports[`runs examples > example "tour" example index 0 1`] = ` ] `; ->>>>>>> main exports[`runs examples > example "transpose" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C2 ]", From 1dd1ce254f6700e708e4afb838733f17a125301b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 8 Jul 2025 00:30:00 -0400 Subject: [PATCH 275/538] rm deadcode --- packages/core/cyclist copy.mjs | 140 --------------------------------- 1 file changed, 140 deletions(-) delete mode 100644 packages/core/cyclist copy.mjs diff --git a/packages/core/cyclist copy.mjs b/packages/core/cyclist copy.mjs deleted file mode 100644 index fec819548..000000000 --- a/packages/core/cyclist copy.mjs +++ /dev/null @@ -1,140 +0,0 @@ -/* -cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see -Copyright (C) 2022 Strudel contributors - see -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 createClock from './zyklus.mjs'; -import { logger } from './logger.mjs'; - -export class Cyclist { - constructor({ - interval, - onTrigger, - onToggle, - onError, - getTime, - latency = 0.1, - setInterval, - clearInterval, - beforeStart, - }) { - this.started = false; - this.beforeStart = beforeStart; - this.cps = 0.5; - this.num_ticks_since_cps_change = 0; - this.lastTick = 0; // absolute time when last tick (clock callback) happened - this.lastBegin = 0; // query begin of last tick - this.lastEnd = 0; // query end of last tick - this.getTime = getTime; // get absolute time - this.num_cycles_at_cps_change = 0; - this.seconds_at_cps_change; // clock phase when cps was changed - this.onToggle = onToggle; - this.latency = latency; // fixed trigger time offset - this.clock = createClock( - getTime, - // called slightly before each cycle - (phase, duration, _, t) => { - if (this.num_ticks_since_cps_change === 0) { - this.num_cycles_at_cps_change = this.lastEnd; - this.seconds_at_cps_change = phase; - } - this.num_ticks_since_cps_change++; - const seconds_since_cps_change = this.num_ticks_since_cps_change * duration; - const num_cycles_since_cps_change = seconds_since_cps_change * this.cps; - - try { - const begin = this.lastEnd; - this.lastBegin = begin; - const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; - this.lastEnd = end; - this.lastTick = phase; - - if (phase < t) { - // avoid querying haps that are in the past anyway - console.log(`skip query: too late`); - return; - } - - // query the pattern for events - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); - - haps.forEach((hap) => { - if (hap.hasOnset()) { - const targetTime = - (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency; - const duration = hap.duration / this.cps; - // the following line is dumb and only here for backwards compatibility - // see https://codeberg.org/uzu/strudel/pulls/1004 - const deadline = targetTime - phase; - // this onTrigger has another signature - onTrigger?.(hap, deadline, duration, this.cps, targetTime); - if (hap.value.cps !== undefined && this.cps != hap.value.cps) { - this.cps = hap.value.cps; - this.num_ticks_since_cps_change = 0; - } - } - }); - } catch (e) { - logger(`[cyclist] error: ${e.message}`); - onError?.(e); - } - }, - interval, // duration of each cycle - 0.1, - 0.1, - setInterval, - clearInterval, - ); - } - now() { - if (!this.started) { - return 0; - } - const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; - return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; - } - setStarted(v) { - this.started = v; - this.onToggle?.(v); - } - async start() { - await this.beforeStart?.(); - this.num_ticks_since_cps_change = 0; - this.num_cycles_at_cps_change = 0; - if (!this.pattern) { - throw new Error('Scheduler: no pattern set! call .setPattern first.'); - } - logger('[cyclist] start'); - this.clock.start(); - this.setStarted(true); - } - pause() { - logger('[cyclist] pause'); - this.clock.pause(); - this.setStarted(false); - } - stop() { - logger('[cyclist] stop'); - this.clock.stop(); - this.lastEnd = 0; - this.setStarted(false); - } - async setPattern(pat, autostart = false) { - this.pattern = pat; - if (autostart && !this.started) { - await this.start(); - } - } - setCps(cps = 0.5) { - if (this.cps === cps) { - return; - } - this.cps = cps; - this.num_ticks_since_cps_change = 0; - } - log(begin, end, haps) { - const onsets = haps.filter((h) => h.hasOnset()); - console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); - } -} From 6327e17feb4b1a7f443134ab24f0e31a086911ec Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 8 Jul 2025 00:44:57 -0400 Subject: [PATCH 276/538] remove unessecary cyclist changes --- packages/core/cyclist.mjs | 112 ++++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 46 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 6864301db..f28dc604c 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -1,6 +1,6 @@ /* -cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see -Copyright (C) 2022 Strudel contributors - see +cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see +Copyright (C) 2022 Strudel contributors - see 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 . */ @@ -8,47 +8,76 @@ import createClock from './zyklus.mjs'; import { logger } from './logger.mjs'; export class Cyclist { - constructor({ interval = 0.05, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) { + constructor({ + interval, + onTrigger, + onToggle, + onError, + getTime, + latency = 0.1, + setInterval, + clearInterval, + beforeStart, + }) { this.started = false; + this.beforeStart = beforeStart; this.cps = 0.5; - this.time_at_last_tick_message = 0; - this.cycle = 0; + this.num_ticks_since_cps_change = 0; + this.lastTick = 0; // absolute time when last tick (clock callback) happened + this.lastBegin = 0; // query begin of last tick + this.lastEnd = 0; // query end of last tick this.getTime = getTime; // get absolute time this.num_cycles_at_cps_change = 0; this.seconds_at_cps_change; // clock phase when cps was changed - this.num_ticks_since_cps_change = 0; this.onToggle = onToggle; this.latency = latency; // fixed trigger time offset - - this.interval = interval; - this.clock = createClock( getTime, // called slightly before each cycle - (lastTick, duration, _, time) => { - if (this.started === false) { - return; + (phase, duration, _, t) => { + if (this.num_ticks_since_cps_change === 0) { + this.num_cycles_at_cps_change = this.lastEnd; + this.seconds_at_cps_change = phase; } - const num_seconds_since_cps_change = this.num_ticks_since_cps_change * duration; - const tickdeadline = lastTick - time; - const num_cycles_since_cps_change = num_seconds_since_cps_change * this.cps; - const begin = this.num_cycles_at_cps_change + num_cycles_since_cps_change; - const secondsSinceLastTick = time - lastTick - duration; - const eventLength = duration * this.cps; - const end = begin + eventLength; - this.cycle = begin + secondsSinceLastTick * this.cps; - - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); - haps.forEach((hap) => { - if (hap.hasOnset()) { - let targetTime = (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps; - targetTime = targetTime + this.latency + tickdeadline + time - num_seconds_since_cps_change; - const duration = hap.duration / this.cps; - onTrigger?.(hap, tickdeadline, duration, this.cps, targetTime); - } - }); - this.time_at_last_tick_message = time; this.num_ticks_since_cps_change++; + const seconds_since_cps_change = this.num_ticks_since_cps_change * duration; + const num_cycles_since_cps_change = seconds_since_cps_change * this.cps; + + try { + const begin = this.lastEnd; + this.lastBegin = begin; + const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; + this.lastEnd = end; + this.lastTick = phase; + + if (phase < t) { + // avoid querying haps that are in the past anyway + console.log(`skip query: too late`); + return; + } + + // query the pattern for events + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + + haps.forEach((hap) => { + if (hap.hasOnset()) { + const targetTime = + (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency; + const duration = hap.duration / this.cps; + // the following line is dumb and only here for backwards compatibility + // see https://codeberg.org/uzu/strudel/pulls/1004 + const deadline = targetTime - phase; + onTrigger?.(hap, deadline, duration, this.cps, targetTime); + if (hap.value.cps !== undefined && this.cps != hap.value.cps) { + this.cps = hap.value.cps; + this.num_ticks_since_cps_change = 0; + } + } + }); + } catch (e) { + logger(`[cyclist] error: ${e.message}`); + onError?.(e); + } }, interval, // duration of each cycle 0.1, @@ -61,24 +90,17 @@ export class Cyclist { if (!this.started) { return 0; } - const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps; - return this.cycle + gap; + const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; + return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; } - - setCycle = (cycle) => { - this.num_ticks_since_cps_change = 0; - this.num_cycles_at_cps_change = cycle; - }; setStarted(v) { this.started = v; - - this.setCycle(0); this.onToggle?.(v); } - start() { + async start() { + await this.beforeStart?.(); this.num_ticks_since_cps_change = 0; this.num_cycles_at_cps_change = 0; - if (!this.pattern) { throw new Error('Scheduler: no pattern set! call .setPattern first.'); } @@ -97,18 +119,16 @@ export class Cyclist { this.lastEnd = 0; this.setStarted(false); } - setPattern(pat, autostart = false) { + async setPattern(pat, autostart = false) { this.pattern = pat; if (autostart && !this.started) { - this.start(); + await this.start(); } } setCps(cps = 0.5) { if (this.cps === cps) { return; } - const num_seconds_since_cps_change = this.num_ticks_since_cps_change * this.interval; - this.num_cycles_at_cps_change = this.num_cycles_at_cps_change + num_seconds_since_cps_change * cps; this.cps = cps; this.num_ticks_since_cps_change = 0; } From 9b3c6b9f9483ccf6fe60a3964bf24afbd9d219db Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 8 Jul 2025 00:46:39 -0400 Subject: [PATCH 277/538] c --- packages/core/cyclist.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index f28dc604c..fec819548 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -67,6 +67,7 @@ export class Cyclist { // the following line is dumb and only here for backwards compatibility // see https://codeberg.org/uzu/strudel/pulls/1004 const deadline = targetTime - phase; + // this onTrigger has another signature onTrigger?.(hap, deadline, duration, this.cps, targetTime); if (hap.value.cps !== undefined && this.cps != hap.value.cps) { this.cps = hap.value.cps; From 8defdc424c94bca5a1341d6d08e4253fd1978f7d Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Tue, 8 Jul 2025 17:57:24 -0700 Subject: [PATCH 278/538] added tab-indent setting --- packages/codemirror/codemirror.mjs | 4 +++- website/src/repl/components/panel/SettingsTab.jsx | 6 ++++++ website/src/settings.mjs | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 193d96b55..6ba1d9c40 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 } from '@codemirror/commands'; +import { history, indentWithTab } from '@codemirror/commands'; import { javascript } from '@codemirror/lang-javascript'; import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language'; import { Compartment, EditorState, Prec } from '@codemirror/state'; @@ -37,6 +37,7 @@ const extensions = { isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []), isFlashEnabled, keybindings, + isTabIndentationEnabled: (on) => (on ? keymap.of([indentWithTab]) : []), }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); @@ -51,6 +52,7 @@ export const defaultSettings = { isFlashEnabled: true, isTooltipEnabled: false, isLineWrappingEnabled: false, + isTabIndentationEnabled: false, theme: 'strudelTheme', fontFamily: 'monospace', fontSize: 18, diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 5be9b602e..e82d03d87 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -109,6 +109,7 @@ export function SettingsTab({ started }) { togglePanelTrigger, maxPolyphony, multiChannelOrbits, + isTabIndentationEnabled, } = useSettings(); const shouldAlwaysSync = isUdels(); const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; @@ -262,6 +263,11 @@ export function SettingsTab({ started }) { onChange={(cbEvent) => settingsMap.setKey('isLineWrappingEnabled', cbEvent.target.checked)} value={isLineWrappingEnabled} /> + settingsMap.setKey('isTabIndentationEnabled', cbEvent.target.checked)} + value={isTabIndentationEnabled} + /> settingsMap.setKey('isFlashEnabled', cbEvent.target.checked)} diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 84b433141..6212a6da0 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -21,6 +21,7 @@ export const defaultSettings = { isSyncEnabled: false, isLineWrappingEnabled: false, isPatternHighlightingEnabled: true, + isTabIndentationEnabled: false, theme: 'strudelTheme', fontFamily: 'monospace', fontSize: 18, @@ -77,6 +78,7 @@ export function useSettings() { isLineWrappingEnabled: parseBoolean(state.isLineWrappingEnabled), isFlashEnabled: parseBoolean(state.isFlashEnabled), isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled), + isTabIndentationEnabled: parseBoolean(state.isTabIndentationEnabled), fontSize: Number(state.fontSize), panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is! isPanelPinned: parseBoolean(state.isPanelPinned), From 63fcce79e000f971badc5e7ddb770a91b8519680 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 8 Jul 2025 21:24:27 -0400 Subject: [PATCH 279/538] default cycle --- packages/core/neocyclist.mjs | 2 +- packages/superdough/superdough.mjs | 2 +- packages/webaudio/webaudio.mjs | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 4e6d385d8..261f08acb 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -45,7 +45,7 @@ export class NeoCyclist { const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps); const targetTime = timeUntilTrigger + currentTime + this.latency; const duration = cycleToSeconds(hap.duration, this.cps); - onTrigger?.(hap, 0, duration, this.cps, targetTime, this.cycle); + onTrigger?.(hap, 0, duration, this.cps, targetTime); } }); }; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 198e7b418..109a521c0 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -482,7 +482,7 @@ function mapChannelNumbers(channels) { return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); } -export const superdough = async (value, t, hapDuration, cps = 0.5, cycle) => { +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(); diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 1e8455b3f..72dc687b0 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -23,8 +23,7 @@ export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { t, hapDuration, cps, - hap.whole.begin.valueOf(), - // cycle + hap.whole?.begin.valueOf(), ); }; From edb347e85a1c56d97a76e92e2a1f6cd8c7e13eec Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 8 Jul 2025 22:24:54 -0400 Subject: [PATCH 280/538] format --- packages/webaudio/webaudio.mjs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 72dc687b0..429d2a26b 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -18,13 +18,7 @@ const hap2value = (hap) => { // uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 // TODO: refactor output callbacks to eliminate deadline export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { - return superdough( - hap2value(hap), - t, - hapDuration, - cps, - hap.whole?.begin.valueOf(), - ); + return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf()); }; export function webaudioRepl(options = {}) { From bfdfab0a007ef880678e34f964a332551dab50b9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 9 Jul 2025 09:33:11 +0200 Subject: [PATCH 281/538] fix: @synonym -> @synonyms --- jsdoc/jsdoc-synonyms.js | 2 +- packages/core/controls.mjs | 2 +- packages/core/pattern.mjs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/jsdoc/jsdoc-synonyms.js b/jsdoc/jsdoc-synonyms.js index 0b52420bc..d59c8dac4 100644 --- a/jsdoc/jsdoc-synonyms.js +++ b/jsdoc/jsdoc-synonyms.js @@ -1,5 +1,5 @@ /* -jsdoc-synonyms.js - Add support for @synonym tag +jsdoc-synonyms.js - Add support for @synonyms tag Copyright (C) 2023 Strudel contributors - see 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 . */ diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index a0f5d10e3..5eb8e5da8 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -448,7 +448,7 @@ export const { coarse } = registerControl('coarse'); * modulate the amplitude of a sound with a continuous waveform * * @name am - * @synonym tremolo + * @synonyms tremolo * @param {number | Pattern} speed modulation speed in HZ * @example * s("triangle").am("2").amshape("").amdepth(.5) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d9bb94075..d59a1285d 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2522,7 +2522,7 @@ export const { fastchunk, fastChunk } = register( /** * Like `chunk`, but the function is applied to a looped subcycle of the source pattern. * @name chunkInto - * @synonym chunkinto + * @synonyms chunkinto * @memberof Pattern * @example * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) @@ -2535,7 +2535,7 @@ export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], fun /** * Like `chunkInto`, but moves backwards through the chunks. * @name chunkBackInto - * @synonym chunkbackinto + * @synonyms chunkbackinto * @memberof Pattern * @example * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) @@ -2565,7 +2565,7 @@ export const bypass = register( * Loops the pattern inside an `offset` for `cycles`. * If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it. * @name ribbon - * @synonym rib + * @synonyms rib * @param {number} offset start point of loop in cycles * @param {number} cycles loop length in cycles * @example From cf7fc23981df38e2fffdc25c675a3ec615188704 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 9 Jul 2025 09:33:28 +0200 Subject: [PATCH 282/538] add new am examples to doc --- website/src/pages/learn/effects.mdx | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index 2f7eaa7f8..9c96dbe31 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -57,6 +57,34 @@ Each filter has 2 parameters: +# Amplitude Modulation + +Amplitude modulation changes the amplitude (gain) periodically over time. + +## am + + + +## amsync + + + +## amdepth + + + +## amskew + + + +## amphase + + + +## amshape + + + # Amplitude Envelope The amplitude [envelope]() controls the dynamic contour of a sound. From d645cc6fe504ef850e3cb03d1113da7969f5e772 Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Wed, 9 Jul 2025 13:28:22 -0700 Subject: [PATCH 283/538] fixed keybinding presedence issue --- packages/codemirror/keybindings.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index 6fe00eda1..05a211636 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -26,6 +26,5 @@ const keymaps = { export function keybindings(name) { const active = keymaps[name]; - return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []]; - // keymap.of(searchKeymap), + return [active ? active() : [], keymap.of(historyKeymap), keymap.of(defaultKeymap)]; } From bf3de368f070ce875847b76f3da9b01d1bcff0e8 Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Thu, 10 Jul 2025 12:11:10 -0700 Subject: [PATCH 284/538] added codemirror as an option + removed fallback on defaults --- packages/codemirror/keybindings.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index 05a211636..1201959a2 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -21,10 +21,11 @@ const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []); const keymaps = { vim, emacs, + codemirror: () => keymap.of(defaultKeymap), vscode: vscodeExtension, }; export function keybindings(name) { const active = keymaps[name]; - return [active ? active() : [], keymap.of(historyKeymap), keymap.of(defaultKeymap)]; + return [active ? active() : [], keymap.of(historyKeymap)]; } From e7e321fe0300103c52f9e795be6addbec437218e Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Thu, 10 Jul 2025 15:03:22 -0700 Subject: [PATCH 285/538] added multicursor support on ctrl/cmd + click --- packages/codemirror/codemirror.mjs | 3 +++ website/src/repl/components/panel/SettingsTab.jsx | 6 ++++++ website/src/settings.mjs | 2 ++ 3 files changed, 11 insertions(+) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 6ba1d9c40..c4bdaca3f 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -38,6 +38,8 @@ const extensions = { isFlashEnabled, keybindings, isTabIndentationEnabled: (on) => (on ? keymap.of([indentWithTab]) : []), + isMultiCursorEnabled: (on) => + on ? [EditorState.allowMultipleSelections.of(true), EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey)] : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); @@ -53,6 +55,7 @@ export const defaultSettings = { isTooltipEnabled: false, isLineWrappingEnabled: false, isTabIndentationEnabled: false, + isMultiCursorEnabled: false, theme: 'strudelTheme', fontFamily: 'monospace', fontSize: 18, diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index e82d03d87..cf2978a1a 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -110,6 +110,7 @@ export function SettingsTab({ started }) { maxPolyphony, multiChannelOrbits, isTabIndentationEnabled, + isMultiCursorEnabled, } = useSettings(); const shouldAlwaysSync = isUdels(); const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; @@ -268,6 +269,11 @@ export function SettingsTab({ started }) { onChange={(cbEvent) => settingsMap.setKey('isTabIndentationEnabled', cbEvent.target.checked)} value={isTabIndentationEnabled} /> + settingsMap.setKey('isMultiCursorEnabled', cbEvent.target.checked)} + value={isMultiCursorEnabled} + /> settingsMap.setKey('isFlashEnabled', cbEvent.target.checked)} diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 6212a6da0..3d99b656c 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -22,6 +22,7 @@ export const defaultSettings = { isLineWrappingEnabled: false, isPatternHighlightingEnabled: true, isTabIndentationEnabled: false, + isMultiCursorEnabled: false, theme: 'strudelTheme', fontFamily: 'monospace', fontSize: 18, @@ -79,6 +80,7 @@ export function useSettings() { isFlashEnabled: parseBoolean(state.isFlashEnabled), isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled), isTabIndentationEnabled: parseBoolean(state.isTabIndentationEnabled), + isMultiCursorEnabled: parseBoolean(state.isMultiCursorEnabled), fontSize: Number(state.fontSize), panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is! isPanelPinned: parseBoolean(state.isPanelPinned), From eb4f1f69f6b3cbc20382444e9ddc07d2ef0d2ce0 Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Thu, 10 Jul 2025 17:28:56 -0700 Subject: [PATCH 286/538] pnpm codeformat --- packages/codemirror/codemirror.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index c4bdaca3f..f47f6eba5 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -39,7 +39,12 @@ const extensions = { keybindings, isTabIndentationEnabled: (on) => (on ? keymap.of([indentWithTab]) : []), isMultiCursorEnabled: (on) => - on ? [EditorState.allowMultipleSelections.of(true), EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey)] : [], + on + ? [ + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] + : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); From 735f57e27610a17113b9e13a2c8b3bfe2408757d Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 10 Jul 2025 23:18:57 -0400 Subject: [PATCH 287/538] working --- packages/superdough/superdough.mjs | 18 +++++++++++++----- packages/superdough/worklets.mjs | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 109a521c0..dbee8ae4b 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -28,6 +28,13 @@ 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 = {}) { @@ -337,6 +344,7 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) { } export function getLfo(audioContext, begin, end, properties = {}) { + const { shape = 0, ...props } = properties; const { dcoffset = -0.5, depth = 1 } = properties; return getWorklet(audioContext, 'lfo-processor', { frequency: 1, @@ -346,12 +354,12 @@ export function getLfo(audioContext, begin, end, properties = {}) { time: begin, begin, end, - shape: 1, + shape: getModulationShapeInput(shape), dcoffset, min: dcoffset - depth * 0.5, max: dcoffset + depth * 0.5, curve: 1, - ...properties, + ...props, }); } @@ -514,9 +522,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) am, amsync, amdepth = 1, - amskew = 1, + amskew, amphase = 0, - amshape = 0, + amshape, s = getDefaultValue('s'), bank, source, @@ -745,7 +753,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const time = cycle / cps; const lfo = getLfo(ac, t, endWithRelease, { - skew: amskew, + skew: amskew ?? (amshape != null ? 0.5 : 1), frequency: am, depth: amdepth, time, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 466be558a..b4b36f6c9 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -31,7 +31,7 @@ function polyBlep(phase, dt) { return 0; } } - +// The order is important for dough integration const waveshapes = { tri(phase, skew = 0.5) { const x = 1 - skew; From 16e9aca2221a08a5c01fd45649b5967f35a4312c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 15 Jul 2025 09:33:14 +0200 Subject: [PATCH 288/538] fix: can now use def in mondough --- packages/mondough/mondough.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 6e8278939..15feb5d86 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -42,6 +42,7 @@ lib['%'] = pace; lib['?'] = degradeBy; // todo: default 0.5 not working.. lib[':'] = tail; lib['..'] = range; +lib['def'] = () => silence; lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]" //lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct From 1fdd87a4f3c4b0ede205d149edfca1325c5bdf78 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 15 Jul 2025 09:35:24 +0200 Subject: [PATCH 289/538] doc: def --- website/src/pages/learn/mondo-notation.mdx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index 3b00b9902..e2b0c4027 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -176,3 +176,16 @@ $ chord # voicing /> The `$` sign is an alias for `,` so it will create a stack behind the scenes. + +## variables + +using the `def` keyword, you can define variables: + + From 5632d83afb124d679be292890a49ffd31b34e690 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 16 Jul 2025 02:21:39 +0200 Subject: [PATCH 290/538] 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 291/538] 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 292/538] 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 293/538] 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 34874862ab283ec700caf1e102e0acd26f08dd14 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 18 Jul 2025 22:03:33 -0400 Subject: [PATCH 294/538] change params --- packages/core/controls.mjs | 56 ++- packages/superdough/superdough.mjs | 33 +- test/__snapshots__/examples.test.mjs.snap | 418 +++++++++++----------- website/src/pages/learn/effects.mdx | 20 +- 4 files changed, 261 insertions(+), 266 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 5eb8e5da8..d09fbbc21 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -447,68 +447,72 @@ export const { coarse } = registerControl('coarse'); /** * modulate the amplitude of a sound with a continuous waveform * - * @name am - * @synonyms tremolo + * @name tremolo + * @synonyms trem * @param {number | Pattern} speed modulation speed in HZ * @example - * s("triangle").am("2").amshape("").amdepth(.5) + * note("d d d# d".fast(4)).s("supersaw").tremolo("<3 2 100> ").tremoloskew("<.5>") * */ -export const { am, tremolo } = registerControl(['am', 'amdepth', 'amskew', 'amphase'], 'tremolo'); +export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem'); /** * modulate the amplitude of a sound with a continuous waveform * - * @name amsync + * @name tremolosync + * @synonyms tremsync * @param {number | Pattern} cycles modulation speed in cycles * @example - * s("supersaw").amsync(1/4).amskew("<0 .5 1>").amdepth(2) + * note("d d d# d".fast(4)).s("supersaw").tremolosync("4").tremoloskew("<1 .5 0>") * */ -export const { amsync } = registerControl(['amsync', 'amdepth', 'amskew', 'amphase']); +export const { tremolosync } = registerControl(['tremolosync', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'tremsync'); /** * depth of amplitude modulation * - * @name amdepth + * @name tremolodepth + * @synonyms tremdepth * @param {number | Pattern} depth * @example - * s("triangle").am(1).amdepth("1") + * note("a1 a1 a#1 a1".fast(4)).s("pulse").tremsync(4).tremolodepth("<1 2 .7>") * */ -export const { amdepth } = registerControl('amdepth'); +export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth'); /** * alter the shape of the modulation waveform * - * @name amskew + * @name tremoloskew + * @synonyms tremskew * @param {number | Pattern} amount between 0 & 1, the shape of the waveform * @example - * note("{f a c e}%16").am(5).amskew(.5).amdepth("<1 0.5 2>").att(.01).rel(.03) + * note("{f a c e}%16").s("sawtooth").tremsync(4).tremoloskew("<.5 0 1>") * */ -export const { amskew } = registerControl('amskew'); +export const { tremoloskew } = registerControl('tremoloskew', 'tremskew'); /** * alter the phase of the modulation waveform * - * @name amphase + * @name tremolophase + * @synonyms tremphase * @param {number | Pattern} offset the offset in cycles of the modulation * @example - * note("{f a c e}%16").s("sawtooth").am(4).amphase("<0 .25 .66>") + * note("{f a c e}%16").s("sawtooth").tremsync(4).tremolophase("<0 .25 .66>") * */ -export const { amphase } = registerControl('amphase'); +export const { tremolophase } = registerControl('tremolophase', 'tremphase'); /** * shape of amplitude modulation * - * @name amshape + * @name tremoloshape * @param {number | Pattern} shape tri | square | sine | saw | ramp * @example - * note("{f g c d}%16").am(4).amshape("ramp").s("sawtooth") + * note("{f g c d}%16").tremsync(4).tremoloshape("").s("sawtooth") * */ -export const { amshape } = registerControl('amshape'); +export const { tremoloshape } = registerControl('tremoloshape', 'tremshape'); /** * filter overdrive for supported filter types * @@ -1650,18 +1654,8 @@ export const { density } = registerControl('density'); // ['modwheel'], export const { expression } = registerControl('expression'); export const { sustainpedal } = registerControl('sustainpedal'); -/* // TODO: doesn't seem to do anything - * - * Tremolo Audio DSP effect - * - * @name tremolodepth - * @param {number | Pattern} depth between 0 and 1 - * @example - * n("0,4,7").tremolodepth("<0 .3 .6 .9>").osc() - * - */ -export const { tremolodepth, tremdp } = registerControl('tremolodepth', 'tremdp'); -export const { tremolorate, tremr } = registerControl('tremolorate', 'tremr'); + + export const { fshift } = registerControl('fshift'); export const { fshiftnote } = registerControl('fshiftnote'); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index dbee8ae4b..6ce673d5b 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -519,12 +519,12 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } // destructure let { - am, - amsync, - amdepth = 1, - amskew, - amphase = 0, - amshape, + tremolo, + tremolosync, + tremolodepth = 1, + tremoloskew, + tremolophase = 0, + tremoloshape, s = getDefaultValue('s'), bank, source, @@ -610,7 +610,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) distortvol = applyGainCurve(distortvol); delay = applyGainCurve(delay); velocity = applyGainCurve(velocity); - amdepth = applyGainCurve(amdepth); + tremolodepth = applyGainCurve(tremolodepth); gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future const end = t + hapDuration; @@ -742,24 +742,25 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); - if (amsync != null) { - am = cps / amsync; + if (tremolosync != null) { + tremolo = cps * tremolosync; } - if (am !== undefined) { + + if (tremolo !== undefined) { // Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload // EX: a triangle waveform will clip like this /-\ when the depth is above 1 - const gain = Math.max(1 - amdepth, 0); + const gain = Math.max(1 - tremolodepth, 0); const amGain = new GainNode(ac, { gain }); const time = cycle / cps; const lfo = getLfo(ac, t, endWithRelease, { - skew: amskew ?? (amshape != null ? 0.5 : 1), - frequency: am, - depth: amdepth, + skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1), + frequency: tremolo, + depth: tremolodepth, time, dcoffset: 0, - shape: amshape, - phaseoffset: amphase, + shape: tremoloshape, + phaseoffset: tremolophase, min: 0, max: 1, curve: 1.5, diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 500392041..7ad7c48b9 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -781,19 +781,19 @@ exports[`runs examples > example "always" example index 0 1`] = ` exports[`runs examples > example "am" example index 0 1`] = ` [ - "[ 0/1 → 1/1 | s:triangle am:2 amshape:tri amdepth:0.5 ]", - "[ 1/1 → 2/1 | s:triangle am:2 amshape:saw amdepth:0.5 ]", - "[ 2/1 → 3/1 | s:triangle am:2 amshape:ramp amdepth:0.5 ]", - "[ 3/1 → 4/1 | s:triangle am:2 amshape:square amdepth:0.5 ]", + "[ 0/1 → 1/1 | s:triangle am:2 tremoloshape:tri tremolodepth:0.5 ]", + "[ 1/1 → 2/1 | s:triangle am:2 tremoloshape:saw tremolodepth:0.5 ]", + "[ 2/1 → 3/1 | s:triangle am:2 tremoloshape:ramp tremolodepth:0.5 ]", + "[ 3/1 → 4/1 | s:triangle am:2 tremoloshape:square tremolodepth:0.5 ]", ] `; -exports[`runs examples > example "amdepth" example index 0 1`] = ` +exports[`runs examples > example "tremolodepth" example index 0 1`] = ` [ - "[ 0/1 → 1/1 | s:triangle am:1 amdepth:1 ]", - "[ 1/1 → 2/1 | s:triangle am:1 amdepth:1 ]", - "[ 2/1 → 3/1 | s:triangle am:1 amdepth:1 ]", - "[ 3/1 → 4/1 | s:triangle am:1 amdepth:1 ]", + "[ 0/1 → 1/1 | s:triangle am:1 tremolodepth:1 ]", + "[ 1/1 → 2/1 | s:triangle am:1 tremolodepth:1 ]", + "[ 2/1 → 3/1 | s:triangle am:1 tremolodepth:1 ]", + "[ 3/1 → 4/1 | s:triangle am:1 tremolodepth:1 ]", ] `; @@ -858,219 +858,219 @@ exports[`runs examples > example "amp" example index 0 1`] = ` ] `; -exports[`runs examples > example "amphase" example index 0 1`] = ` +exports[`runs examples > example "tremolophase" example index 0 1`] = ` [ - "[ 0/1 → 1/16 | note:f s:sawtooth am:4 amphase:0 ]", - "[ 1/16 → 1/8 | note:a s:sawtooth am:4 amphase:0 ]", - "[ 1/8 → 3/16 | note:c s:sawtooth am:4 amphase:0 ]", - "[ 3/16 → 1/4 | note:e s:sawtooth am:4 amphase:0 ]", - "[ 1/4 → 5/16 | note:f s:sawtooth am:4 amphase:0 ]", - "[ 5/16 → 3/8 | note:a s:sawtooth am:4 amphase:0 ]", - "[ 3/8 → 7/16 | note:c s:sawtooth am:4 amphase:0 ]", - "[ 7/16 → 1/2 | note:e s:sawtooth am:4 amphase:0 ]", - "[ 1/2 → 9/16 | note:f s:sawtooth am:4 amphase:0 ]", - "[ 9/16 → 5/8 | note:a s:sawtooth am:4 amphase:0 ]", - "[ 5/8 → 11/16 | note:c s:sawtooth am:4 amphase:0 ]", - "[ 11/16 → 3/4 | note:e s:sawtooth am:4 amphase:0 ]", - "[ 3/4 → 13/16 | note:f s:sawtooth am:4 amphase:0 ]", - "[ 13/16 → 7/8 | note:a s:sawtooth am:4 amphase:0 ]", - "[ 7/8 → 15/16 | note:c s:sawtooth am:4 amphase:0 ]", - "[ 15/16 → 1/1 | note:e s:sawtooth am:4 amphase:0 ]", - "[ 1/1 → 17/16 | note:f s:sawtooth am:4 amphase:0.25 ]", - "[ 17/16 → 9/8 | note:a s:sawtooth am:4 amphase:0.25 ]", - "[ 9/8 → 19/16 | note:c s:sawtooth am:4 amphase:0.25 ]", - "[ 19/16 → 5/4 | note:e s:sawtooth am:4 amphase:0.25 ]", - "[ 5/4 → 21/16 | note:f s:sawtooth am:4 amphase:0.25 ]", - "[ 21/16 → 11/8 | note:a s:sawtooth am:4 amphase:0.25 ]", - "[ 11/8 → 23/16 | note:c s:sawtooth am:4 amphase:0.25 ]", - "[ 23/16 → 3/2 | note:e s:sawtooth am:4 amphase:0.25 ]", - "[ 3/2 → 25/16 | note:f s:sawtooth am:4 amphase:0.25 ]", - "[ 25/16 → 13/8 | note:a s:sawtooth am:4 amphase:0.25 ]", - "[ 13/8 → 27/16 | note:c s:sawtooth am:4 amphase:0.25 ]", - "[ 27/16 → 7/4 | note:e s:sawtooth am:4 amphase:0.25 ]", - "[ 7/4 → 29/16 | note:f s:sawtooth am:4 amphase:0.25 ]", - "[ 29/16 → 15/8 | note:a s:sawtooth am:4 amphase:0.25 ]", - "[ 15/8 → 31/16 | note:c s:sawtooth am:4 amphase:0.25 ]", - "[ 31/16 → 2/1 | note:e s:sawtooth am:4 amphase:0.25 ]", - "[ 2/1 → 33/16 | note:f s:sawtooth am:4 amphase:0.66 ]", - "[ 33/16 → 17/8 | note:a s:sawtooth am:4 amphase:0.66 ]", - "[ 17/8 → 35/16 | note:c s:sawtooth am:4 amphase:0.66 ]", - "[ 35/16 → 9/4 | note:e s:sawtooth am:4 amphase:0.66 ]", - "[ 9/4 → 37/16 | note:f s:sawtooth am:4 amphase:0.66 ]", - "[ 37/16 → 19/8 | note:a s:sawtooth am:4 amphase:0.66 ]", - "[ 19/8 → 39/16 | note:c s:sawtooth am:4 amphase:0.66 ]", - "[ 39/16 → 5/2 | note:e s:sawtooth am:4 amphase:0.66 ]", - "[ 5/2 → 41/16 | note:f s:sawtooth am:4 amphase:0.66 ]", - "[ 41/16 → 21/8 | note:a s:sawtooth am:4 amphase:0.66 ]", - "[ 21/8 → 43/16 | note:c s:sawtooth am:4 amphase:0.66 ]", - "[ 43/16 → 11/4 | note:e s:sawtooth am:4 amphase:0.66 ]", - "[ 11/4 → 45/16 | note:f s:sawtooth am:4 amphase:0.66 ]", - "[ 45/16 → 23/8 | note:a s:sawtooth am:4 amphase:0.66 ]", - "[ 23/8 → 47/16 | note:c s:sawtooth am:4 amphase:0.66 ]", - "[ 47/16 → 3/1 | note:e s:sawtooth am:4 amphase:0.66 ]", - "[ 3/1 → 49/16 | note:f s:sawtooth am:4 amphase:0 ]", - "[ 49/16 → 25/8 | note:a s:sawtooth am:4 amphase:0 ]", - "[ 25/8 → 51/16 | note:c s:sawtooth am:4 amphase:0 ]", - "[ 51/16 → 13/4 | note:e s:sawtooth am:4 amphase:0 ]", - "[ 13/4 → 53/16 | note:f s:sawtooth am:4 amphase:0 ]", - "[ 53/16 → 27/8 | note:a s:sawtooth am:4 amphase:0 ]", - "[ 27/8 → 55/16 | note:c s:sawtooth am:4 amphase:0 ]", - "[ 55/16 → 7/2 | note:e s:sawtooth am:4 amphase:0 ]", - "[ 7/2 → 57/16 | note:f s:sawtooth am:4 amphase:0 ]", - "[ 57/16 → 29/8 | note:a s:sawtooth am:4 amphase:0 ]", - "[ 29/8 → 59/16 | note:c s:sawtooth am:4 amphase:0 ]", - "[ 59/16 → 15/4 | note:e s:sawtooth am:4 amphase:0 ]", - "[ 15/4 → 61/16 | note:f s:sawtooth am:4 amphase:0 ]", - "[ 61/16 → 31/8 | note:a s:sawtooth am:4 amphase:0 ]", - "[ 31/8 → 63/16 | note:c s:sawtooth am:4 amphase:0 ]", - "[ 63/16 → 4/1 | note:e s:sawtooth am:4 amphase:0 ]", + "[ 0/1 → 1/16 | note:f s:sawtooth am:4 tremolophase:0 ]", + "[ 1/16 → 1/8 | note:a s:sawtooth am:4 tremolophase:0 ]", + "[ 1/8 → 3/16 | note:c s:sawtooth am:4 tremolophase:0 ]", + "[ 3/16 → 1/4 | note:e s:sawtooth am:4 tremolophase:0 ]", + "[ 1/4 → 5/16 | note:f s:sawtooth am:4 tremolophase:0 ]", + "[ 5/16 → 3/8 | note:a s:sawtooth am:4 tremolophase:0 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth am:4 tremolophase:0 ]", + "[ 7/16 → 1/2 | note:e s:sawtooth am:4 tremolophase:0 ]", + "[ 1/2 → 9/16 | note:f s:sawtooth am:4 tremolophase:0 ]", + "[ 9/16 → 5/8 | note:a s:sawtooth am:4 tremolophase:0 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth am:4 tremolophase:0 ]", + "[ 11/16 → 3/4 | note:e s:sawtooth am:4 tremolophase:0 ]", + "[ 3/4 → 13/16 | note:f s:sawtooth am:4 tremolophase:0 ]", + "[ 13/16 → 7/8 | note:a s:sawtooth am:4 tremolophase:0 ]", + "[ 7/8 → 15/16 | note:c s:sawtooth am:4 tremolophase:0 ]", + "[ 15/16 → 1/1 | note:e s:sawtooth am:4 tremolophase:0 ]", + "[ 1/1 → 17/16 | note:f s:sawtooth am:4 tremolophase:0.25 ]", + "[ 17/16 → 9/8 | note:a s:sawtooth am:4 tremolophase:0.25 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth am:4 tremolophase:0.25 ]", + "[ 19/16 → 5/4 | note:e s:sawtooth am:4 tremolophase:0.25 ]", + "[ 5/4 → 21/16 | note:f s:sawtooth am:4 tremolophase:0.25 ]", + "[ 21/16 → 11/8 | note:a s:sawtooth am:4 tremolophase:0.25 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth am:4 tremolophase:0.25 ]", + "[ 23/16 → 3/2 | note:e s:sawtooth am:4 tremolophase:0.25 ]", + "[ 3/2 → 25/16 | note:f s:sawtooth am:4 tremolophase:0.25 ]", + "[ 25/16 → 13/8 | note:a s:sawtooth am:4 tremolophase:0.25 ]", + "[ 13/8 → 27/16 | note:c s:sawtooth am:4 tremolophase:0.25 ]", + "[ 27/16 → 7/4 | note:e s:sawtooth am:4 tremolophase:0.25 ]", + "[ 7/4 → 29/16 | note:f s:sawtooth am:4 tremolophase:0.25 ]", + "[ 29/16 → 15/8 | note:a s:sawtooth am:4 tremolophase:0.25 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth am:4 tremolophase:0.25 ]", + "[ 31/16 → 2/1 | note:e s:sawtooth am:4 tremolophase:0.25 ]", + "[ 2/1 → 33/16 | note:f s:sawtooth am:4 tremolophase:0.66 ]", + "[ 33/16 → 17/8 | note:a s:sawtooth am:4 tremolophase:0.66 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth am:4 tremolophase:0.66 ]", + "[ 35/16 → 9/4 | note:e s:sawtooth am:4 tremolophase:0.66 ]", + "[ 9/4 → 37/16 | note:f s:sawtooth am:4 tremolophase:0.66 ]", + "[ 37/16 → 19/8 | note:a s:sawtooth am:4 tremolophase:0.66 ]", + "[ 19/8 → 39/16 | note:c s:sawtooth am:4 tremolophase:0.66 ]", + "[ 39/16 → 5/2 | note:e s:sawtooth am:4 tremolophase:0.66 ]", + "[ 5/2 → 41/16 | note:f s:sawtooth am:4 tremolophase:0.66 ]", + "[ 41/16 → 21/8 | note:a s:sawtooth am:4 tremolophase:0.66 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth am:4 tremolophase:0.66 ]", + "[ 43/16 → 11/4 | note:e s:sawtooth am:4 tremolophase:0.66 ]", + "[ 11/4 → 45/16 | note:f s:sawtooth am:4 tremolophase:0.66 ]", + "[ 45/16 → 23/8 | note:a s:sawtooth am:4 tremolophase:0.66 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth am:4 tremolophase:0.66 ]", + "[ 47/16 → 3/1 | note:e s:sawtooth am:4 tremolophase:0.66 ]", + "[ 3/1 → 49/16 | note:f s:sawtooth am:4 tremolophase:0 ]", + "[ 49/16 → 25/8 | note:a s:sawtooth am:4 tremolophase:0 ]", + "[ 25/8 → 51/16 | note:c s:sawtooth am:4 tremolophase:0 ]", + "[ 51/16 → 13/4 | note:e s:sawtooth am:4 tremolophase:0 ]", + "[ 13/4 → 53/16 | note:f s:sawtooth am:4 tremolophase:0 ]", + "[ 53/16 → 27/8 | note:a s:sawtooth am:4 tremolophase:0 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth am:4 tremolophase:0 ]", + "[ 55/16 → 7/2 | note:e s:sawtooth am:4 tremolophase:0 ]", + "[ 7/2 → 57/16 | note:f s:sawtooth am:4 tremolophase:0 ]", + "[ 57/16 → 29/8 | note:a s:sawtooth am:4 tremolophase:0 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth am:4 tremolophase:0 ]", + "[ 59/16 → 15/4 | note:e s:sawtooth am:4 tremolophase:0 ]", + "[ 15/4 → 61/16 | note:f s:sawtooth am:4 tremolophase:0 ]", + "[ 61/16 → 31/8 | note:a s:sawtooth am:4 tremolophase:0 ]", + "[ 31/8 → 63/16 | note:c s:sawtooth am:4 tremolophase:0 ]", + "[ 63/16 → 4/1 | note:e s:sawtooth am:4 tremolophase:0 ]", ] `; -exports[`runs examples > example "amshape" example index 0 1`] = ` +exports[`runs examples > example "tremoloshape" example index 0 1`] = ` [ - "[ 0/1 → 1/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 1/16 → 1/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 1/8 → 3/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 3/16 → 1/4 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 1/4 → 5/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 5/16 → 3/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 3/8 → 7/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 7/16 → 1/2 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 1/2 → 9/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 9/16 → 5/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 5/8 → 11/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 11/16 → 3/4 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 3/4 → 13/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 13/16 → 7/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 7/8 → 15/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 15/16 → 1/1 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 1/1 → 17/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 17/16 → 9/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 9/8 → 19/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 19/16 → 5/4 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 5/4 → 21/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 21/16 → 11/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 11/8 → 23/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 23/16 → 3/2 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 3/2 → 25/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 25/16 → 13/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 13/8 → 27/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 27/16 → 7/4 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 7/4 → 29/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 29/16 → 15/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 15/8 → 31/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 31/16 → 2/1 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 2/1 → 33/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 33/16 → 17/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 17/8 → 35/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 35/16 → 9/4 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 9/4 → 37/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 37/16 → 19/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 19/8 → 39/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 39/16 → 5/2 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 5/2 → 41/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 41/16 → 21/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 21/8 → 43/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 43/16 → 11/4 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 11/4 → 45/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 45/16 → 23/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 23/8 → 47/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 47/16 → 3/1 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 3/1 → 49/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 49/16 → 25/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 25/8 → 51/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 51/16 → 13/4 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 13/4 → 53/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 53/16 → 27/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 27/8 → 55/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 55/16 → 7/2 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 7/2 → 57/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 57/16 → 29/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 29/8 → 59/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 59/16 → 15/4 | note:d am:4 amshape:ramp s:sawtooth ]", - "[ 15/4 → 61/16 | note:f am:4 amshape:ramp s:sawtooth ]", - "[ 61/16 → 31/8 | note:g am:4 amshape:ramp s:sawtooth ]", - "[ 31/8 → 63/16 | note:c am:4 amshape:ramp s:sawtooth ]", - "[ 63/16 → 4/1 | note:d am:4 amshape:ramp s:sawtooth ]", + "[ 0/1 → 1/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 1/16 → 1/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 1/8 → 3/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 3/16 → 1/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 1/4 → 5/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 5/16 → 3/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 3/8 → 7/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 7/16 → 1/2 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 1/2 → 9/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 9/16 → 5/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 5/8 → 11/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 11/16 → 3/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 3/4 → 13/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 13/16 → 7/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 7/8 → 15/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 15/16 → 1/1 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 1/1 → 17/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 17/16 → 9/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 9/8 → 19/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 19/16 → 5/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 5/4 → 21/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 21/16 → 11/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 11/8 → 23/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 23/16 → 3/2 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 3/2 → 25/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 25/16 → 13/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 13/8 → 27/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 27/16 → 7/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 7/4 → 29/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 29/16 → 15/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 15/8 → 31/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 31/16 → 2/1 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 2/1 → 33/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 33/16 → 17/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 17/8 → 35/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 35/16 → 9/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 9/4 → 37/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 37/16 → 19/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 19/8 → 39/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 39/16 → 5/2 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 5/2 → 41/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 41/16 → 21/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 21/8 → 43/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 43/16 → 11/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 11/4 → 45/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 45/16 → 23/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 23/8 → 47/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 47/16 → 3/1 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 3/1 → 49/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 49/16 → 25/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 25/8 → 51/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 51/16 → 13/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 13/4 → 53/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 53/16 → 27/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 27/8 → 55/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 55/16 → 7/2 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 7/2 → 57/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 57/16 → 29/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 29/8 → 59/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 59/16 → 15/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", + "[ 15/4 → 61/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", + "[ 61/16 → 31/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", + "[ 31/8 → 63/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", + "[ 63/16 → 4/1 | note:d am:4 tremoloshape:ramp s:sawtooth ]", ] `; -exports[`runs examples > example "amskew" example index 0 1`] = ` +exports[`runs examples > example "tremoloskew" example index 0 1`] = ` [ - "[ 0/1 → 1/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 1/16 → 1/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 1/8 → 3/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 3/16 → 1/4 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 1/4 → 5/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 5/16 → 3/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 3/8 → 7/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 7/16 → 1/2 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 1/2 → 9/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 9/16 → 5/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 5/8 → 11/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 11/16 → 3/4 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 3/4 → 13/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 13/16 → 7/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 7/8 → 15/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 15/16 → 1/1 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 1/1 → 17/16 | note:f am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 17/16 → 9/8 | note:a am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 9/8 → 19/16 | note:c am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 19/16 → 5/4 | note:e am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 5/4 → 21/16 | note:f am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 21/16 → 11/8 | note:a am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 11/8 → 23/16 | note:c am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 23/16 → 3/2 | note:e am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 3/2 → 25/16 | note:f am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 25/16 → 13/8 | note:a am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 13/8 → 27/16 | note:c am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 27/16 → 7/4 | note:e am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 7/4 → 29/16 | note:f am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 29/16 → 15/8 | note:a am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 15/8 → 31/16 | note:c am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 31/16 → 2/1 | note:e am:5 amskew:0.5 amdepth:0.5 attack:0.01 release:0.03 ]", - "[ 2/1 → 33/16 | note:f am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 33/16 → 17/8 | note:a am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 17/8 → 35/16 | note:c am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 35/16 → 9/4 | note:e am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 9/4 → 37/16 | note:f am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 37/16 → 19/8 | note:a am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 19/8 → 39/16 | note:c am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 39/16 → 5/2 | note:e am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 5/2 → 41/16 | note:f am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 41/16 → 21/8 | note:a am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 21/8 → 43/16 | note:c am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 43/16 → 11/4 | note:e am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 11/4 → 45/16 | note:f am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 45/16 → 23/8 | note:a am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 23/8 → 47/16 | note:c am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 47/16 → 3/1 | note:e am:5 amskew:0.5 amdepth:2 attack:0.01 release:0.03 ]", - "[ 3/1 → 49/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 49/16 → 25/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 25/8 → 51/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 51/16 → 13/4 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 13/4 → 53/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 53/16 → 27/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 27/8 → 55/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 55/16 → 7/2 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 7/2 → 57/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 57/16 → 29/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 29/8 → 59/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 59/16 → 15/4 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 15/4 → 61/16 | note:f am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 61/16 → 31/8 | note:a am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 31/8 → 63/16 | note:c am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", - "[ 63/16 → 4/1 | note:e am:5 amskew:0.5 amdepth:1 attack:0.01 release:0.03 ]", + "[ 0/1 → 1/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 1/16 → 1/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 1/8 → 3/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 3/16 → 1/4 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 1/4 → 5/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 5/16 → 3/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 3/8 → 7/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 7/16 → 1/2 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 1/2 → 9/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 9/16 → 5/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 5/8 → 11/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 11/16 → 3/4 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 3/4 → 13/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 13/16 → 7/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 7/8 → 15/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 15/16 → 1/1 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 1/1 → 17/16 | note:f am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 17/16 → 9/8 | note:a am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 9/8 → 19/16 | note:c am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 19/16 → 5/4 | note:e am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 5/4 → 21/16 | note:f am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 21/16 → 11/8 | note:a am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 11/8 → 23/16 | note:c am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 23/16 → 3/2 | note:e am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 3/2 → 25/16 | note:f am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 25/16 → 13/8 | note:a am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 13/8 → 27/16 | note:c am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 27/16 → 7/4 | note:e am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 7/4 → 29/16 | note:f am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 29/16 → 15/8 | note:a am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 15/8 → 31/16 | note:c am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 31/16 → 2/1 | note:e am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", + "[ 2/1 → 33/16 | note:f am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 33/16 → 17/8 | note:a am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 17/8 → 35/16 | note:c am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 35/16 → 9/4 | note:e am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 9/4 → 37/16 | note:f am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 37/16 → 19/8 | note:a am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 19/8 → 39/16 | note:c am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 39/16 → 5/2 | note:e am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 5/2 → 41/16 | note:f am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 41/16 → 21/8 | note:a am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 21/8 → 43/16 | note:c am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 43/16 → 11/4 | note:e am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 11/4 → 45/16 | note:f am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 45/16 → 23/8 | note:a am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 23/8 → 47/16 | note:c am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 47/16 → 3/1 | note:e am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", + "[ 3/1 → 49/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 49/16 → 25/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 25/8 → 51/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 51/16 → 13/4 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 13/4 → 53/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 53/16 → 27/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 27/8 → 55/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 55/16 → 7/2 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 7/2 → 57/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 57/16 → 29/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 29/8 → 59/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 59/16 → 15/4 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 15/4 → 61/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 61/16 → 31/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 31/8 → 63/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", + "[ 63/16 → 4/1 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", ] `; -exports[`runs examples > example "amsync" example index 0 1`] = ` +exports[`runs examples > example "tremolosync" example index 0 1`] = ` [ - "[ 0/1 → 1/1 | s:supersaw amsync:0.25 amskew:0 amdepth:2 ]", - "[ 1/1 → 2/1 | s:supersaw amsync:0.25 amskew:0.5 amdepth:2 ]", - "[ 2/1 → 3/1 | s:supersaw amsync:0.25 amskew:1 amdepth:2 ]", - "[ 3/1 → 4/1 | s:supersaw amsync:0.25 amskew:0 amdepth:2 ]", + "[ 0/1 → 1/1 | s:supersaw tremolosync:0.25 tremoloskew:0 tremolodepth:2 ]", + "[ 1/1 → 2/1 | s:supersaw tremolosync:0.25 tremoloskew:0.5 tremolodepth:2 ]", + "[ 2/1 → 3/1 | s:supersaw tremolosync:0.25 tremoloskew:1 tremolodepth:2 ]", + "[ 3/1 → 4/1 | s:supersaw tremolosync:0.25 tremoloskew:0 tremolodepth:2 ]", ] `; diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index 9c96dbe31..8f6a3b8aa 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -65,25 +65,25 @@ Amplitude modulation changes the amplitude (gain) periodically over time. -## amsync +## tremolosync - + -## amdepth +## tremolodepth - + -## amskew +## tremoloskew - + -## amphase +## tremolophase - + -## amshape +## tremoloshape - + # Amplitude Envelope From dd04d49c750f79667f987576ff664bbbd841dd58 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 18 Jul 2025 22:04:18 -0400 Subject: [PATCH 295/538] fix test --- packages/core/controls.mjs | 7 +- packages/superdough/superdough.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 648 ++++++++++++++-------- 3 files changed, 419 insertions(+), 238 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index d09fbbc21..326343e10 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -466,7 +466,10 @@ export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremolos * note("d d d# d".fast(4)).s("supersaw").tremolosync("4").tremoloskew("<1 .5 0>") * */ -export const { tremolosync } = registerControl(['tremolosync', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'tremsync'); +export const { tremolosync } = registerControl( + ['tremolosync', 'tremolodepth', 'tremoloskew', 'tremolophase'], + 'tremsync', +); /** * depth of amplitude modulation @@ -1655,8 +1658,6 @@ export const { density } = registerControl('density'); export const { expression } = registerControl('expression'); export const { sustainpedal } = registerControl('sustainpedal'); - - export const { fshift } = registerControl('fshift'); export const { fshiftnote } = registerControl('fshiftnote'); export const { fshiftphase } = registerControl('fshiftphase'); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 6ce673d5b..f3e04a047 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -745,7 +745,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) if (tremolosync != null) { tremolo = cps * tremolosync; } - + if (tremolo !== undefined) { // Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload // EX: a triangle waveform will clip like this /-\ when the depth is above 1 diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 7ad7c48b9..e6bfa44aa 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -779,24 +779,6 @@ exports[`runs examples > example "always" example index 0 1`] = ` ] `; -exports[`runs examples > example "am" example index 0 1`] = ` -[ - "[ 0/1 → 1/1 | s:triangle am:2 tremoloshape:tri tremolodepth:0.5 ]", - "[ 1/1 → 2/1 | s:triangle am:2 tremoloshape:saw tremolodepth:0.5 ]", - "[ 2/1 → 3/1 | s:triangle am:2 tremoloshape:ramp tremolodepth:0.5 ]", - "[ 3/1 → 4/1 | s:triangle am:2 tremoloshape:square tremolodepth:0.5 ]", -] -`; - -exports[`runs examples > example "tremolodepth" example index 0 1`] = ` -[ - "[ 0/1 → 1/1 | s:triangle am:1 tremolodepth:1 ]", - "[ 1/1 → 2/1 | s:triangle am:1 tremolodepth:1 ]", - "[ 2/1 → 3/1 | s:triangle am:1 tremolodepth:1 ]", - "[ 3/1 → 4/1 | s:triangle am:1 tremolodepth:1 ]", -] -`; - exports[`runs examples > example "amp" example index 0 1`] = ` [ "[ (0/1 → 1/12) ⇝ 1/8 | s:bd amp:0.1 ]", @@ -858,222 +840,6 @@ exports[`runs examples > example "amp" example index 0 1`] = ` ] `; -exports[`runs examples > example "tremolophase" example index 0 1`] = ` -[ - "[ 0/1 → 1/16 | note:f s:sawtooth am:4 tremolophase:0 ]", - "[ 1/16 → 1/8 | note:a s:sawtooth am:4 tremolophase:0 ]", - "[ 1/8 → 3/16 | note:c s:sawtooth am:4 tremolophase:0 ]", - "[ 3/16 → 1/4 | note:e s:sawtooth am:4 tremolophase:0 ]", - "[ 1/4 → 5/16 | note:f s:sawtooth am:4 tremolophase:0 ]", - "[ 5/16 → 3/8 | note:a s:sawtooth am:4 tremolophase:0 ]", - "[ 3/8 → 7/16 | note:c s:sawtooth am:4 tremolophase:0 ]", - "[ 7/16 → 1/2 | note:e s:sawtooth am:4 tremolophase:0 ]", - "[ 1/2 → 9/16 | note:f s:sawtooth am:4 tremolophase:0 ]", - "[ 9/16 → 5/8 | note:a s:sawtooth am:4 tremolophase:0 ]", - "[ 5/8 → 11/16 | note:c s:sawtooth am:4 tremolophase:0 ]", - "[ 11/16 → 3/4 | note:e s:sawtooth am:4 tremolophase:0 ]", - "[ 3/4 → 13/16 | note:f s:sawtooth am:4 tremolophase:0 ]", - "[ 13/16 → 7/8 | note:a s:sawtooth am:4 tremolophase:0 ]", - "[ 7/8 → 15/16 | note:c s:sawtooth am:4 tremolophase:0 ]", - "[ 15/16 → 1/1 | note:e s:sawtooth am:4 tremolophase:0 ]", - "[ 1/1 → 17/16 | note:f s:sawtooth am:4 tremolophase:0.25 ]", - "[ 17/16 → 9/8 | note:a s:sawtooth am:4 tremolophase:0.25 ]", - "[ 9/8 → 19/16 | note:c s:sawtooth am:4 tremolophase:0.25 ]", - "[ 19/16 → 5/4 | note:e s:sawtooth am:4 tremolophase:0.25 ]", - "[ 5/4 → 21/16 | note:f s:sawtooth am:4 tremolophase:0.25 ]", - "[ 21/16 → 11/8 | note:a s:sawtooth am:4 tremolophase:0.25 ]", - "[ 11/8 → 23/16 | note:c s:sawtooth am:4 tremolophase:0.25 ]", - "[ 23/16 → 3/2 | note:e s:sawtooth am:4 tremolophase:0.25 ]", - "[ 3/2 → 25/16 | note:f s:sawtooth am:4 tremolophase:0.25 ]", - "[ 25/16 → 13/8 | note:a s:sawtooth am:4 tremolophase:0.25 ]", - "[ 13/8 → 27/16 | note:c s:sawtooth am:4 tremolophase:0.25 ]", - "[ 27/16 → 7/4 | note:e s:sawtooth am:4 tremolophase:0.25 ]", - "[ 7/4 → 29/16 | note:f s:sawtooth am:4 tremolophase:0.25 ]", - "[ 29/16 → 15/8 | note:a s:sawtooth am:4 tremolophase:0.25 ]", - "[ 15/8 → 31/16 | note:c s:sawtooth am:4 tremolophase:0.25 ]", - "[ 31/16 → 2/1 | note:e s:sawtooth am:4 tremolophase:0.25 ]", - "[ 2/1 → 33/16 | note:f s:sawtooth am:4 tremolophase:0.66 ]", - "[ 33/16 → 17/8 | note:a s:sawtooth am:4 tremolophase:0.66 ]", - "[ 17/8 → 35/16 | note:c s:sawtooth am:4 tremolophase:0.66 ]", - "[ 35/16 → 9/4 | note:e s:sawtooth am:4 tremolophase:0.66 ]", - "[ 9/4 → 37/16 | note:f s:sawtooth am:4 tremolophase:0.66 ]", - "[ 37/16 → 19/8 | note:a s:sawtooth am:4 tremolophase:0.66 ]", - "[ 19/8 → 39/16 | note:c s:sawtooth am:4 tremolophase:0.66 ]", - "[ 39/16 → 5/2 | note:e s:sawtooth am:4 tremolophase:0.66 ]", - "[ 5/2 → 41/16 | note:f s:sawtooth am:4 tremolophase:0.66 ]", - "[ 41/16 → 21/8 | note:a s:sawtooth am:4 tremolophase:0.66 ]", - "[ 21/8 → 43/16 | note:c s:sawtooth am:4 tremolophase:0.66 ]", - "[ 43/16 → 11/4 | note:e s:sawtooth am:4 tremolophase:0.66 ]", - "[ 11/4 → 45/16 | note:f s:sawtooth am:4 tremolophase:0.66 ]", - "[ 45/16 → 23/8 | note:a s:sawtooth am:4 tremolophase:0.66 ]", - "[ 23/8 → 47/16 | note:c s:sawtooth am:4 tremolophase:0.66 ]", - "[ 47/16 → 3/1 | note:e s:sawtooth am:4 tremolophase:0.66 ]", - "[ 3/1 → 49/16 | note:f s:sawtooth am:4 tremolophase:0 ]", - "[ 49/16 → 25/8 | note:a s:sawtooth am:4 tremolophase:0 ]", - "[ 25/8 → 51/16 | note:c s:sawtooth am:4 tremolophase:0 ]", - "[ 51/16 → 13/4 | note:e s:sawtooth am:4 tremolophase:0 ]", - "[ 13/4 → 53/16 | note:f s:sawtooth am:4 tremolophase:0 ]", - "[ 53/16 → 27/8 | note:a s:sawtooth am:4 tremolophase:0 ]", - "[ 27/8 → 55/16 | note:c s:sawtooth am:4 tremolophase:0 ]", - "[ 55/16 → 7/2 | note:e s:sawtooth am:4 tremolophase:0 ]", - "[ 7/2 → 57/16 | note:f s:sawtooth am:4 tremolophase:0 ]", - "[ 57/16 → 29/8 | note:a s:sawtooth am:4 tremolophase:0 ]", - "[ 29/8 → 59/16 | note:c s:sawtooth am:4 tremolophase:0 ]", - "[ 59/16 → 15/4 | note:e s:sawtooth am:4 tremolophase:0 ]", - "[ 15/4 → 61/16 | note:f s:sawtooth am:4 tremolophase:0 ]", - "[ 61/16 → 31/8 | note:a s:sawtooth am:4 tremolophase:0 ]", - "[ 31/8 → 63/16 | note:c s:sawtooth am:4 tremolophase:0 ]", - "[ 63/16 → 4/1 | note:e s:sawtooth am:4 tremolophase:0 ]", -] -`; - -exports[`runs examples > example "tremoloshape" example index 0 1`] = ` -[ - "[ 0/1 → 1/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 1/16 → 1/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 1/8 → 3/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 3/16 → 1/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 1/4 → 5/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 5/16 → 3/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 3/8 → 7/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 7/16 → 1/2 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 1/2 → 9/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 9/16 → 5/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 5/8 → 11/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 11/16 → 3/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 3/4 → 13/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 13/16 → 7/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 7/8 → 15/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 15/16 → 1/1 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 1/1 → 17/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 17/16 → 9/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 9/8 → 19/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 19/16 → 5/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 5/4 → 21/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 21/16 → 11/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 11/8 → 23/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 23/16 → 3/2 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 3/2 → 25/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 25/16 → 13/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 13/8 → 27/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 27/16 → 7/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 7/4 → 29/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 29/16 → 15/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 15/8 → 31/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 31/16 → 2/1 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 2/1 → 33/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 33/16 → 17/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 17/8 → 35/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 35/16 → 9/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 9/4 → 37/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 37/16 → 19/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 19/8 → 39/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 39/16 → 5/2 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 5/2 → 41/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 41/16 → 21/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 21/8 → 43/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 43/16 → 11/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 11/4 → 45/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 45/16 → 23/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 23/8 → 47/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 47/16 → 3/1 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 3/1 → 49/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 49/16 → 25/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 25/8 → 51/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 51/16 → 13/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 13/4 → 53/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 53/16 → 27/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 27/8 → 55/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 55/16 → 7/2 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 7/2 → 57/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 57/16 → 29/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 29/8 → 59/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 59/16 → 15/4 | note:d am:4 tremoloshape:ramp s:sawtooth ]", - "[ 15/4 → 61/16 | note:f am:4 tremoloshape:ramp s:sawtooth ]", - "[ 61/16 → 31/8 | note:g am:4 tremoloshape:ramp s:sawtooth ]", - "[ 31/8 → 63/16 | note:c am:4 tremoloshape:ramp s:sawtooth ]", - "[ 63/16 → 4/1 | note:d am:4 tremoloshape:ramp s:sawtooth ]", -] -`; - -exports[`runs examples > example "tremoloskew" example index 0 1`] = ` -[ - "[ 0/1 → 1/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 1/16 → 1/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 1/8 → 3/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 3/16 → 1/4 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 1/4 → 5/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 5/16 → 3/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 3/8 → 7/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 7/16 → 1/2 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 1/2 → 9/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 9/16 → 5/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 5/8 → 11/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 11/16 → 3/4 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 3/4 → 13/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 13/16 → 7/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 7/8 → 15/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 15/16 → 1/1 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 1/1 → 17/16 | note:f am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 17/16 → 9/8 | note:a am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 9/8 → 19/16 | note:c am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 19/16 → 5/4 | note:e am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 5/4 → 21/16 | note:f am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 21/16 → 11/8 | note:a am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 11/8 → 23/16 | note:c am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 23/16 → 3/2 | note:e am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 3/2 → 25/16 | note:f am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 25/16 → 13/8 | note:a am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 13/8 → 27/16 | note:c am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 27/16 → 7/4 | note:e am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 7/4 → 29/16 | note:f am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 29/16 → 15/8 | note:a am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 15/8 → 31/16 | note:c am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 31/16 → 2/1 | note:e am:5 tremoloskew:0.5 tremolodepth:0.5 attack:0.01 release:0.03 ]", - "[ 2/1 → 33/16 | note:f am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 33/16 → 17/8 | note:a am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 17/8 → 35/16 | note:c am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 35/16 → 9/4 | note:e am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 9/4 → 37/16 | note:f am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 37/16 → 19/8 | note:a am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 19/8 → 39/16 | note:c am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 39/16 → 5/2 | note:e am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 5/2 → 41/16 | note:f am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 41/16 → 21/8 | note:a am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 21/8 → 43/16 | note:c am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 43/16 → 11/4 | note:e am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 11/4 → 45/16 | note:f am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 45/16 → 23/8 | note:a am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 23/8 → 47/16 | note:c am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 47/16 → 3/1 | note:e am:5 tremoloskew:0.5 tremolodepth:2 attack:0.01 release:0.03 ]", - "[ 3/1 → 49/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 49/16 → 25/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 25/8 → 51/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 51/16 → 13/4 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 13/4 → 53/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 53/16 → 27/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 27/8 → 55/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 55/16 → 7/2 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 7/2 → 57/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 57/16 → 29/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 29/8 → 59/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 59/16 → 15/4 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 15/4 → 61/16 | note:f am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 61/16 → 31/8 | note:a am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 31/8 → 63/16 | note:c am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", - "[ 63/16 → 4/1 | note:e am:5 tremoloskew:0.5 tremolodepth:1 attack:0.01 release:0.03 ]", -] -`; - -exports[`runs examples > example "tremolosync" example index 0 1`] = ` -[ - "[ 0/1 → 1/1 | s:supersaw tremolosync:0.25 tremoloskew:0 tremolodepth:2 ]", - "[ 1/1 → 2/1 | s:supersaw tremolosync:0.25 tremoloskew:0.5 tremolodepth:2 ]", - "[ 2/1 → 3/1 | s:supersaw tremolosync:0.25 tremoloskew:1 tremolodepth:2 ]", - "[ 3/1 → 4/1 | s:supersaw tremolosync:0.25 tremoloskew:0 tremolodepth:2 ]", -] -`; - exports[`runs examples > example "apply" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:C3 ]", @@ -10344,6 +10110,420 @@ exports[`runs examples > example "transpose" example index 1 1`] = ` ] `; +exports[`runs examples > example "tremolo" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/16 → 1/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/8 → 3/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 3/16 → 1/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/4 → 5/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 5/16 → 3/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 3/8 → 7/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 7/16 → 1/2 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/2 → 9/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 9/16 → 5/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 5/8 → 11/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 11/16 → 3/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 3/4 → 13/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 13/16 → 7/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 7/8 → 15/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 15/16 → 1/1 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/1 → 17/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 17/16 → 9/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 9/8 → 19/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 19/16 → 5/4 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 5/4 → 21/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 21/16 → 11/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 11/8 → 23/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 23/16 → 3/2 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 3/2 → 25/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 25/16 → 13/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 13/8 → 27/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 27/16 → 7/4 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 7/4 → 29/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 29/16 → 15/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 15/8 → 31/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 31/16 → 2/1 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 2/1 → 33/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 33/16 → 17/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 17/8 → 35/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 35/16 → 9/4 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 9/4 → 37/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 37/16 → 19/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 19/8 → 39/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 39/16 → 5/2 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 5/2 → 41/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 41/16 → 21/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 21/8 → 43/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 43/16 → 11/4 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 11/4 → 45/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 45/16 → 23/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 23/8 → 47/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 47/16 → 3/1 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 3/1 → 49/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 49/16 → 25/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 25/8 → 51/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 51/16 → 13/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 13/4 → 53/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 53/16 → 27/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 27/8 → 55/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 55/16 → 7/2 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 7/2 → 57/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 57/16 → 29/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 29/8 → 59/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 59/16 → 15/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 15/4 → 61/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 61/16 → 31/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 31/8 → 63/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 63/16 → 4/1 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", +] +`; + +exports[`runs examples > example "tremolodepth" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/16 → 1/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/8 → 3/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 3/16 → 1/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/4 → 5/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 5/16 → 3/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 3/8 → 7/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 7/16 → 1/2 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/2 → 9/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 9/16 → 5/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 5/8 → 11/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 11/16 → 3/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 3/4 → 13/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 13/16 → 7/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 7/8 → 15/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 15/16 → 1/1 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/1 → 17/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 17/16 → 9/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 9/8 → 19/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 19/16 → 5/4 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 5/4 → 21/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 21/16 → 11/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 11/8 → 23/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 23/16 → 3/2 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 3/2 → 25/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 25/16 → 13/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 13/8 → 27/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 27/16 → 7/4 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 7/4 → 29/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 29/16 → 15/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 15/8 → 31/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 31/16 → 2/1 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 2/1 → 33/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 33/16 → 17/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 17/8 → 35/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 35/16 → 9/4 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 9/4 → 37/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 37/16 → 19/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 19/8 → 39/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 39/16 → 5/2 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 5/2 → 41/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 41/16 → 21/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 21/8 → 43/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 43/16 → 11/4 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 11/4 → 45/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 45/16 → 23/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 23/8 → 47/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 47/16 → 3/1 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 3/1 → 49/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 49/16 → 25/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 25/8 → 51/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 51/16 → 13/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 13/4 → 53/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 53/16 → 27/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 27/8 → 55/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 55/16 → 7/2 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 7/2 → 57/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 57/16 → 29/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 29/8 → 59/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 59/16 → 15/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 15/4 → 61/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 61/16 → 31/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 31/8 → 63/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 63/16 → 4/1 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", +] +`; + +exports[`runs examples > example "tremolophase" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/16 → 1/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/8 → 3/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 3/16 → 1/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/4 → 5/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 5/16 → 3/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 7/16 → 1/2 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/2 → 9/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 9/16 → 5/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 11/16 → 3/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 3/4 → 13/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 13/16 → 7/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 7/8 → 15/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 15/16 → 1/1 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/1 → 17/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 17/16 → 9/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 19/16 → 5/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 5/4 → 21/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 21/16 → 11/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 23/16 → 3/2 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 3/2 → 25/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 25/16 → 13/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 13/8 → 27/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 27/16 → 7/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 7/4 → 29/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 29/16 → 15/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 31/16 → 2/1 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 2/1 → 33/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 33/16 → 17/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 35/16 → 9/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 9/4 → 37/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 37/16 → 19/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 19/8 → 39/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 39/16 → 5/2 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 5/2 → 41/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 41/16 → 21/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 43/16 → 11/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 11/4 → 45/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 45/16 → 23/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 47/16 → 3/1 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 3/1 → 49/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 49/16 → 25/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 25/8 → 51/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 51/16 → 13/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 13/4 → 53/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 53/16 → 27/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 55/16 → 7/2 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 7/2 → 57/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 57/16 → 29/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 59/16 → 15/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 15/4 → 61/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 61/16 → 31/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 31/8 → 63/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 63/16 → 4/1 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", +] +`; + +exports[`runs examples > example "tremoloshape" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/16 → 1/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/8 → 3/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 3/16 → 1/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/4 → 5/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 5/16 → 3/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 3/8 → 7/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 7/16 → 1/2 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/2 → 9/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 9/16 → 5/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 5/8 → 11/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 11/16 → 3/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 3/4 → 13/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 13/16 → 7/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 7/8 → 15/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 15/16 → 1/1 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/1 → 17/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 17/16 → 9/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 9/8 → 19/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 19/16 → 5/4 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 5/4 → 21/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 21/16 → 11/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 11/8 → 23/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 23/16 → 3/2 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 3/2 → 25/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 25/16 → 13/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 13/8 → 27/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 27/16 → 7/4 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 7/4 → 29/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 29/16 → 15/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 15/8 → 31/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 31/16 → 2/1 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 2/1 → 33/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 33/16 → 17/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 17/8 → 35/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 35/16 → 9/4 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 9/4 → 37/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 37/16 → 19/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 19/8 → 39/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 39/16 → 5/2 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 5/2 → 41/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 41/16 → 21/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 21/8 → 43/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 43/16 → 11/4 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 11/4 → 45/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 45/16 → 23/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 23/8 → 47/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 47/16 → 3/1 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 3/1 → 49/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 49/16 → 25/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 25/8 → 51/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 51/16 → 13/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 13/4 → 53/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 53/16 → 27/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 27/8 → 55/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 55/16 → 7/2 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 7/2 → 57/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 57/16 → 29/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 29/8 → 59/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 59/16 → 15/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 15/4 → 61/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 61/16 → 31/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 31/8 → 63/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 63/16 → 4/1 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", +] +`; + +exports[`runs examples > example "tremoloskew" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/16 → 1/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/8 → 3/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 3/16 → 1/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/4 → 5/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 5/16 → 3/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 7/16 → 1/2 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/2 → 9/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 9/16 → 5/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 11/16 → 3/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 3/4 → 13/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 13/16 → 7/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 7/8 → 15/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 15/16 → 1/1 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/1 → 17/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 17/16 → 9/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 19/16 → 5/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 5/4 → 21/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 21/16 → 11/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 23/16 → 3/2 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 3/2 → 25/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 25/16 → 13/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 13/8 → 27/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 27/16 → 7/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 7/4 → 29/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 29/16 → 15/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 31/16 → 2/1 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 2/1 → 33/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 33/16 → 17/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 35/16 → 9/4 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 9/4 → 37/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 37/16 → 19/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 19/8 → 39/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 39/16 → 5/2 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 5/2 → 41/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 41/16 → 21/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 43/16 → 11/4 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 11/4 → 45/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 45/16 → 23/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 47/16 → 3/1 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 3/1 → 49/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 49/16 → 25/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 25/8 → 51/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 51/16 → 13/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 13/4 → 53/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 53/16 → 27/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 55/16 → 7/2 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 7/2 → 57/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 57/16 → 29/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 59/16 → 15/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 15/4 → 61/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 61/16 → 31/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 31/8 → 63/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 63/16 → 4/1 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", +] +`; + +exports[`runs examples > example "tremolosync" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/16 → 1/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/8 → 3/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 3/16 → 1/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/4 → 5/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 5/16 → 3/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 3/8 → 7/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 7/16 → 1/2 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/2 → 9/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 9/16 → 5/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 5/8 → 11/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 11/16 → 3/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 3/4 → 13/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 13/16 → 7/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 7/8 → 15/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 15/16 → 1/1 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/1 → 17/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 17/16 → 9/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 9/8 → 19/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 19/16 → 5/4 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 5/4 → 21/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 21/16 → 11/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 11/8 → 23/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 23/16 → 3/2 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 3/2 → 25/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 25/16 → 13/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 13/8 → 27/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 27/16 → 7/4 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 7/4 → 29/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 29/16 → 15/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 15/8 → 31/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 31/16 → 2/1 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 2/1 → 33/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 33/16 → 17/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 17/8 → 35/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 35/16 → 9/4 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 9/4 → 37/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 37/16 → 19/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 19/8 → 39/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 39/16 → 5/2 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 5/2 → 41/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 41/16 → 21/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 21/8 → 43/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 43/16 → 11/4 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 11/4 → 45/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 45/16 → 23/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 23/8 → 47/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 47/16 → 3/1 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 3/1 → 49/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 49/16 → 25/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 25/8 → 51/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 51/16 → 13/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 13/4 → 53/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 53/16 → 27/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 27/8 → 55/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 55/16 → 7/2 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 7/2 → 57/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 57/16 → 29/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 29/8 → 59/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 59/16 → 15/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 15/4 → 61/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 61/16 → 31/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 31/8 → 63/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 63/16 → 4/1 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", +] +`; + exports[`runs examples > example "tri" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:C3 ]", From 75b6f59212d301e50ee01a33e7647a44ae4657ca Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 20 Jul 2025 23:26:17 -0400 Subject: [PATCH 296/538] fix --- packages/superdough/superdough.mjs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index f3e04a047..96cf3f673 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -363,6 +363,22 @@ export function getLfo(audioContext, begin, end, properties = {}) { }); } +// export function getLfo(audioContext, time, end, properties = {}) { +// return getWorklet(audioContext, 'lfo-processor', { +// frequency: 1, +// depth: 1, +// skew: 0, +// phaseoffset: 0, +// time, +// begin: time, +// end, +// shape: 1, +// dcoffset: -0.5, +// ...properties, +// }); +// } + + export function getSyncedLfo(audioContext, time, end, cps, cycle, properties = {}) { const frequency = cycle / cps; From 9416f317d4a19446411f9e3e9db98cb030e8baf2 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 20 Jul 2025 23:29:01 -0400 Subject: [PATCH 297/538] fixing... --- packages/superdough/superdough.mjs | 54 ++++++++++++------------- packages/superdough/worklets.mjs | 65 +++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 28 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 96cf3f673..a591df0fe 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -343,41 +343,41 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) { return delays[orbit]; } -export function getLfo(audioContext, begin, end, properties = {}) { - const { shape = 0, ...props } = properties; - const { dcoffset = -0.5, depth = 1 } = properties; - return getWorklet(audioContext, 'lfo-processor', { - frequency: 1, - depth, - skew: 0, - phaseoffset: 0, - time: begin, - begin, - end, - shape: getModulationShapeInput(shape), - dcoffset, - min: dcoffset - depth * 0.5, - max: dcoffset + depth * 0.5, - curve: 1, - ...props, - }); -} - -// export function getLfo(audioContext, time, end, properties = {}) { +// export function getLfo(audioContext, begin, end, properties = {}) { +// const { shape = 0, ...props } = properties; +// const { dcoffset = -0.5, depth = 1 } = properties; // return getWorklet(audioContext, 'lfo-processor', { // frequency: 1, -// depth: 1, +// depth, // skew: 0, // phaseoffset: 0, -// time, -// begin: time, +// time: begin, +// begin, // end, -// shape: 1, -// dcoffset: -0.5, -// ...properties, +// shape: getModulationShapeInput(shape), +// dcoffset, +// min: dcoffset - depth * 0.5, +// max: dcoffset + depth * 0.5, +// curve: 1, +// ...props, // }); // } +export function getLfo(audioContext, time, end, properties = {}) { + return getWorklet(audioContext, 'lfo-processor', { + frequency: 1, + depth: 1, + skew: 0, + phaseoffset: 0, + time, + begin: time, + end, + shape: 1, + dcoffset: -0.5, + ...properties, + }); +} + export function getSyncedLfo(audioContext, time, end, cps, cycle, properties = {}) { const frequency = cycle / cps; diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index b4b36f6c9..25dae03cd 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -81,7 +81,70 @@ function getParamValue(block, param) { } return param[0]; } -const waveShapeNames = Object.keys(waveshapes); +// const waveShapeNames = Object.keys(waveshapes); +// class LFOProcessor extends AudioWorkletProcessor { +// static get parameterDescriptors() { +// return [ +// { name: 'time', defaultValue: 0 }, +// { name: 'end', defaultValue: 0 }, +// { name: 'frequency', defaultValue: 0.5 }, +// { name: 'skew', defaultValue: 0.5 }, +// { name: 'depth', defaultValue: 1 }, +// { name: 'phaseoffset', defaultValue: 0 }, +// { name: 'shape', defaultValue: 0 }, +// { name: 'dcoffset', defaultValue: 0 }, +// ]; +// } + +// constructor() { +// super(); +// this.phase; +// } + +// incrementPhase(dt) { +// this.phase += dt; +// if (this.phase > 1.0) { +// this.phase = this.phase - 1; +// } +// } + +// process(inputs, outputs, parameters) { +// // eslint-disable-next-line no-undef +// if (currentTime >= parameters.end[0]) { +// return false; +// } + +// const output = outputs[0]; +// const frequency = parameters['frequency'][0]; + +// const time = parameters['time'][0]; +// const depth = parameters['depth'][0]; +// const skew = parameters['skew'][0]; +// const phaseoffset = parameters['phaseoffset'][0]; + +// const dcoffset = parameters['dcoffset'][0]; +// const shape = waveShapeNames[parameters['shape'][0]]; + +// const blockSize = output[0].length ?? 0; + +// 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++) { +// const modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth; +// output[i][n] = modval; +// } +// this.incrementPhase(dt); +// } + +// return true; +// } +// } +// registerProcessor('lfo-processor', LFOProcessor); +// const waveShapeNames = Object.keys(waveshapes); class LFOProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ From ccf2a6de52c7a595188c3404e82735f50bb5ed85 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 20 Jul 2025 23:38:16 -0400 Subject: [PATCH 298/538] fixing.. --- packages/superdough/superdough.mjs | 54 +++++++++++++++--------------- packages/superdough/worklets.mjs | 2 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a591df0fe..96cf3f673 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -343,41 +343,41 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) { return delays[orbit]; } -// export function getLfo(audioContext, begin, end, properties = {}) { -// const { shape = 0, ...props } = properties; -// const { dcoffset = -0.5, depth = 1 } = properties; -// return getWorklet(audioContext, 'lfo-processor', { -// frequency: 1, -// depth, -// skew: 0, -// phaseoffset: 0, -// time: begin, -// begin, -// end, -// shape: getModulationShapeInput(shape), -// dcoffset, -// min: dcoffset - depth * 0.5, -// max: dcoffset + depth * 0.5, -// curve: 1, -// ...props, -// }); -// } - -export function getLfo(audioContext, time, end, properties = {}) { +export function getLfo(audioContext, begin, end, properties = {}) { + const { shape = 0, ...props } = properties; + const { dcoffset = -0.5, depth = 1 } = properties; return getWorklet(audioContext, 'lfo-processor', { frequency: 1, - depth: 1, + depth, skew: 0, phaseoffset: 0, - time, - begin: time, + time: begin, + begin, end, - shape: 1, - dcoffset: -0.5, - ...properties, + shape: getModulationShapeInput(shape), + dcoffset, + min: dcoffset - depth * 0.5, + max: dcoffset + depth * 0.5, + curve: 1, + ...props, }); } +// export function getLfo(audioContext, time, end, properties = {}) { +// return getWorklet(audioContext, 'lfo-processor', { +// frequency: 1, +// depth: 1, +// skew: 0, +// phaseoffset: 0, +// time, +// begin: time, +// end, +// shape: 1, +// dcoffset: -0.5, +// ...properties, +// }); +// } + export function getSyncedLfo(audioContext, time, end, cps, cycle, properties = {}) { const frequency = cycle / cps; diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 25dae03cd..c9948916a 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -144,7 +144,7 @@ function getParamValue(block, param) { // } // } // registerProcessor('lfo-processor', LFOProcessor); -// const waveShapeNames = Object.keys(waveshapes); +const waveShapeNames = Object.keys(waveshapes); class LFOProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ From b2f90ecb3ba2fa47fba6cca63afa027df4cd7bb1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 20 Jul 2025 23:56:30 -0400 Subject: [PATCH 299/538] fix def skew --- packages/superdough/superdough.mjs | 9 ++++++--- packages/superdough/synth.mjs | 2 +- packages/superdough/worklets.mjs | 2 ++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 96cf3f673..f9e8a1b8c 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -346,10 +346,10 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) { export function getLfo(audioContext, begin, end, properties = {}) { const { shape = 0, ...props } = properties; const { dcoffset = -0.5, depth = 1 } = properties; - return getWorklet(audioContext, 'lfo-processor', { + const lfoprops = { frequency: 1, depth, - skew: 0, + skew: .5, phaseoffset: 0, time: begin, begin, @@ -360,7 +360,10 @@ export function getLfo(audioContext, begin, end, properties = {}) { max: dcoffset + depth * 0.5, curve: 1, ...props, - }); + } + + console.info(lfoprops) + return getWorklet(audioContext, 'lfo-processor', lfoprops); } // export function getLfo(audioContext, time, end, properties = {}) { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 88e14e5ab..a95ba9df4 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -279,7 +279,7 @@ export function registerSynthSounds() { getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); let lfo; if (pwsweep != 0) { - lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep }); + lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep, skew: .5, dcoffset: 0 }); lfo.connect(o.parameters.get('pulsewidth')); } let timeoutNode = webAudioTimeout( diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index c9948916a..df3c445ac 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -185,6 +185,8 @@ class LFOProcessor extends AudioWorkletProcessor { return true; } + + const output = outputs[0]; const frequency = parameters['frequency'][0]; From 66828e17d30946b217942e7a89af7a1fe6903642 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 21 Jul 2025 01:11:04 -0400 Subject: [PATCH 300/538] working --- packages/core/cyclist.mjs | 4 +- packages/core/logger.mjs | 6 +++ packages/core/repl.mjs | 4 +- packages/superdough/superdough.mjs | 25 ++-------- packages/superdough/synth.mjs | 2 +- packages/superdough/worklets.mjs | 74 +++--------------------------- 6 files changed, 21 insertions(+), 94 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index fec819548..ad0961032 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import createClock from './zyklus.mjs'; -import { logger } from './logger.mjs'; +import { errorLogger, logger } from './logger.mjs'; export class Cyclist { constructor({ @@ -76,7 +76,7 @@ export class Cyclist { } }); } catch (e) { - logger(`[cyclist] error: ${e.message}`); + errorLogger(e); onError?.(e); } }, diff --git a/packages/core/logger.mjs b/packages/core/logger.mjs index e13bf86ca..635505c59 100644 --- a/packages/core/logger.mjs +++ b/packages/core/logger.mjs @@ -4,6 +4,12 @@ let debounce = 1000, lastMessage, lastTime; +export function errorLogger(e, origin = 'cyclist') { + //TODO: add some kind of debug flag that enables this while in dev mode + // console.error(e) + logger(`[${origin}] error: ${e.message}`); +} + export function logger(message, type, data = {}) { let t = performance.now(); if (lastMessage === message && t - lastTime < debounce) { diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index aeaa6418e..47231af4d 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -1,7 +1,7 @@ import { NeoCyclist } from './neocyclist.mjs'; import { Cyclist } from './cyclist.mjs'; import { evaluate as _evaluate } from './evaluate.mjs'; -import { logger } from './logger.mjs'; +import { errorLogger, logger } from './logger.mjs'; import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; @@ -256,6 +256,6 @@ export const getTrigger = await hap.context.onTrigger(hap, getTime(), cps, t); } } catch (err) { - logger(`[cyclist] error: ${err.message}`, 'error'); + errorLogger(err, 'getTrigger'); } }; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index f9e8a1b8c..7d90eb15d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -349,39 +349,22 @@ export function getLfo(audioContext, begin, end, properties = {}) { const lfoprops = { frequency: 1, depth, - skew: .5, + skew: 0.5, phaseoffset: 0, time: begin, begin, end, shape: getModulationShapeInput(shape), dcoffset, - min: dcoffset - depth * 0.5, - max: dcoffset + depth * 0.5, + min: dcoffset * depth, + max: dcoffset * depth + depth, curve: 1, ...props, - } + }; - console.info(lfoprops) return getWorklet(audioContext, 'lfo-processor', lfoprops); } -// export function getLfo(audioContext, time, end, properties = {}) { -// return getWorklet(audioContext, 'lfo-processor', { -// frequency: 1, -// depth: 1, -// skew: 0, -// phaseoffset: 0, -// time, -// begin: time, -// end, -// shape: 1, -// dcoffset: -0.5, -// ...properties, -// }); -// } - - export function getSyncedLfo(audioContext, time, end, cps, cycle, properties = {}) { const frequency = cycle / cps; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index a95ba9df4..88e14e5ab 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -279,7 +279,7 @@ export function registerSynthSounds() { getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); let lfo; if (pwsweep != 0) { - lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep, skew: .5, dcoffset: 0 }); + lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep }); lfo.connect(o.parameters.get('pulsewidth')); } let timeoutNode = webAudioTimeout( diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index df3c445ac..e7928a48f 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -81,69 +81,7 @@ function getParamValue(block, param) { } return param[0]; } -// const waveShapeNames = Object.keys(waveshapes); -// class LFOProcessor extends AudioWorkletProcessor { -// static get parameterDescriptors() { -// return [ -// { name: 'time', defaultValue: 0 }, -// { name: 'end', defaultValue: 0 }, -// { name: 'frequency', defaultValue: 0.5 }, -// { name: 'skew', defaultValue: 0.5 }, -// { name: 'depth', defaultValue: 1 }, -// { name: 'phaseoffset', defaultValue: 0 }, -// { name: 'shape', defaultValue: 0 }, -// { name: 'dcoffset', defaultValue: 0 }, -// ]; -// } -// constructor() { -// super(); -// this.phase; -// } - -// incrementPhase(dt) { -// this.phase += dt; -// if (this.phase > 1.0) { -// this.phase = this.phase - 1; -// } -// } - -// process(inputs, outputs, parameters) { -// // eslint-disable-next-line no-undef -// if (currentTime >= parameters.end[0]) { -// return false; -// } - -// const output = outputs[0]; -// const frequency = parameters['frequency'][0]; - -// const time = parameters['time'][0]; -// const depth = parameters['depth'][0]; -// const skew = parameters['skew'][0]; -// const phaseoffset = parameters['phaseoffset'][0]; - -// const dcoffset = parameters['dcoffset'][0]; -// const shape = waveShapeNames[parameters['shape'][0]]; - -// const blockSize = output[0].length ?? 0; - -// 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++) { -// const modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth; -// output[i][n] = modval; -// } -// this.incrementPhase(dt); -// } - -// return true; -// } -// } -// registerProcessor('lfo-processor', LFOProcessor); const waveShapeNames = Object.keys(waveshapes); class LFOProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { @@ -185,8 +123,6 @@ class LFOProcessor extends AudioWorkletProcessor { return true; } - - const output = outputs[0]; const frequency = parameters['frequency'][0]; @@ -194,11 +130,12 @@ class LFOProcessor extends AudioWorkletProcessor { const depth = parameters['depth'][0]; const skew = parameters['skew'][0]; const phaseoffset = parameters['phaseoffset'][0]; - const min = parameters['min'][0]; - const max = parameters['max'][0]; + const curve = parameters['curve'][0]; const dcoffset = parameters['dcoffset'][0]; + const min = parameters['min'][0]; + const max = parameters['max'][0]; const shape = waveShapeNames[parameters['shape'][0]]; const blockSize = output[0].length ?? 0; @@ -210,8 +147,9 @@ class LFOProcessor extends AudioWorkletProcessor { const dt = frequency / sampleRate; for (let n = 0; n < blockSize; n++) { for (let i = 0; i < output.length; i++) { - const modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth; - output[i][n] = clamp(Math.pow(modval, curve), min, max); + let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth; + modval = Math.pow(modval, curve); + output[i][n] = clamp(modval, min, max); } this.incrementPhase(dt); } From c8cf1dc712847d9802b3a527ed337a8e75303943 Mon Sep 17 00:00:00 2001 From: Chandler Abraham Date: Tue, 22 Jul 2025 21:49:40 -0700 Subject: [PATCH 301/538] 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 302/538] 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 303/538] 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 cf5c502a05e1f74e62dc8bf18effb2fc35250e10 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 23 Jul 2025 23:16:14 -0400 Subject: [PATCH 304/538] rm unessecary json --- packages/repl/prebake.mjs | 2 +- website/public/uzudrumkit.json | 54 ---------------------------------- website/src/repl/prebake.mjs | 2 +- 3 files changed, 2 insertions(+), 56 deletions(-) delete mode 100644 website/public/uzudrumkit.json diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index 5e524bbc8..13027d2d8 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(`${ds}/uzudrumkit.json`), + samples(`https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/strudel.json`), samples(`${ds}/vcsl.json`), samples(`${ds}/mridangam.json`), ]); diff --git a/website/public/uzudrumkit.json b/website/public/uzudrumkit.json deleted file mode 100644 index 102baf0b8..000000000 --- a/website/public/uzudrumkit.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "_base": "https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/", - "bd": [ - "bd/10_bd_switchangel.wav", - "bd/11_bd_mot4i.wav", - "bd/12_bd_mot4i.wav", - "bd/13_bd_mot4i.wav", - "bd/14_bd_switchangel.wav", - "bd/15_bd_switchangel.wav", - "bd/16_bd_switchangel.wav" - ], - "breaks": [ - "breaks/10_break_amen_pprocessed.wav" - ], - "cp": [ - "cp/10_cp_switchangel.wav", - "cp/11_cp_mot4i.wav" - ], - "cr": [ - "cr/10_cr_mot4i.wav" - ], - "hh": [ - "hh/10_hh_mot4i.wav", - "hh/11_hh_switchangel.wav", - "hh/12_hh_switchangel.wav", - "hh/13_hh_mot4i.wav" - ], - "ht": [ - "ht/10_ht_mot4i.wav" - ], - "lt": [ - "lt/10_lt_mot4i.wav" - ], - "mt": [ - "mt/10_mt_mot4i.wav" - ], - "oh": [ - "oh/10_oh_switchangel.wav", - "oh/11_oh_switchangel.wav", - "oh/12_oh_switchangel.wav" - ], - "perc": [ - "perc/10_perc_switchangel.wav" - ], - "rim": [ - "rim/10_rim_switch_angel.wav" - ], - "sd": [ - "sd/10_sd_switchangel_3.wav", - "sd/11_sd_switchangel_2.wav", - "sd/12_sd_switchangel_2.wav", - "sd/13_sd.wav" - ] - } \ No newline at end of file diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 21034dc66..ac3b1926f 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(`${baseNoTrailing}/uzudrumkit.json`, undefined, { prebake: true, tag: 'drum-machines' }), + samples(`https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/strudel.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples( { From 4a894b0be2b31d10a28efc286d5a5b5d378000a4 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 23 Jul 2025 23:25:14 -0400 Subject: [PATCH 305/538] frmt --- website/src/repl/prebake.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index ac3b1926f..af4bdaed8 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -28,7 +28,10 @@ export async function prebake() { prebake: true, tag: 'drum-machines', }), - samples(`https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/strudel.json`, undefined, { prebake: true, tag: 'drum-machines' }), + samples(`https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/strudel.json`, undefined, { + prebake: true, + tag: 'drum-machines', + }), samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples( { From fb7d76a2aba2c5aaa2d7415adc8c90fd510d6ef1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 24 Jul 2025 23:50:27 -0400 Subject: [PATCH 306/538] 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 307/538] 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 308/538] 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 309/538] 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 310/538] 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 311/538] 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 312/538] 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 313/538] 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 314/538] 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 315/538] 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 316/538] 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 317/538] 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 318/538] 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 319/538] 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 320/538] 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 321/538] 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 322/538] 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 323/538] 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 324/538] 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 325/538] 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 326/538] 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 327/538] 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 328/538] 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 329/538] 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 330/538] 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 331/538] 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 332/538] 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 333/538] 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 334/538] 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 335/538] 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 336/538] 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 337/538] 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 338/538] 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 339/538] 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 340/538] 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 341/538] 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 342/538] 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 343/538] 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 344/538] 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 345/538] 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 346/538] 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 347/538] 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 348/538] 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 349/538] 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 350/538] 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 351/538] 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 352/538] 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 353/538] 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 354/538] 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 355/538] 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 356/538] 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 357/538] 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 358/538] 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 359/538] 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 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 + +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 360/538] 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 361/538] 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 362/538] 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 363/538] 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 364/538] 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 365/538] 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 366/538] 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 367/538] 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 368/538] 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 369/538] 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 370/538] 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 371/538] 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 372/538] 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 373/538] 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 374/538] 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 375/538] 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 376/538] 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 377/538] 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 378/538] 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 379/538] 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 380/538] 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 381/538] 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 382/538] 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 383/538] 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 384/538] 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 385/538] 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 386/538] 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 387/538] 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 388/538] 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 389/538] 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 390/538] 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 391/538] 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 392/538] 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 393/538] 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 394/538] 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 395/538] 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 396/538] 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 397/538] 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 398/538] 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 399/538] 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 400/538] 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 401/538] 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 bb982e6b9b9a0a80a334fcdf11248e4d9d12a0c9 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 3 Sep 2025 21:55:48 -0500 Subject: [PATCH 402/538] First pass at modes --- packages/core/controls.mjs | 35 +++++++- packages/superdough/helpers.mjs | 4 + packages/superdough/superdough.mjs | 6 +- packages/superdough/worklets.mjs | 89 +++++++++++++++++-- .../src/repl/components/panel/SettingsTab.jsx | 2 +- 5 files changed, 124 insertions(+), 12 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 58a30652c..01679529c 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1575,7 +1575,7 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size', export const { shape } = registerControl(['shape', 'shapevol']); /** * Wave shaping distortion. CAUTION: it can get loud. - * Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. + * Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. Third option sets the waveshaping type. * Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;) * * @name distort @@ -1585,9 +1585,40 @@ export const { shape } = registerControl(['shape', 'shapevol']); * s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>") * @example * note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4") + * @example + * s("bd*4").bank("tr909").distort("4:0.5:fold") * */ -export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dist'); +export const { distort, dist } = registerControl(['distort', 'distortvol', 'distorttype'], 'dist'); + +/** + * Postgain for waveshaping distortion. + * + * @name distortvol + * @synonyms distvol + * @param {number | Pattern} type + * @example + * s("bd*4").bank("tr909").distort(2).distortvol(0.8) + */ +export const { distortvol } = registerControl('distortvol', 'distvol'); + +/** + * Type of waveshaping distortion to apply. + * + * @name distorttype + * @synonyms disttype + * @param {number | string | Pattern} type + * @example + * s("bd*4").bank("tr909").distort(2).distorttype("<0 1 2>") + * + * @example + * s("sine").note("F1*2").release(1) + * .penv(24).pdecay(0.05) + * .distort(70) + * .distorttype("") + */ +export const { distorttype } = registerControl('distorttype', 'disttype'); + /** * Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")` * More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 81bec9399..def49c4d1 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -307,3 +307,7 @@ export function applyFM(param, value, begin) { } return { stop }; } + +export const getDistortion = (distort, postgain, algorithm) => { + return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain, algorithm }); +}; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a518f49b6..98306115c 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, webAudioTimeout } from './helpers.mjs'; +import { createFilter, gainNode, getCompressor, getDistortion, getWorklet, webAudioTimeout } from './helpers.mjs'; import { map } from 'nanostores'; import { logger, errorLogger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; @@ -608,6 +608,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) shapevol = getDefaultValue('shapevol'), distort, distortvol = getDefaultValue('distortvol'), + distorttype, pan, vowel, delay = getDefaultValue('delay'), @@ -782,8 +783,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // effects coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse })); crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); - shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); - distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); + distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype)); if (tremolosync != null) { tremolo = cps * tremolosync; diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 2406d56dc..0eb2bc72f 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -7,6 +7,9 @@ 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 ffloor = (x) => x | 0; +const pv = (arr, n) => arr[n] ?? arr[0]; +const mix = (a, b, t) => (1 - t) * a + t * b; // Restrict phase to the range [0, maxPhase) via wrapping function wrapPhase(phase, maxPhase = 1) { @@ -341,11 +344,85 @@ class LadderProcessor extends AudioWorkletProcessor { } registerProcessor('ladder-processor', LadderProcessor); +// Saturation curves + +const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1) + +const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x)); +const _soft = (x, k) => Math.tanh(x * (1 + k)); +const _hard = (x, k) => clamp((1 + k) * x, -1, 1); +const _sine = (x, k) => Math.sin(x * (1 + k)); + +const _fold = (x, k) => { + let y = (1 + k) * x; + while (y > 1 || y < -1) { + y = y > 1 ? 2 - y : -2 - y; + } + return y; +}; + +const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k)); + +const _pow = (x, k) => { + const t = __squash(k); + const p = 1 / (1 + 0.5 * t); // tame k + return _soft(Math.sign(x) * Math.pow(Math.abs(x), p), 0.5 * k); +}; + +const _diode = (x, k, asym = false) => { + const g = 1 + k; // gain + const t = __squash(k); + const bias = 0.15 * t; + const pos = _soft(x + bias, k); + const neg = _soft(asym ? bias : -x + bias, k); + const y = pos - neg; + // We divide by the derivative at 0 so that the distortion is roughly + // the identity map near 0 => small values are preserved and undistorted + const sech = 1 / Math.cosh(g * bias); + const sech2 = sech * sech; // derivative of tanh is sech^2 + const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x + return _soft(y / denom, k); +}; + +const _asym = (x, k) => _diode(x, k, true); + +const _cubic = (x, k) => { + const t = __squash(k); + const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1) + return _soft(cubic, k); +}; + +export const saturationAlgos = { + scurve: _scurve, + soft: _soft, + hard: _hard, + sine: _sine, + fold: _fold, + sinefold: _sineFold, + pow: _pow, + cubic: _cubic, + diode: _diode, + asym: _asym, +}; +const _algoNames = Object.freeze(Object.keys(saturationAlgos)); + +const _getAlgorithm = (algo) => { + let algoName = typeof algo === 'string' ? algo : _algoNames[algo % _algoNames.length]; + if (!_algoNames.includes(algoName)) { + algoName = _algoNames[0]; + logger(`[superdough] Could not find waveshaping algorithm ${algo}. + Available options are ${_algoNames.join(', ')}. + Defaulting to ${algoName}.`); + } + return saturationAlgos[algoName]; +}; + class DistortProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ { name: 'distort', defaultValue: 0 }, { name: 'postgain', defaultValue: 1 }, + { name: 'algorithm', defaultValue: 0, min: 0, max: _algoNames.length - 1 }, ]; } @@ -363,13 +440,13 @@ class DistortProcessor extends AudioWorkletProcessor { return false; } this.started = hasInput; - - const shape = Math.expm1(parameters.distort[0]); - const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0])); - for (let n = 0; n < blockSize; n++) { - for (let i = 0; i < input.length; i++) { - output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain; + const postgain = clamp(pv(parameters.postgain, n), 0.001, 1); + const shape = Math.expm1(pv(parameters.distort, n)); + const algorithm = _getAlgorithm(pv(parameters.algorithm, n)); + for (let ch = 0; ch < input.length; ch++) { + const x = input[ch][n]; + output[ch][n] = postgain * algorithm(x, shape); } } return true; 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 42ab65abdb6d55e8227f39c6318652519efa3ef8 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 4 Sep 2025 11:47:01 -0500 Subject: [PATCH 403/538] Move things around to allow selection by name and avoid circular imports; fixed chebyshev --- packages/superdough/audioContext.mjs | 18 +++++ packages/superdough/dspworklet.mjs | 2 +- packages/superdough/helpers.mjs | 112 ++++++++++++++++++++++++++- packages/superdough/index.mjs | 1 + packages/superdough/noise.mjs | 2 +- packages/superdough/sampler.mjs | 3 +- packages/superdough/superdough.mjs | 20 +---- packages/superdough/synth.mjs | 3 +- packages/superdough/util.mjs | 2 + packages/superdough/worklets.mjs | 80 +------------------ packages/superdough/zzfx.mjs | 3 +- packages/superdough/zzfx_fork.mjs | 2 +- 12 files changed, 142 insertions(+), 106 deletions(-) create mode 100644 packages/superdough/audioContext.mjs diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs new file mode 100644 index 000000000..9a3c71167 --- /dev/null +++ b/packages/superdough/audioContext.mjs @@ -0,0 +1,18 @@ +let audioContext; + +export const setDefaultAudioContext = () => { + audioContext = new AudioContext(); + return audioContext; +}; + +export const getAudioContext = () => { + if (!audioContext) { + return setDefaultAudioContext(); + } + + return audioContext; +}; + +export function getAudioContextCurrentTime() { + return getAudioContext().currentTime; +} \ No newline at end of file diff --git a/packages/superdough/dspworklet.mjs b/packages/superdough/dspworklet.mjs index 5b02ad298..aac08061e 100644 --- a/packages/superdough/dspworklet.mjs +++ b/packages/superdough/dspworklet.mjs @@ -1,4 +1,4 @@ -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; let worklet; export async function dspWorklet(ac, code) { diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index def49c4d1..b5d83e7bc 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 { getAudioContext } from './audioContext.mjs'; +import { clamp, ffloor, nanFallback } from './util.mjs'; import { getNoiseBuffer } from './noise.mjs'; export const noises = ['pink', 'white', 'brown', 'crackle']; @@ -308,6 +308,110 @@ export function applyFM(param, value, begin) { return { stop }; } -export const getDistortion = (distort, postgain, algorithm) => { - return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain, algorithm }); +// Saturation curves + +const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1) + +const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x)); +const _soft = (x, k) => Math.tanh(x * (1 + k)); +const _hard = (x, k) => clamp((1 + k) * x, -1, 1); +const _sine = (x, k) => Math.sin(x * (1 + k)); + +const _fold = (x, k) => { + let y = (1 + k) * x; + while (y > 1 || y < -1) { + y = y > 1 ? 2 - y : -2 - y; + } + return y; +}; + +const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k)); + +const _pow = (x, k) => { + const t = __squash(k); + const p = 1 / (1 + 0.5 * t); // tame k + return _soft(Math.sign(x) * Math.pow(Math.abs(x), p), 0.5 * k); +}; + +const _cubic = (x, k) => { + const t = __squash(k); + const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1) + return _soft(cubic, k); +}; + +const _diode = (x, k, asym = false) => { + const g = 1 + k; // gain + const t = __squash(k); + const bias = 0.15 * t; + const pos = _soft(x + bias, k); + const neg = _soft(asym ? bias : -x + bias, k); + const y = pos - neg; + // We divide by the derivative at 0 so that the distortion is roughly + // the identity map near 0 => small values are preserved and undistorted + const sech = 1 / Math.cosh(g * bias); + const sech2 = sech * sech; // derivative of tanh is sech^2 + const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x + return _soft(y / denom, k); +}; + +const _asym = (x, k) => _diode(x, k, true); + +const _chebyshev = (x, k) => { + const kl = Math.log1p(k); + let tnm1 = 1; + let tnm2 = x; + let tn; + let y = 0; + const iterations = 1 + ffloor(Math.min(16 * kl, 255)); + for (let i = 1; i < iterations; i++) { + if (i < 2) { + // Already set inital conditions + y += i == 0 ? tnm1 : tnm2; + continue; + } + tn = 2 * x * tnm1 - tnm2; // https://en.wikipedia.org/wiki/Chebyshev_polynomials#Recurrence_definition + tnm2 = tnm1; + tnm1 = tn; + if (i % 2 === 0) { + y += tn; + } + } + // Soft clip + return _soft(y, 0); +}; + +export const distortionAlgorithms = { + scurve: _scurve, + soft: _soft, + hard: _hard, + sine: _sine, + fold: _fold, + sinefold: _sineFold, + pow: _pow, + cubic: _cubic, + diode: _diode, + asym: _asym, + chebyshev: _chebyshev, +}; +const _algoNames = Object.freeze(Object.keys(distortionAlgorithms)); + +export const getDistortionAlgorithm = (algo) => { + let index = algo; + if (typeof algo === 'string') { + index = _algoNames.indexOf(algo); + if (index === -1) { + logger(`[superdough] Could not find waveshaping algorithm ${algo}. + Available options are ${_algoNames.join(', ')}. + Defaulting to ${_algoNames[0]}.`); + index = 0; + } + } + const name = _algoNames[index % _algoNames.length]; // allow for wrapping if algo was a number + const algorithm = distortionAlgorithms[name]; + return { algorithm, index }; +}; + +export const getDistortion = (distort, postgain, algorithm) => { + const { _algorithm, index } = getDistortionAlgorithm(algorithm); + return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain, algorithm: index }); }; diff --git a/packages/superdough/index.mjs b/packages/superdough/index.mjs index fd49fe338..184fc078c 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 './audioContext.mjs'; diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 5411f2f2f..816dd252b 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -1,5 +1,5 @@ import { drywet } from './helpers.mjs'; -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; let noiseCache = {}; diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 18d1b7797..7d69aef02 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,5 +1,6 @@ import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs'; -import { getAudioContext, registerSound } from './index.mjs'; +import { registerSound } from './index.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { logger } from './logger.mjs'; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 98306115c..10d8ed9f4 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -13,6 +13,7 @@ import { createFilter, gainNode, getCompressor, getDistortion, getWorklet, webAu import { map } from 'nanostores'; import { logger, errorLogger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; +import { getAudioContext } from './audioContext.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; @@ -193,25 +194,6 @@ export function setVersionDefaults(version) { export const resetLoadedSounds = () => soundMap.set({}); -let audioContext; - -export const setDefaultAudioContext = () => { - audioContext = new AudioContext(); - return audioContext; -}; - -export const getAudioContext = () => { - if (!audioContext) { - return setDefaultAudioContext(); - } - - return audioContext; -}; - -export function getAudioContextCurrentTime() { - return getAudioContext().currentTime; -} - let workletsLoading; function loadWorklets() { if (!workletsLoading) { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 71138ae41..5f303753a 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,5 +1,6 @@ import { clamp, midiToFreq, noteToMidi } from './util.mjs'; -import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs'; +import { registerSound, soundMap, getLfo } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { applyFM, gainNode, diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index f4d59024e..c3d511fd4 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -76,3 +76,5 @@ export function cycleToSeconds(cycle, cps) { export function secondsToCycle(t, cps) { return t * cps; } + +export const ffloor = (x) => x | 0; // fast floor for positive numbers \ No newline at end of file diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 0eb2bc72f..ae03e9603 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -4,12 +4,11 @@ import OLAProcessor from './ola-processor'; import FFT from './fft.js'; +import { getDistortionAlgorithm } from './helpers.mjs'; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const _mod = (n, m) => ((n % m) + m) % m; -const ffloor = (x) => x | 0; const pv = (arr, n) => arr[n] ?? arr[0]; -const mix = (a, b, t) => (1 - t) * a + t * b; // Restrict phase to the range [0, maxPhase) via wrapping function wrapPhase(phase, maxPhase = 1) { @@ -344,85 +343,12 @@ class LadderProcessor extends AudioWorkletProcessor { } registerProcessor('ladder-processor', LadderProcessor); -// Saturation curves - -const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1) - -const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x)); -const _soft = (x, k) => Math.tanh(x * (1 + k)); -const _hard = (x, k) => clamp((1 + k) * x, -1, 1); -const _sine = (x, k) => Math.sin(x * (1 + k)); - -const _fold = (x, k) => { - let y = (1 + k) * x; - while (y > 1 || y < -1) { - y = y > 1 ? 2 - y : -2 - y; - } - return y; -}; - -const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k)); - -const _pow = (x, k) => { - const t = __squash(k); - const p = 1 / (1 + 0.5 * t); // tame k - return _soft(Math.sign(x) * Math.pow(Math.abs(x), p), 0.5 * k); -}; - -const _diode = (x, k, asym = false) => { - const g = 1 + k; // gain - const t = __squash(k); - const bias = 0.15 * t; - const pos = _soft(x + bias, k); - const neg = _soft(asym ? bias : -x + bias, k); - const y = pos - neg; - // We divide by the derivative at 0 so that the distortion is roughly - // the identity map near 0 => small values are preserved and undistorted - const sech = 1 / Math.cosh(g * bias); - const sech2 = sech * sech; // derivative of tanh is sech^2 - const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x - return _soft(y / denom, k); -}; - -const _asym = (x, k) => _diode(x, k, true); - -const _cubic = (x, k) => { - const t = __squash(k); - const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1) - return _soft(cubic, k); -}; - -export const saturationAlgos = { - scurve: _scurve, - soft: _soft, - hard: _hard, - sine: _sine, - fold: _fold, - sinefold: _sineFold, - pow: _pow, - cubic: _cubic, - diode: _diode, - asym: _asym, -}; -const _algoNames = Object.freeze(Object.keys(saturationAlgos)); - -const _getAlgorithm = (algo) => { - let algoName = typeof algo === 'string' ? algo : _algoNames[algo % _algoNames.length]; - if (!_algoNames.includes(algoName)) { - algoName = _algoNames[0]; - logger(`[superdough] Could not find waveshaping algorithm ${algo}. - Available options are ${_algoNames.join(', ')}. - Defaulting to ${algoName}.`); - } - return saturationAlgos[algoName]; -}; - class DistortProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ { name: 'distort', defaultValue: 0 }, { name: 'postgain', defaultValue: 1 }, - { name: 'algorithm', defaultValue: 0, min: 0, max: _algoNames.length - 1 }, + { name: 'algorithm', defaultValue: 0, min: 0 }, ]; } @@ -440,10 +366,10 @@ class DistortProcessor extends AudioWorkletProcessor { return false; } this.started = hasInput; + const { algorithm } = getDistortionAlgorithm(pv(parameters.algorithm, 0)); // do not allow audio rate algo changes for (let n = 0; n < blockSize; n++) { const postgain = clamp(pv(parameters.postgain, n), 0.001, 1); const shape = Math.expm1(pv(parameters.distort, n)); - const algorithm = _getAlgorithm(pv(parameters.algorithm, n)); for (let ch = 0; ch < input.length; ch++) { const x = input[ch][n]; output[ch][n] = postgain * algorithm(x, shape); diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index a6af82609..32db0395a 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -1,6 +1,7 @@ //import { ZZFX } from 'zzfx'; import { midiToFreq, noteToMidi } from './util.mjs'; -import { registerSound, getAudioContext } from './superdough.mjs'; +import { registerSound } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { buildSamples } from './zzfx_fork.mjs'; export const getZZFX = (value, t) => { diff --git a/packages/superdough/zzfx_fork.mjs b/packages/superdough/zzfx_fork.mjs index 7235d1bf8..f3ee6a051 100644 --- a/packages/superdough/zzfx_fork.mjs +++ b/packages/superdough/zzfx_fork.mjs @@ -1,4 +1,4 @@ -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; // https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6 // changes: replaced this.volume with 1 + using sampleRate from getAudioContext() From 9b1c23d49f6cdddc792f74a6af06991d9bb6c951 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 4 Sep 2025 20:05:28 -0500 Subject: [PATCH 404/538] Some tuning experimentally and adding back chebyshev --- packages/superdough/helpers.mjs | 38 ++++++++++++--------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index b5d83e7bc..28c14f79d 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -315,10 +315,9 @@ const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1) const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x)); const _soft = (x, k) => Math.tanh(x * (1 + k)); const _hard = (x, k) => clamp((1 + k) * x, -1, 1); -const _sine = (x, k) => Math.sin(x * (1 + k)); const _fold = (x, k) => { - let y = (1 + k) * x; + let y = (1 + 0.5 * k) * x; while (y > 1 || y < -1) { y = y > 1 ? 2 - y : -2 - y; } @@ -327,29 +326,23 @@ const _fold = (x, k) => { const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k)); -const _pow = (x, k) => { - const t = __squash(k); - const p = 1 / (1 + 0.5 * t); // tame k - return _soft(Math.sign(x) * Math.pow(Math.abs(x), p), 0.5 * k); -}; - const _cubic = (x, k) => { - const t = __squash(k); + const t = __squash(Math.log1p(k)); const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1) return _soft(cubic, k); }; const _diode = (x, k, asym = false) => { - const g = 1 + k; // gain - const t = __squash(k); - const bias = 0.15 * t; - const pos = _soft(x + bias, k); - const neg = _soft(asym ? bias : -x + bias, k); + const g = 1 + 2 * k; // gain + const t = __squash(Math.log1p(k)); + const bias = 0.07 * t; + const pos = _soft(x + bias, 2 * k); + const neg = _soft(asym ? bias : -x + bias, 2 * k); const y = pos - neg; // We divide by the derivative at 0 so that the distortion is roughly // the identity map near 0 => small values are preserved and undistorted const sech = 1 / Math.cosh(g * bias); - const sech2 = sech * sech; // derivative of tanh is sech^2 + const sech2 = sech * sech; // derivative of soft (i.e. tanh) is sech^2 const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x return _soft(y / denom, k); }; @@ -357,13 +350,12 @@ const _diode = (x, k, asym = false) => { const _asym = (x, k) => _diode(x, k, true); const _chebyshev = (x, k) => { - const kl = Math.log1p(k); + const kl = 10 * Math.log1p(k); let tnm1 = 1; let tnm2 = x; let tn; let y = 0; - const iterations = 1 + ffloor(Math.min(16 * kl, 255)); - for (let i = 1; i < iterations; i++) { + for (let i = 1; i < 64; i++) { if (i < 2) { // Already set inital conditions y += i == 0 ? tnm1 : tnm2; @@ -373,24 +365,22 @@ const _chebyshev = (x, k) => { tnm2 = tnm1; tnm1 = tn; if (i % 2 === 0) { - y += tn; + y += Math.min(1.3 * kl / i, 2) * tn; } } // Soft clip - return _soft(y, 0); + return _soft(y, kl / 20); }; export const distortionAlgorithms = { scurve: _scurve, soft: _soft, hard: _hard, - sine: _sine, - fold: _fold, - sinefold: _sineFold, - pow: _pow, cubic: _cubic, diode: _diode, asym: _asym, + fold: _fold, + sinefold: _sineFold, chebyshev: _chebyshev, }; const _algoNames = Object.freeze(Object.keys(distortionAlgorithms)); From 46a45b4596902e734cada6dfadf3742cd8b445f9 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 4 Sep 2025 20:51:06 -0500 Subject: [PATCH 405/538] 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 406/538] 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 407/538] 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 408/538] 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 409/538] 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 410/538] 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 411/538] 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 412/538] 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 413/538] 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 414/538] 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 415/538] 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 416/538] 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 417/538] 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 418/538] 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 419/538] 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 420/538] 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 421/538] 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 51b150b8298ba2839ef787d7bb58b17d03e995c1 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 10 Sep 2025 21:27:56 -0500 Subject: [PATCH 422/538] Optimize folding for audio rate --- packages/superdough/helpers.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 28c14f79d..b56065856 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -311,17 +311,17 @@ export function applyFM(param, value, begin) { // Saturation curves const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1) +const _mod = (n, m) => ((n % m) + m) % m; const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x)); const _soft = (x, k) => Math.tanh(x * (1 + k)); const _hard = (x, k) => clamp((1 + k) * x, -1, 1); const _fold = (x, k) => { + // Closed form folding for audio rate let y = (1 + 0.5 * k) * x; - while (y > 1 || y < -1) { - y = y > 1 ? 2 - y : -2 - y; - } - return y; + const window = _mod(y + 1, 4); + return 1 - Math.abs(window - 2); }; const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k)); From 9848740e180c920384ef6cba01d70ac481b66df3 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 10 Sep 2025 21:39:02 -0500 Subject: [PATCH 423/538] Formatting and tests --- packages/core/controls.mjs | 6 +- packages/superdough/audioContext.mjs | 2 +- packages/superdough/helpers.mjs | 5 +- packages/superdough/util.mjs | 2 +- packages/superdough/worklets.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 76 +++++++++++++++++++++++ 6 files changed, 85 insertions(+), 8 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 01679529c..47626fda9 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1586,7 +1586,7 @@ export const { shape } = registerControl(['shape', 'shapevol']); * @example * note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4") * @example - * s("bd*4").bank("tr909").distort("4:0.5:fold") + * s("bd:4*4").bank("tr808").distort("3:0.5:diode") * */ export const { distort, dist } = registerControl(['distort', 'distortvol', 'distorttype'], 'dist'); @@ -1614,8 +1614,8 @@ export const { distortvol } = registerControl('distortvol', 'distvol'); * @example * s("sine").note("F1*2").release(1) * .penv(24).pdecay(0.05) - * .distort(70) - * .distorttype("") + * .distort(rand.range(1, 8)) + * .distorttype("") */ export const { distorttype } = registerControl('distorttype', 'disttype'); diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs index 9a3c71167..71e01d57d 100644 --- a/packages/superdough/audioContext.mjs +++ b/packages/superdough/audioContext.mjs @@ -15,4 +15,4 @@ export const getAudioContext = () => { export function getAudioContextCurrentTime() { return getAudioContext().currentTime; -} \ No newline at end of file +} diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index b56065856..9ee660ae2 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,6 +1,7 @@ import { getAudioContext } from './audioContext.mjs'; -import { clamp, ffloor, nanFallback } from './util.mjs'; +import { clamp, nanFallback } from './util.mjs'; import { getNoiseBuffer } from './noise.mjs'; +import { logger } from './logger.mjs'; export const noises = ['pink', 'white', 'brown', 'crackle']; @@ -365,7 +366,7 @@ const _chebyshev = (x, k) => { tnm2 = tnm1; tnm1 = tn; if (i % 2 === 0) { - y += Math.min(1.3 * kl / i, 2) * tn; + y += Math.min((1.3 * kl) / i, 2) * tn; } } // Soft clip diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index c3d511fd4..2477e3413 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -77,4 +77,4 @@ export function secondsToCycle(t, cps) { return t * cps; } -export const ffloor = (x) => x | 0; // fast floor for positive numbers \ No newline at end of file +export const ffloor = (x) => x | 0; // fast floor for positive numbers diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index ae03e9603..e593db17e 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -4,7 +4,7 @@ import OLAProcessor from './ola-processor'; import FFT from './fft.js'; -import { getDistortionAlgorithm } from './helpers.mjs'; +import { getDistortionAlgorithm } from './helpers.mjs'; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const _mod = (n, m) => ((n % m) + m) % m; diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 41207620f..e70a5d494 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2775,6 +2775,82 @@ exports[`runs examples > example "distort" example index 1 1`] = ` ] `; +exports[`runs examples > example "distort" example index 2 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 1/4 → 1/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 1/2 → 3/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 3/4 → 1/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 1/1 → 5/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 5/4 → 3/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 3/2 → 7/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 7/4 → 2/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 2/1 → 9/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 9/4 → 5/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 5/2 → 11/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 11/4 → 3/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 3/1 → 13/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 13/4 → 7/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 7/2 → 15/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 15/4 → 4/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", +] +`; + +exports[`runs examples > example "distorttype" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 1/4 → 1/2 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 1/2 → 3/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 3/4 → 1/1 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 1/1 → 5/4 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 5/4 → 3/2 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 3/2 → 7/4 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 7/4 → 2/1 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 2/1 → 9/4 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 9/4 → 5/2 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 5/2 → 11/4 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 11/4 → 3/1 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 3/1 → 13/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 13/4 → 7/2 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 7/2 → 15/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 15/4 → 4/1 | s:bd bank:tr909 distort:2 distorttype:0 ]", +] +`; + +exports[`runs examples > example "distorttype" example index 1 1`] = ` +[ + "[ (0/1 → 1/2) ⇝ 1/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]", + "[ 0/1 ⇜ (1/2 → 1/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]", + "[ (1/1 → 3/2) ⇝ 2/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]", + "[ 1/1 ⇜ (3/2 → 2/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]", + "[ (2/1 → 5/2) ⇝ 3/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]", + "[ 2/1 ⇜ (5/2 → 3/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]", + "[ (3/1 → 7/2) ⇝ 4/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]", + "[ 3/1 ⇜ (7/2 → 4/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]", +] +`; + +exports[`runs examples > example "distortvol" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 1/4 → 1/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 1/2 → 3/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 3/4 → 1/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 1/1 → 5/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 5/4 → 3/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 3/2 → 7/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 7/4 → 2/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 2/1 → 9/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 9/4 → 5/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 5/2 → 11/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 11/4 → 3/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 3/1 → 13/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 13/4 → 7/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 7/2 → 15/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 15/4 → 4/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", +] +`; + exports[`runs examples > example "djf" example index 0 1`] = ` [ "[ 0/1 → 1/4 | n:0 s:superzow octave:3 djf:0.5 ]", From 5e2e6411e5e67cb2458132f3e643ebe1d265c5cd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 11 Sep 2025 08:26:20 +0200 Subject: [PATCH 424/538] 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 425/538] 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 426/538] 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 427/538] 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 428/538] 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 429/538] 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 430/538] 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 431/538] 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 432/538] 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 433/538] 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 434/538] 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 69b034349a02a9db8c0406f6a530e00403a0037f Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Sep 2025 14:30:05 -0500 Subject: [PATCH 435/538] 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 436/538] 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 437/538] 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 438/538] 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 439/538] 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 440/538] 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 441/538] 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 442/538] 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 443/538] 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 444/538] 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 445/538] 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 446/538] 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 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 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 447/538] 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 448/538] 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 449/538] 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 450/538] 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 451/538] 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 452/538] 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 453/538] 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 454/538] 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 455/538] 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 456/538] 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 457/538] 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 458/538] 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 459/538] 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 460/538] 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 461/538] 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 462/538] 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 463/538] 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 464/538] 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 465/538] 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 466/538] 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 467/538] 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 468/538] 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 469/538] 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 470/538] 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 471/538] 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 472/538] 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 473/538] 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 474/538] 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 475/538] 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 476/538] 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 477/538] 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 478/538] 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 f0669ce3bad299bf52de86b49f0eee1a146e96a1 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 24 Sep 2025 17:32:03 -0700 Subject: [PATCH 479/538] Move algorithm into constructor --- packages/superdough/helpers.mjs | 9 +++++++-- packages/superdough/worklets.mjs | 7 +++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 9ee660ae2..8b92bd4e4 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -403,6 +403,11 @@ export const getDistortionAlgorithm = (algo) => { }; export const getDistortion = (distort, postgain, algorithm) => { - const { _algorithm, index } = getDistortionAlgorithm(algorithm); - return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain, algorithm: index }); + const { index } = getDistortionAlgorithm(algorithm); + return getWorklet( + getAudioContext(), + 'distort-processor', + { distort, postgain }, + { processorOptions: { algorithm: index } }, + ); }; diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index e593db17e..f1d95e449 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -348,13 +348,13 @@ class DistortProcessor extends AudioWorkletProcessor { return [ { name: 'distort', defaultValue: 0 }, { name: 'postgain', defaultValue: 1 }, - { name: 'algorithm', defaultValue: 0, min: 0 }, ]; } - constructor() { + constructor({ processorOptions }) { super(); this.started = false; + this.algorithm = getDistortionAlgorithm(processorOptions.algorithm).algorithm; } process(inputs, outputs, parameters) { @@ -366,13 +366,12 @@ class DistortProcessor extends AudioWorkletProcessor { return false; } this.started = hasInput; - const { algorithm } = getDistortionAlgorithm(pv(parameters.algorithm, 0)); // do not allow audio rate algo changes for (let n = 0; n < blockSize; n++) { const postgain = clamp(pv(parameters.postgain, n), 0.001, 1); const shape = Math.expm1(pv(parameters.distort, n)); for (let ch = 0; ch < input.length; ch++) { const x = input[ch][n]; - output[ch][n] = postgain * algorithm(x, shape); + output[ch][n] = postgain * this.algorithm(x, shape); } } 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 480/538] 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 481/538] 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 482/538] 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 b3537a9acb6cbf3480de7afbf93c7921969dfd25 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 24 Sep 2025 22:17:30 -0700 Subject: [PATCH 483/538] Some cleanup --- packages/superdough/helpers.mjs | 11 ++--------- packages/superdough/worklets.mjs | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 8b92bd4e4..5ef132d00 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -398,16 +398,9 @@ export const getDistortionAlgorithm = (algo) => { } } const name = _algoNames[index % _algoNames.length]; // allow for wrapping if algo was a number - const algorithm = distortionAlgorithms[name]; - return { algorithm, index }; + return distortionAlgorithms[name]; }; export const getDistortion = (distort, postgain, algorithm) => { - const { index } = getDistortionAlgorithm(algorithm); - return getWorklet( - getAudioContext(), - 'distort-processor', - { distort, postgain }, - { processorOptions: { algorithm: index } }, - ); + return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } }); }; diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index f1d95e449..5ab326226 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -354,7 +354,7 @@ class DistortProcessor extends AudioWorkletProcessor { constructor({ processorOptions }) { super(); this.started = false; - this.algorithm = getDistortionAlgorithm(processorOptions.algorithm).algorithm; + this.algorithm = getDistortionAlgorithm(processorOptions.algorithm); } process(inputs, outputs, parameters) { From e3741ae8faad7f1b9c160a6949c71a0ff3b03ee2 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 25 Sep 2025 00:05:50 -0700 Subject: [PATCH 484/538] 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 485/538] 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 486/538] 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 487/538] 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 488/538] 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 489/538] 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 490/538] 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 491/538] 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 492/538] 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 493/538] 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 494/538] 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 495/538] 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 496/538] 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 497/538] 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 498/538] 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 499/538] 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 500/538] 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 501/538] 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 502/538] 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 503/538] 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 504/538] 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 505/538] 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 506/538] 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 507/538] 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 From 4de0c11f8c0834868ae346125d35e77134bf4d7d Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 29 Sep 2025 14:15:50 -0700 Subject: [PATCH 508/538] Remove mipmaps, fix clamping of parameters --- packages/superdough/wavetable.mjs | 32 +++++--------------- packages/superdough/worklets.mjs | 49 +++++++++++++------------------ 2 files changed, 27 insertions(+), 54 deletions(-) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 48b7e661c..505e6ac38 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -5,7 +5,6 @@ import { destroyAudioWorkletNode, getADSRValues, getFrequencyFromValue, - getLfo, getParamADSR, getPitchEnvelope, getVibratoOscillator, @@ -14,7 +13,6 @@ import { } from './helpers.mjs'; import { logger } from './logger.mjs'; -const WT_MAX_MIP_LEVELS = 6; export const Warpmode = Object.freeze({ NONE: 0, ASYM: 1, @@ -50,25 +48,7 @@ async function loadWavetableFrames(url, label, frameLen = 2048) { 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 }; + return { frames, frameLen, numFrames }; } const loadCache = {}; @@ -222,7 +202,7 @@ 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 { s, n = 0, duration, clip } = value; const ac = getAudioContext(); const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); let { warpmode } = value; @@ -232,7 +212,10 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const frequency = getFrequencyFromValue(value); const { url, label } = getCommonSampleInfo(value, tables); const payload = await loadWavetableFrames(url, label, frameLen); - const holdEnd = t + duration; + let holdEnd = t + duration; + if (clip !== undefined) { + holdEnd = Math.min(t + clip * duration, holdEnd); + } const endWithRelease = holdEnd + release; const envEnd = endWithRelease + 0.01; const source = getWorklet( @@ -252,7 +235,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { }, { outputChannelCount: [2] }, ); - source.port.postMessage({ type: 'tables', payload }); + source.port.postMessage({ type: 'table', payload }); if (ac.currentTime > t) { logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight'); return; @@ -328,7 +311,6 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const timeoutNode = webAudioTimeout( ac, () => { - source.disconnect(); destroyAudioWorkletNode(source); vibratoOscillator?.stop(); node.disconnect(); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 7f5cbcf23..814c97f41 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -137,7 +137,7 @@ class LFOProcessor extends AudioWorkletProcessor { } } - process(inputs, outputs, parameters) { + process(_inputs, outputs, parameters) { const begin = parameters['begin'][0]; if (currentTime >= parameters.end[0]) { return false; @@ -510,7 +510,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { }, ]; } - process(input, outputs, params) { + process(_input, outputs, params) { if (currentTime <= params.begin[0]) { return true; } @@ -1061,7 +1061,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { constructor(options) { super(options); - this.tables = null; + this.frames = null; this.frameLen = 0; this.numFrames = 0; this.phase = []; @@ -1069,23 +1069,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { this.port.onmessage = (e) => { const { type, payload } = e.data || {}; - if (type === 'tables') { - this.tables = payload.mipmaps; + if (type === 'table') { + this.frames = payload.frames; this.frameLen = payload.frameLen; - this.numFrames = this.tables[0].length; + this.numFrames = this.frames.length; } }; } - _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); } @@ -1222,11 +1213,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } _sampleFrame(frame, phase) { - const pos = phase * frame.length; + const len = frame.length; + const pos = phase * len; const i = pos | 0; const frac = pos - i; - const a = frame[i % frame.length]; - const b = frame[(i + 1) % frame.length]; + const a = frame[i]; + const i1 = i + 1 < len ? i + 1 : 0; // fast wrap + const b = frame[i1]; return a + (b - a) * frac; } @@ -1240,23 +1233,23 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const outL = outputs[0][0]; const outR = outputs[0][1] || outputs[0][0]; - if (!this.tables) { + if (!this.frames) { outL.fill(0); if (outR !== outL) outR.set(outL); return true; } - + const invSR = 1 / sampleRate; for (let i = 0; i < outL.length; i++) { const detune = pv(parameters.detune, i); - const tablePos = pv(parameters.position, i); + const tablePos = clamp(pv(parameters.position, i), 0, 1); const idx = tablePos * (this.numFrames - 1); const fIdx = idx | 0; const frac = idx - fIdx; - const warpAmount = pv(parameters.warp, i); + const warpAmount = clamp(pv(parameters.warp, i), 0, 1); const warpMode = pv(parameters.warpMode, i); const voices = pv(parameters.voices, i); - const phaseRand = pv(parameters.phaserand, i); - const spread = voices > 1 ? pv(parameters.spread, i) : 0; + const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1); + const spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 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); @@ -1272,15 +1265,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { gainR = gain1; } const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune - const dPhase = fVoice / sampleRate; - const level = this._chooseMip(dPhase); - const table = this.tables[level]; + const dPhase = fVoice * invSR; // 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(table[fIdx], ph); - const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph); + const s0 = this._sampleFrame(this.frames[fIdx], ph); + const s1 = this._sampleFrame(this.frames[Math.min(this.numFrames - 1, fIdx + 1)], ph); let s = s0 + (s1 - s0) * frac; if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { s = -s; From bad498a1e512061c663bd42408968e9d5d348a0d Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 30 Sep 2025 16:21:28 -0700 Subject: [PATCH 509/538] Remove unused var --- packages/superdough/worklets.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 814c97f41..aa49ea32a 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1065,7 +1065,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { this.frameLen = 0; this.numFrames = 0; this.phase = []; - this.syncRatio = 1; this.port.onmessage = (e) => { const { type, payload } = e.data || {}; From cdc5e5f3f33d0fcf06d346120124d76c575dd22a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 30 Sep 2025 19:45:30 -0400 Subject: [PATCH 510/538] working --- packages/superdough/superdoughoutput.mjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index 9ef7bcbf3..221f80a26 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -135,12 +135,14 @@ export class SuperdoughOutput { } reset() { + this.disconnect() + this.initializeAudio(); + } + disconnect() { 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, @@ -172,6 +174,7 @@ export class SuperdoughAudioController { Array.from(this.nodes).forEach((node) => { node.disconnect(); }); + this.nodes = {} this.output.reset(); } From 9f8143e062239658426c483f7ee9433ed839e3cd Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 30 Sep 2025 19:46:53 -0400 Subject: [PATCH 511/538] format --- packages/superdough/superdoughoutput.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index 221f80a26..afe91e71e 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -135,7 +135,7 @@ export class SuperdoughOutput { } reset() { - this.disconnect() + this.disconnect(); this.initializeAudio(); } disconnect() { @@ -174,7 +174,7 @@ export class SuperdoughAudioController { Array.from(this.nodes).forEach((node) => { node.disconnect(); }); - this.nodes = {} + this.nodes = {}; this.output.reset(); } From a2b8407fbb86477ae4e684481c1cb4915f64307b Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 30 Sep 2025 23:08:28 -0500 Subject: [PATCH 512/538] Add back mipmaps; add caching --- packages/superdough/wavetable.mjs | 33 +++++++++++++---------- packages/superdough/worklets.mjs | 45 +++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 505e6ac38..44367f786 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -38,21 +38,25 @@ export const Warpmode = Object.freeze({ FLIP: 21, }); -async function loadWavetableFrames(url, label, frameLen = 2048) { - const buf = await loadBuffer(url, label); - const ch0 = buf.getChannelData(0); - const total = ch0.length; - 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; - frames[i] = ch0.subarray(start, start + frameLen); +const seenKeys = new Set(); +async function getPayload(url, label, frameLen = 2048) { + const key = `${url},${frameLen}`; + if (!seenKeys.has(key)) { + const buf = await loadBuffer(url, label); + const ch0 = buf.getChannelData(0); + const total = ch0.length; + 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; + frames[i] = ch0.subarray(start, start + frameLen); + } + seenKeys.add(key); + return { frames, frameLen, numFrames, key }; } - return { frames, frameLen, numFrames }; + return { frameLen, key }; // worklet will use the cached version } -const loadCache = {}; - function humanFileSize(bytes, si) { var thresh = si ? 1000 : 1024; if (bytes < thresh) return bytes + ' B'; @@ -97,6 +101,7 @@ async function decodeAtNativeRate(arr) { return await tempAC.decodeAudioData(arr); } +const loadCache = {}; const loadBuffer = (url, label) => { url = url.replace('#', '%23'); if (!loadCache[url]) { @@ -211,7 +216,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { } const frequency = getFrequencyFromValue(value); const { url, label } = getCommonSampleInfo(value, tables); - const payload = await loadWavetableFrames(url, label, frameLen); + const payload = await getPayload(url, label, frameLen); let holdEnd = t + duration; if (clip !== undefined) { holdEnd = Math.min(t + clip * duration, holdEnd); @@ -305,7 +310,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { 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'); + getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd); const handle = { node, source }; const timeoutNode = webAudioTimeout( diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index aa49ea32a..9b190a456 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1043,6 +1043,7 @@ function brownian(x, oct = 4) { return (sum / norm) * 2 - 1; } +const tablesCache = {}; class WavetableOscillatorProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ @@ -1061,7 +1062,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { constructor(options) { super(options); - this.frames = null; this.frameLen = 0; this.numFrames = 0; this.phase = []; @@ -1069,9 +1069,28 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { this.port.onmessage = (e) => { const { type, payload } = e.data || {}; if (type === 'table') { - this.frames = payload.frames; + const key = payload.key; this.frameLen = payload.frameLen; - this.numFrames = this.frames.length; + if (!tablesCache[key]) { + const tables = [payload.frames]; + let table = tables[0]; + for (let level = 1; level < 1; level++) { + const nextLen = table.length >> 1; + const nextTable = table.map((frame) => { + const avg = new Float32Array(nextLen); + for (let i = 0; i < nextLen; i++) { + avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2; + } + return avg; + }); + tables.push(nextTable); + table = nextTable; + if (nextLen <= 32) break; + } + tablesCache[key] = tables; + } + this.tables = tablesCache[key]; + this.numFrames = this.tables[0].length; } }; } @@ -1222,6 +1241,15 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { return a + (b - a) * frac; } + _chooseMip(dphi) { + const approxHarm = clamp(dphi, 1e-6, 64); + let level = 0; + while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) { + level++; + } + return level; + } + process(_inputs, outputs, parameters) { if (currentTime >= parameters.end[0]) { return false; @@ -1231,8 +1259,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } const outL = outputs[0][0]; const outR = outputs[0][1] || outputs[0][0]; - - if (!this.frames) { + if (!this.tables) { outL.fill(0); if (outR !== outL) outR.set(outL); return true; @@ -1253,7 +1280,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 = 1 / Math.sqrt(voices); for (let n = 0; n < voices; n++) { const isOdd = (n & 1) == 1; let gainL = gain1; @@ -1265,12 +1292,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune const dPhase = fVoice * invSR; + const level = this._chooseMip(dPhase); + 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(this.frames[fIdx], ph); - const s1 = this._sampleFrame(this.frames[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; From 3a7ec18d46f5e051796d9b0e92155e40665ba981 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 30 Sep 2025 23:19:55 -0500 Subject: [PATCH 513/538] Move invsr to constructor --- 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 9b190a456..ccfaf5107 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1065,6 +1065,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { this.frameLen = 0; this.numFrames = 0; this.phase = []; + this.invSR = 1 / sampleRate; this.port.onmessage = (e) => { const { type, payload } = e.data || {}; @@ -1264,7 +1265,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { if (outR !== outL) outR.set(outL); return true; } - const invSR = 1 / sampleRate; for (let i = 0; i < outL.length; i++) { const detune = pv(parameters.detune, i); const tablePos = clamp(pv(parameters.position, i), 0, 1); @@ -1291,7 +1291,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { gainR = gain1; } const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune - const dPhase = fVoice * invSR; + const dPhase = fVoice * this.invSR; const level = this._chooseMip(dPhase); const table = this.tables[level]; From c5f1085bd9e8a0a061247eb2e165fde574809ea7 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 30 Sep 2025 23:51:59 -0500 Subject: [PATCH 514/538] WIP --- packages/core/pattern.mjs | 18 ++++++++++++++++++ packages/superdough/helpers.mjs | 1 + packages/superdough/sampler.mjs | 4 ++-- packages/superdough/wavetable.mjs | 1 - 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d0be3f599..dcee7f174 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3536,3 +3536,21 @@ export const morph = (frompat, topat, bypat) => { bypat = reify(bypat); return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by)))); }; + +/** + * Wavefolding distortion + * + * @name fold + * @param {number | Pattern} distortion + * + */ +export const fold = register('fold', function (amt, vol = 1, pat) { + return pat.withvValue((v) => { + return { + ...v, + distortion: amt, + distortvol: vol, + distorttype: 'fold', + }; + }).innerJoin(); +}); diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index a471da9dd..6db3b04ab 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -475,6 +475,7 @@ export const getDistortionAlgorithm = (algo) => { }; export const getDistortion = (distort, postgain, algorithm) => { + debugger; return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } }); }; diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 325b2e2a6..f229cd338 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,5 +1,5 @@ -import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs'; -import { registerSound, registerWavetable } from './index.mjs'; +import { getCommonSampleInfo } from './util.mjs'; +import { registerSound, registerWaveTable } from './index.mjs'; import { getAudioContext } from './audioContext.mjs'; import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { logger } from './logger.mjs'; diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 48b7e661c..c340efa8c 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -5,7 +5,6 @@ import { destroyAudioWorkletNode, getADSRValues, getFrequencyFromValue, - getLfo, getParamADSR, getPitchEnvelope, getVibratoOscillator, From a14bf4d97bc26a70a637464a0543d6861c5d9b69 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 1 Oct 2025 00:36:59 -0500 Subject: [PATCH 515/538] Add aliases for distort types --- packages/core/pattern.mjs | 67 +++++++++++++++++++++++++----- packages/superdough/helpers.mjs | 1 - packages/superdough/superdough.mjs | 3 +- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index dcee7f174..96f7d687e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3537,6 +3537,41 @@ export const morph = (frompat, topat, bypat) => { return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by)))); }; +/** + * Soft-clipping distortion + * + * @name soft + * @param {number | Pattern} distortion + * + */ +/** + * Hard-clipping distortion + * + * @name hard + * @param {number | Pattern} distortion + * + */ +/** + * Cubic polynomial distortion + * + * @name cubic + * @param {number | Pattern} distortion + * + */ +/** + * Diode-emulating distortion + * + * @name diode + * @param {number | Pattern} distortion + * + */ +/** + * Asymmetrical diode distortion + * + * @name asym + * @param {number | Pattern} distortion + * + */ /** * Wavefolding distortion * @@ -3544,13 +3579,25 @@ export const morph = (frompat, topat, bypat) => { * @param {number | Pattern} distortion * */ -export const fold = register('fold', function (amt, vol = 1, pat) { - return pat.withvValue((v) => { - return { - ...v, - distortion: amt, - distortvol: vol, - distorttype: 'fold', - }; - }).innerJoin(); -}); +/** + * Wavefolding distortion composed with sinusoid + * + * @name sinefold + * @param {number | Pattern} distortion + * + */ +/** + * Distortion via Chebyshev polynomials + * + * @name chebyshev + * @param {number | Pattern} distortion + * + */ + +const algoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev']; +for (const name of algoNames) { + Pattern.prototype[name] = function (args) { + const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name])); + return this.distort(argsPat); + }; +} diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 6db3b04ab..a471da9dd 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -475,7 +475,6 @@ export const getDistortionAlgorithm = (algo) => { }; export const getDistortion = (distort, postgain, algorithm) => { - debugger; return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } }); }; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 8240b45b9..a37504b1d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -155,6 +155,7 @@ let defaultDefaultValues = { phaserdepth: 0.75, shapevol: 1, distortvol: 1, + distorttype: 0, delay: 0, byteBeatExpression: '0', delayfeedback: 0.5, @@ -455,7 +456,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) shapevol = getDefaultValue('shapevol'), distort, distortvol = getDefaultValue('distortvol'), - distorttype, + distorttype = getDefaultValue('distorttype'), pan, vowel, delay = getDefaultValue('delay'), From c885d84785f73a4624d26bd75f608e58baa0206a Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 1 Oct 2025 00:39:44 -0500 Subject: [PATCH 516/538] Add comment --- packages/core/pattern.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 96f7d687e..e2e563e4f 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3594,8 +3594,9 @@ export const morph = (frompat, topat, bypat) => { * */ -const algoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev']; -for (const name of algoNames) { +const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev']; +for (const name of distAlgoNames) { + // Add aliases for distortion algorithms Pattern.prototype[name] = function (args) { const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name])); return this.distort(argsPat); From 72298c5f037f43470d9be6929b1a099a2b3b4228 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 1 Oct 2025 00:45:26 -0500 Subject: [PATCH 517/538] Docstring cleanup --- packages/core/controls.mjs | 8 +++++--- packages/core/pattern.mjs | 25 ++++++++++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 5148e47e7..3475fc2f4 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1892,7 +1892,9 @@ export const { shape } = registerControl(['shape', 'shapevol']); * * @name distort * @synonyms dist - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * @param {number | string | Pattern} type type of distortion to apply * @example * s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>") * @example @@ -1908,7 +1910,7 @@ export const { distort, dist } = registerControl(['distort', 'distortvol', 'dist * * @name distortvol * @synonyms distvol - * @param {number | Pattern} type + * @param {number | Pattern} volume linear postgain of the distortion * @example * s("bd*4").bank("tr909").distort(2).distortvol(0.8) */ @@ -1919,7 +1921,7 @@ export const { distortvol } = registerControl('distortvol', 'distvol'); * * @name distorttype * @synonyms disttype - * @param {number | string | Pattern} type + * @param {number | string | Pattern} type type of distortion to apply * @example * s("bd*4").bank("tr909").distort(2).distorttype("<0 1 2>") * diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index e2e563e4f..a823e7bca 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3541,59 +3541,66 @@ export const morph = (frompat, topat, bypat) => { * Soft-clipping distortion * * @name soft - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ /** * Hard-clipping distortion * * @name hard - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ /** * Cubic polynomial distortion * * @name cubic - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ /** * Diode-emulating distortion * * @name diode - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ /** * Asymmetrical diode distortion * * @name asym - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ /** * Wavefolding distortion * * @name fold - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ /** * Wavefolding distortion composed with sinusoid * * @name sinefold - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ /** * Distortion via Chebyshev polynomials * * @name chebyshev - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ - const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev']; for (const name of distAlgoNames) { // Add aliases for distortion algorithms From 8cd497abb245361b4ad2cf469e46b8b13d14247c Mon Sep 17 00:00:00 2001 From: yaxu Date: Wed, 1 Oct 2025 12:54:58 +0200 Subject: [PATCH 518/538] Update website/src/pages/technical-manual/project-start.mdx --- .../src/pages/technical-manual/project-start.mdx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/website/src/pages/technical-manual/project-start.mdx b/website/src/pages/technical-manual/project-start.mdx index 4e6e37636..0866d3e43 100644 --- a/website/src/pages/technical-manual/project-start.mdx +++ b/website/src/pages/technical-manual/project-start.mdx @@ -7,6 +7,21 @@ layout: ../../layouts/MainLayout.astro This Guide shows you the different ways to get started with using Strudel in your own project. +## Respect the license + +First, please take a moment to understand Strudel's free/open source license, +[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.en.html). + +Here is a lay summary, but check the license for legal definitions and responsibilities. + +* You can distribute modified versions if you keep track of the changes and the date you made them. +* You must license derivative work under the same license. +* Source code must be distributed along with web publication. + +Among other things, it means that when you share your work, the whole application must be shared under the same free/open source license, or one compatible with it. This is because we want Strudel to stay free/open source. In other words, you are not permitted to distribute integrations of Strudel with libraries or other code that does not have a compatible free/open source license. + +This also applies to clones informed by reading Strudel's source code, as legally speaking, that counts as a 'derivative work'. Again, please [read the licence](https://www.gnu.org/licenses/agpl-3.0.en.html) for details. + ## Embedding the Strudel REPL There are 3 quick ways to embed strudel in your website: From 0ecacae71eddeb7f975b4614a58d3a59c0b67264 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 1 Oct 2025 12:02:09 +0100 Subject: [PATCH 519/538] format --- website/src/pages/technical-manual/project-start.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/src/pages/technical-manual/project-start.mdx b/website/src/pages/technical-manual/project-start.mdx index 0866d3e43..4a269d1b9 100644 --- a/website/src/pages/technical-manual/project-start.mdx +++ b/website/src/pages/technical-manual/project-start.mdx @@ -9,16 +9,16 @@ This Guide shows you the different ways to get started with using Strudel in you ## Respect the license -First, please take a moment to understand Strudel's free/open source license, +First, please take a moment to understand Strudel's free/open source license, [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.en.html). Here is a lay summary, but check the license for legal definitions and responsibilities. -* You can distribute modified versions if you keep track of the changes and the date you made them. -* You must license derivative work under the same license. -* Source code must be distributed along with web publication. +- You can distribute modified versions if you keep track of the changes and the date you made them. +- You must license derivative work under the same license. +- Source code must be distributed along with web publication. -Among other things, it means that when you share your work, the whole application must be shared under the same free/open source license, or one compatible with it. This is because we want Strudel to stay free/open source. In other words, you are not permitted to distribute integrations of Strudel with libraries or other code that does not have a compatible free/open source license. +Among other things, it means that when you share your work, the whole application must be shared under the same free/open source license, or one compatible with it. This is because we want Strudel to stay free/open source. In other words, you are not permitted to distribute integrations of Strudel with libraries or other code that does not have a compatible free/open source license. This also applies to clones informed by reading Strudel's source code, as legally speaking, that counts as a 'derivative work'. Again, please [read the licence](https://www.gnu.org/licenses/agpl-3.0.en.html) for details. From b7d98dd3709b3837fba16a517aac565c7e732ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Va=CC=81clav=20Volhejn?= Date: Wed, 1 Oct 2025 21:53:28 +0200 Subject: [PATCH 520/538] Fix case sensitivity for synonym search --- website/src/repl/components/panel/Reference.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index 6007667fa..505cf50d2 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -44,10 +44,10 @@ export function Reference() { return true; } - const lowCaseSearch = search.toLowerCase(); + const lowerCaseSearch = search.toLowerCase(); return ( - entry.name.toLowerCase().includes(lowCaseSearch) || - (entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false) + entry.name.toLowerCase().includes(lowerCaseSearch) || + (entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false) ); }); }, [search]); From ac582b4d40791276f3eda4f2012ab31491981861 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 2 Oct 2025 01:06:29 -0500 Subject: [PATCH 521/538] Wrap properly when phase === 1 --- packages/superdough/worklets.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index ccfaf5107..058559885 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1234,7 +1234,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { _sampleFrame(frame, phase) { const len = frame.length; const pos = phase * len; - const i = pos | 0; + let i = pos | 0; + if (i >= len) i = 0; const frac = pos - i; const a = frame[i]; const i1 = i + 1 < len ? i + 1 : 0; // fast wrap From 99e14dae5ca88443c098abc4fcc56b7e1859832e Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 2 Oct 2025 01:12:42 -0500 Subject: [PATCH 522/538] Consistency --- packages/superdough/worklets.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 058559885..7858871c2 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1235,10 +1235,11 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const len = frame.length; const pos = phase * len; let i = pos | 0; - if (i >= len) i = 0; + if (i >= len) i = 0; // fast wrap const frac = pos - i; const a = frame[i]; - const i1 = i + 1 < len ? i + 1 : 0; // fast wrap + let i1 = i + 1; + if (i1 >= len) i1 = 0; const b = frame[i1]; return a + (b - a) * frac; } From f369de13d7116ce24f952251e6e955e193a14126 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 2 Oct 2025 22:45:48 -0500 Subject: [PATCH 523/538] Hook up FM to wavetables --- packages/superdough/wavetable.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 44367f786..220b95896 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -308,6 +308,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { }, ); const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t); + const fm = applyFM(source.parameters.get('frequency'), value, t); const envGain = ac.createGain(); const node = source.connect(envGain); getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); @@ -318,6 +319,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { () => { destroyAudioWorkletNode(source); vibratoOscillator?.stop(); + fm?.stop(); node.disconnect(); wtPosModulators?.disconnect(); wtWarpModulators?.disconnect(); From d496919fe507caa6323ce5d33b553e2b2a15d974 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 2 Oct 2025 22:48:41 -0500 Subject: [PATCH 524/538] Import --- packages/superdough/wavetable.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 220b95896..da6abcf76 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 { + applyFM, applyParameterModulators, destroyAudioWorkletNode, getADSRValues, From 120f89d57fc38a70f082d1bd1b977e2d4bb8d943 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 4 Oct 2025 01:37:57 -0500 Subject: [PATCH 525/538] Handle detune in the presence of pitch envelope --- packages/core/test/pattern.test.mjs | 4 ++-- packages/superdough/wavetable.mjs | 4 ++-- packages/superdough/worklets.mjs | 24 +++++++++++++----------- website/src/components/Header/Search.css | 4 ++-- website/src/pages/learn/samples.mdx | 2 +- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 1df5c8776..625f40756 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -796,7 +796,7 @@ describe('Pattern', () => { }); }); describe('apply', () => { - it('Can apply a function', () => { + (it('Can apply a function', () => { expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle()); }), it('Can apply a pattern of functions', () => { @@ -804,7 +804,7 @@ describe('Pattern', () => { expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual( sequence('a', 'b').fast(2, 3).firstCycle(), ); - }); + })); }); describe('layer', () => { it('Can layer up multiple functions', () => { diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 44367f786..3329dd96b 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -230,12 +230,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { begin: t, end: envEnd, frequency, - detune: value.detune, + freqspread: value.detune, position: value.wt, warp: value.warp, warpMode: warpmode, voices: Math.max(value.unison ?? 1, 1), - spread: value.spread, + panspread: value.spread, phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0, }, { outputChannelCount: [2] }, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 7858871c2..dc9d93b27 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1049,14 +1049,15 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { 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.18 }, - { name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 }, - { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, + { name: 'frequency', defaultValue: 440, min: Number.EPSILON }, + { name: 'detune', defaultValue: 0 }, + { name: 'freqspread', defaultValue: 0.18, min: 0 }, + { name: 'position', defaultValue: 0, min: 0, max: 1 }, + { name: 'warp', defaultValue: 0, min: 0, max: 1 }, { name: 'warpMode', defaultValue: 0 }, - { name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 }, - { name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 }, - { name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 }, + { name: 'voices', defaultValue: 1, min: 1 }, + { name: 'panspread', defaultValue: 0.7, min: 0, max: 1 }, + { name: 'phaserand', defaultValue: 0, min: 0, max: 1 }, ]; } @@ -1269,6 +1270,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } for (let i = 0; i < outL.length; i++) { const detune = pv(parameters.detune, i); + const freqspread = pv(parameters.freqspread, i); const tablePos = clamp(pv(parameters.position, i), 0, 1); const idx = tablePos * (this.numFrames - 1); const fIdx = idx | 0; @@ -1277,9 +1279,9 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const warpMode = pv(parameters.warpMode, i); const voices = pv(parameters.voices, i); const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1); - const spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 0; - const gain1 = Math.sqrt(0.5 - 0.5 * spread); - const gain2 = Math.sqrt(0.5 + 0.5 * spread); + const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0; + const gain1 = Math.sqrt(0.5 - 0.5 * panspread); + const gain2 = Math.sqrt(0.5 + 0.5 * panspread); let f = pv(parameters.frequency, i); f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune const normalizer = 1 / Math.sqrt(voices); @@ -1292,7 +1294,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { gainL = gain2; gainR = gain1; } - const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune + const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune const dPhase = fVoice * this.invSR; const level = this._chooseMip(dPhase); const table = this.tables[level]; diff --git a/website/src/components/Header/Search.css b/website/src/components/Header/Search.css index 456ef9f6b..b14146cbd 100644 --- a/website/src/components/Header/Search.css +++ b/website/src/components/Header/Search.css @@ -18,8 +18,8 @@ --docsearch-modal-background: var(--background); --docsearch-muted-color: color-mix(in srgb, var(--foreground), #fff 30%); --docsearch-key-gradient: var(--foreground); - --docsearch-key-shadow: inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), - 0 1px 2px 1px var(--gutterBackground); + --docsearch-key-shadow: + inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), 0 1px 2px 1px var(--gutterBackground); } .dark { --docsearch-muted-color: color-mix(in srgb, var(--foreground), #000 30%); diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index eb79cccf7..201d57dfa 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample ### scrub -{' '} + ### speed From 0d0822598351fcb7b0050a2f452f0fada4375fee Mon Sep 17 00:00:00 2001 From: Jieren Chen Date: Sun, 5 Oct 2025 13:53:00 -0400 Subject: [PATCH 526/538] change the trigger handler to match new hap --- packages/mqtt/mqtt.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mqtt/mqtt.mjs b/packages/mqtt/mqtt.mjs index aef01bd93..d74de342d 100644 --- a/packages/mqtt/mqtt.mjs +++ b/packages/mqtt/mqtt.mjs @@ -82,7 +82,7 @@ Pattern.prototype.mqtt = function ( cx.connect(props); } return this.withHap((hap) => { - const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => { + const onTrigger = (hap, currentTime, cps, targetTime) => { let msg_topic = topic; if (!cx || !cx.isConnected()) { return; From 694162a7b1d0c916ffa76461bcb5fb77bf046459 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 14:13:33 +0100 Subject: [PATCH 527/538] fix purity of 'withState' so it returns a new pattern --- packages/core/pattern.mjs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d0be3f599..72f6371b3 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -98,10 +98,7 @@ export class Pattern { // runs func on query state withState(func) { - return this.withHaps((haps, state) => { - func(state); - return haps; - }); + return new Pattern((state) => this.query(func(state))); } /** From fdde05e75114e8aedb38d89648d83d1c58b3b303 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 14:51:21 +0100 Subject: [PATCH 528/538] add pattern id to query state controls --- packages/core/repl.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 53562f216..8dbfbec75 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -214,7 +214,10 @@ export function repl({ } let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { - let patterns = Object.values(pPatterns); + let patterns = []; + for (const [key, value] of Object.entries(pPatterns)) { + patterns.push(value.withState(state => state.setControls({id: key}))); + } if (eachTransform) { // Explicit lambda so only element (not index and array) are passed patterns = patterns.map((x) => eachTransform(x)); @@ -228,6 +231,7 @@ export function repl({ pattern = allTransforms[i](pattern); } } + if (!isPattern(pattern)) { const message = `got "${typeof evaluated}" instead of pattern`; throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.')); From 086596c47ca564743510301a963815572d6b48b0 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 14:52:00 +0100 Subject: [PATCH 529/538] fix setControls to do union with existing controls --- packages/core/state.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/state.mjs b/packages/core/state.mjs index 162dc7da9..928710814 100644 --- a/packages/core/state.mjs +++ b/packages/core/state.mjs @@ -19,9 +19,9 @@ export class State { return this.setSpan(func(this.span)); } - // Returns new State with different controls + // Returns new State with added controls. setControls(controls) { - return new State(this.span, controls); + return new State(this.span, {...this.controls, ...controls}); } } From ffbbc43c8973059a95cfd69d0594e5cac71905ef Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 15:34:51 +0100 Subject: [PATCH 530/538] add cyclist version to the query metadata --- packages/core/cyclist.mjs | 2 +- packages/core/neocyclist.mjs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index ad0961032..59e410c53 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -57,7 +57,7 @@ export class Cyclist { } // query the pattern for events - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' }); haps.forEach((hap) => { if (hap.hasOnset()) { diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 261f08acb..3e412074d 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -38,8 +38,7 @@ export class NeoCyclist { if (this.started === false) { return; } - - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' }); haps.forEach((hap) => { if (hap.hasOnset()) { const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps); From 64f16c4f337c63692b5f89e8226e37a0db82c923 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 15:35:34 +0100 Subject: [PATCH 531/538] placate format gods --- packages/core/repl.mjs | 2 +- packages/core/state.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 8dbfbec75..171697eb9 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -216,7 +216,7 @@ export function repl({ if (Object.keys(pPatterns).length) { let patterns = []; for (const [key, value] of Object.entries(pPatterns)) { - patterns.push(value.withState(state => state.setControls({id: key}))); + patterns.push(value.withState((state) => state.setControls({ id: key }))); } if (eachTransform) { // Explicit lambda so only element (not index and array) are passed diff --git a/packages/core/state.mjs b/packages/core/state.mjs index 928710814..8aa581954 100644 --- a/packages/core/state.mjs +++ b/packages/core/state.mjs @@ -21,7 +21,7 @@ export class State { // Returns new State with added controls. setControls(controls) { - return new State(this.span, {...this.controls, ...controls}); + return new State(this.span, { ...this.controls, ...controls }); } } From 0065db8569d351954b83469c1e644087f5e7da80 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sat, 11 Oct 2025 17:40:55 -0700 Subject: [PATCH 532/538] Docs: add example of custom chained function This adds a subsection to the "Understand/Coding Syntax" documentation that shows a simple example of converting the previous chain of effects into a custom, reusable chained function. There's also a prompt for the reader to experiment. --- website/src/pages/learn/code.mdx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/website/src/pages/learn/code.mdx b/website/src/pages/learn/code.mdx index f9c6b3a25..11b8b0dac 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -60,6 +60,25 @@ Strudel makes heavy use of chained functions. Here is a more sophisticated examp .room(0.5)`} /> +## Write your own chained function + +You can write your own chained function using `register`. Here's the above chain but registered as a reusable, chained function. + + pat + .s("sawtooth") + .cutoff(500) + //.delay(0.5) + .room(0.5) + ); +note("a3 c#4 e4 a4").effectChain() +`} +/> + +Try adding `.rev()` after `effectChain()` to see further effects added. + # Comments The `//` in the example above is a line comment, resulting in the `delay` function being ignored. From af53bab2596750b8dc8818a54797e9ccf7d0e5d7 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sat, 11 Oct 2025 17:50:58 -0700 Subject: [PATCH 533/538] Fixing some syntax and a typo --- website/src/pages/learn/code.mdx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/website/src/pages/learn/code.mdx b/website/src/pages/learn/code.mdx index 11b8b0dac..6ff9e4a95 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -66,18 +66,16 @@ You can write your own chained function using `register`. Here's the above chain pat + tune={`const effectChain = register('effectChain', (pat) => pat .s("sawtooth") .cutoff(500) //.delay(0.5) .room(0.5) ); -note("a3 c#4 e4 a4").effectChain() -`} +note("a3 c#4 e4 a4").effectChain()`} /> -Try adding `.rev()` after `effectChain()` to see further effects added. +Try adding `.rev()` after `effectChain()` to hear further effects added. # Comments From 9c5c71c31a08321348e9eeaf4139c1f4e8a4c853 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sat, 11 Oct 2025 17:52:26 -0700 Subject: [PATCH 534/538] Removing semicolon to be more idiomatic;;; --- 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 6ff9e4a95..c454dcceb 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -71,7 +71,7 @@ You can write your own chained function using `register`. Here's the above chain .cutoff(500) //.delay(0.5) .room(0.5) - ); + ) note("a3 c#4 e4 a4").effectChain()`} /> From 72eaf96b4fb922fc4aa36ef92d90843f8395b62c Mon Sep 17 00:00:00 2001 From: Erik Fox Date: Sun, 12 Oct 2025 12:30:36 -0400 Subject: [PATCH 535/538] fix: repair REPL sample sources and URL concat (#1640) --- packages/repl/prebake.mjs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index 3f421f985..26d875c2b 100644 --- a/packages/repl/prebake.mjs +++ b/packages/repl/prebake.mjs @@ -20,10 +20,13 @@ export async function prebake() { // import('@strudel/osc'), ); // load samples - const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/'; + const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main'; // TODO: move this onto the strudel repo - const ts = 'https://raw.githubusercontent.com/todepond/samples/main/'; + const ts = 'https://raw.githubusercontent.com/todepond/samples/main'; + + const tc = 'https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main'; + await Promise.all([ modulesLoading, registerSynthSounds(), @@ -36,9 +39,9 @@ export async function prebake() { samples(`${ds}/tidal-drum-machines.json`), samples(`${ds}/piano.json`), samples(`${ds}/Dirt-Samples.json`), - samples(`${ds}/uzu-drumkit.json`), samples(`${ds}/vcsl.json`), samples(`${ds}/mridangam.json`), + samples(`${tc}/strudel.json`), ]); aliasBank(`${ts}/tidal-drum-machines-alias.json`); From 3c1a8c8bdb60ec9e0a89d74fe5b4e79722d36d13 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 14 Oct 2025 11:41:11 -0500 Subject: [PATCH 536/538] Format --- packages/core/test/pattern.test.mjs | 4 ++-- website/src/components/Header/Search.css | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 625f40756..1df5c8776 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -796,7 +796,7 @@ describe('Pattern', () => { }); }); describe('apply', () => { - (it('Can apply a function', () => { + it('Can apply a function', () => { expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle()); }), it('Can apply a pattern of functions', () => { @@ -804,7 +804,7 @@ describe('Pattern', () => { expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual( sequence('a', 'b').fast(2, 3).firstCycle(), ); - })); + }); }); describe('layer', () => { it('Can layer up multiple functions', () => { diff --git a/website/src/components/Header/Search.css b/website/src/components/Header/Search.css index b14146cbd..456ef9f6b 100644 --- a/website/src/components/Header/Search.css +++ b/website/src/components/Header/Search.css @@ -18,8 +18,8 @@ --docsearch-modal-background: var(--background); --docsearch-muted-color: color-mix(in srgb, var(--foreground), #fff 30%); --docsearch-key-gradient: var(--foreground); - --docsearch-key-shadow: - inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), 0 1px 2px 1px var(--gutterBackground); + --docsearch-key-shadow: inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), + 0 1px 2px 1px var(--gutterBackground); } .dark { --docsearch-muted-color: color-mix(in srgb, var(--foreground), #000 30%); From 909e0154fe9aea8c33d20779e1b083404447501b Mon Sep 17 00:00:00 2001 From: prezmop Date: Sun, 12 Oct 2025 08:01:30 +0200 Subject: [PATCH 537/538] default to samples if repo not found --- packages/superdough/sampler.mjs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index b195ba79c..02271a689 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -120,13 +120,20 @@ function githubPath(base, subpath = '') { if (!base.startsWith('github:')) { throw new Error('expected "github:" at the start of pseudoUrl'); } - let [_, path] = base.split('github:'); + let path = base.slice('github:'.length); path = path.endsWith('/') ? path.slice(0, -1) : path; - if (path.split('/').length === 2) { - // assume main as default branch if none set - path += '/main'; + + let components = path.split('/'); + let user = components[0]; + let repo = components.length >= 2 ? components[1] : 'samples'; + let branch = components.length >= 3 ? components[2] : 'main'; + let other = components.slice(3); + if (subpath) { + other.push(subpath); } - return `https://raw.githubusercontent.com/${path}/${subpath}`; + other = other.join('/'); + + return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`; } export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => { From 83c7e63432fec16ea59968863b6ad3f80ad20063 Mon Sep 17 00:00:00 2001 From: prezmop Date: Sun, 12 Oct 2025 08:05:04 +0200 Subject: [PATCH 538/538] update docs --- packages/superdough/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/superdough/README.md b/packages/superdough/README.md index 0c6e4f14c..f5947d83f 100644 --- a/packages/superdough/README.md +++ b/packages/superdough/README.md @@ -153,6 +153,7 @@ samples('github:tidalcycles/dirt-samples') The format is `github://`. +If `` and `` are not specified, they will default to `samples` and `main` respectively. It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo. The format is also expected to be the same as explained above.