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/371] 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/371] 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/371] 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/371] 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/371] 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/371] - 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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/371] 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 cbd448e9347f2e30c3060b277882ec122a17086f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 24 Mar 2025 00:09:46 -0400 Subject: [PATCH 074/371] update docs --- packages/core/pattern.mjs | 16 +++++++++++----- packages/core/signal.mjs | 3 +++ .../src/repl/components/panel/SettingsTab.jsx | 18 ------------------ 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index c1e95211d..4adb9bd61 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2106,10 +2106,11 @@ export const linger = register( /** * Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one. * @param {number} segments number of segments per cycle + * @synonyms seg * @example * note(saw.range(40,52).segment(24)) */ -export const segment = register('segment', function (rate, pat) { +export const {segment, seg} = register(['segment', 'seg'], function (rate, pat) { return pat.struct(pure(true)._fast(rate)).setSteps(rate); }); @@ -2485,16 +2486,21 @@ export const bypass = register( ); /** - * Loops the pattern inside at `offset` for `cycles`. + * Loops the pattern inside an `offset` for `cycles`. + * If you think of the entire span of time in cycles as a ribbon, you can grab a single piece and loop it. + * @synonym rib * @param {number} offset start point of loop in cycles * @param {number} cycles loop length in cycles * @example - * note("").ribbon(1, 2).fast(2) + * note("").ribbon(1, 2) * @example * // Looping a portion of randomness - * note(irand(8).segment(4).scale('C3 minor')).ribbon(1337, 2) + * n(irand(8).segment(4)).scale("c:pentatonic").ribbon(1337, 2) + * @example + * // rhythm generator + * s("bd!16?").ribbon(29,.5)._punchcard() */ -export const ribbon = register('ribbon', (offset, cycles, pat) => pat.early(offset).restart(pure(1).slow(cycles))); +export const {ribbon, rib} = register(['ribbon', 'rib'], (offset, cycles, pat) => pat.early(offset).restart(pure(1).slow(cycles))); export const hsla = register('hsla', (h, s, l, a, pat) => { return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 5348173d3..f09c49661 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -520,12 +520,15 @@ export const degradeByWith = register( * * @name degradeBy * @memberof Pattern + * @synonym * @param {number} amount - a number between 0 and 1 * @returns Pattern * @example * s("hh*8").degradeBy(0.2) * @example * s("[hh?0.2]*8") + * @example + * s("bd").segment(16).degradeBy(.5).ribbon(16,1)._punchcard() */ export const degradeBy = register( 'degradeBy', diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 4ad3cc9ac..3a44cd0ac 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -179,25 +179,7 @@ export function SettingsTab({ started }) { value={togglePanelTrigger} onChange={(value) => settingsMap.setKey('togglePanelTrigger', value)} items={{ click: 'Click', hover: 'Hover' }} - > - {/* { - if (cbEvent.target.checked) { - settingsMap.setKey('togglePanelTrigger', 'click'); - } - }} - value={togglePanelTrigger != 'hover'} /> - { - if (cbEvent.target.checked) { - settingsMap.setKey('togglePanelTrigger', 'hover'); - } - }} - value={togglePanelTrigger == 'hover'} - /> */} Date: Mon, 24 Mar 2025 00:15:36 -0400 Subject: [PATCH 075/371] update test --- packages/core/pattern.mjs | 8 +- packages/core/signal.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 344 +++++++++++++--------- 3 files changed, 215 insertions(+), 139 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 4adb9bd61..49caeaf0e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2110,7 +2110,7 @@ export const linger = register( * @example * note(saw.range(40,52).segment(24)) */ -export const {segment, seg} = register(['segment', 'seg'], function (rate, pat) { +export const { segment, seg } = register(['segment', 'seg'], function (rate, pat) { return pat.struct(pure(true)._fast(rate)).setSteps(rate); }); @@ -2498,9 +2498,11 @@ export const bypass = register( * n(irand(8).segment(4)).scale("c:pentatonic").ribbon(1337, 2) * @example * // rhythm generator - * s("bd!16?").ribbon(29,.5)._punchcard() + * s("bd!16?").ribbon(29,.5) */ -export const {ribbon, rib} = register(['ribbon', 'rib'], (offset, cycles, pat) => pat.early(offset).restart(pure(1).slow(cycles))); +export const { ribbon, rib } = register(['ribbon', 'rib'], (offset, cycles, pat) => + pat.early(offset).restart(pure(1).slow(cycles)), +); export const hsla = register('hsla', (h, s, l, a, pat) => { return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index f09c49661..22738ea98 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -528,7 +528,7 @@ export const degradeByWith = register( * @example * s("[hh?0.2]*8") * @example - * s("bd").segment(16).degradeBy(.5).ribbon(16,1)._punchcard() + * s("bd").segment(16).degradeBy(.5).ribbon(16,1) */ export const degradeBy = register( 'degradeBy', diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 71a26c813..80ee7344c 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1,5 +1,181 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`runs examples > example "" example index 0 1`] = ` +[ + "[ 0/1 → 1/24 | note:40 ]", + "[ 1/24 → 1/12 | note:40.5 ]", + "[ 1/12 → 1/8 | note:41 ]", + "[ 1/8 → 1/6 | note:41.5 ]", + "[ 1/6 → 5/24 | note:42 ]", + "[ 5/24 → 1/4 | note:42.5 ]", + "[ 1/4 → 7/24 | note:43 ]", + "[ 7/24 → 1/3 | note:43.5 ]", + "[ 1/3 → 3/8 | note:44 ]", + "[ 3/8 → 5/12 | note:44.5 ]", + "[ 5/12 → 11/24 | note:45 ]", + "[ 11/24 → 1/2 | note:45.5 ]", + "[ 1/2 → 13/24 | note:46 ]", + "[ 13/24 → 7/12 | note:46.5 ]", + "[ 7/12 → 5/8 | note:47 ]", + "[ 5/8 → 2/3 | note:47.5 ]", + "[ 2/3 → 17/24 | note:48 ]", + "[ 17/24 → 3/4 | note:48.5 ]", + "[ 3/4 → 19/24 | note:49 ]", + "[ 19/24 → 5/6 | note:49.5 ]", + "[ 5/6 → 7/8 | note:50 ]", + "[ 7/8 → 11/12 | note:50.5 ]", + "[ 11/12 → 23/24 | note:51 ]", + "[ 23/24 → 1/1 | note:51.5 ]", + "[ 1/1 → 25/24 | note:40 ]", + "[ 25/24 → 13/12 | note:40.5 ]", + "[ 13/12 → 9/8 | note:41 ]", + "[ 9/8 → 7/6 | note:41.5 ]", + "[ 7/6 → 29/24 | note:42 ]", + "[ 29/24 → 5/4 | note:42.5 ]", + "[ 5/4 → 31/24 | note:43 ]", + "[ 31/24 → 4/3 | note:43.5 ]", + "[ 4/3 → 11/8 | note:44 ]", + "[ 11/8 → 17/12 | note:44.5 ]", + "[ 17/12 → 35/24 | note:45 ]", + "[ 35/24 → 3/2 | note:45.5 ]", + "[ 3/2 → 37/24 | note:46 ]", + "[ 37/24 → 19/12 | note:46.5 ]", + "[ 19/12 → 13/8 | note:47 ]", + "[ 13/8 → 5/3 | note:47.5 ]", + "[ 5/3 → 41/24 | note:48 ]", + "[ 41/24 → 7/4 | note:48.5 ]", + "[ 7/4 → 43/24 | note:49 ]", + "[ 43/24 → 11/6 | note:49.5 ]", + "[ 11/6 → 15/8 | note:50 ]", + "[ 15/8 → 23/12 | note:50.5 ]", + "[ 23/12 → 47/24 | note:51 ]", + "[ 47/24 → 2/1 | note:51.5 ]", + "[ 2/1 → 49/24 | note:40 ]", + "[ 49/24 → 25/12 | note:40.5 ]", + "[ 25/12 → 17/8 | note:41 ]", + "[ 17/8 → 13/6 | note:41.5 ]", + "[ 13/6 → 53/24 | note:42 ]", + "[ 53/24 → 9/4 | note:42.5 ]", + "[ 9/4 → 55/24 | note:43 ]", + "[ 55/24 → 7/3 | note:43.5 ]", + "[ 7/3 → 19/8 | note:44 ]", + "[ 19/8 → 29/12 | note:44.5 ]", + "[ 29/12 → 59/24 | note:45 ]", + "[ 59/24 → 5/2 | note:45.5 ]", + "[ 5/2 → 61/24 | note:46 ]", + "[ 61/24 → 31/12 | note:46.5 ]", + "[ 31/12 → 21/8 | note:47 ]", + "[ 21/8 → 8/3 | note:47.5 ]", + "[ 8/3 → 65/24 | note:48 ]", + "[ 65/24 → 11/4 | note:48.5 ]", + "[ 11/4 → 67/24 | note:49 ]", + "[ 67/24 → 17/6 | note:49.5 ]", + "[ 17/6 → 23/8 | note:50 ]", + "[ 23/8 → 35/12 | note:50.5 ]", + "[ 35/12 → 71/24 | note:51 ]", + "[ 71/24 → 3/1 | note:51.5 ]", + "[ 3/1 → 73/24 | note:40 ]", + "[ 73/24 → 37/12 | note:40.5 ]", + "[ 37/12 → 25/8 | note:41 ]", + "[ 25/8 → 19/6 | note:41.5 ]", + "[ 19/6 → 77/24 | note:42 ]", + "[ 77/24 → 13/4 | note:42.5 ]", + "[ 13/4 → 79/24 | note:43 ]", + "[ 79/24 → 10/3 | note:43.5 ]", + "[ 10/3 → 27/8 | note:44 ]", + "[ 27/8 → 41/12 | note:44.5 ]", + "[ 41/12 → 83/24 | note:45 ]", + "[ 83/24 → 7/2 | note:45.5 ]", + "[ 7/2 → 85/24 | note:46 ]", + "[ 85/24 → 43/12 | note:46.5 ]", + "[ 43/12 → 29/8 | note:47 ]", + "[ 29/8 → 11/3 | note:47.5 ]", + "[ 11/3 → 89/24 | note:48 ]", + "[ 89/24 → 15/4 | note:48.5 ]", + "[ 15/4 → 91/24 | note:49 ]", + "[ 91/24 → 23/6 | note:49.5 ]", + "[ 23/6 → 31/8 | note:50 ]", + "[ 31/8 → 47/12 | note:50.5 ]", + "[ 47/12 → 95/24 | note:51 ]", + "[ 95/24 → 4/1 | note:51.5 ]", +] +`; + +exports[`runs examples > example "" example index 0 2`] = ` +[ + "[ 0/1 → 1/1 | note:d ]", + "[ 1/1 → 2/1 | note:e ]", + "[ 2/1 → 3/1 | note:d ]", + "[ 3/1 → 4/1 | note:e ]", +] +`; + +exports[`runs examples > example "" example index 1 1`] = ` +[ + "[ 0/1 → 1/4 | note:A3 ]", + "[ 1/4 → 1/2 | note:C3 ]", + "[ 1/2 → 3/4 | note:D3 ]", + "[ 3/4 → 1/1 | note:G3 ]", + "[ 1/1 → 5/4 | note:E3 ]", + "[ 5/4 → 3/2 | note:C4 ]", + "[ 3/2 → 7/4 | note:A3 ]", + "[ 7/4 → 2/1 | note:E4 ]", + "[ 2/1 → 9/4 | note:A3 ]", + "[ 9/4 → 5/2 | note:C3 ]", + "[ 5/2 → 11/4 | note:D3 ]", + "[ 11/4 → 3/1 | note:G3 ]", + "[ 3/1 → 13/4 | note:E3 ]", + "[ 13/4 → 7/2 | note:C4 ]", + "[ 7/2 → 15/4 | note:A3 ]", + "[ 15/4 → 4/1 | note:E4 ]", +] +`; + +exports[`runs examples > example "" example index 2 1`] = ` +[ + "[ 1/16 → 1/8 | s:bd ]", + "[ 1/8 → 3/16 | s:bd ]", + "[ 3/16 → 1/4 | s:bd ]", + "[ 1/4 → 5/16 | s:bd ]", + "[ 3/8 → 7/16 | s:bd ]", + "[ 9/16 → 5/8 | s:bd ]", + "[ 5/8 → 11/16 | s:bd ]", + "[ 11/16 → 3/4 | s:bd ]", + "[ 3/4 → 13/16 | s:bd ]", + "[ 7/8 → 15/16 | s:bd ]", + "[ 17/16 → 9/8 | s:bd ]", + "[ 9/8 → 19/16 | s:bd ]", + "[ 19/16 → 5/4 | s:bd ]", + "[ 5/4 → 21/16 | s:bd ]", + "[ 11/8 → 23/16 | s:bd ]", + "[ 25/16 → 13/8 | s:bd ]", + "[ 13/8 → 27/16 | s:bd ]", + "[ 27/16 → 7/4 | s:bd ]", + "[ 7/4 → 29/16 | s:bd ]", + "[ 15/8 → 31/16 | s:bd ]", + "[ 33/16 → 17/8 | s:bd ]", + "[ 17/8 → 35/16 | s:bd ]", + "[ 35/16 → 9/4 | s:bd ]", + "[ 9/4 → 37/16 | s:bd ]", + "[ 19/8 → 39/16 | s:bd ]", + "[ 41/16 → 21/8 | s:bd ]", + "[ 21/8 → 43/16 | s:bd ]", + "[ 43/16 → 11/4 | s:bd ]", + "[ 11/4 → 45/16 | s:bd ]", + "[ 23/8 → 47/16 | s:bd ]", + "[ 49/16 → 25/8 | s:bd ]", + "[ 25/8 → 51/16 | s:bd ]", + "[ 51/16 → 13/4 | s:bd ]", + "[ 13/4 → 53/16 | s:bd ]", + "[ 27/8 → 55/16 | s:bd ]", + "[ 57/16 → 29/8 | s:bd ]", + "[ 29/8 → 59/16 | s:bd ]", + "[ 59/16 → 15/4 | s:bd ]", + "[ 15/4 → 61/16 | s:bd ]", + "[ 31/8 → 63/16 | s:bd ]", +] +`; + exports[`runs examples > example "_euclidRot" example index 0 1`] = ` [ "[ 0/1 → 1/5 | note:c3 ]", @@ -2322,6 +2498,39 @@ exports[`runs examples > example "degradeBy" example index 1 1`] = ` ] `; +exports[`runs examples > example "degradeBy" example index 2 1`] = ` +[ + "[ 1/8 → 3/16 | s:bd ]", + "[ 1/4 → 5/16 | s:bd ]", + "[ 5/16 → 3/8 | s:bd ]", + "[ 1/2 → 9/16 | s:bd ]", + "[ 9/16 → 5/8 | s:bd ]", + "[ 11/16 → 3/4 | s:bd ]", + "[ 15/16 → 1/1 | s:bd ]", + "[ 9/8 → 19/16 | s:bd ]", + "[ 5/4 → 21/16 | s:bd ]", + "[ 21/16 → 11/8 | s:bd ]", + "[ 3/2 → 25/16 | s:bd ]", + "[ 25/16 → 13/8 | s:bd ]", + "[ 27/16 → 7/4 | s:bd ]", + "[ 31/16 → 2/1 | s:bd ]", + "[ 17/8 → 35/16 | s:bd ]", + "[ 9/4 → 37/16 | s:bd ]", + "[ 37/16 → 19/8 | s:bd ]", + "[ 5/2 → 41/16 | s:bd ]", + "[ 41/16 → 21/8 | s:bd ]", + "[ 43/16 → 11/4 | s:bd ]", + "[ 47/16 → 3/1 | s:bd ]", + "[ 25/8 → 51/16 | s:bd ]", + "[ 13/4 → 53/16 | s:bd ]", + "[ 53/16 → 27/8 | s:bd ]", + "[ 7/2 → 57/16 | s:bd ]", + "[ 57/16 → 29/8 | s:bd ]", + "[ 59/16 → 15/4 | s:bd ]", + "[ 63/16 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "delay" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:bd delay:0 ]", @@ -7121,40 +7330,6 @@ exports[`runs examples > example "rev" example index 0 1`] = ` ] `; -exports[`runs examples > example "ribbon" example index 0 1`] = ` -[ - "[ 0/1 → 1/2 | note:d ]", - "[ 1/2 → 1/1 | note:e ]", - "[ 1/1 → 3/2 | note:d ]", - "[ 3/2 → 2/1 | note:e ]", - "[ 2/1 → 5/2 | note:d ]", - "[ 5/2 → 3/1 | note:e ]", - "[ 3/1 → 7/2 | note:d ]", - "[ 7/2 → 4/1 | note:e ]", -] -`; - -exports[`runs examples > example "ribbon" example index 1 1`] = ` -[ - "[ 0/1 → 1/4 | note:G3 ]", - "[ 1/4 → 1/2 | note:C3 ]", - "[ 1/2 → 3/4 | note:D3 ]", - "[ 3/4 → 1/1 | note:F3 ]", - "[ 1/1 → 5/4 | note:Eb3 ]", - "[ 5/4 → 3/2 | note:Ab3 ]", - "[ 3/2 → 7/4 | note:G3 ]", - "[ 7/4 → 2/1 | note:C4 ]", - "[ 2/1 → 9/4 | note:G3 ]", - "[ 9/4 → 5/2 | note:C3 ]", - "[ 5/2 → 11/4 | note:D3 ]", - "[ 11/4 → 3/1 | note:F3 ]", - "[ 3/1 → 13/4 | note:Eb3 ]", - "[ 13/4 → 7/2 | note:Ab3 ]", - "[ 7/2 → 15/4 | note:G3 ]", - "[ 15/4 → 4/1 | note:C4 ]", -] -`; - exports[`runs examples > example "room" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd room:0 ]", @@ -7797,107 +7972,6 @@ but parts might be played more than once, or not at all, per cycle." example ind ] `; -exports[`runs examples > example "segment" example index 0 1`] = ` -[ - "[ 0/1 → 1/24 | note:40 ]", - "[ 1/24 → 1/12 | note:40.5 ]", - "[ 1/12 → 1/8 | note:41 ]", - "[ 1/8 → 1/6 | note:41.5 ]", - "[ 1/6 → 5/24 | note:42 ]", - "[ 5/24 → 1/4 | note:42.5 ]", - "[ 1/4 → 7/24 | note:43 ]", - "[ 7/24 → 1/3 | note:43.5 ]", - "[ 1/3 → 3/8 | note:44 ]", - "[ 3/8 → 5/12 | note:44.5 ]", - "[ 5/12 → 11/24 | note:45 ]", - "[ 11/24 → 1/2 | note:45.5 ]", - "[ 1/2 → 13/24 | note:46 ]", - "[ 13/24 → 7/12 | note:46.5 ]", - "[ 7/12 → 5/8 | note:47 ]", - "[ 5/8 → 2/3 | note:47.5 ]", - "[ 2/3 → 17/24 | note:48 ]", - "[ 17/24 → 3/4 | note:48.5 ]", - "[ 3/4 → 19/24 | note:49 ]", - "[ 19/24 → 5/6 | note:49.5 ]", - "[ 5/6 → 7/8 | note:50 ]", - "[ 7/8 → 11/12 | note:50.5 ]", - "[ 11/12 → 23/24 | note:51 ]", - "[ 23/24 → 1/1 | note:51.5 ]", - "[ 1/1 → 25/24 | note:40 ]", - "[ 25/24 → 13/12 | note:40.5 ]", - "[ 13/12 → 9/8 | note:41 ]", - "[ 9/8 → 7/6 | note:41.5 ]", - "[ 7/6 → 29/24 | note:42 ]", - "[ 29/24 → 5/4 | note:42.5 ]", - "[ 5/4 → 31/24 | note:43 ]", - "[ 31/24 → 4/3 | note:43.5 ]", - "[ 4/3 → 11/8 | note:44 ]", - "[ 11/8 → 17/12 | note:44.5 ]", - "[ 17/12 → 35/24 | note:45 ]", - "[ 35/24 → 3/2 | note:45.5 ]", - "[ 3/2 → 37/24 | note:46 ]", - "[ 37/24 → 19/12 | note:46.5 ]", - "[ 19/12 → 13/8 | note:47 ]", - "[ 13/8 → 5/3 | note:47.5 ]", - "[ 5/3 → 41/24 | note:48 ]", - "[ 41/24 → 7/4 | note:48.5 ]", - "[ 7/4 → 43/24 | note:49 ]", - "[ 43/24 → 11/6 | note:49.5 ]", - "[ 11/6 → 15/8 | note:50 ]", - "[ 15/8 → 23/12 | note:50.5 ]", - "[ 23/12 → 47/24 | note:51 ]", - "[ 47/24 → 2/1 | note:51.5 ]", - "[ 2/1 → 49/24 | note:40 ]", - "[ 49/24 → 25/12 | note:40.5 ]", - "[ 25/12 → 17/8 | note:41 ]", - "[ 17/8 → 13/6 | note:41.5 ]", - "[ 13/6 → 53/24 | note:42 ]", - "[ 53/24 → 9/4 | note:42.5 ]", - "[ 9/4 → 55/24 | note:43 ]", - "[ 55/24 → 7/3 | note:43.5 ]", - "[ 7/3 → 19/8 | note:44 ]", - "[ 19/8 → 29/12 | note:44.5 ]", - "[ 29/12 → 59/24 | note:45 ]", - "[ 59/24 → 5/2 | note:45.5 ]", - "[ 5/2 → 61/24 | note:46 ]", - "[ 61/24 → 31/12 | note:46.5 ]", - "[ 31/12 → 21/8 | note:47 ]", - "[ 21/8 → 8/3 | note:47.5 ]", - "[ 8/3 → 65/24 | note:48 ]", - "[ 65/24 → 11/4 | note:48.5 ]", - "[ 11/4 → 67/24 | note:49 ]", - "[ 67/24 → 17/6 | note:49.5 ]", - "[ 17/6 → 23/8 | note:50 ]", - "[ 23/8 → 35/12 | note:50.5 ]", - "[ 35/12 → 71/24 | note:51 ]", - "[ 71/24 → 3/1 | note:51.5 ]", - "[ 3/1 → 73/24 | note:40 ]", - "[ 73/24 → 37/12 | note:40.5 ]", - "[ 37/12 → 25/8 | note:41 ]", - "[ 25/8 → 19/6 | note:41.5 ]", - "[ 19/6 → 77/24 | note:42 ]", - "[ 77/24 → 13/4 | note:42.5 ]", - "[ 13/4 → 79/24 | note:43 ]", - "[ 79/24 → 10/3 | note:43.5 ]", - "[ 10/3 → 27/8 | note:44 ]", - "[ 27/8 → 41/12 | note:44.5 ]", - "[ 41/12 → 83/24 | note:45 ]", - "[ 83/24 → 7/2 | note:45.5 ]", - "[ 7/2 → 85/24 | note:46 ]", - "[ 85/24 → 43/12 | note:46.5 ]", - "[ 43/12 → 29/8 | note:47 ]", - "[ 29/8 → 11/3 | note:47.5 ]", - "[ 11/3 → 89/24 | note:48 ]", - "[ 89/24 → 15/4 | note:48.5 ]", - "[ 15/4 → 91/24 | note:49 ]", - "[ 91/24 → 23/6 | note:49.5 ]", - "[ 23/6 → 31/8 | note:50 ]", - "[ 31/8 → 47/12 | note:50.5 ]", - "[ 47/12 → 95/24 | note:51 ]", - "[ 95/24 → 4/1 | note:51.5 ]", -] -`; - exports[`runs examples > example "seq" example index 0 1`] = ` [ "[ 0/1 → 1/3 | note:e5 ]", From bfbd920b1d3d2e90c15fd3a0b8ebc8df5fe994e5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 24 Mar 2025 00:18:26 -0400 Subject: [PATCH 076/371] fix doc --- packages/core/signal.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 22738ea98..fdd7476fd 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -520,7 +520,6 @@ export const degradeByWith = register( * * @name degradeBy * @memberof Pattern - * @synonym * @param {number} amount - a number between 0 and 1 * @returns Pattern * @example From 3eea908636bce6936b410fa3053dcf70bd5f5252 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 24 Mar 2025 00:33:00 -0400 Subject: [PATCH 077/371] fix doc --- packages/core/pattern.mjs | 6 ++++-- packages/core/signal.mjs | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 49caeaf0e..6e294129e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2105,8 +2105,9 @@ export const linger = register( /** * Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one. - * @param {number} segments number of segments per cycle + * @name segment * @synonyms seg + * @param {number} segments number of segments per cycle * @example * note(saw.range(40,52).segment(24)) */ @@ -2487,7 +2488,8 @@ 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 grab a single piece and loop it. + * 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 * @param {number} offset start point of loop in cycles * @param {number} cycles loop length in cycles diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index fdd7476fd..66ac8a5a6 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -527,6 +527,7 @@ export const degradeByWith = register( * @example * s("[hh?0.2]*8") * @example + * //beat generator * s("bd").segment(16).degradeBy(.5).ribbon(16,1) */ export const degradeBy = register( From 282180ef1cec109f270736ef2712ee06fa990fc6 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 24 Mar 2025 01:28:31 -0400 Subject: [PATCH 078/371] style update --- website/src/repl/components/UserFacingErrorMessage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/components/UserFacingErrorMessage.jsx b/website/src/repl/components/UserFacingErrorMessage.jsx index ec8d8be33..9ccfacd88 100644 --- a/website/src/repl/components/UserFacingErrorMessage.jsx +++ b/website/src/repl/components/UserFacingErrorMessage.jsx @@ -4,5 +4,5 @@ export default function UserFacingErrorMessage(Props) { if (error == null) { return; } - return
{error.message || 'Unknown Error :-/'}
; + return
Error: {error.message || 'Unknown Error :-/'}
; } From dc8e5d53946612cb97b6fcb666e3525193a77738 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 24 Mar 2025 01:28:43 -0400 Subject: [PATCH 079/371] lint --- website/src/repl/components/UserFacingErrorMessage.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/src/repl/components/UserFacingErrorMessage.jsx b/website/src/repl/components/UserFacingErrorMessage.jsx index 9ccfacd88..1d9de037d 100644 --- a/website/src/repl/components/UserFacingErrorMessage.jsx +++ b/website/src/repl/components/UserFacingErrorMessage.jsx @@ -4,5 +4,9 @@ export default function UserFacingErrorMessage(Props) { if (error == null) { return; } - return
Error: {error.message || 'Unknown Error :-/'}
; + return ( +
+ Error: {error.message || 'Unknown Error :-/'} +
+ ); } From dee75bc75954f0c5e00fddd563d8fee51c9b43ae Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 24 Mar 2025 01:32:48 -0400 Subject: [PATCH 080/371] update tests --- test/__snapshots__/examples.test.mjs.snap | 352 +++++++++++----------- 1 file changed, 176 insertions(+), 176 deletions(-) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 80ee7344c..b9da0f538 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1,181 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`runs examples > example "" example index 0 1`] = ` -[ - "[ 0/1 → 1/24 | note:40 ]", - "[ 1/24 → 1/12 | note:40.5 ]", - "[ 1/12 → 1/8 | note:41 ]", - "[ 1/8 → 1/6 | note:41.5 ]", - "[ 1/6 → 5/24 | note:42 ]", - "[ 5/24 → 1/4 | note:42.5 ]", - "[ 1/4 → 7/24 | note:43 ]", - "[ 7/24 → 1/3 | note:43.5 ]", - "[ 1/3 → 3/8 | note:44 ]", - "[ 3/8 → 5/12 | note:44.5 ]", - "[ 5/12 → 11/24 | note:45 ]", - "[ 11/24 → 1/2 | note:45.5 ]", - "[ 1/2 → 13/24 | note:46 ]", - "[ 13/24 → 7/12 | note:46.5 ]", - "[ 7/12 → 5/8 | note:47 ]", - "[ 5/8 → 2/3 | note:47.5 ]", - "[ 2/3 → 17/24 | note:48 ]", - "[ 17/24 → 3/4 | note:48.5 ]", - "[ 3/4 → 19/24 | note:49 ]", - "[ 19/24 → 5/6 | note:49.5 ]", - "[ 5/6 → 7/8 | note:50 ]", - "[ 7/8 → 11/12 | note:50.5 ]", - "[ 11/12 → 23/24 | note:51 ]", - "[ 23/24 → 1/1 | note:51.5 ]", - "[ 1/1 → 25/24 | note:40 ]", - "[ 25/24 → 13/12 | note:40.5 ]", - "[ 13/12 → 9/8 | note:41 ]", - "[ 9/8 → 7/6 | note:41.5 ]", - "[ 7/6 → 29/24 | note:42 ]", - "[ 29/24 → 5/4 | note:42.5 ]", - "[ 5/4 → 31/24 | note:43 ]", - "[ 31/24 → 4/3 | note:43.5 ]", - "[ 4/3 → 11/8 | note:44 ]", - "[ 11/8 → 17/12 | note:44.5 ]", - "[ 17/12 → 35/24 | note:45 ]", - "[ 35/24 → 3/2 | note:45.5 ]", - "[ 3/2 → 37/24 | note:46 ]", - "[ 37/24 → 19/12 | note:46.5 ]", - "[ 19/12 → 13/8 | note:47 ]", - "[ 13/8 → 5/3 | note:47.5 ]", - "[ 5/3 → 41/24 | note:48 ]", - "[ 41/24 → 7/4 | note:48.5 ]", - "[ 7/4 → 43/24 | note:49 ]", - "[ 43/24 → 11/6 | note:49.5 ]", - "[ 11/6 → 15/8 | note:50 ]", - "[ 15/8 → 23/12 | note:50.5 ]", - "[ 23/12 → 47/24 | note:51 ]", - "[ 47/24 → 2/1 | note:51.5 ]", - "[ 2/1 → 49/24 | note:40 ]", - "[ 49/24 → 25/12 | note:40.5 ]", - "[ 25/12 → 17/8 | note:41 ]", - "[ 17/8 → 13/6 | note:41.5 ]", - "[ 13/6 → 53/24 | note:42 ]", - "[ 53/24 → 9/4 | note:42.5 ]", - "[ 9/4 → 55/24 | note:43 ]", - "[ 55/24 → 7/3 | note:43.5 ]", - "[ 7/3 → 19/8 | note:44 ]", - "[ 19/8 → 29/12 | note:44.5 ]", - "[ 29/12 → 59/24 | note:45 ]", - "[ 59/24 → 5/2 | note:45.5 ]", - "[ 5/2 → 61/24 | note:46 ]", - "[ 61/24 → 31/12 | note:46.5 ]", - "[ 31/12 → 21/8 | note:47 ]", - "[ 21/8 → 8/3 | note:47.5 ]", - "[ 8/3 → 65/24 | note:48 ]", - "[ 65/24 → 11/4 | note:48.5 ]", - "[ 11/4 → 67/24 | note:49 ]", - "[ 67/24 → 17/6 | note:49.5 ]", - "[ 17/6 → 23/8 | note:50 ]", - "[ 23/8 → 35/12 | note:50.5 ]", - "[ 35/12 → 71/24 | note:51 ]", - "[ 71/24 → 3/1 | note:51.5 ]", - "[ 3/1 → 73/24 | note:40 ]", - "[ 73/24 → 37/12 | note:40.5 ]", - "[ 37/12 → 25/8 | note:41 ]", - "[ 25/8 → 19/6 | note:41.5 ]", - "[ 19/6 → 77/24 | note:42 ]", - "[ 77/24 → 13/4 | note:42.5 ]", - "[ 13/4 → 79/24 | note:43 ]", - "[ 79/24 → 10/3 | note:43.5 ]", - "[ 10/3 → 27/8 | note:44 ]", - "[ 27/8 → 41/12 | note:44.5 ]", - "[ 41/12 → 83/24 | note:45 ]", - "[ 83/24 → 7/2 | note:45.5 ]", - "[ 7/2 → 85/24 | note:46 ]", - "[ 85/24 → 43/12 | note:46.5 ]", - "[ 43/12 → 29/8 | note:47 ]", - "[ 29/8 → 11/3 | note:47.5 ]", - "[ 11/3 → 89/24 | note:48 ]", - "[ 89/24 → 15/4 | note:48.5 ]", - "[ 15/4 → 91/24 | note:49 ]", - "[ 91/24 → 23/6 | note:49.5 ]", - "[ 23/6 → 31/8 | note:50 ]", - "[ 31/8 → 47/12 | note:50.5 ]", - "[ 47/12 → 95/24 | note:51 ]", - "[ 95/24 → 4/1 | note:51.5 ]", -] -`; - -exports[`runs examples > example "" example index 0 2`] = ` -[ - "[ 0/1 → 1/1 | note:d ]", - "[ 1/1 → 2/1 | note:e ]", - "[ 2/1 → 3/1 | note:d ]", - "[ 3/1 → 4/1 | note:e ]", -] -`; - -exports[`runs examples > example "" example index 1 1`] = ` -[ - "[ 0/1 → 1/4 | note:A3 ]", - "[ 1/4 → 1/2 | note:C3 ]", - "[ 1/2 → 3/4 | note:D3 ]", - "[ 3/4 → 1/1 | note:G3 ]", - "[ 1/1 → 5/4 | note:E3 ]", - "[ 5/4 → 3/2 | note:C4 ]", - "[ 3/2 → 7/4 | note:A3 ]", - "[ 7/4 → 2/1 | note:E4 ]", - "[ 2/1 → 9/4 | note:A3 ]", - "[ 9/4 → 5/2 | note:C3 ]", - "[ 5/2 → 11/4 | note:D3 ]", - "[ 11/4 → 3/1 | note:G3 ]", - "[ 3/1 → 13/4 | note:E3 ]", - "[ 13/4 → 7/2 | note:C4 ]", - "[ 7/2 → 15/4 | note:A3 ]", - "[ 15/4 → 4/1 | note:E4 ]", -] -`; - -exports[`runs examples > example "" example index 2 1`] = ` -[ - "[ 1/16 → 1/8 | s:bd ]", - "[ 1/8 → 3/16 | s:bd ]", - "[ 3/16 → 1/4 | s:bd ]", - "[ 1/4 → 5/16 | s:bd ]", - "[ 3/8 → 7/16 | s:bd ]", - "[ 9/16 → 5/8 | s:bd ]", - "[ 5/8 → 11/16 | s:bd ]", - "[ 11/16 → 3/4 | s:bd ]", - "[ 3/4 → 13/16 | s:bd ]", - "[ 7/8 → 15/16 | s:bd ]", - "[ 17/16 → 9/8 | s:bd ]", - "[ 9/8 → 19/16 | s:bd ]", - "[ 19/16 → 5/4 | s:bd ]", - "[ 5/4 → 21/16 | s:bd ]", - "[ 11/8 → 23/16 | s:bd ]", - "[ 25/16 → 13/8 | s:bd ]", - "[ 13/8 → 27/16 | s:bd ]", - "[ 27/16 → 7/4 | s:bd ]", - "[ 7/4 → 29/16 | s:bd ]", - "[ 15/8 → 31/16 | s:bd ]", - "[ 33/16 → 17/8 | s:bd ]", - "[ 17/8 → 35/16 | s:bd ]", - "[ 35/16 → 9/4 | s:bd ]", - "[ 9/4 → 37/16 | s:bd ]", - "[ 19/8 → 39/16 | s:bd ]", - "[ 41/16 → 21/8 | s:bd ]", - "[ 21/8 → 43/16 | s:bd ]", - "[ 43/16 → 11/4 | s:bd ]", - "[ 11/4 → 45/16 | s:bd ]", - "[ 23/8 → 47/16 | s:bd ]", - "[ 49/16 → 25/8 | s:bd ]", - "[ 25/8 → 51/16 | s:bd ]", - "[ 51/16 → 13/4 | s:bd ]", - "[ 13/4 → 53/16 | s:bd ]", - "[ 27/8 → 55/16 | s:bd ]", - "[ 57/16 → 29/8 | s:bd ]", - "[ 29/8 → 59/16 | s:bd ]", - "[ 59/16 → 15/4 | s:bd ]", - "[ 15/4 → 61/16 | s:bd ]", - "[ 31/8 → 63/16 | s:bd ]", -] -`; - exports[`runs examples > example "_euclidRot" example index 0 1`] = ` [ "[ 0/1 → 1/5 | note:c3 ]", @@ -7330,6 +7154,81 @@ exports[`runs examples > example "rev" example index 0 1`] = ` ] `; +exports[`runs examples > example "ribbon" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:d ]", + "[ 1/1 → 2/1 | note:e ]", + "[ 2/1 → 3/1 | note:d ]", + "[ 3/1 → 4/1 | note:e ]", +] +`; + +exports[`runs examples > example "ribbon" example index 1 1`] = ` +[ + "[ 0/1 → 1/4 | note:A3 ]", + "[ 1/4 → 1/2 | note:C3 ]", + "[ 1/2 → 3/4 | note:D3 ]", + "[ 3/4 → 1/1 | note:G3 ]", + "[ 1/1 → 5/4 | note:E3 ]", + "[ 5/4 → 3/2 | note:C4 ]", + "[ 3/2 → 7/4 | note:A3 ]", + "[ 7/4 → 2/1 | note:E4 ]", + "[ 2/1 → 9/4 | note:A3 ]", + "[ 9/4 → 5/2 | note:C3 ]", + "[ 5/2 → 11/4 | note:D3 ]", + "[ 11/4 → 3/1 | note:G3 ]", + "[ 3/1 → 13/4 | note:E3 ]", + "[ 13/4 → 7/2 | note:C4 ]", + "[ 7/2 → 15/4 | note:A3 ]", + "[ 15/4 → 4/1 | note:E4 ]", +] +`; + +exports[`runs examples > example "ribbon" example index 2 1`] = ` +[ + "[ 1/16 → 1/8 | s:bd ]", + "[ 1/8 → 3/16 | s:bd ]", + "[ 3/16 → 1/4 | s:bd ]", + "[ 1/4 → 5/16 | s:bd ]", + "[ 3/8 → 7/16 | s:bd ]", + "[ 9/16 → 5/8 | s:bd ]", + "[ 5/8 → 11/16 | s:bd ]", + "[ 11/16 → 3/4 | s:bd ]", + "[ 3/4 → 13/16 | s:bd ]", + "[ 7/8 → 15/16 | s:bd ]", + "[ 17/16 → 9/8 | s:bd ]", + "[ 9/8 → 19/16 | s:bd ]", + "[ 19/16 → 5/4 | s:bd ]", + "[ 5/4 → 21/16 | s:bd ]", + "[ 11/8 → 23/16 | s:bd ]", + "[ 25/16 → 13/8 | s:bd ]", + "[ 13/8 → 27/16 | s:bd ]", + "[ 27/16 → 7/4 | s:bd ]", + "[ 7/4 → 29/16 | s:bd ]", + "[ 15/8 → 31/16 | s:bd ]", + "[ 33/16 → 17/8 | s:bd ]", + "[ 17/8 → 35/16 | s:bd ]", + "[ 35/16 → 9/4 | s:bd ]", + "[ 9/4 → 37/16 | s:bd ]", + "[ 19/8 → 39/16 | s:bd ]", + "[ 41/16 → 21/8 | s:bd ]", + "[ 21/8 → 43/16 | s:bd ]", + "[ 43/16 → 11/4 | s:bd ]", + "[ 11/4 → 45/16 | s:bd ]", + "[ 23/8 → 47/16 | s:bd ]", + "[ 49/16 → 25/8 | s:bd ]", + "[ 25/8 → 51/16 | s:bd ]", + "[ 51/16 → 13/4 | s:bd ]", + "[ 13/4 → 53/16 | s:bd ]", + "[ 27/8 → 55/16 | s:bd ]", + "[ 57/16 → 29/8 | s:bd ]", + "[ 29/8 → 59/16 | s:bd ]", + "[ 59/16 → 15/4 | s:bd ]", + "[ 15/4 → 61/16 | s:bd ]", + "[ 31/8 → 63/16 | s:bd ]", +] +`; + exports[`runs examples > example "room" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd room:0 ]", @@ -7972,6 +7871,107 @@ but parts might be played more than once, or not at all, per cycle." example ind ] `; +exports[`runs examples > example "segment" example index 0 1`] = ` +[ + "[ 0/1 → 1/24 | note:40 ]", + "[ 1/24 → 1/12 | note:40.5 ]", + "[ 1/12 → 1/8 | note:41 ]", + "[ 1/8 → 1/6 | note:41.5 ]", + "[ 1/6 → 5/24 | note:42 ]", + "[ 5/24 → 1/4 | note:42.5 ]", + "[ 1/4 → 7/24 | note:43 ]", + "[ 7/24 → 1/3 | note:43.5 ]", + "[ 1/3 → 3/8 | note:44 ]", + "[ 3/8 → 5/12 | note:44.5 ]", + "[ 5/12 → 11/24 | note:45 ]", + "[ 11/24 → 1/2 | note:45.5 ]", + "[ 1/2 → 13/24 | note:46 ]", + "[ 13/24 → 7/12 | note:46.5 ]", + "[ 7/12 → 5/8 | note:47 ]", + "[ 5/8 → 2/3 | note:47.5 ]", + "[ 2/3 → 17/24 | note:48 ]", + "[ 17/24 → 3/4 | note:48.5 ]", + "[ 3/4 → 19/24 | note:49 ]", + "[ 19/24 → 5/6 | note:49.5 ]", + "[ 5/6 → 7/8 | note:50 ]", + "[ 7/8 → 11/12 | note:50.5 ]", + "[ 11/12 → 23/24 | note:51 ]", + "[ 23/24 → 1/1 | note:51.5 ]", + "[ 1/1 → 25/24 | note:40 ]", + "[ 25/24 → 13/12 | note:40.5 ]", + "[ 13/12 → 9/8 | note:41 ]", + "[ 9/8 → 7/6 | note:41.5 ]", + "[ 7/6 → 29/24 | note:42 ]", + "[ 29/24 → 5/4 | note:42.5 ]", + "[ 5/4 → 31/24 | note:43 ]", + "[ 31/24 → 4/3 | note:43.5 ]", + "[ 4/3 → 11/8 | note:44 ]", + "[ 11/8 → 17/12 | note:44.5 ]", + "[ 17/12 → 35/24 | note:45 ]", + "[ 35/24 → 3/2 | note:45.5 ]", + "[ 3/2 → 37/24 | note:46 ]", + "[ 37/24 → 19/12 | note:46.5 ]", + "[ 19/12 → 13/8 | note:47 ]", + "[ 13/8 → 5/3 | note:47.5 ]", + "[ 5/3 → 41/24 | note:48 ]", + "[ 41/24 → 7/4 | note:48.5 ]", + "[ 7/4 → 43/24 | note:49 ]", + "[ 43/24 → 11/6 | note:49.5 ]", + "[ 11/6 → 15/8 | note:50 ]", + "[ 15/8 → 23/12 | note:50.5 ]", + "[ 23/12 → 47/24 | note:51 ]", + "[ 47/24 → 2/1 | note:51.5 ]", + "[ 2/1 → 49/24 | note:40 ]", + "[ 49/24 → 25/12 | note:40.5 ]", + "[ 25/12 → 17/8 | note:41 ]", + "[ 17/8 → 13/6 | note:41.5 ]", + "[ 13/6 → 53/24 | note:42 ]", + "[ 53/24 → 9/4 | note:42.5 ]", + "[ 9/4 → 55/24 | note:43 ]", + "[ 55/24 → 7/3 | note:43.5 ]", + "[ 7/3 → 19/8 | note:44 ]", + "[ 19/8 → 29/12 | note:44.5 ]", + "[ 29/12 → 59/24 | note:45 ]", + "[ 59/24 → 5/2 | note:45.5 ]", + "[ 5/2 → 61/24 | note:46 ]", + "[ 61/24 → 31/12 | note:46.5 ]", + "[ 31/12 → 21/8 | note:47 ]", + "[ 21/8 → 8/3 | note:47.5 ]", + "[ 8/3 → 65/24 | note:48 ]", + "[ 65/24 → 11/4 | note:48.5 ]", + "[ 11/4 → 67/24 | note:49 ]", + "[ 67/24 → 17/6 | note:49.5 ]", + "[ 17/6 → 23/8 | note:50 ]", + "[ 23/8 → 35/12 | note:50.5 ]", + "[ 35/12 → 71/24 | note:51 ]", + "[ 71/24 → 3/1 | note:51.5 ]", + "[ 3/1 → 73/24 | note:40 ]", + "[ 73/24 → 37/12 | note:40.5 ]", + "[ 37/12 → 25/8 | note:41 ]", + "[ 25/8 → 19/6 | note:41.5 ]", + "[ 19/6 → 77/24 | note:42 ]", + "[ 77/24 → 13/4 | note:42.5 ]", + "[ 13/4 → 79/24 | note:43 ]", + "[ 79/24 → 10/3 | note:43.5 ]", + "[ 10/3 → 27/8 | note:44 ]", + "[ 27/8 → 41/12 | note:44.5 ]", + "[ 41/12 → 83/24 | note:45 ]", + "[ 83/24 → 7/2 | note:45.5 ]", + "[ 7/2 → 85/24 | note:46 ]", + "[ 85/24 → 43/12 | note:46.5 ]", + "[ 43/12 → 29/8 | note:47 ]", + "[ 29/8 → 11/3 | note:47.5 ]", + "[ 11/3 → 89/24 | note:48 ]", + "[ 89/24 → 15/4 | note:48.5 ]", + "[ 15/4 → 91/24 | note:49 ]", + "[ 91/24 → 23/6 | note:49.5 ]", + "[ 23/6 → 31/8 | note:50 ]", + "[ 31/8 → 47/12 | note:50.5 ]", + "[ 47/12 → 95/24 | note:51 ]", + "[ 95/24 → 4/1 | note:51.5 ]", +] +`; + exports[`runs examples > example "seq" example index 0 1`] = ` [ "[ 0/1 → 1/3 | note:e5 ]", From 2938bfd88f9e74449a585d49c41462dda2cb9beb Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Mar 2025 02:46:26 +0100 Subject: [PATCH 081/371] 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 082/371] 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 083/371] 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 084/371] 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 085/371] 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 086/371] 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 6d355a803ae490e0940ec24c00a91da75776f8d4 Mon Sep 17 00:00:00 2001 From: Rene Nyffenegger Date: Wed, 26 Mar 2025 14:54:15 +0100 Subject: [PATCH 087/371] Fix typo pattnr (#1316) --- website/src/pages/learn/stepwise.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/stepwise.mdx b/website/src/pages/learn/stepwise.mdx index d3ca334af..e632ee7ec 100644 --- a/website/src/pages/learn/stepwise.mdx +++ b/website/src/pages/learn/stepwise.mdx @@ -24,7 +24,7 @@ With the new stepwise `stepcat` function, the steps of the two patterns will be By default, steps are counted according to the 'top level' in mini-notation. For example `"a [b c] d e"` has five events in it per cycle, but is counted as four steps, where `[b c]` is counted as a single step. -However, you can mark a different metrical level to count steps relative to, using a `^` at the start of a sub-pattern. If we do this to the subpattern in our example: `"a [^b c] d e"`, then the pattern is now counted as having _eight_ steps. This is because 'b' and 'c' are each counted as single steps, and the events in the pattenr are twice as long, and so counted as two steps each. +However, you can mark a different metrical level to count steps relative to, using a `^` at the start of a sub-pattern. If we do this to the subpattern in our example: `"a [^b c] d e"`, then the pattern is now counted as having _eight_ steps. This is because 'b' and 'c' are each counted as single steps, and the events in the pattern are twice as long, and so counted as two steps each. ## Pacing the steps From e9de45993e2c9fe9fa002ccf2af361fc8dea6214 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 29 Mar 2025 19:54:04 +0100 Subject: [PATCH 088/371] 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 089/371] 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 090/371] 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 091/371] 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 e55b7c6ce338302691cbe16e1666e928ec8c7732 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 30 Mar 2025 00:55:12 -0400 Subject: [PATCH 092/371] mostly working --- packages/superdough/sampler.mjs | 1 + packages/superdough/superdough.mjs | 28 ++++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index f5e46d6bb..45f6b33e1 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -278,6 +278,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option const cutGroups = []; + export async function onTriggerSample(t, value, onended, bank, resolveUrl) { let { s, diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fc81f565e..c6daabbfc 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -374,6 +374,9 @@ export function resetGlobalEffects() { analysersData = {}; } +let allAudioNodeChains = []; +let maxPolyphony = 12; + export const superdough = async (value, t, hapDuration) => { const ac = getAudioContext(); t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; @@ -472,14 +475,30 @@ export const superdough = async (value, t, hapDuration) => { } = value; gain = nanFallback(gain, 1); + console.info(allAudioNodeChains.length) + + for (let i = 0; i <= allAudioNodeChains.length - maxPolyphony; i++) { + const chain = allAudioNodeChains.shift() + chain.forEach(node => node?.disconnect()) + } + // if (allAudioNodeChains.length > maxPolyphony) { + + + // allAudioNodeChains.slice(0, allAudioNodeChains.length - maxPolyphony).flat().forEach((node) => { + // node.disconnect(); + // }); + // console.info(allAudioNodes.length) + // } //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future - let toDisconnect = []; // audio nodes that will be disconnected when the source has ended + let audioNodes = []; + // audio nodes that will be disconnected when the source has ended + const onended = () => { - toDisconnect.forEach((n) => n?.disconnect()); + audioNodes.forEach((n) => n?.disconnect()); }; if (bank && s) { s = `${bank}_${s}`; @@ -654,9 +673,10 @@ export const superdough = async (value, t, hapDuration) => { // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); - // toDisconnect = all the node that should be disconnected in onended callback + // audioNodes = all the node that should be disconnected in onended callback // this is crucial for performance - toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]); + audioNodes = chain.concat([delaySend, reverbSend, analyserSend]); + allAudioNodeChains.push(audioNodes) }; export const superdoughTrigger = (t, hap, ct, cps) => { From 8962e5a4f01b289dabe4b5f3e8dac8c555db3cb5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 30 Mar 2025 01:35:25 -0400 Subject: [PATCH 093/371] max poly --- packages/superdough/superdough.mjs | 46 ++++++++++++++--------------- website/src/repl/useReplContext.jsx | 4 +-- website/src/settings.mjs | 1 + 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c6daabbfc..c306f54ec 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -14,6 +14,10 @@ import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; +let maxPolyphony = 128; +export function setMaxPolyphony(polyphony) { + maxPolyphony = polyphony; +} export const soundMap = map(); export function registerSound(key, onTrigger, data = {}) { @@ -162,7 +166,8 @@ function loadWorklets() { // this function should be called on first user interaction (to avoid console warning) export async function initAudio(options = {}) { - const { disableWorklets = false } = options; + const { disableWorklets = false, polyphony = maxPolyphony } = options; + setMaxPolyphony(polyphony); if (typeof window === 'undefined') { return; } @@ -374,8 +379,7 @@ export function resetGlobalEffects() { analysersData = {}; } -let allAudioNodeChains = []; -let maxPolyphony = 12; +let allAudioNodeChains = new Map(); export const superdough = async (value, t, hapDuration) => { const ac = getAudioContext(); @@ -475,30 +479,30 @@ export const superdough = async (value, t, hapDuration) => { } = value; gain = nanFallback(gain, 1); - console.info(allAudioNodeChains.length) - for (let i = 0; i <= allAudioNodeChains.length - maxPolyphony; i++) { - const chain = allAudioNodeChains.shift() - chain.forEach(node => node?.disconnect()) - } - // if (allAudioNodeChains.length > maxPolyphony) { + const chainID = Math.round(Math.random() * 10000); - - // allAudioNodeChains.slice(0, allAudioNodeChains.length - maxPolyphony).flat().forEach((node) => { - // node.disconnect(); - // }); - // console.info(allAudioNodes.length) - // } + // oldest audio nodes will be removed if maximum polyphony is exceeded + for (let i = 0; i <= allAudioNodeChains.size - maxPolyphony; i++) { + const ch = allAudioNodeChains.entries().next(); + const key = ch.value[0]; + const chain = ch.value[1]; + if (key == null) { + continue; + } + chain?.forEach((node) => node?.disconnect()); + allAudioNodeChains.delete(key); + } //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future - let audioNodes = []; - // audio nodes that will be disconnected when the source has ended const onended = () => { - audioNodes.forEach((n) => n?.disconnect()); + const chain = allAudioNodeChains.get(chainID); + chain?.forEach((n) => n?.disconnect()); + allAudioNodeChains.delete(chainID); }; if (bank && s) { s = `${bank}_${s}`; @@ -672,11 +676,7 @@ export const superdough = async (value, t, hapDuration) => { // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); - - // audioNodes = all the node that should be disconnected in onended callback - // this is crucial for performance - audioNodes = chain.concat([delaySend, reverbSend, analyserSend]); - allAudioNodeChains.push(audioNodes) + allAudioNodeChains.set(chainID, [...chain, delaySend, reverbSend, analyserSend]); }; export const superdoughTrigger = (t, hap, ct, cps) => { diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index ba8616272..801a0886d 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -36,11 +36,11 @@ import './Repl.css'; import { setInterval, clearInterval } from 'worker-timers'; import { getMetadata } from '../metadata_parser'; -const { latestCode } = settingsMap.get(); +const { latestCode, maxPolyphony } = settingsMap.get(); let modulesLoading, presets, drawContext, clearCanvas, audioReady; if (typeof window !== 'undefined') { - audioReady = initAudioOnFirstClick(); + audioReady = initAudioOnFirstClick({ maxPolyphony }); modulesLoading = loadModules(); presets = prebake(); drawContext = getDrawContext(); diff --git a/website/src/settings.mjs b/website/src/settings.mjs index cd3d89366..132d84bd5 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -40,6 +40,7 @@ export const defaultSettings = { audioEngineTarget: audioEngineTargets.webaudio, isButtonRowHidden: false, isCSSAnimationDisabled: false, + maxPolyphony: 128, }; let search = null; From b4027fd92ac38f9426e29982bd444fa197754f33 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 30 Mar 2025 11:36:50 +0200 Subject: [PATCH 094/371] 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 095/371] 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 096/371] 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 6f9bcc53dc43c3166564e0eca0c3ccae20a871ee Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 30 Mar 2025 16:33:47 -0400 Subject: [PATCH 097/371] ramp cut audio node --- packages/superdough/helpers.mjs | 1 + packages/superdough/sampler.mjs | 4 ++- packages/superdough/superdough.mjs | 45 ++++++++++++++++-------------- packages/superdough/synth.mjs | 30 +++++++++++++------- packages/superdough/worklets.mjs | 3 ++ 5 files changed, 51 insertions(+), 32 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index e51156fec..dfa8660f4 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -212,6 +212,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { constantNode.onended = () => { onComplete(); }; + return constantNode } const mod = (freq, range = 1, type = 'sine') => { const ctx = getAudioContext(); diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 45f6b33e1..f8eddffc2 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -345,7 +345,9 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { }; let envEnd = holdEnd + release + 0.01; bufferSource.stop(envEnd); - const stop = (endTime, playWholeBuffer) => {}; + const stop = (endTime) => { + bufferSource.stop(endTime) + }; const handle = { node: out, bufferSource, stop }; // cut groups diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c306f54ec..3cb5349aa 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -379,7 +379,7 @@ export function resetGlobalEffects() { analysersData = {}; } -let allAudioNodeChains = new Map(); +let activeSoundSources = new Map(); export const superdough = async (value, t, hapDuration) => { const ac = getAudioContext(); @@ -480,30 +480,24 @@ export const superdough = async (value, t, hapDuration) => { gain = nanFallback(gain, 1); - const chainID = Math.round(Math.random() * 10000); + const chainID = Math.round(Math.random() * 1000000); - // oldest audio nodes will be removed if maximum polyphony is exceeded - for (let i = 0; i <= allAudioNodeChains.size - maxPolyphony; i++) { - const ch = allAudioNodeChains.entries().next(); - const key = ch.value[0]; - const chain = ch.value[1]; - if (key == null) { - continue; - } - chain?.forEach((node) => node?.disconnect()); - allAudioNodeChains.delete(key); + // oldest audio nodes will be destroyed if maximum polyphony is exceeded + for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) { + const ch = activeSoundSources.entries().next(); + const source = ch.value[1]; + const chainID = ch.value[0]; + const endTime = t + .25; + source?.node?.gain?.linearRampToValueAtTime(0, endTime); + source?.stop?.(endTime); + activeSoundSources.delete(chainID); } //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); - gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future + let audioNodes = []; - const onended = () => { - const chain = allAudioNodeChains.get(chainID); - chain?.forEach((n) => n?.disconnect()); - allAudioNodeChains.delete(chainID); - }; if (bank && s) { s = `${bank}_${s}`; value.s = s; @@ -515,10 +509,15 @@ export const superdough = async (value, t, hapDuration) => { sourceNode = source(t, value, hapDuration); } else if (getSound(s)) { const { onTrigger } = getSound(s); - const soundHandle = await onTrigger(t, value, onended); + const onEnded = () => { + audioNodes.forEach((n) => n?.disconnect()); + activeSoundSources.delete(chainID); + }; + const soundHandle = await onTrigger(t, value, onEnded); + if (soundHandle) { sourceNode = soundHandle.node; - soundHandle.stop(t + hapDuration); + activeSoundSources.set(chainID, soundHandle); } } else { throw new Error(`sound ${s} not found! Is it loaded?`); @@ -648,6 +647,7 @@ export const superdough = async (value, t, hapDuration) => { if (delay > 0 && delaytime > 0 && delayfeedback > 0) { const delyNode = getDelay(orbit, delaytime, delayfeedback, t); delaySend = effectSend(post, delyNode, delay); + audioNodes.push(delaySend); } // reverb let reverbSend; @@ -665,6 +665,7 @@ export const superdough = async (value, t, hapDuration) => { } const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR); reverbSend = effectSend(post, reverbNode, room); + audioNodes.push(reverbSend); } // analyser @@ -672,11 +673,13 @@ export const superdough = async (value, t, hapDuration) => { if (analyze) { const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); analyserSend = effectSend(post, analyserNode, 1); + audioNodes.push(analyserSend); } // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); - allAudioNodeChains.set(chainID, [...chain, delaySend, reverbSend, analyserSend]); + audioNodes = audioNodes.concat(chain); + // activeSoundSources.set(chainID, [...chain, delaySend, reverbSend, analyserSend].filter(Boolean)); }; export const superdoughTrigger = (t, hap, ct, cps) => { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 86c073d0b..f40b40c6d 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -63,7 +63,9 @@ export function registerSynthSounds() { stop(envEnd); return { node, - stop: (releaseTime) => {}, + stop: (endTime) => { + stop(endTime); + }, }; }, { type: 'synth', prebake: true }, @@ -110,7 +112,9 @@ export function registerSynthSounds() { let envGain = gainNode(1); envGain = o.connect(envGain); - webAudioTimeout( + getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); + + let timeoutNode = webAudioTimeout( ac, () => { o.disconnect(); @@ -123,11 +127,11 @@ export function registerSynthSounds() { end, ); - getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); - return { node: envGain, - stop: (time) => {}, + stop: (time) => { + timeoutNode.stop(time); + }, }; }, { prebake: true, type: 'synth' }, @@ -169,7 +173,10 @@ export function registerSynthSounds() { let envGain = gainNode(1); envGain = o.connect(envGain); - webAudioTimeout( + + getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); + + let timeoutNode = webAudioTimeout( ac, () => { o.disconnect(); @@ -182,11 +189,11 @@ export function registerSynthSounds() { end, ); - getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); - return { node: envGain, - stop: (time) => {}, + stop: (time) => { + timeoutNode.stop(time); + }, }; }, { prebake: true, type: 'synth' }, @@ -229,7 +236,9 @@ export function registerSynthSounds() { stop(envEnd); return { node, - stop: (releaseTime) => {}, + stop: (endTime) => { + stop(endTime) + }, }; }, { type: 'synth', prebake: true }, @@ -299,6 +308,7 @@ export function getOscillator(s, t, value) { return { node: noiseMix?.node || o, stop: (time) => { + // console.info(time) fmModulator.stop(time); vibratoOscillator?.stop(time); noiseMix?.stop(time); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 61d5d96e9..286d8c039 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -663,6 +663,9 @@ registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor); class PulseOscillatorProcessor extends AudioWorkletProcessor { constructor() { super(); + // this.port.onmessage = (event) => { + // console.info(event) + // }; this.pi = _PI; this.phi = -this.pi; // phase this.Y0 = 0; // feedback memories From d8e4055c6e5dd037386763252e83836e9edc842e Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 30 Mar 2025 20:22:24 -0400 Subject: [PATCH 098/371] settings panel --- packages/superdough/superdough.mjs | 11 ++++---- packages/superdough/synth.mjs | 12 +++++---- packages/superdough/worklets.mjs | 6 ++--- .../src/repl/components/panel/SettingsTab.jsx | 25 ++++++++++++++++++- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 3cb5349aa..e59930242 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -14,9 +14,10 @@ import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; -let maxPolyphony = 128; +export const DEFAULT_MAX_POLYPHONY = 128; +let maxPolyphony = DEFAULT_MAX_POLYPHONY; export function setMaxPolyphony(polyphony) { - maxPolyphony = polyphony; + maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; } export const soundMap = map(); @@ -166,8 +167,8 @@ function loadWorklets() { // this function should be called on first user interaction (to avoid console warning) export async function initAudio(options = {}) { - const { disableWorklets = false, polyphony = maxPolyphony } = options; - setMaxPolyphony(polyphony); + const { disableWorklets = false, maxPolyphony } = options; + setMaxPolyphony(maxPolyphony); if (typeof window === 'undefined') { return; } @@ -487,7 +488,7 @@ export const superdough = async (value, t, hapDuration) => { const ch = activeSoundSources.entries().next(); const source = ch.value[1]; const chainID = ch.value[0]; - const endTime = t + .25; + const endTime = t + 0.25; source?.node?.gain?.linearRampToValueAtTime(0, endTime); source?.stop?.(endTime); activeSoundSources.delete(chainID); diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index f40b40c6d..8bb47bb7d 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -25,6 +25,10 @@ const getFrequencyFromValue = (value) => { return Number(freq); }; +function destroyAudioWorkletNode(node) { + node.disconnect(); + node.parameters.get('end')?.setValueAtTime(0, 0); +} const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; const noises = ['pink', 'white', 'brown', 'crackle']; @@ -117,7 +121,7 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - o.disconnect(); + destroyAudioWorkletNode(o); envGain.disconnect(); onended(); fm?.stop(); @@ -173,13 +177,12 @@ export function registerSynthSounds() { let envGain = gainNode(1); envGain = o.connect(envGain); - getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); let timeoutNode = webAudioTimeout( ac, () => { - o.disconnect(); + destroyAudioWorkletNode(o); envGain.disconnect(); onended(); fm?.stop(); @@ -237,7 +240,7 @@ export function registerSynthSounds() { return { node, stop: (endTime) => { - stop(endTime) + stop(endTime); }, }; }, @@ -308,7 +311,6 @@ export function getOscillator(s, t, value) { return { node: noiseMix?.node || o, stop: (time) => { - // console.info(time) fmModulator.stop(time); vibratoOscillator?.stop(time); noiseMix?.stop(time); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 286d8c039..03d3a966f 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -663,9 +663,6 @@ registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor); class PulseOscillatorProcessor extends AudioWorkletProcessor { constructor() { super(); - // this.port.onmessage = (event) => { - // console.info(event) - // }; this.pi = _PI; this.phi = -this.pi; // phase this.Y0 = 0; // feedback memories @@ -713,6 +710,9 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor { } process(inputs, outputs, params) { + if (this.disconnected) { + return false; + } if (currentTime <= params.begin[0]) { return true; } diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 4ad3cc9ac..4c174fa6c 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -1,10 +1,12 @@ import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs'; import { themes } from '@strudel/codemirror'; +import { Textbox } from '../textbox/Textbox.jsx'; import { isUdels } from '../../util.mjs'; import { ButtonGroup } from './Forms.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx'; import { confirmDialog } from '../../util.mjs'; +import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony } from '@strudel/webaudio'; function Checkbox({ label, value, onChange, disabled = false }) { return ( @@ -53,7 +55,7 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) { ); } -function FormItem({ label, children }) { +function FormItem({ label, children, sublabel }) { return (
@@ -105,6 +107,7 @@ export function SettingsTab({ started }) { audioDeviceName, audioEngineTarget, togglePanelTrigger, + maxPolyphony, } = useSettings(); const shouldAlwaysSync = isUdels(); const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; @@ -139,6 +142,26 @@ export function SettingsTab({ started }) { }} /> + + + { + let v = parseInt(e.target.value); + v = isNaN(v) ? DEFAULT_MAX_POLYPHONY : v; + setMaxPolyphony(v); + settingsMap.setKey('maxPolyphony', v); + }} + onChange={(v) => { + v = Math.max(1, parseInt(v)); + settingsMap.setKey('maxPolyphony', isNaN(v) ? undefined : v); + }} + type="number" + placeholder="" + value={maxPolyphony ?? ''} + /> + settingsMap.setKey('theme', theme)} /> From 2f4cba4e9436c0962e4917d16d7564a12eadedb1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 30 Mar 2025 20:26:44 -0400 Subject: [PATCH 099/371] format --- packages/superdough/helpers.mjs | 2 +- packages/superdough/sampler.mjs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index dfa8660f4..6bde69373 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -212,7 +212,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { constantNode.onended = () => { onComplete(); }; - return constantNode + return constantNode; } const mod = (freq, range = 1, type = 'sine') => { const ctx = getAudioContext(); diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index f8eddffc2..18d1b7797 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -278,7 +278,6 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option const cutGroups = []; - export async function onTriggerSample(t, value, onended, bank, resolveUrl) { let { s, @@ -346,7 +345,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { let envEnd = holdEnd + release + 0.01; bufferSource.stop(envEnd); const stop = (endTime) => { - bufferSource.stop(endTime) + bufferSource.stop(endTime); }; const handle = { node: out, bufferSource, stop }; From bceb37e8eab5de964f5386ac70f2cbbb51b42ca7 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 30 Mar 2025 20:32:19 -0400 Subject: [PATCH 100/371] rm comment --- packages/superdough/superdough.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index e59930242..9a3a0aa86 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -680,7 +680,6 @@ export const superdough = async (value, t, hapDuration) => { // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); audioNodes = audioNodes.concat(chain); - // activeSoundSources.set(chainID, [...chain, delaySend, reverbSend, analyserSend].filter(Boolean)); }; export const superdoughTrigger = (t, hap, ct, cps) => { From 5ed28d6168b8d323ed64efa049c0feb285d73678 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 31 Mar 2025 00:17:26 -0400 Subject: [PATCH 101/371] gain curve func --- packages/superdough/helpers.mjs | 4 ++-- packages/superdough/superdough.mjs | 24 ++++++++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index e51156fec..2ad1567c5 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,9 +1,9 @@ -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './superdough.mjs'; import { clamp, nanFallback } from './util.mjs'; export function gainNode(value) { const node = getAudioContext().createGain(); - node.gain.value = value; + node.gain.value = value return node; } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fc81f565e..a9372ca1d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -20,6 +20,16 @@ export function registerSound(key, onTrigger, data = {}) { soundMap.setKey(key.toLowerCase(), { onTrigger, data }); } +let gainCurveFunc = (val) => Math.pow(val,2) + +export function applyGainCurve(val) { + return gainCurveFunc(val) +} + +export function setGainCurve(newGainCurveFunc) { + gainCurveFunc = newGainCurveFunc +} + function aliasBankMap(aliasMap) { // Make all bank keys lower case for case insensitivity for (const key in aliasMap) { @@ -87,7 +97,7 @@ export function getSound(s) { const defaultDefaultValues = { s: 'triangle', - gain: 0.8, + gain: 1, postgain: 1, density: '.03', ftype: '12db', @@ -471,12 +481,17 @@ export const superdough = async (value, t, hapDuration) => { compressorRelease, } = value; - gain = nanFallback(gain, 1); + gain = applyGainCurve(nanFallback(gain, 1)); + postgain = applyGainCurve(postgain) + shapevol = applyGainCurve(shapevol) + distortvol = applyGainCurve(distortvol) + delay = applyGainCurve(delay) + velocity = applyGainCurve(velocity) + //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); - gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future let toDisconnect = []; // audio nodes that will be disconnected when the source has ended const onended = () => { toDisconnect.forEach((n) => n?.disconnect()); @@ -516,6 +531,7 @@ export const superdough = async (value, t, hapDuration) => { // gain stage chain.push(gainNode(gain)); + chain.push(gainNode(.8 * velocity)) //filter const ftype = getFilterType(value.ftype); @@ -616,7 +632,7 @@ export const superdough = async (value, t, hapDuration) => { } // last gain - const post = new GainNode(ac, { gain: postgain }); + const post = new GainNode(ac, { gain:postgain }); chain.push(post); connectToDestination(post, channels); From 999c0b23495583a96c89cea6d787390a8d40ad0b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 31 Mar 2025 00:21:01 -0400 Subject: [PATCH 102/371] lint --- packages/superdough/helpers.mjs | 4 ++-- packages/superdough/superdough.mjs | 21 ++++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 2ad1567c5..e51156fec 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,9 +1,9 @@ -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './superdough.mjs'; import { clamp, nanFallback } from './util.mjs'; export function gainNode(value) { const node = getAudioContext().createGain(); - node.gain.value = value + node.gain.value = value; return node; } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a9372ca1d..d2c825ab7 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -20,14 +20,14 @@ export function registerSound(key, onTrigger, data = {}) { soundMap.setKey(key.toLowerCase(), { onTrigger, data }); } -let gainCurveFunc = (val) => Math.pow(val,2) +let gainCurveFunc = (val) => Math.pow(val, 2); export function applyGainCurve(val) { - return gainCurveFunc(val) + return gainCurveFunc(val); } export function setGainCurve(newGainCurveFunc) { - gainCurveFunc = newGainCurveFunc + gainCurveFunc = newGainCurveFunc; } function aliasBankMap(aliasMap) { @@ -482,12 +482,11 @@ export const superdough = async (value, t, hapDuration) => { } = value; gain = applyGainCurve(nanFallback(gain, 1)); - postgain = applyGainCurve(postgain) - shapevol = applyGainCurve(shapevol) - distortvol = applyGainCurve(distortvol) - delay = applyGainCurve(delay) - velocity = applyGainCurve(velocity) - + postgain = applyGainCurve(postgain); + shapevol = applyGainCurve(shapevol); + distortvol = applyGainCurve(distortvol); + delay = applyGainCurve(delay); + velocity = applyGainCurve(velocity); //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); @@ -531,7 +530,7 @@ export const superdough = async (value, t, hapDuration) => { // gain stage chain.push(gainNode(gain)); - chain.push(gainNode(.8 * velocity)) + chain.push(gainNode(0.8 * velocity)); //filter const ftype = getFilterType(value.ftype); @@ -632,7 +631,7 @@ export const superdough = async (value, t, hapDuration) => { } // last gain - const post = new GainNode(ac, { gain:postgain }); + const post = new GainNode(ac, { gain: postgain }); chain.push(post); connectToDestination(post, channels); From 94a3a60e4917e7c5592aea35b1ded71e6005ad60 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 1 Apr 2025 21:49:28 +0200 Subject: [PATCH 103/371] 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 104/371] 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 105/371] 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 106/371] 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 107/371] 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 108/371] 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 109/371] 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 c2d4b529f6a49c9714b65812d4991b1ed56bb98d Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 3 Apr 2025 00:59:46 -0400 Subject: [PATCH 110/371] working --- packages/superdough/superdough.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fc81f565e..cd9148228 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -17,7 +17,9 @@ import { loadBuffer } from './sampler.mjs'; export const soundMap = map(); export function registerSound(key, onTrigger, data = {}) { - soundMap.setKey(key.toLowerCase(), { onTrigger, data }); + key = key.toLowerCase().replace(/\s+/g, '_') + console.info(key, data) + soundMap.setKey(key, { onTrigger, data }); } function aliasBankMap(aliasMap) { From 6b8ee8ad11c92cd883d0fcf9554149a62feb59c8 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 3 Apr 2025 01:05:26 -0400 Subject: [PATCH 111/371] lint --- packages/superdough/superdough.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index cd9148228..1126af0fc 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -17,8 +17,7 @@ import { loadBuffer } from './sampler.mjs'; export const soundMap = map(); export function registerSound(key, onTrigger, data = {}) { - key = key.toLowerCase().replace(/\s+/g, '_') - console.info(key, data) + key = key.toLowerCase().replace(/\s+/g, '_'); soundMap.setKey(key, { onTrigger, data }); } From 989bd0990e638f19d694826c5509683459f1a6b9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 3 Apr 2025 17:01:36 +0200 Subject: [PATCH 112/371] 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 113/371] 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 114/371] 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 115/371] 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 05ea9c86fbe37ddc4beb4ea43db5c894a13fd33b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 6 Apr 2025 16:08:42 -0400 Subject: [PATCH 116/371] working --- packages/core/controls.mjs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index e58bc8caf..85b4c2398 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1780,3 +1780,32 @@ export const as = register('as', (mapping, pat) => { return v; }); }); + +/** + * Allows you to scrub an audio file like a tape loop by passing values that represents the position in the audio file + * in the optional array syntax ex: "0.5:2", the second value controls the speed of playback + * @name scrub + * @memberof Pattern + * @returns Pattern + * @example + * samples('github:switchangel/pad') + * s("swpad:0").scrub("{0.1!2 .25@3 0.7!2 <0.8:1.5>}%8") + * @example + * samples('github:yaxu/clean-breaks/main'); + * s("amen/4").fit().scrub("{0@3 0@2 4@3}%8".div(16)) + */ + +register( + 'scrub', + (beginPat, pat) => { + return beginPat.outerBind((v) => { + if (!Array.isArray(v)) { + v = [v]; + } + const [beginVal, speedMultiplier = 1] = v; + + return pat.begin(beginVal).mul(speed(speedMultiplier)).clip(1); + }); + }, + false, +); From a4682018224d3ed72b540ea3dfa35d0c7bffbaad Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 6 Apr 2025 16:32:37 -0400 Subject: [PATCH 117/371] add 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 b9da0f538..2d88d8355 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -7871,6 +7871,52 @@ but parts might be played more than once, or not at all, per cycle." example ind ] `; +exports[`runs examples > example "scrub" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 1/8 → 1/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 1/4 → 5/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 5/8 → 3/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 3/4 → 7/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 7/8 → 1/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", + "[ 1/1 → 9/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 9/8 → 5/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 5/4 → 13/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 13/8 → 7/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 7/4 → 15/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 15/8 → 2/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", + "[ 2/1 → 17/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 17/8 → 9/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 9/4 → 21/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 21/8 → 11/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 11/4 → 23/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 23/8 → 3/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", + "[ 3/1 → 25/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 25/8 → 13/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 13/4 → 29/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 29/8 → 15/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 15/4 → 31/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 31/8 → 4/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", +] +`; + +exports[`runs examples > example "scrub" example index 1 1`] = ` +[ + "[ 0/1 → 3/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 3/8 → 5/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 5/8 → 1/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", + "[ 1/1 → 11/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 11/8 → 13/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 13/8 → 2/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", + "[ 2/1 → 19/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 19/8 → 21/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 21/8 → 3/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", + "[ 3/1 → 27/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 27/8 → 29/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 29/8 → 4/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", +] +`; + exports[`runs examples > example "segment" example index 0 1`] = ` [ "[ 0/1 → 1/24 | note:40 ]", From 695ac6a5d3891d5c45fe570e33628c1b055ba32a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 6 Apr 2025 16:34:14 -0400 Subject: [PATCH 118/371] var --- 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 85b4c2398..a6c650e03 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1795,7 +1795,7 @@ export const as = register('as', (mapping, pat) => { * s("amen/4").fit().scrub("{0@3 0@2 4@3}%8".div(16)) */ -register( +export const scrub = register( 'scrub', (beginPat, pat) => { return beginPat.outerBind((v) => { From 344d9ca1a8692f4bef1fe63ba201edab9434c3e5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 7 Apr 2025 23:43:29 -0400 Subject: [PATCH 119/371] working --- packages/superdough/superdough.mjs | 61 ++++++++++++++++++- .../components/panel/AudioDeviceSelector.jsx | 5 +- website/src/repl/useReplContext.jsx | 22 ++----- website/src/repl/util.mjs | 35 +---------- website/src/settings.mjs | 5 +- 5 files changed, 68 insertions(+), 60 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 8c471eb3a..bb152dd98 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -15,6 +15,8 @@ import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; +export const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; + let maxPolyphony = DEFAULT_MAX_POLYPHONY; export function setMaxPolyphony(polyphony) { maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; @@ -91,6 +93,38 @@ export function getSound(s) { return soundMap.get()[s.toLowerCase()]; } +export const getAudioDevices = async () => { + await navigator.mediaDevices.getUserMedia({ audio: true }); + let mediaDevices = await navigator.mediaDevices.enumerateDevices(); + mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default'); + const devicesMap = new Map(); + devicesMap.set(DEFAULT_AUDIO_DEVICE_NAME, ''); + mediaDevices.forEach((device) => { + devicesMap.set(device.label, device.deviceId); + }); + return devicesMap; +}; + +export const setAudioDevice = async (id) => { + let audioCtx = getAudioContext(); + if (audioCtx.sinkId === id) { + return; + } + await audioCtx.suspend(); + await audioCtx.close(); + audioCtx = setDefaultAudioContext(); + await audioCtx.resume(); + const isValidID = (id ?? '').length > 0; + if (isValidID) { + try { + await audioCtx.setSinkId(id); + } catch { + logger('failed to set audio interface', 'warning'); + } + } + initializeAudioOutput(); +}; + const defaultDefaultValues = { s: 'triangle', gain: 0.8, @@ -161,19 +195,40 @@ export function getAudioContextCurrentTime() { let workletsLoading; function loadWorklets() { if (!workletsLoading) { - workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl); + const contextPromise = getAudioContext(); + workletsLoading = contextPromise.audioWorklet.addModule(workletsUrl); } + return workletsLoading; } // this function should be called on first user interaction (to avoid console warning) export async function initAudio(options = {}) { - const { disableWorklets = false, maxPolyphony } = options; + const { disableWorklets = false, maxPolyphony, audioDeviceName } = options; setMaxPolyphony(maxPolyphony); if (typeof window === 'undefined') { return; } - await getAudioContext().resume(); + + const audioCtx = getAudioContext(); + + if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) { + try { + const devices = await getAudioDevices(); + const id = devices.get(audioDeviceName); + const isValidID = (id ?? '').length > 0; + if (audioCtx.sinkId !== id && isValidID) { + await audioCtx.setSinkId(id); + } + logger( + `[superdough] Audio Device set to ${audioDeviceName}, it might take a few seconds before audio plays on all output channels`, + ); + } catch { + logger('[superdough] failed to set audio interface', 'warning'); + } + } + + await audioCtx.resume(); if (disableWorklets) { logger('[superdough]: AudioWorklets disabled with disableWorklets'); return; diff --git a/website/src/repl/components/panel/AudioDeviceSelector.jsx b/website/src/repl/components/panel/AudioDeviceSelector.jsx index d1f13c22d..288ffc0a5 100644 --- a/website/src/repl/components/panel/AudioDeviceSelector.jsx +++ b/website/src/repl/components/panel/AudioDeviceSelector.jsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; -import { getAudioDevices, setAudioDevice } from '../../util.mjs'; + import { SelectInput } from './SelectInput'; +import { getAudioDevices } from '@strudel/webaudio'; const initdevices = new Map(); @@ -21,9 +22,7 @@ export function AudioDeviceSelector({ audioDeviceName, onChange, isDisabled }) { if (!devicesInitialized) { return; } - const deviceID = devices.get(deviceName); onChange(deviceName); - setAudioDevice(deviceID); }; const options = new Map(); Array.from(devices.keys()).forEach((deviceName) => { diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 801a0886d..f0895aaa4 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -14,7 +14,7 @@ import { resetLoadedSounds, initAudioOnFirstClick, } from '@strudel/webaudio'; -import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs'; +import { setVersionDefaultsFrom } from './util.mjs'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; import { clearHydra } from '@strudel/hydra'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -28,7 +28,7 @@ import { setViewingPatternData, } from '../user_pattern_utils.mjs'; import { superdirtOutput } from '@strudel/osc/superdirtoutput'; -import { audioEngineTargets, defaultAudioDeviceName } from '../settings.mjs'; +import { audioEngineTargets } from '../settings.mjs'; import { useStore } from '@nanostores/react'; import { prebake } from './prebake.mjs'; import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs'; @@ -36,11 +36,11 @@ import './Repl.css'; import { setInterval, clearInterval } from 'worker-timers'; import { getMetadata } from '../metadata_parser'; -const { latestCode, maxPolyphony } = settingsMap.get(); +const { latestCode, maxPolyphony, audioDeviceName } = settingsMap.get(); let modulesLoading, presets, drawContext, clearCanvas, audioReady; if (typeof window !== 'undefined') { - audioReady = initAudioOnFirstClick({ maxPolyphony }); + audioReady = initAudioOnFirstClick({ maxPolyphony, audioDeviceName }); modulesLoading = loadModules(); presets = prebake(); drawContext = getDrawContext(); @@ -159,20 +159,6 @@ export function useReplContext() { editorRef.current?.updateSettings(editorSettings); }, [_settings]); - // on first load, set stored audio device if possible - useEffect(() => { - const { audioDeviceName } = _settings; - if (audioDeviceName !== defaultAudioDeviceName) { - getAudioDevices().then((devices) => { - const deviceID = devices.get(audioDeviceName); - if (deviceID == null) { - return; - } - setAudioDevice(deviceID); - }); - } - }, []); - // // UI Actions // diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index a8d184285..82f66cd4c 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -1,6 +1,6 @@ import { evalScope, hash2code, logger } from '@strudel/core'; -import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs'; -import { getAudioContext, initializeAudioOutput, setDefaultAudioContext, setVersionDefaults } from '@strudel/webaudio'; +import { settingPatterns } from '../settings.mjs'; +import { setVersionDefaults } from '@strudel/webaudio'; import { getMetadata } from '../metadata_parser'; import { isTauri } from '../tauri.mjs'; import './Repl.css'; @@ -159,37 +159,6 @@ export const isUdels = () => { return window.top?.location?.pathname.includes('udels'); }; -export const getAudioDevices = async () => { - await navigator.mediaDevices.getUserMedia({ audio: true }); - let mediaDevices = await navigator.mediaDevices.enumerateDevices(); - mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default'); - const devicesMap = new Map(); - devicesMap.set(defaultAudioDeviceName, ''); - mediaDevices.forEach((device) => { - devicesMap.set(device.label, device.deviceId); - }); - return devicesMap; -}; - -export const setAudioDevice = async (id) => { - let audioCtx = getAudioContext(); - if (audioCtx.sinkId === id) { - return; - } - await audioCtx.suspend(); - await audioCtx.close(); - audioCtx = setDefaultAudioContext(); - await audioCtx.resume(); - const isValidID = (id ?? '').length > 0; - if (isValidID) { - try { - await audioCtx.setSinkId(id); - } catch { - logger('failed to set audio interface', 'warning'); - } - } - initializeAudioOutput(); -}; export function setVersionDefaultsFrom(code) { try { diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 132d84bd5..c50c31f6e 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -2,8 +2,7 @@ import { persistentMap } from '@nanostores/persistent'; import { useStore } from '@nanostores/react'; import { register } from '@strudel/core'; import { isUdels } from './repl/util.mjs'; - -export const defaultAudioDeviceName = 'System Standard'; +import { DEFAULT_AUDIO_DEVICE_NAME } from '@strudel/webaudio'; export const audioEngineTargets = { webaudio: 'webaudio', @@ -36,7 +35,7 @@ export const defaultSettings = { isPanelOpen: true, togglePanelTrigger: 'click', //click | hover userPatterns: '{}', - audioDeviceName: defaultAudioDeviceName, + audioDeviceName: DEFAULT_AUDIO_DEVICE_NAME, audioEngineTarget: audioEngineTargets.webaudio, isButtonRowHidden: false, isCSSAnimationDisabled: false, From b1090a1dd8a0ccb0a6c4c2e153a644afd5601eed Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 7 Apr 2025 23:52:15 -0400 Subject: [PATCH 120/371] rm deadcode --- packages/superdough/superdough.mjs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index bb152dd98..b94d5d312 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -105,26 +105,6 @@ export const getAudioDevices = async () => { return devicesMap; }; -export const setAudioDevice = async (id) => { - let audioCtx = getAudioContext(); - if (audioCtx.sinkId === id) { - return; - } - await audioCtx.suspend(); - await audioCtx.close(); - audioCtx = setDefaultAudioContext(); - await audioCtx.resume(); - const isValidID = (id ?? '').length > 0; - if (isValidID) { - try { - await audioCtx.setSinkId(id); - } catch { - logger('failed to set audio interface', 'warning'); - } - } - initializeAudioOutput(); -}; - const defaultDefaultValues = { s: 'triangle', gain: 0.8, From 9e233fe587ed720d56de84a3d4ee4f0d3c7ca9f0 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 7 Apr 2025 23:55:34 -0400 Subject: [PATCH 121/371] var name --- 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 b94d5d312..55ff2eabf 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -175,8 +175,8 @@ export function getAudioContextCurrentTime() { let workletsLoading; function loadWorklets() { if (!workletsLoading) { - const contextPromise = getAudioContext(); - workletsLoading = contextPromise.audioWorklet.addModule(workletsUrl); + const audioCtx = getAudioContext(); + workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl); } return workletsLoading; From a32bb58fb324e1520deeca9eb1fdfe5539fd0ac9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 8 Apr 2025 00:01:34 -0400 Subject: [PATCH 122/371] rm dependency --- packages/superdough/superdough.mjs | 4 ++-- website/src/settings.mjs | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 55ff2eabf..e936d38e1 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -15,7 +15,7 @@ import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; -export const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; +const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; let maxPolyphony = DEFAULT_MAX_POLYPHONY; export function setMaxPolyphony(polyphony) { @@ -184,7 +184,7 @@ function loadWorklets() { // this function should be called on first user interaction (to avoid console warning) export async function initAudio(options = {}) { - const { disableWorklets = false, maxPolyphony, audioDeviceName } = options; + const { disableWorklets = false, maxPolyphony, audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME } = options; setMaxPolyphony(maxPolyphony); if (typeof window === 'undefined') { return; diff --git a/website/src/settings.mjs b/website/src/settings.mjs index c50c31f6e..1b4ba33f9 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -2,7 +2,6 @@ import { persistentMap } from '@nanostores/persistent'; import { useStore } from '@nanostores/react'; import { register } from '@strudel/core'; import { isUdels } from './repl/util.mjs'; -import { DEFAULT_AUDIO_DEVICE_NAME } from '@strudel/webaudio'; export const audioEngineTargets = { webaudio: 'webaudio', @@ -35,7 +34,6 @@ export const defaultSettings = { isPanelOpen: true, togglePanelTrigger: 'click', //click | hover userPatterns: '{}', - audioDeviceName: DEFAULT_AUDIO_DEVICE_NAME, audioEngineTarget: audioEngineTargets.webaudio, isButtonRowHidden: false, isCSSAnimationDisabled: false, From 2b69004eb442913072eacbcd22b0217c6571b4a0 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 8 Apr 2025 00:04:22 -0400 Subject: [PATCH 123/371] lint --- website/src/repl/util.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 82f66cd4c..f49dd568f 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -1,6 +1,6 @@ import { evalScope, hash2code, logger } from '@strudel/core'; import { settingPatterns } from '../settings.mjs'; -import { setVersionDefaults } from '@strudel/webaudio'; +import { setVersionDefaults } from '@strudel/webaudio'; import { getMetadata } from '../metadata_parser'; import { isTauri } from '../tauri.mjs'; import './Repl.css'; @@ -159,7 +159,6 @@ export const isUdels = () => { return window.top?.location?.pathname.includes('udels'); }; - export function setVersionDefaultsFrom(code) { try { const metadata = getMetadata(code); From ab76b4424a83967296987939df1299352a0356b5 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Fri, 11 Apr 2025 08:18:57 +0100 Subject: [PATCH 124/371] send delta in seconds, to match tidal/superdirt (#1323) --- packages/osc/osc.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index 4b9ab14e7..3c7b92d4e 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -37,7 +37,7 @@ function connect() { export function parseControlsFromHap(hap, cps) { hap.ensureObjectValue(); const cycle = hap.wholeOrPart().begin.valueOf(); - const delta = hap.duration.valueOf(); + const delta = hap.duration.valueOf() / cps; const controls = Object.assign({}, { cps, cycle, delta }, hap.value); // make sure n and note are numbers controls.n && (controls.n = parseNumeral(controls.n)); From 2968017300c9cd6bd2b112191f2d799540ab2c70 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 17 Apr 2025 23:32:20 -0400 Subject: [PATCH 125/371] disabled --- website/astro.config.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 631584220..8b4fa4f86 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -58,6 +58,9 @@ const options = { // https://astro.build/config export default defineConfig({ + devToolbar: { + enabled: false + }, integrations: [ react(), mdx(options), From 6e3aa693d5898f9982f23bde05aeec14af98c0d5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 17 Apr 2025 23:35:10 -0400 Subject: [PATCH 126/371] format --- website/astro.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 8b4fa4f86..94c4e613a 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -59,7 +59,7 @@ const options = { // https://astro.build/config export default defineConfig({ devToolbar: { - enabled: false + enabled: false, }, integrations: [ react(), From ad25ce7b2199c200870615a4a82ced83570081cf Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 18 Apr 2025 01:01:10 -0400 Subject: [PATCH 127/371] working --- website/src/pages/udels/index.astro | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/website/src/pages/udels/index.astro b/website/src/pages/udels/index.astro index 4ab699e73..157e53151 100644 --- a/website/src/pages/udels/index.astro +++ b/website/src/pages/udels/index.astro @@ -1,12 +1,14 @@ --- +import HeadCommon from '@components/HeadCommon.astro'; import { Udels } from '../../components/Udels/Udels.jsx'; - - -const { BASE_URL } = import.meta.env; -const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; --- - - - - \ No newline at end of file + + + + Strudel UDELS + + + + + From 17e1ebeb93df8fa64d8b23f5abf29ddf9f122857 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 18 Apr 2025 01:02:13 -0400 Subject: [PATCH 128/371] udelsconfig --- website/astro.config.mjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 94c4e613a..631584220 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -58,9 +58,6 @@ const options = { // https://astro.build/config export default defineConfig({ - devToolbar: { - enabled: false, - }, integrations: [ react(), mdx(options), From bd08b74ebc5ffd7c23fe708d98dd4ac09765a5d9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 19 Apr 2025 16:10:38 -0400 Subject: [PATCH 129/371] fixed strudel bool --- packages/codemirror/flash.mjs | 4 +- packages/codemirror/themes.mjs | 58 +++++++++---------- packages/codemirror/themes/algoboy.mjs | 1 + packages/codemirror/themes/atomone.mjs | 2 + packages/codemirror/themes/green-text.mjs | 39 +++++++++++++ packages/codemirror/themes/red-text.mjs | 39 +++++++++++++ packages/codemirror/themes/sonic-pink.mjs | 39 +++++++++++++ packages/codemirror/themes/strudel-theme.mjs | 4 +- packages/codemirror/themes/theme-helper.mjs | 3 + .../src/repl/components/panel/SelectInput.jsx | 2 +- .../src/repl/components/panel/SettingsTab.jsx | 2 +- .../src/repl/components/textbox/Textbox.jsx | 2 +- 12 files changed, 157 insertions(+), 38 deletions(-) create mode 100644 packages/codemirror/themes/green-text.mjs create mode 100644 packages/codemirror/themes/red-text.mjs create mode 100644 packages/codemirror/themes/sonic-pink.mjs diff --git a/packages/codemirror/flash.mjs b/packages/codemirror/flash.mjs index 6b37038ff..243500bff 100644 --- a/packages/codemirror/flash.mjs +++ b/packages/codemirror/flash.mjs @@ -11,7 +11,9 @@ export const flashField = StateField.define({ for (let e of tr.effects) { if (e.is(setFlash)) { if (e.value && tr.newDoc.length > 0) { - const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } }); + const mark = Decoration.mark({ + attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` }, + }); flash = Decoration.set([mark.range(0, tr.newDoc.length)]); } else { flash = Decoration.set([]); diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs index c2f49065a..771b6bdc1 100644 --- a/packages/codemirror/themes.mjs +++ b/packages/codemirror/themes.mjs @@ -5,16 +5,15 @@ import whitescreen, { settings as whitescreenSettings } from './themes/whitescre import teletext, { settings as teletextSettings } from './themes/teletext.mjs'; import algoboy, { settings as algoboySettings } from './themes/algoboy.mjs'; import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs'; -import terminal, { settings as terminalSettings } from './themes/terminal.mjs'; -import abcdef, { settings as abcdefSettings } from './themes/abcdef.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 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'; -import bespin, { settings as bespinSettings } from './themes/bespin.mjs'; import darcula, { settings as darculaSettings } from './themes/darcula.mjs'; import dracula, { settings as draculaSettings } from './themes/dracula.mjs'; import duotoneDark, { settings as duotoneDarkSettings } from './themes/duotoneDark.mjs'; -import duotoneLight, { settings as duotoneLightSettings } from './themes/duotoneLight.mjs'; import eclipse, { settings as eclipseSettings } from './themes/eclipse.mjs'; import githubDark, { settings as githubDarkSettings } from './themes/githubDark.mjs'; import githubLight, { settings as githubLightSettings } from './themes/githubLight.mjs'; @@ -38,46 +37,45 @@ import bbedit, { settings as bbeditSettings } from './themes/bbedit.mjs'; import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLilac.mjs'; import { setTheme } from '@strudel/draw'; - export const themes = { strudelTheme, - bluescreen, - blackscreen, - whitescreen, - teletext, algoboy, + androidstudio, atomone, aura, - darcula, - dracula, // todo: optimize // bespin, //abcdef, - androidstudio, - duotoneDark, - githubDark, + bbedit, + blackscreen, + bluescreen, CutiePi, + darcula, + dracula, + duotoneDark, + eclipse, + githubDark, + githubLight, + greenText, gruvboxDark, + gruvboxLight, + sonicPink, materialDark, - nord, + materialLight, monokai, + noctisLilac, + nord, + redText, solarizedDark, + solarizedLight, sublime, + teletext, tokyoNight, + tokyoNightDay, tokyoNightStorm, vscodeDark, - //xcodeDark, - // LIGHT - bbedit, - //duotoneLight, - eclipse, - githubLight, - gruvboxLight, - materialLight, vscodeLight, - noctisLilac, - solarizedLight, - tokyoNightDay, + whitescreen, xcodeLight, }; @@ -88,21 +86,19 @@ export const settings = { whitescreen: whitescreenSettings, teletext: teletextSettings, algoboy: algoboySettings, - terminal: terminalSettings, - abcdef: abcdefSettings, androidstudio: androidstudioSettings, atomone: atomOneSettings, aura: auraSettings, bbedit: bbeditSettings, - bespin: bespinSettings, darcula: darculaSettings, dracula: draculaSettings, - duotoneLight: duotoneLightSettings, duotoneDark: duotoneDarkSettings, eclipse: eclipseSettings, CutiePi: CutiePiSettings, + sonicPink: sonicPinkSettings, githubLight: githubLightSettings, githubDark: githubDarkSettings, + greenText: greenTextSettings, gruvboxDark: gruvboxDarkSettings, gruvboxLight: gruvboxLightSettings, materialDark: materialDarkSettings, @@ -110,6 +106,7 @@ export const settings = { noctisLilac: noctisLilacSettings, nord: nordSettings, monokai: monokaiSettings, + redText: redTextSettings, solarizedLight: solarizedLightSettings, solarizedDark: solarizedDarkSettings, sublime: sublimeSettings, @@ -118,7 +115,6 @@ export const settings = { vscodeDark: vscodeDarkSettings, vscodeLight: vscodeLightSettings, xcodeLight: xcodeLightSettings, - //xcodeDark: xcodeDarkSettings, tokyoNightDay: tokyoNightDaySettings, }; diff --git a/packages/codemirror/themes/algoboy.mjs b/packages/codemirror/themes/algoboy.mjs index 30812d126..d3c7e069d 100644 --- a/packages/codemirror/themes/algoboy.mjs +++ b/packages/codemirror/themes/algoboy.mjs @@ -54,6 +54,7 @@ export default createTheme({ tag: [t.keyword, t.tagName, t.arithmeticOperator], color: palette[1], }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: palette[0]}, { tag: [t.function(t.variableName), t.propertyName], color: palette[0] }, { tag: t.atom, color: palette[1] }, ], diff --git a/packages/codemirror/themes/atomone.mjs b/packages/codemirror/themes/atomone.mjs index 85951d157..eda87d4e3 100644 --- a/packages/codemirror/themes/atomone.mjs +++ b/packages/codemirror/themes/atomone.mjs @@ -38,12 +38,14 @@ export default createTheme({ tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], color: 'hsl(207, 82%, 66%)', }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: 'hsl( 29, 54%, 61%)'}, { tag: [t.tagName, t.heading], color: '#e06c75' }, { tag: t.comment, color: '#54636D' }, { tag: [t.variableName, t.propertyName, t.labelName], color: 'hsl(220, 14%, 71%)' }, { tag: [t.attributeName, t.number], color: 'hsl( 29, 54%, 61%)' }, { tag: t.className, color: 'hsl( 39, 67%, 69%)' }, { tag: t.keyword, color: 'hsl(286, 60%, 67%)' }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: '#98c379' }, ], }); diff --git a/packages/codemirror/themes/green-text.mjs b/packages/codemirror/themes/green-text.mjs new file mode 100644 index 000000000..bd21f6d3a --- /dev/null +++ b/packages/codemirror/themes/green-text.mjs @@ -0,0 +1,39 @@ +/** + * @name 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 = ['#000000', '#8ed675', '#56bd2a', '#54636D', '#171717']; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[2], + selection: hex[4], + selectionMatch: hex[0], + gutterBackground: hex[0], + gutterForeground: hex[3], + 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[2], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] }, + { tag: t.comment, color: hex[3] }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] }, + { tag: [t.attributeName, t.number], color: hex[1] }, + { tag: t.keyword, color: hex[2] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] }, + ], +}); diff --git a/packages/codemirror/themes/red-text.mjs b/packages/codemirror/themes/red-text.mjs new file mode 100644 index 000000000..9fe7f516a --- /dev/null +++ b/packages/codemirror/themes/red-text.mjs @@ -0,0 +1,39 @@ +/** + * @name 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 = ['#000000', '#ff5356', '#bd312a', '#54636D', '#171717']; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[2], + selection: hex[4], + selectionMatch: hex[0], + gutterBackground: hex[0], + gutterForeground: hex[3], + 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[2], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] }, + { tag: t.comment, color: hex[3] }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] }, + { tag: [t.attributeName, t.number], color: hex[1] }, + { tag: t.keyword, color: hex[2] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] }, + ], +}); diff --git a/packages/codemirror/themes/sonic-pink.mjs b/packages/codemirror/themes/sonic-pink.mjs new file mode 100644 index 000000000..976597ecc --- /dev/null +++ b/packages/codemirror/themes/sonic-pink.mjs @@ -0,0 +1,39 @@ +/** + * @name 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 = ['#1e1e1e', '#fbde2d', '#ff1493', '#4c83ff', '#ededed', '#cccccc', '#ffffff30', '#dc2f8c']; + +export const settings = { + background: '#000000', + lineBackground: 'transparent', + foreground: hex[4], + selection: hex[6], + gutterBackground: hex[0], + gutterForeground: hex[5], + 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[4], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[3] }, + + { tag: t.comment, color: '#54636D' }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[4] }, + { tag: [t.attributeName, t.number], color: hex[3] }, + { tag: t.keyword, color: hex[1] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] }, + ], +}); diff --git a/packages/codemirror/themes/strudel-theme.mjs b/packages/codemirror/themes/strudel-theme.mjs index 7fd6bb651..49e4fad13 100644 --- a/packages/codemirror/themes/strudel-theme.mjs +++ b/packages/codemirror/themes/strudel-theme.mjs @@ -5,14 +5,11 @@ export const settings = { background: '#222', lineBackground: '#22222299', foreground: '#fff', - // foreground: '#75baff', caret: '#ffcc00', selection: 'rgba(128, 203, 196, 0.5)', selectionMatch: '#036dd626', - // lineHighlight: '#8a91991a', // original lineHighlight: '#00000050', gutterBackground: 'transparent', - // gutterForeground: '#8a919966', gutterForeground: '#8a919966', }; @@ -20,6 +17,7 @@ export default createTheme({ theme: 'dark', settings, styles: [ + { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#89ddff'}, { tag: t.labelName, color: '#89ddff' }, { tag: t.keyword, color: '#c792ea' }, { tag: t.operator, color: '#89ddff' }, diff --git a/packages/codemirror/themes/theme-helper.mjs b/packages/codemirror/themes/theme-helper.mjs index ee9bad8ef..952212990 100644 --- a/packages/codemirror/themes/theme-helper.mjs +++ b/packages/codemirror/themes/theme-helper.mjs @@ -1,7 +1,10 @@ import { EditorView } from '@codemirror/view'; +import { tags as t } from '@lezer/highlight'; import { syntaxHighlighting } from '@codemirror/language'; import { HighlightStyle } from '@codemirror/language'; + + export const createTheme = ({ theme, settings, styles }) => { const _theme = EditorView.theme( { diff --git a/website/src/repl/components/panel/SelectInput.jsx b/website/src/repl/components/panel/SelectInput.jsx index a28c39362..b47809671 100644 --- a/website/src/repl/components/panel/SelectInput.jsx +++ b/website/src/repl/components/panel/SelectInput.jsx @@ -5,7 +5,7 @@ export function SelectInput({ value, options, onChange, onClick, isDisabled }) { onChange(e.target.value)} > diff --git a/website/src/repl/components/textbox/Textbox.jsx b/website/src/repl/components/textbox/Textbox.jsx index f391dd6d9..1984007d9 100644 --- a/website/src/repl/components/textbox/Textbox.jsx +++ b/website/src/repl/components/textbox/Textbox.jsx @@ -3,7 +3,7 @@ import cx from '@src/cx.mjs'; export function Textbox({ onChange, className, ...inputProps }) { return ( onChange(e.target.value)} {...inputProps} /> From c852b5fab01be439d8f004f87c4635da136aa41f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 19 Apr 2025 16:21:11 -0400 Subject: [PATCH 130/371] fix lint --- packages/codemirror/themes.mjs | 4 -- packages/codemirror/themes/abcdef.mjs | 55 ------------------- packages/codemirror/themes/algoboy.mjs | 2 +- packages/codemirror/themes/atomone.mjs | 4 +- packages/codemirror/themes/bespin.mjs | 39 ------------- packages/codemirror/themes/strudel-theme.mjs | 2 +- packages/codemirror/themes/theme-helper.mjs | 3 - packages/codemirror/themes/xcodeDark.mjs | 34 ------------ .../src/repl/components/textbox/Textbox.jsx | 5 +- 9 files changed, 8 insertions(+), 140 deletions(-) delete mode 100644 packages/codemirror/themes/abcdef.mjs delete mode 100644 packages/codemirror/themes/bespin.mjs delete mode 100644 packages/codemirror/themes/xcodeDark.mjs diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs index 771b6bdc1..6aa70471d 100644 --- a/packages/codemirror/themes.mjs +++ b/packages/codemirror/themes.mjs @@ -31,7 +31,6 @@ import tokyoNightStorm, { settings as tokyoNightStormSettings } from './themes/t import tokyoNightDay, { settings as tokyoNightDaySettings } from './themes/tokyoNightDay.mjs'; import vscodeDark, { settings as vscodeDarkSettings } from './themes/vscodeDark.mjs'; import vscodeLight, { settings as vscodeLightSettings } from './themes/vscodeLight.mjs'; -// import xcodeDark, { settings as xcodeDarkSettings } from './themes/xcodeDark.mjs'; import xcodeLight, { settings as xcodeLightSettings } from './themes/xcodeLight.mjs'; import bbedit, { settings as bbeditSettings } from './themes/bbedit.mjs'; import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLilac.mjs'; @@ -43,9 +42,6 @@ export const themes = { androidstudio, atomone, aura, - // todo: optimize - // bespin, - //abcdef, bbedit, blackscreen, bluescreen, diff --git a/packages/codemirror/themes/abcdef.mjs b/packages/codemirror/themes/abcdef.mjs deleted file mode 100644 index 917823b9f..000000000 --- a/packages/codemirror/themes/abcdef.mjs +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @name abcdef - * @author codemirror.net - * https://codemirror.net/5/theme/abcdef.css - */ -import { tags as t } from '@lezer/highlight'; -import { createTheme } from './theme-helper.mjs'; - -export const settings = { - background: '#0f0f0f', - lineBackground: '#0f0f0f99', - foreground: '#defdef', - caret: '#00FF00', - selection: '#515151', - selectionMatch: '#515151', - gutterBackground: '#555', - gutterForeground: '#FFFFFF', - lineHighlight: '#314151', -}; - -export default createTheme({ - theme: 'dark', - settings: { - background: '#0f0f0f', - foreground: '#defdef', - caret: '#00FF00', - selection: '#515151', - selectionMatch: '#515151', - // gutterBackground: '#555', - gutterBackground: 'transparent', - /* gutterForeground: '#FFFFFF', */ - gutterForeground: '#7a7b7c', - lineHighlight: '#0a6bcb3d', - }, - styles: [ - { tag: t.labelName, color: 'inherit' }, - { tag: t.keyword, color: 'darkgoldenrod', fontWeight: 'bold' }, - { tag: t.atom, color: '#77F' }, - { tag: t.comment, color: '#7a7b7c', fontStyle: 'italic' }, - { tag: t.number, color: 'violet' }, - { tag: t.definition(t.variableName), color: '#fffabc' }, - { tag: t.variableName, color: '#abcdef' }, - { tag: t.function(t.variableName), color: '#fffabc' }, - { tag: t.typeName, color: '#FFDD44' }, - { tag: t.tagName, color: '#def' }, - { tag: t.string, color: '#2b4' }, - { tag: t.meta, color: '#C9F' }, - // { tag: t.qualifier, color: '#FFF700' }, - // { tag: t.builtin, color: '#30aabc' }, - { tag: t.bracket, color: '#8a8a8a' }, - { tag: t.attributeName, color: '#DDFF00' }, - { tag: t.heading, color: 'aquamarine', fontWeight: 'bold' }, - { tag: t.link, color: 'blueviolet', fontWeight: 'bold' }, - ], -}); diff --git a/packages/codemirror/themes/algoboy.mjs b/packages/codemirror/themes/algoboy.mjs index d3c7e069d..049ed596d 100644 --- a/packages/codemirror/themes/algoboy.mjs +++ b/packages/codemirror/themes/algoboy.mjs @@ -54,7 +54,7 @@ export default createTheme({ tag: [t.keyword, t.tagName, t.arithmeticOperator], color: palette[1], }, - { tag: [t.atom, t.bool, t.special(t.variableName)], color: palette[0]}, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: palette[0] }, { tag: [t.function(t.variableName), t.propertyName], color: palette[0] }, { tag: t.atom, color: palette[1] }, ], diff --git a/packages/codemirror/themes/atomone.mjs b/packages/codemirror/themes/atomone.mjs index eda87d4e3..4c9fdec6e 100644 --- a/packages/codemirror/themes/atomone.mjs +++ b/packages/codemirror/themes/atomone.mjs @@ -38,14 +38,14 @@ export default createTheme({ tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], color: 'hsl(207, 82%, 66%)', }, - { tag: [t.atom, t.bool, t.special(t.variableName)], color: 'hsl( 29, 54%, 61%)'}, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: 'hsl( 29, 54%, 61%)' }, { tag: [t.tagName, t.heading], color: '#e06c75' }, { tag: t.comment, color: '#54636D' }, { tag: [t.variableName, t.propertyName, t.labelName], color: 'hsl(220, 14%, 71%)' }, { tag: [t.attributeName, t.number], color: 'hsl( 29, 54%, 61%)' }, { tag: t.className, color: 'hsl( 39, 67%, 69%)' }, { tag: t.keyword, color: 'hsl(286, 60%, 67%)' }, - + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: '#98c379' }, ], }); diff --git a/packages/codemirror/themes/bespin.mjs b/packages/codemirror/themes/bespin.mjs deleted file mode 100644 index 41af821b0..000000000 --- a/packages/codemirror/themes/bespin.mjs +++ /dev/null @@ -1,39 +0,0 @@ -// this is different from https://thememirror.net/bespin -import { tags as t } from '@lezer/highlight'; -import { createTheme } from './theme-helper.mjs'; - -export const settings = { - background: '#28211c', - lineBackground: '#28211c99', - foreground: '#9d9b97', - caret: '#797977', - selection: '#36312e', - selectionMatch: '#4f382b', - gutterBackground: '#28211c', - gutterForeground: '#666666', - lineHighlight: 'rgba(255, 255, 255, 0.1)', -}; -export default createTheme({ - theme: 'dark', - settings: { - background: '#28211c', - foreground: '#9d9b97', - caret: '#797977', - selection: '#4f382b', - selectionMatch: '#4f382b', - gutterBackground: '#28211c', - gutterForeground: '#666666', - lineHighlight: '#ffffff1a', - }, - styles: [ - { tag: [t.atom, t.number, t.link, t.bool], color: '#9b859d' }, - { tag: t.comment, color: '#937121' }, - { tag: [t.keyword, t.tagName], color: '#cf6a4c' }, - { tag: t.string, color: '#f9ee98' }, - { tag: t.bracket, color: '#9d9b97' }, - { tag: [t.variableName], color: '#5ea6ea' }, - { tag: t.definition(t.variableName), color: '#cf7d34' }, - { tag: [t.function(t.variableName), t.className], color: '#cf7d34' }, - { tag: [t.propertyName, t.attributeName], color: '#54be0d' }, - ], -}); diff --git a/packages/codemirror/themes/strudel-theme.mjs b/packages/codemirror/themes/strudel-theme.mjs index 49e4fad13..7c4a3a919 100644 --- a/packages/codemirror/themes/strudel-theme.mjs +++ b/packages/codemirror/themes/strudel-theme.mjs @@ -17,7 +17,7 @@ export default createTheme({ theme: 'dark', settings, styles: [ - { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#89ddff'}, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#89ddff' }, { tag: t.labelName, color: '#89ddff' }, { tag: t.keyword, color: '#c792ea' }, { tag: t.operator, color: '#89ddff' }, diff --git a/packages/codemirror/themes/theme-helper.mjs b/packages/codemirror/themes/theme-helper.mjs index 952212990..ee9bad8ef 100644 --- a/packages/codemirror/themes/theme-helper.mjs +++ b/packages/codemirror/themes/theme-helper.mjs @@ -1,10 +1,7 @@ import { EditorView } from '@codemirror/view'; -import { tags as t } from '@lezer/highlight'; import { syntaxHighlighting } from '@codemirror/language'; import { HighlightStyle } from '@codemirror/language'; - - export const createTheme = ({ theme, settings, styles }) => { const _theme = EditorView.theme( { diff --git a/packages/codemirror/themes/xcodeDark.mjs b/packages/codemirror/themes/xcodeDark.mjs deleted file mode 100644 index 41440cfd1..000000000 --- a/packages/codemirror/themes/xcodeDark.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import { tags as t } from '@lezer/highlight'; -import { createTheme } from './theme-helper.mjs'; - -export const settings = { - background: '#292A30', - lineBackground: '#292A3099', - foreground: '#CECFD0', - caret: '#fff', - selection: '#727377', - selectionMatch: '#727377', - lineHighlight: '#2F3239', -}; - -export default createTheme({ - theme: 'dark', - settings: { - background: '#292A30', - foreground: '#CECFD0', - caret: '#fff', - selection: '#727377', - selectionMatch: '#727377', - lineHighlight: '#ffffff0f', - }, - styles: [ - { tag: [t.comment, t.quote], color: '#7F8C98' }, - { tag: [t.keyword], color: '#FF7AB2', fontWeight: 'bold' }, - { tag: [t.string, t.meta], color: '#FF8170' }, - { tag: [t.typeName], color: '#DABAFF' }, - { tag: [t.definition(t.variableName)], color: '#6BDFFF' }, - { tag: [t.name], color: '#6BAA9F' }, - { tag: [t.variableName], color: '#ACF2E4' }, - { tag: [t.regexp, t.link], color: '#FF8170' }, - ], -}); diff --git a/website/src/repl/components/textbox/Textbox.jsx b/website/src/repl/components/textbox/Textbox.jsx index 1984007d9..6c3bc3283 100644 --- a/website/src/repl/components/textbox/Textbox.jsx +++ b/website/src/repl/components/textbox/Textbox.jsx @@ -3,7 +3,10 @@ import cx from '@src/cx.mjs'; export function Textbox({ onChange, className, ...inputProps }) { return ( onChange(e.target.value)} {...inputProps} /> From 3dae2a80ab113b484664abdced52beeef2e55937 Mon Sep 17 00:00:00 2001 From: hpunq <128979576+hpunq@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:51:44 +0100 Subject: [PATCH 131/371] change import sounds label to import sounds folder --- website/src/repl/components/panel/ImportSoundsButton.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/website/src/repl/components/panel/ImportSoundsButton.jsx b/website/src/repl/components/panel/ImportSoundsButton.jsx index 7862b7669..b668cba61 100644 --- a/website/src/repl/components/panel/ImportSoundsButton.jsx +++ b/website/src/repl/components/panel/ImportSoundsButton.jsx @@ -21,8 +21,9 @@ export default function ImportSoundsButton({ onComplete }) { return ( ); } From 759cdb627673f6883150b108fc3733e677bf624b Mon Sep 17 00:00:00 2001 From: hpunq <128979576+hpunq@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:53:42 +0100 Subject: [PATCH 132/371] add new import sounds tab with explanation and hyperlinks --- .../src/repl/components/panel/SoundsTab.jsx | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index c91721e00..4c88e8ae0 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -14,6 +14,8 @@ export function SoundsTab() { const sounds = useStore(soundMap); const { soundsFilter } = useSettings(); const [search, setSearch] = useState(''); + const { BASE_URL } = import.meta.env; + const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; const soundEntries = useMemo(() => { if (!sounds) { @@ -37,6 +39,9 @@ export function SoundsTab() { if (soundsFilter === 'synths') { return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); } + if (soundsFilter === 'importSounds') { + return []; + } return filtered; }, [sounds, soundsFilter, search]); @@ -51,7 +56,6 @@ export function SoundsTab() { ref?.stop(getAudioContext().currentTime + 0.01); }); }); - return (
setSearch(v)} /> @@ -65,9 +69,9 @@ export function SoundsTab() { drums: 'drum-machines', synths: 'Synths', user: 'User', + importSounds: 'import-sounds', }} > - settingsMap.setKey('soundsFilter', 'user')} />
@@ -101,7 +105,55 @@ export function SoundsTab() { ); })} - {!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''} + {!soundEntries.length && soundsFilter === 'importSounds' ? ( +
+ settingsMap.setKey('soundsFilter', 'user')} /> +

+ To import sounds into strudel, they must be contained{' '} + + within a folder or subfolder + + . The best way to do this is to upload a “samples” folder containing subfolders of individual sounds or + soundbanks (see diagram below).{' '} +

+
+              {`└─ samples <-- import this folder
+   ├─ swoop
+   │  ├─ swoopshort.wav
+   │  ├─ swooplong.wav
+   │  └─ swooptight.wav
+   └─ smash
+      ├─ smashhigh.wav
+      ├─ smashlow.wav
+      └─ smashmiddle.wav`}
+            
+

+ The name of a subfolder corresponds to the sound name under the “user” tab. Multiple samples within a + subfolder are all labelled with the same name, but can be accessed using “.n( )” - remember sounds are + zero-indexed and in alphabetical order! +

+

+ For more information, and other ways to use your own sounds in strudel,{' '} + + check out the docs + + ! +

+

Preview Sounds

+
+              n("0 1 2 3 4 5").s("sample-name")
+            
+

+ Paste the line above into the main editor to hear the uploaded folder. Remember to use the name of your + sample as it appears under the "user" tab. +

+
+ ) : ( + '' + )} + {!soundEntries.length && soundsFilter !== 'importSounds' + ? 'No custom sounds loaded in this pattern (yet).' + : ''}
); From f539930509ec7c108f346c66534b54c0f2bc611b Mon Sep 17 00:00:00 2001 From: hpunq <128979576+hpunq@users.noreply.github.com> Date: Sat, 26 Apr 2025 01:04:11 +0100 Subject: [PATCH 133/371] formatted code with prettier --- website/src/repl/components/panel/ImportSoundsButton.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/src/repl/components/panel/ImportSoundsButton.jsx b/website/src/repl/components/panel/ImportSoundsButton.jsx index b668cba61..999bbf69c 100644 --- a/website/src/repl/components/panel/ImportSoundsButton.jsx +++ b/website/src/repl/components/panel/ImportSoundsButton.jsx @@ -21,8 +21,7 @@ export default function ImportSoundsButton({ onComplete }) { return (