+
+
+ {entry.name}
+
+ {entry.tags && (
+
+ {entry.tags.join(', ')}
+
+ )}
+
{!!entry.synonyms_text && (
Synonyms: {entry.synonyms_text}
From a4212589e5e0a2d3d6e9a31bf196f30e557b24e3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sun, 12 Oct 2025 16:33:07 +0200
Subject: [PATCH 012/200] Recategorize
---
packages/core/controls.mjs | 36 ++++++++++++++--------------
packages/core/pattern.mjs | 20 ++++++++--------
packages/core/signal.mjs | 48 +++++++++++++++++++-------------------
3 files changed, 52 insertions(+), 52 deletions(-)
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index 4e60fc48..db689e1a 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -1621,7 +1621,7 @@ export const { fadeInTime } = registerControl('fadeInTime');
* Set frequency of sound.
*
* @name freq
- * @tags transforms
+ * @tags temporal
* @param {number | Pattern} frequency in Hz. the audible range is between 20 and 20000 Hz
* @example
* freq("220 110 440 110").s("superzow").osc()
@@ -2205,7 +2205,7 @@ export const { cps } = registerControl('cps');
* Multiplies the duration with the given number. Also cuts samples off at the end if they exceed the duration.
*
* @name clip
- * @tags transforms
+ * @tags temporal
* @synonyms legato
* @param {number | Pattern} factor >= 0
* @example
@@ -2218,7 +2218,7 @@ export const { clip, legato } = registerControl('clip', 'legato');
* Sets the duration of the event in cycles. Similar to clip / legato, it also cuts samples off at the end if they exceed the duration.
*
* @name duration
- * @tags transforms
+ * @tags temporal
* @synonyms dur
* @param {number | Pattern} seconds >= 0
* @example
@@ -2297,7 +2297,7 @@ export const ar = register('ar', (t, pat) => {
* MIDI channel: Sets the MIDI channel for the event.
*
* @name midichan
- * @tags examples
+ * @tags external_io
* @param {number | Pattern} channel MIDI channel number (0-15)
* @example
* note("c4").midichan(1).midi()
@@ -2310,7 +2310,7 @@ export const { midimap } = registerControl('midimap');
* MIDI port: Sets the MIDI port for the event.
*
* @name midiport
- * @tags examples
+ * @tags external_io
* @param {number | Pattern} port MIDI port
* @example
* note("c a f e").midiport("<0 1 2 3>").midi()
@@ -2321,7 +2321,7 @@ export const { midiport } = registerControl('midiport');
* MIDI command: Sends a MIDI command message.
*
* @name midicmd
- * @tags examples
+ * @tags external_io
* @param {number | Pattern} command MIDI command
* @example
* midicmd("clock*48,/2").midi()
@@ -2332,7 +2332,7 @@ export const { midicmd } = registerControl('midicmd');
* MIDI control: Sends a MIDI control change message.
*
* @name control
- * @tags examples
+ * @tags external_io
* @param {number | Pattern} MIDI control number (0-127)
* @param {number | Pattern} MIDI controller value (0-127)
*/
@@ -2348,7 +2348,7 @@ export const control = register('control', (args, pat) => {
* MIDI control number: Sends a MIDI control change message.
*
* @name ccn
- * @tags examples
+ * @tags external_io
* @param {number | Pattern} MIDI control number (0-127)
*/
export const { ccn } = registerControl('ccn');
@@ -2356,7 +2356,7 @@ export const { ccn } = registerControl('ccn');
* MIDI control value: Sends a MIDI control change message.
*
* @name ccv
- * @tags examples
+ * @tags external_io
* @param {number | Pattern} MIDI control value (0-127)
*/
export const { ccv } = registerControl('ccv');
@@ -2366,7 +2366,7 @@ export const { ctlNum } = registerControl('ctlNum');
/**
* MIDI NRPN non-registered parameter number: Sends a MIDI NRPN non-registered parameter number message.
* @name nrpnn
- * @tags examples
+ * @tags external_io
* @param {number | Pattern} nrpnn MIDI NRPN non-registered parameter number (0-127)
* @example
* note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi()
@@ -2375,7 +2375,7 @@ export const { nrpnn } = registerControl('nrpnn');
/**
* MIDI NRPN non-registered parameter value: Sends a MIDI NRPN non-registered parameter value message.
* @name nrpv
- * @tags examples
+ * @tags external_io
* @param {number | Pattern} nrpv MIDI NRPN non-registered parameter value (0-127)
* @example
* note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi()
@@ -2386,7 +2386,7 @@ export const { nrpv } = registerControl('nrpv');
* MIDI program number: Sends a MIDI program change message.
*
* @name progNum
- * @tags examples
+ * @tags external_io
* @param {number | Pattern} program MIDI program number (0-127)
* @example
* note("c4").progNum(10).midichan(1).midi()
@@ -2396,7 +2396,7 @@ export const { progNum } = registerControl('progNum');
/**
* MIDI sysex: Sends a MIDI sysex message.
* @name sysex
- * @tags examples
+ * @tags external_io
* @param {number | Pattern} id Sysex ID
* @param {number | Pattern} data Sysex data
* @example
@@ -2412,7 +2412,7 @@ export const sysex = register('sysex', (args, pat) => {
/**
* MIDI sysex ID: Sends a MIDI sysex identifier message.
* @name sysexid
- * @tags examples
+ * @tags external_io
* @param {number | Pattern} id Sysex ID
* @example
* note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi()
@@ -2421,7 +2421,7 @@ export const { sysexid } = registerControl('sysexid');
/**
* MIDI sysex data: Sends a MIDI sysex message.
* @name sysexdata
- * @tags examples
+ * @tags external_io
* @param {number | Pattern} data Sysex data
* @example
* note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi()
@@ -2431,7 +2431,7 @@ export const { sysexdata } = registerControl('sysexdata');
/**
* MIDI pitch bend: Sends a MIDI pitch bend message.
* @name midibend
- * @tags examples
+ * @tags external_io
* @param {number | Pattern} midibend MIDI pitch bend (-1 - 1)
* @example
* note("c4").midibend(sine.slow(4).range(-0.4,0.4)).midi()
@@ -2440,7 +2440,7 @@ export const { midibend } = registerControl('midibend');
/**
* MIDI key after touch: Sends a MIDI key after touch message.
* @name miditouch
- * @tags examples
+ * @tags external_io
* @param {number | Pattern} miditouch MIDI key after touch (0-1)
* @example
* note("c4").miditouch(sine.slow(4).range(0,1)).midi()
@@ -2461,7 +2461,7 @@ export const getControlName = (alias) => {
* Sets properties in a batch.
*
* @name as
- * @tags transforms
+ * @tags temporal
* @param {String | Array} mapping the control names that are set
* @example
* "c:.5 a:1 f:.25 e:.8".as("note:clip")
diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs
index 2d6459a8..4a0dbe58 100644
--- a/packages/core/pattern.mjs
+++ b/packages/core/pattern.mjs
@@ -978,7 +978,7 @@ Pattern.prototype.collect = function () {
/**
* Selects indices in in stacked notes.
- * @tags transforms
+ * @tags temporal
* @example
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arpWith(haps => haps[2])
@@ -993,7 +993,7 @@ export const arpWith = register('arpWith', (func, pat) => {
/**
* Selects indices in in stacked notes.
- * @tags transforms
+ * @tags temporal
* @example
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arp("0 [0,2] 1 [0,2]")
@@ -1342,7 +1342,7 @@ export function reify(thing) {
/**
* Takes a list of patterns, and returns a pattern of lists.
*
- * @tags transforms
+ * @tags temporal
*/
export function sequenceP(pats) {
let result = pure([]);
@@ -3354,7 +3354,7 @@ Pattern.prototype.steps = Pattern.prototype.pace;
* Cuts each sample into the given number of parts, allowing you to explore a technique known as 'granular synthesis'.
* It turns a pattern of samples into a pattern of parts of samples.
* @name chop
- * @tags transforms
+ * @tags temporal
* @memberof Pattern
* @returns Pattern
* @example
@@ -3385,7 +3385,7 @@ export const chop = register('chop', function (n, pat) {
/**
* Cuts each sample into the given number of parts, triggering progressive portions of each sample at each loop.
* @name striate
- * @tags transforms
+ * @tags temporal
* @memberof Pattern
* @returns Pattern
* @example
@@ -3404,7 +3404,7 @@ export const striate = register('striate', function (n, pat) {
/**
* Makes the sample fit the given number of cycles by changing the speed.
* @name loopAt
- * @tags transforms
+ * @tags temporal
* @memberof Pattern
* @returns Pattern
* @example
@@ -3423,7 +3423,7 @@ const _loopAt = function (factor, pat, cps = 0.5) {
* Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers.
* Instead of a number, it also accepts a list of numbers from 0 to 1 to slice at specific points.
* @name slice
- * @tags transforms
+ * @tags temporal
* @memberof Pattern
* @returns Pattern
* @example
@@ -3477,7 +3477,7 @@ Pattern.prototype.onTriggerTime = function (func) {
/**
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
* @name splice
- * @tags transforms
+ * @tags temporal
* @example
* samples('github:tidalcycles/dirt-samples')
* s("breaks165")
@@ -3515,7 +3515,7 @@ export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (facto
* Makes the sample fit its event duration. Good for rhythmical loops like drum breaks.
* Similar to `loopAt`.
* @name fit
- * @tags transforms
+ * @tags temporal
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes/2").fit()
@@ -3541,7 +3541,7 @@ export const fit = register('fit', (pat) =>
* given by a global clock and this function will be
* deprecated/removed.
* @name loopAtCps
- * @tags transforms
+ * @tags temporal
* @memberof Pattern
* @returns Pattern
* @example
diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs
index 2c3a6332..ce17fdea 100644
--- a/packages/core/signal.mjs
+++ b/packages/core/signal.mjs
@@ -300,7 +300,7 @@ const _rearrangeWith = (ipat, n, pat) => {
* 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
- * @tags transforms
+ * @tags temporal
* @example
* note("c d e f").sound("piano").shuffle(4)
* @example
@@ -314,7 +314,7 @@ export const shuffle = register('shuffle', (n, pat) => {
* 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
- * @tags transforms
+ * @tags temporal
* @example
* note("c d e f").sound("piano").scramble(4)
* @example
@@ -393,7 +393,7 @@ export const __chooseWith = (pat, xs) => {
/**
* Choose from the list of values (or patterns of values) using the given
* pattern of numbers, which should be in the range of 0..1
- * @tags transforms
+ * @tags temporal
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -407,7 +407,7 @@ export const chooseWith = (pat, xs) => {
/**
* As with {chooseWith}, but the structure comes from the chosen values, rather
* than the pattern you're using to choose with.
- * @tags transforms
+ * @tags temporal
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -418,7 +418,7 @@ export const chooseInWith = (pat, xs) => {
/**
* Chooses randomly from the given list of elements.
- * @tags transforms
+ * @tags temporal
* @param {...any} xs values / patterns to choose from.
* @returns {Pattern} - a continuous pattern.
* @example
@@ -434,7 +434,7 @@ 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
* the range 0 .. 1.
- * @tags transforms
+ * @tags temporal
* @param {...any} xs
* @returns {Pattern}
*/
@@ -445,7 +445,7 @@ Pattern.prototype.choose = function (...xs) {
/**
* As with choose, but the pattern that this method is called on should be
* in the range -1 .. 1
- * @tags transforms
+ * @tags temporal
* @param {...any} xs
* @returns {Pattern}
*/
@@ -455,7 +455,7 @@ Pattern.prototype.choose2 = function (...xs) {
/**
* Picks one of the elements at random each cycle.
- * @tags transforms
+ * @tags temporal
* @synonyms randcat
* @returns {Pattern}
* @example
@@ -498,7 +498,7 @@ const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
/**
* Chooses randomly from the given list of elements by giving a probability to each element
- * @tags transforms
+ * @tags temporal
* @param {...any} pairs arrays of value and weight
* @returns {Pattern} - a continuous pattern.
* @example
@@ -508,7 +508,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
/**
* Picks one of the elements at random each cycle by giving a probability to each element
- * @tags transforms
+ * @tags temporal
* @synonyms wrandcat
* @returns {Pattern}
* @example
@@ -590,7 +590,7 @@ export const degradeByWith = register(
* 0 = 0% chance of removal
* 1 = 100% chance of removal
*
- * @tags transforms
+ * @tags temporal
* @name degradeBy
* @memberof Pattern
* @param {number} amount - a number between 0 and 1
@@ -616,7 +616,7 @@ export const degradeBy = register(
*
* Randomly removes 50% of events from the pattern. Shorthand for `.degradeBy(0.5)`
*
- * @tags transforms
+ * @tags temporal
* @name degrade
* @memberof Pattern
* @returns Pattern
@@ -633,7 +633,7 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, t
* 1 = 0% chance of removal
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
*
- * @tags transforms
+ * @tags temporal
* @name undegradeBy
* @memberof Pattern
* @param {number} amount - a number between 0 and 1
@@ -662,7 +662,7 @@ export const undegradeBy = register(
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
* Events that would be removed by degrade are let through by undegrade and vice versa (see second example).
*
- * @tags transforms
+ * @tags temporal
* @name undegrade
* @memberof Pattern
* @returns Pattern
@@ -681,7 +681,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5), t
* Randomly applies the given function by the given probability.
* Similar to `someCyclesBy`
*
- * @tags transforms
+ * @tags temporal
* @name sometimesBy
* @memberof Pattern
* @param {number | Pattern} probability - a number between 0 and 1
@@ -701,7 +701,7 @@ export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
*
* Applies the given function with a 50% chance
*
- * @tags transforms
+ * @tags temporal
* @name sometimes
* @memberof Pattern
* @param {function} function - the transformation to apply
@@ -723,7 +723,7 @@ export const sometimes = register('sometimes', function (func, pat) {
* @param {number | Pattern} probability - a number between 0 and 1
* @param {function} function - the transformation to apply
* @returns Pattern
- * @tags transforms
+ * @tags temporal
* @example
* s("bd,hh*8").someCyclesBy(.3, x=>x.speed("0.5"))
*/
@@ -746,7 +746,7 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat)
* @name someCycles
* @memberof Pattern
* @returns Pattern
- * @tags transforms
+ * @tags temporal
* @example
* s("bd,hh*8").someCycles(x=>x.speed("0.5"))
*/
@@ -761,7 +761,7 @@ export const someCycles = register('someCycles', function (func, pat) {
* @name often
* @memberof Pattern
* @returns Pattern
- * @tags transforms
+ * @tags temporal
* @example
* s("hh*8").often(x=>x.speed("0.5"))
*/
@@ -776,7 +776,7 @@ export const often = register('often', function (func, pat) {
* @name rarely
* @memberof Pattern
* @returns Pattern
- * @tags transforms
+ * @tags temporal
* @example
* s("hh*8").rarely(x=>x.speed("0.5"))
*/
@@ -788,7 +788,7 @@ export const rarely = register('rarely', function (func, pat) {
*
* Shorthand for `.sometimesBy(0.1, fn)`
*
- * @tags transforms
+ * @tags temporal
* @name almostNever
* @memberof Pattern
* @returns Pattern
@@ -803,7 +803,7 @@ export const almostNever = register('almostNever', function (func, pat) {
*
* Shorthand for `.sometimesBy(0.9, fn)`
*
- * @tags transforms
+ * @tags temporal
* @name almostAlways
* @memberof Pattern
* @returns Pattern
@@ -818,7 +818,7 @@ export const almostAlways = register('almostAlways', function (func, pat) {
*
* Shorthand for `.sometimesBy(0, fn)` (never calls fn)
*
- * @tags transforms
+ * @tags temporal
* @name never
* @memberof Pattern
* @returns Pattern
@@ -833,7 +833,7 @@ export const never = register('never', function (_, pat) {
*
* Shorthand for `.sometimesBy(1, fn)` (always calls fn)
*
- * @tags transforms
+ * @tags temporal
* @name always
* @memberof Pattern
* @returns Pattern
From b952e4c735a9f77592824e5542a6e3e93e9530aa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sun, 19 Oct 2025 20:54:22 +0200
Subject: [PATCH 013/200] Add superdough tags
---
packages/core/controls.mjs | 132 ++++++++++++++++++-------------------
1 file changed, 66 insertions(+), 66 deletions(-)
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index db689e1a..2add9e41 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -351,7 +351,7 @@ export const { warpsync } = registerControl('warpsync');
* Define a custom webaudio node to use as a sound source.
*
* @name source
- * @tags external_io
+ * @tags external_io, superdough
* @synonyms src
* @param {function} getSource
* @synonyms src
@@ -411,7 +411,7 @@ export const { accelerate } = registerControl('accelerate');
* Sets the velocity from 0 to 1. Is multiplied together with gain.
*
* @name velocity
- * @tags effects
+ * @tags effects, superdough
* @example
* s("hh*8")
* .gain(".4!2 1 .4!2 1 .4 1")
@@ -422,7 +422,7 @@ export const { velocity } = registerControl('velocity');
* Controls the gain by an exponential amount.
*
* @name gain
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} amount gain.
* @example
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1").fast(2)
@@ -433,7 +433,7 @@ export const { gain } = registerControl('gain');
* Gain applied after all effects have been processed.
*
* @name postgain
- * @tags effects
+ * @tags effects, superdough
* @example
* s("bd sd [~ bd] sd,hh*8")
* .compressor("-20:20:10:.002:.02").postgain(1.5)
@@ -581,7 +581,7 @@ export const { fmvelocity } = registerControl('fmvelocity');
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
*
* @name bank
- * @tags samples
+ * @tags samples, superdough
* @param {string | Pattern} bank the name of the bank
* @example
* s("bd sd [~ bd] sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
@@ -635,7 +635,7 @@ export const { sustain, sus } = registerControl('sustain', 'sus');
* Amplitude envelope release time: The time it takes after the offset to go from sustain level to zero.
*
* @name release
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} time release time in seconds
* @synonyms rel
* @example
@@ -650,7 +650,7 @@ export const { hold } = registerControl('hold');
* can also optionally supply the 'bpq' parameter separated by ':'.
*
* @name bpf
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} frequency center frequency
* @synonyms bandf, bp
* @example
@@ -663,7 +663,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
* Sets the **b**and-**p**ass **q**-factor (resonance).
*
* @name bpq
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} q q factor
* @synonyms bandq
* @example
@@ -743,7 +743,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
* Bit crusher effect.
*
* @name crush
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
* @example
* s(",hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
@@ -755,7 +755,7 @@ export const { crush } = registerControl('crush');
* Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
*
* @name coarse
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
* @example
* s("bd sd [~ bd] sd,hh*8").coarse("<1 4 8 16 32>")
@@ -767,7 +767,7 @@ export const { coarse } = registerControl('coarse');
* Modulate the amplitude of a sound with a continuous waveform
*
* @name tremolo
- * @tags effects
+ * @tags effects, superdough
* @synonyms trem
* @param {number | Pattern} speed modulation speed in HZ
* @example
@@ -780,7 +780,7 @@ export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremolos
* Modulate the amplitude of a sound with a continuous waveform
*
* @name tremolosync
- * @tags effects
+ * @tags effects, superdough
* @synonyms tremsync
* @param {number | Pattern} cycles modulation speed in cycles
* @example
@@ -796,7 +796,7 @@ export const { tremolosync } = registerControl(
* Depth of amplitude modulation
*
* @name tremolodepth
- * @tags effects
+ * @tags effects, superdough
* @synonyms tremdepth
* @param {number | Pattern} depth
* @example
@@ -808,7 +808,7 @@ export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
* Alter the shape of the modulation waveform
*
* @name tremoloskew
- * @tags effects
+ * @tags effects, superdough
* @synonyms tremskew
* @param {number | Pattern} amount between 0 & 1, the shape of the waveform
* @example
@@ -821,7 +821,7 @@ export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
* Alter the phase of the modulation waveform
*
* @name tremolophase
- * @tags effects
+ * @tags effects, superdough
* @synonyms tremphase
* @param {number | Pattern} offset the offset in cycles of the modulation
* @example
@@ -834,7 +834,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
* Shape of amplitude modulation
*
* @name tremoloshape
- * @tags effects
+ * @tags effects, superdough
* @synonyms tremshape
* @param {number | Pattern} shape tri | square | sine | saw | ramp
* @example
@@ -846,7 +846,7 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
* Filter overdrive for supported filter types
*
* @name drive
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} amount
* @example
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
@@ -860,7 +860,7 @@ export const { drive } = registerControl('drive');
* Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")`
*
* @name duckorbit
- * @tags effects
+ * @tags effects, superdough
* @synonyms duck
* @param {number | Pattern} orbit target orbit
* @example
@@ -881,7 +881,7 @@ export const { duck } = registerControl('duckorbit', 'duck');
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckdepth
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} depth depth of modulation from 0 to 1
* @example
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>"))
@@ -901,7 +901,7 @@ export const { duckdepth } = registerControl('duckdepth');
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckonset
- * @tags effects
+ * @tags effects, superdough
* @synonyms duckons
*
* @param {number | Pattern} time The onset time in seconds
@@ -929,7 +929,7 @@ export const { duckonset } = registerControl('duckonset', 'duckons');
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckattack
- * @tags effects
+ * @tags effects, superdough
* @synonyms duckatt
*
* @param {number | Pattern} time The attack time in seconds
@@ -1028,7 +1028,7 @@ export const { pwsweep } = registerControl('pwsweep');
* Phaser audio effect that approximates popular guitar pedals.
*
* @name phaser
- * @tags effects
+ * @tags effects, superdough
* @synonyms ph
* @param {number | Pattern} speed speed of modulation
* @example
@@ -1046,7 +1046,7 @@ export const { phaserrate, ph, phaser } = registerControl(
* The frequency sweep range of the lfo for the phaser effect. Defaults to 2000
*
* @name phasersweep
- * @tags effects
+ * @tags effects, superdough
* @synonyms phs
* @param {number | Pattern} phasersweep most useful values are between 0 and 4000
* @example
@@ -1060,7 +1060,7 @@ export const { phasersweep, phs } = registerControl('phasersweep', 'phs');
* The center frequency of the phaser in HZ. Defaults to 1000
*
* @name phasercenter
- * @tags effects
+ * @tags effects, superdough
* @synonyms phc
* @param {number | Pattern} centerfrequency in HZ
* @example
@@ -1075,7 +1075,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
* The amount the signal is affected by the phaser effect. Defaults to 0.75
*
* @name phaserdepth
- * @tags effects
+ * @tags effects, superdough
* @synonyms phd, phasdp
* @param {number | Pattern} depth number between 0 and 1
* @example
@@ -1112,7 +1112,7 @@ export const { cut } = registerControl('cut');
* When using mininotation, you can also optionally add the 'lpq' parameter, separated by ':'.
*
* @name lpf
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms cutoff, ctf, lp
* @example
@@ -1126,7 +1126,7 @@ export const { cutoff, ctf, lpf, lp } = registerControl(['cutoff', 'resonance',
/**
* Sets the lowpass filter envelope modulation depth.
* @name lpenv
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} modulation depth of the lowpass filter envelope between 0 and _n_
* @synonyms lpe
* @example
@@ -1140,7 +1140,7 @@ export const { lpenv, lpe } = registerControl('lpenv', 'lpe');
/**
* Sets the highpass filter envelope modulation depth.
* @name hpenv
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} modulation depth of the highpass filter envelope between 0 and _n_
* @synonyms hpe
* @example
@@ -1154,7 +1154,7 @@ export const { hpenv, hpe } = registerControl('hpenv', 'hpe');
/**
* Sets the bandpass filter envelope modulation depth.
* @name bpenv
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} modulation depth of the bandpass filter envelope between 0 and _n_
* @synonyms bpe
* @example
@@ -1168,7 +1168,7 @@ export const { bpenv, bpe } = registerControl('bpenv', 'bpe');
/**
* Sets the attack duration for the lowpass filter envelope.
* @name lpattack
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} attack time of the filter envelope
* @synonyms lpa
* @example
@@ -1182,7 +1182,7 @@ export const { lpattack, lpa } = registerControl('lpattack', 'lpa');
/**
* Sets the attack duration for the highpass filter envelope.
* @name hpattack
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} attack time of the highpass filter envelope
* @synonyms hpa
* @example
@@ -1196,7 +1196,7 @@ export const { hpattack, hpa } = registerControl('hpattack', 'hpa');
/**
* Sets the attack duration for the bandpass filter envelope.
* @name bpattack
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} attack time of the bandpass filter envelope
* @synonyms bpa
* @example
@@ -1210,7 +1210,7 @@ export const { bpattack, bpa } = registerControl('bpattack', 'bpa');
/**
* Sets the decay duration for the lowpass filter envelope.
* @name lpdecay
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} decay time of the filter envelope
* @synonyms lpd
* @example
@@ -1224,7 +1224,7 @@ export const { lpdecay, lpd } = registerControl('lpdecay', 'lpd');
/**
* Sets the decay duration for the highpass filter envelope.
* @name hpdecay
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} decay time of the highpass filter envelope
* @synonyms hpd
* @example
@@ -1239,7 +1239,7 @@ export const { hpdecay, hpd } = registerControl('hpdecay', 'hpd');
/**
* Sets the decay duration for the bandpass filter envelope.
* @name bpdecay
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} decay time of the bandpass filter envelope
* @synonyms bpd
* @example
@@ -1254,7 +1254,7 @@ export const { bpdecay, bpd } = registerControl('bpdecay', 'bpd');
/**
* Sets the sustain amplitude for the lowpass filter envelope.
* @name lpsustain
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} sustain amplitude of the lowpass filter envelope
* @synonyms lps
* @example
@@ -1269,7 +1269,7 @@ export const { lpsustain, lps } = registerControl('lpsustain', 'lps');
/**
* Sets the sustain amplitude for the highpass filter envelope.
* @name hpsustain
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} sustain amplitude of the highpass filter envelope
* @synonyms hps
* @example
@@ -1284,7 +1284,7 @@ export const { hpsustain, hps } = registerControl('hpsustain', 'hps');
/**
* Sets the sustain amplitude for the bandpass filter envelope.
* @name bpsustain
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} sustain amplitude of the bandpass filter envelope
* @synonyms bps
* @example
@@ -1299,7 +1299,7 @@ export const { bpsustain, bps } = registerControl('bpsustain', 'bps');
/**
* Sets the release time for the lowpass filter envelope.
* @name lprelease
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} release time of the filter envelope
* @synonyms lpr
* @example
@@ -1315,7 +1315,7 @@ export const { lprelease, lpr } = registerControl('lprelease', 'lpr');
/**
* Sets the release time for the highpass filter envelope.
* @name hprelease
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} release time of the highpass filter envelope
* @synonyms hpr
* @example
@@ -1331,7 +1331,7 @@ export const { hprelease, hpr } = registerControl('hprelease', 'hpr');
/**
* Sets the release time for the bandpass filter envelope.
* @name bprelease
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} release time of the bandpass filter envelope
* @synonyms bpr
* @example
@@ -1363,7 +1363,7 @@ export const { ftype } = registerControl('ftype');
/**
* controls the center of the filter envelope. 0 is unipolar positive, .5 is bipolar, 1 is unipolar negative
* @name fanchor
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} center 0 to 1
* @example
* note("{f g g c d a a#}%8").s("sawtooth").lpf("{1000}%2")
@@ -1376,7 +1376,7 @@ export const { fanchor } = registerControl('fanchor');
* When using mininotation, you can also optionally add the 'hpq' parameter, separated by ':'.
*
* @name hpf
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms hp, hcutoff
* @example
@@ -1438,7 +1438,7 @@ export const { hcutoff, hpf, hp } = registerControl(['hcutoff', 'hresonance', 'h
* Controls the **h**igh-**p**ass **q**-value.
*
* @name hpq
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms hresonance
* @example
@@ -1450,7 +1450,7 @@ export const { hresonance, hpq } = registerControl('hresonance', 'hpq');
* Controls the **l**ow-**p**ass **q**-value.
*
* @name lpq
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms resonance
* @example
@@ -1463,7 +1463,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq');
* DJ filter, below 0.5 is low pass filter, above is high pass filter.
*
* @name djf
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
* @example
* n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>")
@@ -1480,7 +1480,7 @@ export const { djf } = registerControl('djf');
*
*
* @name delay
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd bd").delay("<0 .25 .5 1>")
@@ -1494,7 +1494,7 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
@@ -1508,7 +1508,7 @@ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback',
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
@@ -1533,7 +1533,7 @@ export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt',
* Sets the time of the delay effect in cycles.
*
* @name delaysync
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} cycles delay length in cycles
* @synonyms delayt, dt
* @example
@@ -1595,7 +1595,7 @@ export const { spread } = registerControl('spread');
* Set dryness of reverb. See `room` and `size` for more information about reverb.
*
* @name dry
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} dry 0 = wet, 1 = dry
* @example
* n("[0,3,7](3,8)").s("superpiano").room(.7).dry("<0 .5 .75 1>").osc()
@@ -1814,7 +1814,7 @@ export const { octave } = registerControl('octave');
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
*
* @name orbit
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} number
* @example
* stack(
@@ -1831,7 +1831,7 @@ export const { overshape } = registerControl('overshape');
* Sets position in stereo.
*
* @name pan
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)
* @example
* s("[bd hh]*2").pan("<.5 1 .5 0>")
@@ -1897,7 +1897,7 @@ export const { mode } = registerControl(['mode', 'anchor']);
* When using mininotation, you can also optionally add the 'size' parameter, separated by ':'.
*
* @name room
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd sd [~ bd] sd").room("<0 .2 .4 .6 .8 1>")
@@ -1911,7 +1911,7 @@ export const { room } = registerControl(['room', 'size']);
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomlp
- * @tags effects
+ * @tags effects, superdough
* @synonyms rlp
* @param {number} frequency between 0 and 20000hz
* @example
@@ -1925,7 +1925,7 @@ export const { roomlp, rlp } = registerControl('roomlp', 'rlp');
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomdim
- * @tags effects
+ * @tags effects, superdough
* @synonyms rdim
* @param {number} frequency between 0 and 20000hz
* @example
@@ -1940,7 +1940,7 @@ export const { roomdim, rdim } = registerControl('roomdim', 'rdim');
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomfade
- * @tags effects
+ * @tags effects, superdough
* @synonyms rfade
* @param {number} seconds for the reverb to fade
* @example
@@ -1953,7 +1953,7 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade');
/**
* Sets the sample to use as an impulse response for the reverb.
* @name iresponse
- * @tags effects
+ * @tags effects, superdough
* @param {string | Pattern} sample to use as an impulse response
* @synonyms ir
* @example
@@ -1965,7 +1965,7 @@ export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse');
/**
* Sets speed of the sample for the impulse response.
* @name irspeed
- * @tags effects
+ * @tags effects, superdough
* @param {string | Pattern} speed
* @example
* samples('github:switchangel/pad')
@@ -1977,7 +1977,7 @@ export const { irspeed } = registerControl('irspeed');
/**
* Sets the beginning of the IR response sample
* @name irbegin
- * @tags effects
+ * @tags effects, superdough
* @param {string | Pattern} begin between 0 and 1
* @synonyms ir
* @example
@@ -1991,7 +1991,7 @@ export const { irbegin } = registerControl('irbegin');
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomsize
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} size between 0 and 10
* @synonyms rsize, sz, size
* @example
@@ -2015,7 +2015,7 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size',
*
*
* @name shape
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} distortion between 0 and 1
* @example
* s("bd sd [~ bd] sd,hh*8").shape("<0 .2 .4 .6 .8>")
@@ -2028,7 +2028,7 @@ export const { shape } = registerControl(['shape', 'shapevol']);
* Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)
*
* @name distort
- * @tags effects
+ * @tags effects, superdough
* @synonyms dist
* @param {number | Pattern} distortion
* @example
@@ -2043,7 +2043,7 @@ export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dis
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
*
* @name compressor
- * @tags effects
+ * @tags effects, superdough
* @example
* s("bd sd [~ bd] sd,hh*8")
* .compressor("-20:20:10:.002:.02")
@@ -2129,7 +2129,7 @@ export const { squiz } = registerControl('squiz');
* Formant filter to make things sound like vowels.
*
* @name vowel
- * @tags effects
+ * @tags effects, superdough
* @param {string | Pattern} vowel You can use a e i o u ae aa oe ue y uh un en an on, corresponding to [a] [e] [i] [o] [u] [æ] [ɑ] [ø] [y] [ɯ] [ʌ] [œ̃] [ɛ̃] [ɑ̃] [ɔ̃]. Aliases: aa = å = ɑ, oe = ø = ö, y = ı, ae = æ.
* @example
* note("[c2 >]*2").s('sawtooth')
@@ -2154,7 +2154,7 @@ export const { waveloss } = registerControl('waveloss');
* Noise crackle density
*
* @name density
- * @tags effects
+ * @tags effects, superdough
* @param {number | Pattern} density between 0 and x
* @example
* s("crackle*4").density("<0.01 0.04 0.2 0.5>".slow(4))
From 96d0f2053e7fc0afbf515668945f3a4f3346fca7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sun, 19 Oct 2025 21:13:14 +0200
Subject: [PATCH 014/200] Add supradough controls
---
packages/core/controls.mjs | 96 +++++++++++++++++++-------------------
1 file changed, 48 insertions(+), 48 deletions(-)
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index 2add9e41..dda12767 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -411,7 +411,7 @@ export const { accelerate } = registerControl('accelerate');
* Sets the velocity from 0 to 1. Is multiplied together with gain.
*
* @name velocity
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @example
* s("hh*8")
* .gain(".4!2 1 .4!2 1 .4 1")
@@ -422,7 +422,7 @@ export const { velocity } = registerControl('velocity');
* Controls the gain by an exponential amount.
*
* @name gain
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} amount gain.
* @example
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1").fast(2)
@@ -433,7 +433,7 @@ export const { gain } = registerControl('gain');
* Gain applied after all effects have been processed.
*
* @name postgain
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @example
* s("bd sd [~ bd] sd,hh*8")
* .compressor("-20:20:10:.002:.02").postgain(1.5)
@@ -456,7 +456,7 @@ export const { amp } = registerControl('amp');
* Amplitude envelope attack time: Specifies how long it takes for the sound to reach its peak value, relative to the onset.
*
* @name attack
- * @tags effects
+ * @tags effects, superdough, supradough
* @param {number | Pattern} attack time in seconds.
* @synonyms att
* @example
@@ -472,7 +472,7 @@ export const { attack, att } = registerControl('attack', 'att');
* while decimal numbers and complex ratios sound metallic.
*
* @name fmh
- * @tags effects
+ * @tags effects, superdough, supradough
* @param {number | Pattern} harmonicity
* @example
* note("c e g b g e")
@@ -487,7 +487,7 @@ export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh');
* Controls the modulation index, which defines the brightness of the sound.
*
* @name fm
- * @tags effects
+ * @tags effects, superdough, supradough
* @param {number | Pattern} brightness modulation index
* @synonyms fmi
* @example
@@ -502,7 +502,7 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
* Ramp type of fm envelope. Exp might be a bit broken..
*
* @name fmenv
- * @tags effects
+ * @tags effects, superdough, supradough
* @param {number | Pattern} type lin | exp
* @example
* note("c e g b g e")
@@ -518,7 +518,7 @@ export const { fmenv } = registerControl('fmenv');
* Attack time for the FM envelope: time it takes to reach maximum modulation
*
* @name fmattack
- * @tags effects
+ * @tags effects, superdough, supradough
* @param {number | Pattern} time attack time
* @example
* note("c e g b g e")
@@ -533,7 +533,7 @@ export const { fmattack } = registerControl('fmattack');
* Waveform of the fm modulator
*
* @name fmwave
- * @tags effects
+ * @tags effects, superdough, supradough
* @param {number | Pattern} wave waveform
* @example
* n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("").fm(4).fmh(2.01)
@@ -547,7 +547,7 @@ export const { fmwave } = registerControl('fmwave');
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
*
* @name fmdecay
- * @tags effects
+ * @tags effects, superdough, supradough
* @param {number | Pattern} time decay time
* @example
* note("c e g b g e")
@@ -562,7 +562,7 @@ export const { fmdecay } = registerControl('fmdecay');
* Sustain level for the FM envelope: how much modulation is applied after the decay phase
*
* @name fmsustain
- * @tags effects
+ * @tags effects, superdough, supradough
* @param {number | Pattern} level sustain level
* @example
* note("c e g b g e")
@@ -611,7 +611,7 @@ export const { fft } = registerControl('fft');
* Note that the decay is only audible if the sustain value is lower than 1.
*
* @name decay
- * @tags effects
+ * @tags effects, superdough, supradough
* @param {number | Pattern} time decay time in seconds
* @synonyms dec
* @example
@@ -623,7 +623,7 @@ export const { decay, dec } = registerControl('decay', 'dec');
* Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset.
*
* @name sustain
- * @tags effects
+ * @tags effects, superdough, supradough
* @param {number | Pattern} gain sustain level between 0 and 1
* @synonyms sus
* @example
@@ -635,7 +635,7 @@ export const { sustain, sus } = registerControl('sustain', 'sus');
* Amplitude envelope release time: The time it takes after the offset to go from sustain level to zero.
*
* @name release
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} time release time in seconds
* @synonyms rel
* @example
@@ -650,7 +650,7 @@ export const { hold } = registerControl('hold');
* can also optionally supply the 'bpq' parameter separated by ':'.
*
* @name bpf
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} frequency center frequency
* @synonyms bandf, bp
* @example
@@ -663,7 +663,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
* Sets the **b**and-**p**ass **q**-factor (resonance).
*
* @name bpq
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} q q factor
* @synonyms bandq
* @example
@@ -743,7 +743,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
* Bit crusher effect.
*
* @name crush
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
* @example
* s(",hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
@@ -755,7 +755,7 @@ export const { crush } = registerControl('crush');
* Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
*
* @name coarse
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
* @example
* s("bd sd [~ bd] sd,hh*8").coarse("<1 4 8 16 32>")
@@ -1112,7 +1112,7 @@ export const { cut } = registerControl('cut');
* When using mininotation, you can also optionally add the 'lpq' parameter, separated by ':'.
*
* @name lpf
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms cutoff, ctf, lp
* @example
@@ -1126,7 +1126,7 @@ export const { cutoff, ctf, lpf, lp } = registerControl(['cutoff', 'resonance',
/**
* Sets the lowpass filter envelope modulation depth.
* @name lpenv
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} modulation depth of the lowpass filter envelope between 0 and _n_
* @synonyms lpe
* @example
@@ -1140,7 +1140,7 @@ export const { lpenv, lpe } = registerControl('lpenv', 'lpe');
/**
* Sets the highpass filter envelope modulation depth.
* @name hpenv
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} modulation depth of the highpass filter envelope between 0 and _n_
* @synonyms hpe
* @example
@@ -1154,7 +1154,7 @@ export const { hpenv, hpe } = registerControl('hpenv', 'hpe');
/**
* Sets the bandpass filter envelope modulation depth.
* @name bpenv
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} modulation depth of the bandpass filter envelope between 0 and _n_
* @synonyms bpe
* @example
@@ -1168,7 +1168,7 @@ export const { bpenv, bpe } = registerControl('bpenv', 'bpe');
/**
* Sets the attack duration for the lowpass filter envelope.
* @name lpattack
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} attack time of the filter envelope
* @synonyms lpa
* @example
@@ -1182,7 +1182,7 @@ export const { lpattack, lpa } = registerControl('lpattack', 'lpa');
/**
* Sets the attack duration for the highpass filter envelope.
* @name hpattack
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} attack time of the highpass filter envelope
* @synonyms hpa
* @example
@@ -1196,7 +1196,7 @@ export const { hpattack, hpa } = registerControl('hpattack', 'hpa');
/**
* Sets the attack duration for the bandpass filter envelope.
* @name bpattack
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} attack time of the bandpass filter envelope
* @synonyms bpa
* @example
@@ -1210,7 +1210,7 @@ export const { bpattack, bpa } = registerControl('bpattack', 'bpa');
/**
* Sets the decay duration for the lowpass filter envelope.
* @name lpdecay
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} decay time of the filter envelope
* @synonyms lpd
* @example
@@ -1224,7 +1224,7 @@ export const { lpdecay, lpd } = registerControl('lpdecay', 'lpd');
/**
* Sets the decay duration for the highpass filter envelope.
* @name hpdecay
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} decay time of the highpass filter envelope
* @synonyms hpd
* @example
@@ -1239,7 +1239,7 @@ export const { hpdecay, hpd } = registerControl('hpdecay', 'hpd');
/**
* Sets the decay duration for the bandpass filter envelope.
* @name bpdecay
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} decay time of the bandpass filter envelope
* @synonyms bpd
* @example
@@ -1254,7 +1254,7 @@ export const { bpdecay, bpd } = registerControl('bpdecay', 'bpd');
/**
* Sets the sustain amplitude for the lowpass filter envelope.
* @name lpsustain
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} sustain amplitude of the lowpass filter envelope
* @synonyms lps
* @example
@@ -1269,7 +1269,7 @@ export const { lpsustain, lps } = registerControl('lpsustain', 'lps');
/**
* Sets the sustain amplitude for the highpass filter envelope.
* @name hpsustain
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} sustain amplitude of the highpass filter envelope
* @synonyms hps
* @example
@@ -1284,7 +1284,7 @@ export const { hpsustain, hps } = registerControl('hpsustain', 'hps');
/**
* Sets the sustain amplitude for the bandpass filter envelope.
* @name bpsustain
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} sustain amplitude of the bandpass filter envelope
* @synonyms bps
* @example
@@ -1299,7 +1299,7 @@ export const { bpsustain, bps } = registerControl('bpsustain', 'bps');
/**
* Sets the release time for the lowpass filter envelope.
* @name lprelease
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} release time of the filter envelope
* @synonyms lpr
* @example
@@ -1315,7 +1315,7 @@ export const { lprelease, lpr } = registerControl('lprelease', 'lpr');
/**
* Sets the release time for the highpass filter envelope.
* @name hprelease
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} release time of the highpass filter envelope
* @synonyms hpr
* @example
@@ -1331,7 +1331,7 @@ export const { hprelease, hpr } = registerControl('hprelease', 'hpr');
/**
* Sets the release time for the bandpass filter envelope.
* @name bprelease
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} release time of the bandpass filter envelope
* @synonyms bpr
* @example
@@ -1376,7 +1376,7 @@ export const { fanchor } = registerControl('fanchor');
* When using mininotation, you can also optionally add the 'hpq' parameter, separated by ':'.
*
* @name hpf
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms hp, hcutoff
* @example
@@ -1391,7 +1391,7 @@ export const { fanchor } = registerControl('fanchor');
* Applies a vibrato to the frequency of the oscillator.
*
* @name vib
- * @tags effects
+ * @tags effects, superdough, supradough
* @synonyms vibrato, v
* @param {number | Pattern} frequency of the vibrato in hertz
* @example
@@ -1419,7 +1419,7 @@ export const { noise } = registerControl('noise');
* Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set
*
* @name vibmod
- * @tags effects
+ * @tags effects, superdough, supradough
* @synonyms vmod
* @param {number | Pattern} depth of vibrato (in semitones)
* @example
@@ -1438,7 +1438,7 @@ export const { hcutoff, hpf, hp } = registerControl(['hcutoff', 'hresonance', 'h
* Controls the **h**igh-**p**ass **q**-value.
*
* @name hpq
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms hresonance
* @example
@@ -1450,7 +1450,7 @@ export const { hresonance, hpq } = registerControl('hresonance', 'hpq');
* Controls the **l**ow-**p**ass **q**-value.
*
* @name lpq
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms resonance
* @example
@@ -1480,7 +1480,7 @@ export const { djf } = registerControl('djf');
*
*
* @name delay
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd bd").delay("<0 .25 .5 1>")
@@ -1494,7 +1494,7 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
@@ -1508,7 +1508,7 @@ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback',
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
@@ -1635,7 +1635,7 @@ export const { freq } = registerControl('freq');
* Attack time of pitch envelope.
*
* @name pattack
- * @tags effects
+ * @tags effects, superdough, supradough
* @synonyms patt
* @param {number | Pattern} time time in seconds
* @example
@@ -1647,7 +1647,7 @@ export const { pattack, patt } = registerControl('pattack', 'patt');
* Decay time of pitch envelope.
*
* @name pdecay
- * @tags effects
+ * @tags effects, superdough, supradough
* @synonyms pdec
* @param {number | Pattern} time time in seconds
* @example
@@ -1661,7 +1661,7 @@ export const { psustain, psus } = registerControl('psustain', 'psus');
* Release time of pitch envelope
*
* @name prelease
- * @tags effects
+ * @tags effects, superdough, supradough
* @synonyms prel
* @param {number | Pattern} time time in seconds
* @example
@@ -1676,7 +1676,7 @@ export const { prelease, prel } = registerControl('prelease', 'prel');
* If you don't set other pitch envelope controls, `pattack:.2` will be the default.
*
* @name penv
- * @tags effects
+ * @tags effects, superdough, supradough
* @param {number | Pattern} semitones change in semitones
* @example
* note("c")
@@ -1831,7 +1831,7 @@ export const { overshape } = registerControl('overshape');
* Sets position in stereo.
*
* @name pan
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)
* @example
* s("[bd hh]*2").pan("<.5 1 .5 0>")
@@ -2028,7 +2028,7 @@ export const { shape } = registerControl(['shape', 'shapevol']);
* Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)
*
* @name distort
- * @tags effects, superdough
+ * @tags effects, superdough, supradough
* @synonyms dist
* @param {number | Pattern} distortion
* @example
From 69360b3ec8731e75045f44b9f08840739fbc7e61 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sun, 19 Oct 2025 21:17:25 +0200
Subject: [PATCH 015/200] Also tag "noise"
---
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 dda12767..c6ff8d79 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -1409,7 +1409,7 @@ export const { vib, vibrato, v } = registerControl(['vib', 'vibmod'], 'vibrato',
* Adds pink noise to the mix
*
* @name noise
- * @tags generators
+ * @tags generators, superdough, supradough
* @param {number | Pattern} wet wet amount
* @example
* sound("/2")
From 2483c673a4ae08ac33fc60eb2155fdcd4ba091cc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sun, 19 Oct 2025 21:19:42 +0200
Subject: [PATCH 016/200] Effects -> fx, untag "crossfade"
---
packages/core/controls.mjs | 262 +++++++++++++++---------------
packages/superdough/wavetable.mjs | 2 +-
packages/supradough/dough.mjs | 2 +-
3 files changed, 133 insertions(+), 133 deletions(-)
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index c6ff8d79..1bfd5a89 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -92,7 +92,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
* Position in the wavetable of the wavetable oscillator
*
* @name wt
- * @tags effects
+ * @tags fx
* @param {number | Pattern} position Position in the wavetable from 0 to 1
* @synonyms wavetablePosition
* @example
@@ -104,7 +104,7 @@ export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePositio
* Amount of envelope applied wavetable oscillator's position envelope
*
* @name wtenv
- * @tags effects
+ * @tags fx
* @param {number | Pattern} amount between 0 and 1
*/
export const { wtenv } = registerControl('wtenv');
@@ -112,7 +112,7 @@ export const { wtenv } = registerControl('wtenv');
* Attack time of the wavetable oscillator's position envelope
*
* @name wtattack
- * @tags effects
+ * @tags fx
* @synonyms wtatt
* @param {number | Pattern} time attack time in seconds
*/
@@ -122,7 +122,7 @@ export const { wtattack, wtatt } = registerControl('wtattack', 'wtatt');
* Decay time of the wavetable oscillator's position envelope
*
* @name wtdecay
- * @tags effects
+ * @tags fx
* @synonyms wtdec
* @param {number | Pattern} time decay time in seconds
*/
@@ -132,7 +132,7 @@ export const { wtdecay, wtdec } = registerControl('wtdecay', 'wtdec');
* Sustain time of the wavetable oscillator's position envelope
*
* @name wtsustain
- * @tags effects
+ * @tags fx
* @synonyms wtsus
* @param {number | Pattern} gain sustain level (0 to 1)
*/
@@ -142,7 +142,7 @@ export const { wtsustain, wtsus } = registerControl('wtsustain', 'wtsus');
* Release time of the wavetable oscillator's position envelope
*
* @name wtrelease
- * @tags effects
+ * @tags fx
* @synonyms wtrel
* @param {number | Pattern} time release time in seconds
*/
@@ -152,7 +152,7 @@ export const { wtrelease, wtrel } = registerControl('wtrelease', 'wtrel');
* Rate of the LFO for the wavetable oscillator's position
*
* @name wtrate
- * @tags effects
+ * @tags fx
* @param {number | Pattern} rate rate in hertz
*/
export const { wtrate } = registerControl('wtrate');
@@ -160,7 +160,7 @@ export const { wtrate } = registerControl('wtrate');
* cycle synced rate of the LFO for the wavetable oscillator's position
*
* @name wtsync
- * @tags effects
+ * @tags fx
* @param {number | Pattern} rate rate in cycles
*/
export const { wtsync } = registerControl('wtsync');
@@ -169,7 +169,7 @@ export const { wtsync } = registerControl('wtsync');
* Depth of the LFO for the wavetable oscillator's position
*
* @name wtdepth
- * @tags effects
+ * @tags fx
* @param {number | Pattern} depth depth of modulation
*/
export const { wtdepth } = registerControl('wtdepth');
@@ -178,7 +178,7 @@ export const { wtdepth } = registerControl('wtdepth');
* Shape of the LFO for the wavetable oscillator's position
*
* @name wtshape
- * @tags effects
+ * @tags fx
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { wtshape } = registerControl('wtshape');
@@ -187,7 +187,7 @@ export const { wtshape } = registerControl('wtshape');
* DC offset of the LFO for the wavetable oscillator's position
*
* @name wtdc
- * @tags effects
+ * @tags fx
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { wtdc } = registerControl('wtdc');
@@ -196,7 +196,7 @@ export const { wtdc } = registerControl('wtdc');
* Skew of the LFO for the wavetable oscillator's position
*
* @name wtskew
- * @tags effects
+ * @tags fx
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { wtskew } = registerControl('wtskew');
@@ -205,7 +205,7 @@ export const { wtskew } = registerControl('wtskew');
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator
*
* @name warp
- * @tags effects
+ * @tags fx
* @param {number | Pattern} amount Warp of the wavetable from 0 to 1
* @synonyms wavetableWarp
* @example
@@ -218,7 +218,7 @@ export const { warp, wavetableWarp } = registerControl('warp', 'wavetableWarp');
* Attack time of the wavetable oscillator's warp envelope
*
* @name warpattack
- * @tags effects
+ * @tags fx
* @synonyms warpatt
* @param {number | Pattern} time attack time in seconds
*/
@@ -228,7 +228,7 @@ export const { warpattack, warpatt } = registerControl('warpattack', 'warpatt');
* Decay time of the wavetable oscillator's warp envelope
*
* @name warpdecay
- * @tags effects
+ * @tags fx
* @synonyms warpdec
* @param {number | Pattern} time decay time in seconds
*/
@@ -238,7 +238,7 @@ export const { warpdecay, warpdec } = registerControl('warpdecay', 'warpdec');
* Sustain time of the wavetable oscillator's warp envelope
*
* @name warpsustain
- * @tags effects
+ * @tags fx
* @synonyms warpsus
* @param {number | Pattern} gain sustain level (0 to 1)
*/
@@ -248,7 +248,7 @@ export const { warpsustain, warpsus } = registerControl('warpsustain', 'warpsus'
* Release time of the wavetable oscillator's warp envelope
*
* @name warprelease
- * @tags effects
+ * @tags fx
* @synonyms warprel
* @param {number | Pattern} time release time in seconds
*/
@@ -258,7 +258,7 @@ export const { warprelease, warprel } = registerControl('warprelease', 'warprel'
* Rate of the LFO for the wavetable oscillator's warp
*
* @name warprate
- * @tags effects
+ * @tags fx
* @param {number | Pattern} rate rate in hertz
*/
export const { warprate } = registerControl('warprate');
@@ -267,7 +267,7 @@ export const { warprate } = registerControl('warprate');
* Depth of the LFO for the wavetable oscillator's warp
*
* @name warpdepth
- * @tags effects
+ * @tags fx
* @param {number | Pattern} depth depth of modulation
*/
export const { warpdepth } = registerControl('warpdepth');
@@ -276,7 +276,7 @@ export const { warpdepth } = registerControl('warpdepth');
* Shape of the LFO for the wavetable oscillator's warp
*
* @name warpshape
- * @tags effects
+ * @tags fx
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { warpshape } = registerControl('warpshape');
@@ -285,7 +285,7 @@ export const { warpshape } = registerControl('warpshape');
* DC offset of the LFO for the wavetable oscillator's warp
*
* @name warpdc
- * @tags effects
+ * @tags fx
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { warpdc } = registerControl('warpdc');
@@ -294,7 +294,7 @@ export const { warpdc } = registerControl('warpdc');
* Skew of the LFO for the wavetable oscillator's warp
*
* @name warpskew
- * @tags effects
+ * @tags fx
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { warpskew } = registerControl('warpskew');
@@ -306,7 +306,7 @@ export const { warpskew } = registerControl('warpskew');
* spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip
*
* @name warpmode
- * @tags effects
+ * @tags fx
* @param {number | string | Pattern} mode Warp mode
* @synonyms wavetableWarpMode
* @example
@@ -320,7 +320,7 @@ export const { warpmode, wavetableWarpMode } = registerControl('warpmode', 'wave
* Amount of randomness of the initial phase of the wavetable oscillator.
*
* @name wtphaserand
- * @tags effects
+ * @tags fx
* @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random)
* @synonyms wavetablePhaseRand
* @example
@@ -333,7 +333,7 @@ export const { wtphaserand, wavetablePhaseRand } = registerControl('wtphaserand'
* Amount of envelope applied wavetable oscillator's position envelope
*
* @name warpenv
- * @tags effects
+ * @tags fx
* @param {number | Pattern} amount between 0 and 1
*/
export const { warpenv } = registerControl('warpenv');
@@ -342,7 +342,7 @@ export const { warpenv } = registerControl('warpenv');
* cycle synced rate of the LFO for the wavetable warp position
*
* @name warpsync
- * @tags effects
+ * @tags fx
* @param {number | Pattern} rate rate in cycles
*/
export const { warpsync } = registerControl('warpsync');
@@ -411,7 +411,7 @@ export const { accelerate } = registerControl('accelerate');
* Sets the velocity from 0 to 1. Is multiplied together with gain.
*
* @name velocity
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @example
* s("hh*8")
* .gain(".4!2 1 .4!2 1 .4 1")
@@ -422,7 +422,7 @@ export const { velocity } = registerControl('velocity');
* Controls the gain by an exponential amount.
*
* @name gain
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} amount gain.
* @example
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1").fast(2)
@@ -433,7 +433,7 @@ export const { gain } = registerControl('gain');
* Gain applied after all effects have been processed.
*
* @name postgain
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @example
* s("bd sd [~ bd] sd,hh*8")
* .compressor("-20:20:10:.002:.02").postgain(1.5)
@@ -444,7 +444,7 @@ export const { postgain } = registerControl('postgain');
* Like `gain`, but linear.
*
* @name amp
- * @tags effects
+ * @tags fx
* @param {number | Pattern} amount gain.
* @superdirtOnly
* @example
@@ -456,7 +456,7 @@ export const { amp } = registerControl('amp');
* Amplitude envelope attack time: Specifies how long it takes for the sound to reach its peak value, relative to the onset.
*
* @name attack
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} attack time in seconds.
* @synonyms att
* @example
@@ -472,7 +472,7 @@ export const { attack, att } = registerControl('attack', 'att');
* while decimal numbers and complex ratios sound metallic.
*
* @name fmh
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} harmonicity
* @example
* note("c e g b g e")
@@ -487,7 +487,7 @@ export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh');
* Controls the modulation index, which defines the brightness of the sound.
*
* @name fm
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} brightness modulation index
* @synonyms fmi
* @example
@@ -502,7 +502,7 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
* Ramp type of fm envelope. Exp might be a bit broken..
*
* @name fmenv
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} type lin | exp
* @example
* note("c e g b g e")
@@ -518,7 +518,7 @@ export const { fmenv } = registerControl('fmenv');
* Attack time for the FM envelope: time it takes to reach maximum modulation
*
* @name fmattack
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} time attack time
* @example
* note("c e g b g e")
@@ -533,7 +533,7 @@ export const { fmattack } = registerControl('fmattack');
* Waveform of the fm modulator
*
* @name fmwave
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} wave waveform
* @example
* n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("").fm(4).fmh(2.01)
@@ -547,7 +547,7 @@ export const { fmwave } = registerControl('fmwave');
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
*
* @name fmdecay
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} time decay time
* @example
* note("c e g b g e")
@@ -562,7 +562,7 @@ export const { fmdecay } = registerControl('fmdecay');
* Sustain level for the FM envelope: how much modulation is applied after the decay phase
*
* @name fmsustain
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} level sustain level
* @example
* note("c e g b g e")
@@ -593,7 +593,7 @@ export const { bank } = registerControl('bank');
* mix control for the chorus effect
*
* @name chorus
- * @tags effects
+ * @tags fx
* @param {string | Pattern} chorus mix amount between 0 and 1
* @example
* note("d d a# a").s("sawtooth").chorus(.5)
@@ -611,7 +611,7 @@ export const { fft } = registerControl('fft');
* Note that the decay is only audible if the sustain value is lower than 1.
*
* @name decay
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} time decay time in seconds
* @synonyms dec
* @example
@@ -623,7 +623,7 @@ export const { decay, dec } = registerControl('decay', 'dec');
* Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset.
*
* @name sustain
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} gain sustain level between 0 and 1
* @synonyms sus
* @example
@@ -635,7 +635,7 @@ export const { sustain, sus } = registerControl('sustain', 'sus');
* Amplitude envelope release time: The time it takes after the offset to go from sustain level to zero.
*
* @name release
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} time release time in seconds
* @synonyms rel
* @example
@@ -650,7 +650,7 @@ export const { hold } = registerControl('hold');
* can also optionally supply the 'bpq' parameter separated by ':'.
*
* @name bpf
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} frequency center frequency
* @synonyms bandf, bp
* @example
@@ -663,7 +663,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
* Sets the **b**and-**p**ass **q**-factor (resonance).
*
* @name bpq
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} q q factor
* @synonyms bandq
* @example
@@ -743,7 +743,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
* Bit crusher effect.
*
* @name crush
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
* @example
* s(",hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
@@ -755,7 +755,7 @@ export const { crush } = registerControl('crush');
* Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
*
* @name coarse
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
* @example
* s("bd sd [~ bd] sd,hh*8").coarse("<1 4 8 16 32>")
@@ -767,7 +767,7 @@ export const { coarse } = registerControl('coarse');
* Modulate the amplitude of a sound with a continuous waveform
*
* @name tremolo
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms trem
* @param {number | Pattern} speed modulation speed in HZ
* @example
@@ -780,7 +780,7 @@ export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremolos
* Modulate the amplitude of a sound with a continuous waveform
*
* @name tremolosync
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms tremsync
* @param {number | Pattern} cycles modulation speed in cycles
* @example
@@ -796,7 +796,7 @@ export const { tremolosync } = registerControl(
* Depth of amplitude modulation
*
* @name tremolodepth
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms tremdepth
* @param {number | Pattern} depth
* @example
@@ -808,7 +808,7 @@ export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
* Alter the shape of the modulation waveform
*
* @name tremoloskew
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms tremskew
* @param {number | Pattern} amount between 0 & 1, the shape of the waveform
* @example
@@ -821,7 +821,7 @@ export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
* Alter the phase of the modulation waveform
*
* @name tremolophase
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms tremphase
* @param {number | Pattern} offset the offset in cycles of the modulation
* @example
@@ -834,7 +834,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
* Shape of amplitude modulation
*
* @name tremoloshape
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms tremshape
* @param {number | Pattern} shape tri | square | sine | saw | ramp
* @example
@@ -846,7 +846,7 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
* Filter overdrive for supported filter types
*
* @name drive
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {number | Pattern} amount
* @example
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
@@ -860,7 +860,7 @@ export const { drive } = registerControl('drive');
* Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")`
*
* @name duckorbit
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms duck
* @param {number | Pattern} orbit target orbit
* @example
@@ -881,7 +881,7 @@ export const { duck } = registerControl('duckorbit', 'duck');
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckdepth
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {number | Pattern} depth depth of modulation from 0 to 1
* @example
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>"))
@@ -901,7 +901,7 @@ export const { duckdepth } = registerControl('duckdepth');
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckonset
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms duckons
*
* @param {number | Pattern} time The onset time in seconds
@@ -929,7 +929,7 @@ export const { duckonset } = registerControl('duckonset', 'duckons');
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckattack
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms duckatt
*
* @param {number | Pattern} time The attack time in seconds
@@ -949,7 +949,7 @@ export const { duckattack } = registerControl('duckattack', 'duckatt');
*
* @name byteBeatExpression
* @synonyms bbexpr
- * @tags effects
+ * @tags fx
*
* @param {number | Pattern} byteBeatExpression bitwise expression for creating bytebeat
* @example
@@ -963,7 +963,7 @@ export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpressio
*
* @name byteBeatStartTime
* @synonyms bbst
- * @tags effects
+ * @tags fx
*
* @param {number | Pattern} byteBeatStartTime in samples (t)
* @example
@@ -990,7 +990,7 @@ export const { channels, ch } = registerControl('channels', 'ch');
* Controls the pulsewidth of the pulse oscillator
*
* @name pw
- * @tags effects
+ * @tags fx
* @param {number | Pattern} pulsewidth
* @example
* note("{f a c e}%16").s("pulse").pw(".8:1:.2")
@@ -1003,7 +1003,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
* Controls the lfo rate for the pulsewidth of the pulse oscillator
*
* @name pwrate
- * @tags effects
+ * @tags fx
* @param {number | Pattern} rate
* @example
* n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>")
@@ -1016,7 +1016,7 @@ export const { pwrate } = registerControl('pwrate');
* Controls the lfo sweep for the pulsewidth of the pulse oscillator
*
* @name pwsweep
- * @tags effects
+ * @tags fx
* @param {number | Pattern} sweep
* @example
* n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>")
@@ -1028,7 +1028,7 @@ export const { pwsweep } = registerControl('pwsweep');
* Phaser audio effect that approximates popular guitar pedals.
*
* @name phaser
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms ph
* @param {number | Pattern} speed speed of modulation
* @example
@@ -1046,7 +1046,7 @@ export const { phaserrate, ph, phaser } = registerControl(
* The frequency sweep range of the lfo for the phaser effect. Defaults to 2000
*
* @name phasersweep
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms phs
* @param {number | Pattern} phasersweep most useful values are between 0 and 4000
* @example
@@ -1060,7 +1060,7 @@ export const { phasersweep, phs } = registerControl('phasersweep', 'phs');
* The center frequency of the phaser in HZ. Defaults to 1000
*
* @name phasercenter
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms phc
* @param {number | Pattern} centerfrequency in HZ
* @example
@@ -1075,7 +1075,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
* The amount the signal is affected by the phaser effect. Defaults to 0.75
*
* @name phaserdepth
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms phd, phasdp
* @param {number | Pattern} depth number between 0 and 1
* @example
@@ -1090,7 +1090,7 @@ export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd'
* Choose the channel the pattern is sent to in superdirt
*
* @name channel
- * @tags effects
+ * @tags fx
* @param {number | Pattern} channel channel number
*
*/
@@ -1099,7 +1099,7 @@ export const { channel } = registerControl('channel');
* In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.
*
* @name cut
- * @tags effects
+ * @tags fx
* @param {number | Pattern} group cut group number
* @example
* s("[oh hh]*4").cut(1)
@@ -1112,7 +1112,7 @@ export const { cut } = registerControl('cut');
* When using mininotation, you can also optionally add the 'lpq' parameter, separated by ':'.
*
* @name lpf
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms cutoff, ctf, lp
* @example
@@ -1126,7 +1126,7 @@ export const { cutoff, ctf, lpf, lp } = registerControl(['cutoff', 'resonance',
/**
* Sets the lowpass filter envelope modulation depth.
* @name lpenv
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} modulation depth of the lowpass filter envelope between 0 and _n_
* @synonyms lpe
* @example
@@ -1140,7 +1140,7 @@ export const { lpenv, lpe } = registerControl('lpenv', 'lpe');
/**
* Sets the highpass filter envelope modulation depth.
* @name hpenv
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} modulation depth of the highpass filter envelope between 0 and _n_
* @synonyms hpe
* @example
@@ -1154,7 +1154,7 @@ export const { hpenv, hpe } = registerControl('hpenv', 'hpe');
/**
* Sets the bandpass filter envelope modulation depth.
* @name bpenv
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} modulation depth of the bandpass filter envelope between 0 and _n_
* @synonyms bpe
* @example
@@ -1168,7 +1168,7 @@ export const { bpenv, bpe } = registerControl('bpenv', 'bpe');
/**
* Sets the attack duration for the lowpass filter envelope.
* @name lpattack
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} attack time of the filter envelope
* @synonyms lpa
* @example
@@ -1182,7 +1182,7 @@ export const { lpattack, lpa } = registerControl('lpattack', 'lpa');
/**
* Sets the attack duration for the highpass filter envelope.
* @name hpattack
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} attack time of the highpass filter envelope
* @synonyms hpa
* @example
@@ -1196,7 +1196,7 @@ export const { hpattack, hpa } = registerControl('hpattack', 'hpa');
/**
* Sets the attack duration for the bandpass filter envelope.
* @name bpattack
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} attack time of the bandpass filter envelope
* @synonyms bpa
* @example
@@ -1210,7 +1210,7 @@ export const { bpattack, bpa } = registerControl('bpattack', 'bpa');
/**
* Sets the decay duration for the lowpass filter envelope.
* @name lpdecay
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} decay time of the filter envelope
* @synonyms lpd
* @example
@@ -1224,7 +1224,7 @@ export const { lpdecay, lpd } = registerControl('lpdecay', 'lpd');
/**
* Sets the decay duration for the highpass filter envelope.
* @name hpdecay
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} decay time of the highpass filter envelope
* @synonyms hpd
* @example
@@ -1239,7 +1239,7 @@ export const { hpdecay, hpd } = registerControl('hpdecay', 'hpd');
/**
* Sets the decay duration for the bandpass filter envelope.
* @name bpdecay
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} decay time of the bandpass filter envelope
* @synonyms bpd
* @example
@@ -1254,7 +1254,7 @@ export const { bpdecay, bpd } = registerControl('bpdecay', 'bpd');
/**
* Sets the sustain amplitude for the lowpass filter envelope.
* @name lpsustain
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} sustain amplitude of the lowpass filter envelope
* @synonyms lps
* @example
@@ -1269,7 +1269,7 @@ export const { lpsustain, lps } = registerControl('lpsustain', 'lps');
/**
* Sets the sustain amplitude for the highpass filter envelope.
* @name hpsustain
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} sustain amplitude of the highpass filter envelope
* @synonyms hps
* @example
@@ -1284,7 +1284,7 @@ export const { hpsustain, hps } = registerControl('hpsustain', 'hps');
/**
* Sets the sustain amplitude for the bandpass filter envelope.
* @name bpsustain
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} sustain amplitude of the bandpass filter envelope
* @synonyms bps
* @example
@@ -1299,7 +1299,7 @@ export const { bpsustain, bps } = registerControl('bpsustain', 'bps');
/**
* Sets the release time for the lowpass filter envelope.
* @name lprelease
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} release time of the filter envelope
* @synonyms lpr
* @example
@@ -1315,7 +1315,7 @@ export const { lprelease, lpr } = registerControl('lprelease', 'lpr');
/**
* Sets the release time for the highpass filter envelope.
* @name hprelease
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} release time of the highpass filter envelope
* @synonyms hpr
* @example
@@ -1331,7 +1331,7 @@ export const { hprelease, hpr } = registerControl('hprelease', 'hpr');
/**
* Sets the release time for the bandpass filter envelope.
* @name bprelease
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} release time of the bandpass filter envelope
* @synonyms bpr
* @example
@@ -1347,7 +1347,7 @@ export const { bprelease, bpr } = registerControl('bprelease', 'bpr');
/**
* Sets the filter type. The ladder filter is more aggressive. More types might be added in the future.
* @name ftype
- * @tags effects
+ * @tags fx
* @param {number | Pattern} type 12db (0), ladder (1), or 24db (2)
* @example
* note("{f g g c d a a#}%8").s("sawtooth").lpenv(4).lpf(500).ftype("<0 1 2>").lpq(1)
@@ -1363,7 +1363,7 @@ export const { ftype } = registerControl('ftype');
/**
* controls the center of the filter envelope. 0 is unipolar positive, .5 is bipolar, 1 is unipolar negative
* @name fanchor
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {number | Pattern} center 0 to 1
* @example
* note("{f g g c d a a#}%8").s("sawtooth").lpf("{1000}%2")
@@ -1376,7 +1376,7 @@ export const { fanchor } = registerControl('fanchor');
* When using mininotation, you can also optionally add the 'hpq' parameter, separated by ':'.
*
* @name hpf
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms hp, hcutoff
* @example
@@ -1391,7 +1391,7 @@ export const { fanchor } = registerControl('fanchor');
* Applies a vibrato to the frequency of the oscillator.
*
* @name vib
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @synonyms vibrato, v
* @param {number | Pattern} frequency of the vibrato in hertz
* @example
@@ -1419,7 +1419,7 @@ export const { noise } = registerControl('noise');
* Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set
*
* @name vibmod
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @synonyms vmod
* @param {number | Pattern} depth of vibrato (in semitones)
* @example
@@ -1438,7 +1438,7 @@ export const { hcutoff, hpf, hp } = registerControl(['hcutoff', 'hresonance', 'h
* Controls the **h**igh-**p**ass **q**-value.
*
* @name hpq
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms hresonance
* @example
@@ -1450,7 +1450,7 @@ export const { hresonance, hpq } = registerControl('hresonance', 'hpq');
* Controls the **l**ow-**p**ass **q**-value.
*
* @name lpq
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms resonance
* @example
@@ -1463,7 +1463,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq');
* DJ filter, below 0.5 is low pass filter, above is high pass filter.
*
* @name djf
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
* @example
* n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>")
@@ -1480,7 +1480,7 @@ export const { djf } = registerControl('djf');
*
*
* @name delay
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd bd").delay("<0 .25 .5 1>")
@@ -1494,7 +1494,7 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
@@ -1508,7 +1508,7 @@ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback',
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
@@ -1520,7 +1520,7 @@ export const { delayspeed } = registerControl('delayspeed');
* Sets the time of the delay effect.
*
* @name delayspeed
- * @tags effects
+ * @tags fx
* @param {number | Pattern} delayspeed controls the pitch of the delay feedback
* @synonyms delayt, dt
* @example
@@ -1533,7 +1533,7 @@ export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt',
* Sets the time of the delay effect in cycles.
*
* @name delaysync
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {number | Pattern} cycles delay length in cycles
* @synonyms delayt, dt
* @example
@@ -1546,7 +1546,7 @@ export const { delaysync } = registerControl('delaysync');
* Specifies whether delaytime is calculated relative to cps.
*
* @name lock
- * @tags effects
+ * @tags fx
* @param {number | Pattern} enable When set to 1, delaytime is a direct multiple of a cycle.
* @superdirtOnly
* @example
@@ -1560,7 +1560,7 @@ export const { lock } = registerControl('lock');
* Set detune for stacked voices of supported oscillators
*
* @name detune
- * @tags effects
+ * @tags fx
* @param {number | Pattern} amount
* @synonyms det
* @example
@@ -1572,7 +1572,7 @@ export const { detune, det } = registerControl('detune', 'det');
* Set number of stacked voices for supported oscillators
*
* @name unison
- * @tags effects
+ * @tags fx
* @param {number | Pattern} numvoices
* @example
* note("d f a a# a d3").fast(2).s("supersaw").unison("<1 2 7>")
@@ -1584,7 +1584,7 @@ export const { unison } = registerControl('unison');
* Set the stereo pan spread for supported oscillators
*
* @name spread
- * @tags effects
+ * @tags fx
* @param {number | Pattern} spread between 0 and 1
* @example
* note("d f a a# a d3").fast(2).s("supersaw").spread("<0 .3 1>")
@@ -1595,7 +1595,7 @@ export const { spread } = registerControl('spread');
* Set dryness of reverb. See `room` and `size` for more information about reverb.
*
* @name dry
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {number | Pattern} dry 0 = wet, 1 = dry
* @example
* n("[0,3,7](3,8)").s("superpiano").room(.7).dry("<0 .5 .75 1>").osc()
@@ -1635,7 +1635,7 @@ export const { freq } = registerControl('freq');
* Attack time of pitch envelope.
*
* @name pattack
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @synonyms patt
* @param {number | Pattern} time time in seconds
* @example
@@ -1647,7 +1647,7 @@ export const { pattack, patt } = registerControl('pattack', 'patt');
* Decay time of pitch envelope.
*
* @name pdecay
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @synonyms pdec
* @param {number | Pattern} time time in seconds
* @example
@@ -1661,7 +1661,7 @@ export const { psustain, psus } = registerControl('psustain', 'psus');
* Release time of pitch envelope
*
* @name prelease
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @synonyms prel
* @param {number | Pattern} time time in seconds
* @example
@@ -1676,7 +1676,7 @@ export const { prelease, prel } = registerControl('prelease', 'prel');
* If you don't set other pitch envelope controls, `pattack:.2` will be the default.
*
* @name penv
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} semitones change in semitones
* @example
* note("c")
@@ -1688,7 +1688,7 @@ export const { penv } = registerControl('penv');
* Curve of envelope. Defaults to linear. exponential is good for kicks
*
* @name pcurve
- * @tags effects
+ * @tags fx
* @param {number | Pattern} type 0 = linear, 1 = exponential
* @example
* note("g1*4")
@@ -1705,7 +1705,7 @@ export const { pcurve } = registerControl('pcurve');
* If you don't set an anchor, the value will default to the psustain value.
*
* @name panchor
- * @tags effects
+ * @tags fx
* @param {number | Pattern} anchor anchor offset
* @example
* note("c c4").penv(12).panchor("<0 .5 1 .5>")
@@ -1727,7 +1727,7 @@ export const { gate, gat } = registerControl('gate', 'gat');
* Emulation of a Leslie speaker: speakers rotating in a wooden amplified cabinet.
*
* @name leslie
- * @tags effects
+ * @tags fx
* @param {number | Pattern} wet between 0 and 1
* @example
* n("0,4,7").s("supersquare").leslie("<0 .4 .6 1>").osc()
@@ -1739,7 +1739,7 @@ export const { leslie } = registerControl('leslie');
* Rate of modulation / rotation for leslie effect
*
* @name lrate
- * @tags effects
+ * @tags fx
* @param {number | Pattern} rate 6.7 for fast, 0.7 for slow
* @example
* n("0,4,7").s("supersquare").leslie(1).lrate("<1 2 4 8>").osc()
@@ -1752,7 +1752,7 @@ export const { lrate } = registerControl('lrate');
* Physical size of the cabinet in meters. Be careful, it might be slightly larger than your computer. Affects the Doppler amount (pitch warble)
*
* @name lsize
- * @tags effects
+ * @tags fx
* @param {number | Pattern} meters somewhere between 0 and 1
* @example
* n("0,4,7").s("supersquare").leslie(1).lrate(2).lsize("<.1 .5 1>").osc()
@@ -1814,7 +1814,7 @@ export const { octave } = registerControl('octave');
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
*
* @name orbit
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {number | Pattern} number
* @example
* stack(
@@ -1831,7 +1831,7 @@ export const { overshape } = registerControl('overshape');
* Sets position in stereo.
*
* @name pan
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)
* @example
* s("[bd hh]*2").pan("<.5 1 .5 0>")
@@ -1897,7 +1897,7 @@ export const { mode } = registerControl(['mode', 'anchor']);
* When using mininotation, you can also optionally add the 'size' parameter, separated by ':'.
*
* @name room
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd sd [~ bd] sd").room("<0 .2 .4 .6 .8 1>")
@@ -1911,7 +1911,7 @@ export const { room } = registerControl(['room', 'size']);
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomlp
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms rlp
* @param {number} frequency between 0 and 20000hz
* @example
@@ -1925,7 +1925,7 @@ export const { roomlp, rlp } = registerControl('roomlp', 'rlp');
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomdim
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms rdim
* @param {number} frequency between 0 and 20000hz
* @example
@@ -1940,7 +1940,7 @@ export const { roomdim, rdim } = registerControl('roomdim', 'rdim');
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomfade
- * @tags effects, superdough
+ * @tags fx, superdough
* @synonyms rfade
* @param {number} seconds for the reverb to fade
* @example
@@ -1953,7 +1953,7 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade');
/**
* Sets the sample to use as an impulse response for the reverb.
* @name iresponse
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {string | Pattern} sample to use as an impulse response
* @synonyms ir
* @example
@@ -1965,7 +1965,7 @@ export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse');
/**
* Sets speed of the sample for the impulse response.
* @name irspeed
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {string | Pattern} speed
* @example
* samples('github:switchangel/pad')
@@ -1977,7 +1977,7 @@ export const { irspeed } = registerControl('irspeed');
/**
* Sets the beginning of the IR response sample
* @name irbegin
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {string | Pattern} begin between 0 and 1
* @synonyms ir
* @example
@@ -1991,7 +1991,7 @@ export const { irbegin } = registerControl('irbegin');
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomsize
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {number | Pattern} size between 0 and 10
* @synonyms rsize, sz, size
* @example
@@ -2015,7 +2015,7 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size',
*
*
* @name shape
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {number | Pattern} distortion between 0 and 1
* @example
* s("bd sd [~ bd] sd,hh*8").shape("<0 .2 .4 .6 .8>")
@@ -2028,7 +2028,7 @@ export const { shape } = registerControl(['shape', 'shapevol']);
* Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)
*
* @name distort
- * @tags effects, superdough, supradough
+ * @tags fx, superdough, supradough
* @synonyms dist
* @param {number | Pattern} distortion
* @example
@@ -2043,7 +2043,7 @@ export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dis
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
*
* @name compressor
- * @tags effects, superdough
+ * @tags fx, superdough
* @example
* s("bd sd [~ bd] sd,hh*8")
* .compressor("-20:20:10:.002:.02")
@@ -2064,7 +2064,7 @@ export const { compressorRelease } = registerControl('compressorRelease');
* Changes the speed of sample playback, i.e. a cheap way of changing pitch.
*
* @name speed
- * @tags effects
+ * @tags fx
* @param {number | Pattern} speed -inf to inf, negative numbers play the sample backwards.
* @example
* s("bd*6").speed("1 2 4 1 -2 -4")
@@ -2078,7 +2078,7 @@ export const { speed } = registerControl('speed');
* Changes the speed of sample playback, i.e. a cheap way of changing pitch.
*
* @name stretch
- * @tags effects
+ * @tags fx
* @param {number | Pattern} factor -inf to inf, negative numbers play the sample backwards.
* @example
* s("gm_flute").stretch("1 2 .5")
@@ -2089,7 +2089,7 @@ export const { stretch } = registerControl('stretch');
* Used in conjunction with `speed`, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`.
*
* @name unit
- * @tags effects
+ * @tags fx
* @param {number | string | Pattern} unit see description above
* @example
* speed("1 2 .5 3").s("bd").unit("c").osc()
@@ -2104,7 +2104,7 @@ export const { unit } = registerControl('unit');
* "A simplistic pitch-raising algorithm. It's not meant to sound natural; its sound is reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter, depending on the input. The algorithm works by cutting the signal into fragments (delimited by upwards-going zero-crossings) and squeezing those fragments in the time domain (i.e. simply playing them back faster than they came in), leaving silences inbetween. All the parameters apart from memlen can be modulated."
*
* @name squiz
- * @tags effects
+ * @tags fx
* @param {number | Pattern} squiz Try passing multiples of 2 to it - 2, 4, 8 etc.
* @example
* squiz("2 4/2 6 [8 16]").s("bd").osc()
@@ -2129,7 +2129,7 @@ export const { squiz } = registerControl('squiz');
* Formant filter to make things sound like vowels.
*
* @name vowel
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {string | Pattern} vowel You can use a e i o u ae aa oe ue y uh un en an on, corresponding to [a] [e] [i] [o] [u] [æ] [ɑ] [ø] [y] [ɯ] [ʌ] [œ̃] [ɛ̃] [ɑ̃] [ɔ̃]. Aliases: aa = å = ɑ, oe = ø = ö, y = ı, ae = æ.
* @example
* note("[c2 >]*2").s('sawtooth')
@@ -2154,7 +2154,7 @@ export const { waveloss } = registerControl('waveloss');
* Noise crackle density
*
* @name density
- * @tags effects, superdough
+ * @tags fx, superdough
* @param {number | Pattern} density between 0 and x
* @example
* s("crackle*4").density("<0.01 0.04 0.2 0.5>".slow(4))
@@ -2262,7 +2262,7 @@ export let createParams = (...names) =>
* ADSR envelope: Combination of Attack, Decay, Sustain, and Release.
*
* @name adsr
- * @tags effects
+ * @tags fx
* @param {number | Pattern} time attack time in seconds
* @param {number | Pattern} time decay time in seconds
* @param {number | Pattern} gain sustain level (0 to 1)
diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs
index cfc669c8..677d8362 100644
--- a/packages/superdough/wavetable.mjs
+++ b/packages/superdough/wavetable.mjs
@@ -180,7 +180,7 @@ export function registerWaveTable(key, tables, params) {
* Loads a collection of wavetables to use with `s`
*
* @name tables
- * @tags effects
+ * @tags fx
*/
export const tables = async (url, frameLen, json, options = {}) => {
if (json !== undefined) return _processTables(json, url, frameLen);
diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs
index 23dfbee0..9b5bf189 100644
--- a/packages/supradough/dough.mjs
+++ b/packages/supradough/dough.mjs
@@ -18,7 +18,7 @@ function applyGainCurve(val) {
* @param {number} a - Signal A (can be a single value or an array value in buffer processing).
* @param {number} b - Signal B (can be a single value or an array value in buffer processing).
* @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix).
- * @tags effects
+ * @noAutocomplete
* @returns {number} Crossfaded output value.
*/
function crossfade(a, b, m) {
From 7e4cbefc8fa812769f35eb9da29d3f50839518e1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sun, 19 Oct 2025 21:22:57 +0200
Subject: [PATCH 017/200] Tag crossfade as internals
---
packages/supradough/dough.mjs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs
index 9b5bf189..a3109733 100644
--- a/packages/supradough/dough.mjs
+++ b/packages/supradough/dough.mjs
@@ -18,7 +18,7 @@ function applyGainCurve(val) {
* @param {number} a - Signal A (can be a single value or an array value in buffer processing).
* @param {number} b - Signal B (can be a single value or an array value in buffer processing).
* @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix).
- * @noAutocomplete
+ * @tags internals
* @returns {number} Crossfaded output value.
*/
function crossfade(a, b, m) {
From 466bf819d883b7f2e35a5dc617727c53765d71cb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sun, 19 Oct 2025 21:38:46 +0200
Subject: [PATCH 018/200] Fix formatting (trailing space)
---
packages/superdough/worklets.mjs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs
index 3e827622..b99b9caf 100644
--- a/packages/superdough/worklets.mjs
+++ b/packages/superdough/worklets.mjs
@@ -852,7 +852,7 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
/** BYTE BEATS
- * @tags internals
+ * @tags internals
*/
const chyx = {
/*bit*/ bitC: function (x, y, z) {
From 0830a9df69ddd49bcec08f25b58b390a103bbb74 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sat, 1 Nov 2025 20:19:57 +0100
Subject: [PATCH 019/200] Add superdirt tags
---
packages/core/controls.mjs | 21 +++++++++++----------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index 1bfd5a89..da5337ec 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -399,7 +399,7 @@ export const { note } = registerControl(['note', 'n']);
* A pattern of numbers that speed up (or slow down) samples while they play. Currently only supported by osc / superdirt.
*
* @name accelerate
- * @tags samples
+ * @tags samples, superdirt
* @param {number | Pattern} amount acceleration.
* @superdirtOnly
* @example
@@ -444,7 +444,7 @@ export const { postgain } = registerControl('postgain');
* Like `gain`, but linear.
*
* @name amp
- * @tags fx
+ * @tags fx, superdirt
* @param {number | Pattern} amount gain.
* @superdirtOnly
* @example
@@ -1075,7 +1075,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
* The amount the signal is affected by the phaser effect. Defaults to 0.75
*
* @name phaserdepth
- * @tags fx, superdough
+ * @tags fx, superdough, superdirt
* @synonyms phd, phasdp
* @param {number | Pattern} depth number between 0 and 1
* @example
@@ -1546,7 +1546,7 @@ export const { delaysync } = registerControl('delaysync');
* Specifies whether delaytime is calculated relative to cps.
*
* @name lock
- * @tags fx
+ * @tags fx, superdirt
* @param {number | Pattern} enable When set to 1, delaytime is a direct multiple of a cycle.
* @superdirtOnly
* @example
@@ -1595,7 +1595,7 @@ export const { spread } = registerControl('spread');
* Set dryness of reverb. See `room` and `size` for more information about reverb.
*
* @name dry
- * @tags fx, superdough
+ * @tags fx, superdough, superdirt
* @param {number | Pattern} dry 0 = wet, 1 = dry
* @example
* n("[0,3,7](3,8)").s("superpiano").room(.7).dry("<0 .5 .75 1>").osc()
@@ -1727,7 +1727,7 @@ export const { gate, gat } = registerControl('gate', 'gat');
* Emulation of a Leslie speaker: speakers rotating in a wooden amplified cabinet.
*
* @name leslie
- * @tags fx
+ * @tags fx, superdirt
* @param {number | Pattern} wet between 0 and 1
* @example
* n("0,4,7").s("supersquare").leslie("<0 .4 .6 1>").osc()
@@ -1739,7 +1739,7 @@ export const { leslie } = registerControl('leslie');
* Rate of modulation / rotation for leslie effect
*
* @name lrate
- * @tags fx
+ * @tags fx, superdirt
* @param {number | Pattern} rate 6.7 for fast, 0.7 for slow
* @example
* n("0,4,7").s("supersquare").leslie(1).lrate("<1 2 4 8>").osc()
@@ -1752,7 +1752,7 @@ export const { lrate } = registerControl('lrate');
* Physical size of the cabinet in meters. Be careful, it might be slightly larger than your computer. Affects the Doppler amount (pitch warble)
*
* @name lsize
- * @tags fx
+ * @tags fx, superdirt
* @param {number | Pattern} meters somewhere between 0 and 1
* @example
* n("0,4,7").s("supersquare").leslie(1).lrate(2).lsize("<.1 .5 1>").osc()
@@ -1801,6 +1801,7 @@ export const { nudge } = registerControl('nudge');
* Sets the default octave of a synth.
*
* @name octave
+ * @tags fx, superdirt
* @param {number | Pattern} octave octave number
* @example
* n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc()
@@ -2089,7 +2090,7 @@ export const { stretch } = registerControl('stretch');
* Used in conjunction with `speed`, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`.
*
* @name unit
- * @tags fx
+ * @tags fx, superdirt
* @param {number | string | Pattern} unit see description above
* @example
* speed("1 2 .5 3").s("bd").unit("c").osc()
@@ -2104,7 +2105,7 @@ export const { unit } = registerControl('unit');
* "A simplistic pitch-raising algorithm. It's not meant to sound natural; its sound is reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter, depending on the input. The algorithm works by cutting the signal into fragments (delimited by upwards-going zero-crossings) and squeezing those fragments in the time domain (i.e. simply playing them back faster than they came in), leaving silences inbetween. All the parameters apart from memlen can be modulated."
*
* @name squiz
- * @tags fx
+ * @tags fx, superdirt
* @param {number | Pattern} squiz Try passing multiples of 2 to it - 2, 4, 8 etc.
* @example
* squiz("2 4/2 6 [8 16]").s("bd").osc()
From b7827cb89f16624773f9c5717d64419d1a729154 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sat, 1 Nov 2025 21:11:57 +0100
Subject: [PATCH 020/200] Tag new functions
---
packages/core/controls.mjs | 4 ++
packages/core/pattern.mjs | 9 ++++
.../src/repl/components/panel/Reference.jsx | 42 ++++---------------
3 files changed, 20 insertions(+), 35 deletions(-)
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index 50a49486..7c66b400 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -2049,6 +2049,7 @@ export const { distort, dist } = registerControl(['distort', 'distortvol', 'dist
*
* @name distortvol
* @synonyms distvol
+ * @tags fx, superdough, supradough
* @param {number | Pattern} volume linear postgain of the distortion
* @example
* s("bd*4").bank("tr909").distort(2).distortvol(0.8)
@@ -2059,6 +2060,7 @@ export const { distortvol } = registerControl('distortvol', 'distvol');
* Type of waveshaping distortion to apply.
*
* @name distorttype
+ * @tags fx, superdough, supradough
* @synonyms disttype
* @param {number | string | Pattern} type type of distortion to apply
* @example
@@ -2487,6 +2489,7 @@ export const { polyTouch } = registerControl('polyTouch');
/**
* The host to send open sound control messages to. Requires running the OSC bridge.
* @name oschost
+ * @tags external_io
* @param {string | Pattern} oschost e.g. 'localhost'
* @example
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
@@ -2496,6 +2499,7 @@ export const { oschost } = registerControl('oschost');
/**
* The port to send open sound control messages to. Requires running the OSC bridge.
* @name oscport
+ * @tags external_io
* @param {number | Pattern} oscport e.g. 57120
* @example
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs
index 8cb30e15..e8cd3d23 100644
--- a/packages/core/pattern.mjs
+++ b/packages/core/pattern.mjs
@@ -3128,6 +3128,7 @@ export const extend = stepRegister('extend', function (factor, pat) {
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
*
* TODO: find out how this function differs from extend
+ * @tags temporal
* @example
* stepcat(
* sound("bd bd - cp").replicate(2),
@@ -3697,6 +3698,7 @@ export const morph = (frompat, topat, bypat) => {
* Soft-clipping distortion
*
* @name soft
+ * @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3705,6 +3707,7 @@ export const morph = (frompat, topat, bypat) => {
* Hard-clipping distortion
*
* @name hard
+ * @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3713,6 +3716,7 @@ export const morph = (frompat, topat, bypat) => {
* Cubic polynomial distortion
*
* @name cubic
+ * @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3721,6 +3725,7 @@ export const morph = (frompat, topat, bypat) => {
* Diode-emulating distortion
*
* @name diode
+ * @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3729,6 +3734,7 @@ export const morph = (frompat, topat, bypat) => {
* Asymmetrical diode distortion
*
* @name asym
+ * @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3737,6 +3743,7 @@ export const morph = (frompat, topat, bypat) => {
* Wavefolding distortion
*
* @name fold
+ * @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3745,6 +3752,7 @@ export const morph = (frompat, topat, bypat) => {
* Wavefolding distortion composed with sinusoid
*
* @name sinefold
+ * @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3753,6 +3761,7 @@ export const morph = (frompat, topat, bypat) => {
* Distortion via Chebyshev polynomials
*
* @name chebyshev
+ * @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx
index 2a7dcc2a..3053f03d 100644
--- a/website/src/repl/components/panel/Reference.jsx
+++ b/website/src/repl/components/panel/Reference.jsx
@@ -11,8 +11,13 @@ const availableFunctions = (() => {
for (const doc of jsdocJson.docs) {
if (!isValid(doc)) continue;
if (seen.has(doc.name)) continue;
- doc.tags = doc.tags?.filter((t) => t && typeof t === 'string') || [];
+
+ // jsdoc also uses "tags" for when you use @something in the comments and it doesn't know what
+ // @something is. We only want data from comments like `@tags fx, superdough` here.
+ // If nothing is specified, we default to "untagged" for debugging
+ doc.tags = doc.tags?.filter((t) => t && typeof t === 'string') || ['untagged'];
functions.push(doc);
+
const synonyms = doc.synonyms || [];
seen.add(doc.name);
for (const s of synonyms) {
@@ -38,16 +43,6 @@ const getInnerText = (html) => {
return div.textContent || div.innerText || '';
};
-const GROUP_DISPLAY_NAMES = {
- external_io: 'External I/O',
- effects: 'Effects',
- untagged: 'Untagged',
- structure: 'Structure',
- transforms: 'Transforms',
-};
-
-const GROUP_ORDER = ['effects', 'transforms', 'structure', 'untagged', 'external_io'];
-
export function Reference() {
const [search, setSearch] = useState('');
const [selectedTag, setSelectedTag] = useState(null);
@@ -80,29 +75,6 @@ export function Reference() {
});
}, [search, selectedTag]);
- const visibleFunctionsByGroup = (() => {
- const groups = {};
- for (const doc of visibleFunctions) {
- const group = (doc.tags || ['untagged'])[0];
- if (!groups[group]) {
- groups[group] = [];
- }
- groups[group].push(doc);
- }
- return groups;
- })();
- // console.log(visibleFunctionsByGroup);
-
- // Sort and map group entries
- const sortedGroups = Object.entries(visibleFunctionsByGroup).sort(([a], [b]) => {
- const ai = GROUP_ORDER.indexOf(a);
- const bi = GROUP_ORDER.indexOf(b);
- if (ai === -1 && bi === -1) return a.localeCompare(b);
- if (ai === -1) return 1;
- if (bi === -1) return -1;
- return ai - bi;
- });
-
const tagCounts = {};
for (const doc of availableFunctions) {
(doc.tags || ['untagged']).forEach((t) => {
@@ -114,7 +86,7 @@ export function Reference() {
return (
-
+
From 2fa8ed14245b45cce91708495befa82afe50745b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sun, 9 Nov 2025 20:21:19 +0100
Subject: [PATCH 021/200] Revert seq synonym refactor
---
packages/core/pattern.mjs | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs
index e8cd3d23..b8376c8f 100644
--- a/packages/core/pattern.mjs
+++ b/packages/core/pattern.mjs
@@ -1566,6 +1566,11 @@ export function fastcat(...pats) {
return result;
}
+/** See `fastcat` */
+export function sequence(...pats) {
+ return fastcat(...pats);
+}
+
/** Like **cat**, but the items are crammed into one cycle.
* @tags combiners
* @synonyms seq, fastcat
@@ -1579,9 +1584,6 @@ export function fastcat(...pats) {
* note("c4(5,8)")
* )
*/
-export function sequence(...pats) {
- return fastcat(...pats);
-}
export function seq(...pats) {
return fastcat(...pats);
From 65dd79e374ce791bf67c7de217b45c10de20f7ff Mon Sep 17 00:00:00 2001
From: "Lu[ke] Wilson"
Date: Sat, 29 Nov 2025 17:44:33 +0000
Subject: [PATCH 022/200] add lots of synonyms
---
packages/core/controls.mjs | 34 ++++++++++++++++++++--------------
1 file changed, 20 insertions(+), 14 deletions(-)
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index 21a6d962..0f9ba4d8 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -465,6 +465,7 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
*
* @name fmenv
* @param {number | Pattern} type lin | exp
+ * @synonyms fme
* @example
* note("c e g b g e")
* .fm(4)
@@ -474,12 +475,13 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
* ._scope()
*
*/
-export const { fmenv } = registerControl('fmenv');
+export const { fmenv } = registerControl('fmenv', 'fme');
/**
* Attack time for the FM envelope: time it takes to reach maximum modulation
*
* @name fmattack
* @param {number | Pattern} time attack time
+ * @synonyms fmatt
* @example
* note("c e g b g e")
* .fm(4)
@@ -487,7 +489,7 @@ export const { fmenv } = registerControl('fmenv');
* ._scope()
*
*/
-export const { fmattack } = registerControl('fmattack');
+export const { fmattack } = registerControl('fmattack', 'fmatt');
/**
* Waveform of the fm modulator
@@ -507,6 +509,7 @@ export const { fmwave } = registerControl('fmwave');
*
* @name fmdecay
* @param {number | Pattern} time decay time
+ * @synonyms fmdec
* @example
* note("c e g b g e")
* .fm(4)
@@ -515,12 +518,13 @@ export const { fmwave } = registerControl('fmwave');
* ._scope()
*
*/
-export const { fmdecay } = registerControl('fmdecay');
+export const { fmdecay } = registerControl('fmdecay', 'fmdec');
/**
* Sustain level for the FM envelope: how much modulation is applied after the decay phase
*
* @name fmsustain
* @param {number | Pattern} level sustain level
+ * @synonyms fmsus
* @example
* note("c e g b g e")
* .fm(4)
@@ -529,10 +533,10 @@ export const { fmdecay } = registerControl('fmdecay');
* ._scope()
*
*/
-export const { fmsustain } = registerControl('fmsustain');
+export const { fmsustain } = registerControl('fmsustain', 'fmsus');
// these are not really useful... skipping for now
-export const { fmrelease } = registerControl('fmrelease');
-export const { fmvelocity } = registerControl('fmvelocity');
+export const { fmrelease } = registerControl('fmrelease', 'fmrel');
+export const { fmvelocity } = registerControl('fmvelocity', 'fmvel');
/**
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
@@ -862,7 +866,7 @@ export const { duckonset } = registerControl('duckonset', 'duckons');
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckattack
- * @synonyms duckatt
+ * @synonyms duckatt, datt
*
* @param {number | Pattern} time The attack time in seconds
* @example
@@ -874,20 +878,20 @@ export const { duckonset } = registerControl('duckonset', 'duckons');
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1")
*
*/
-export const { duckattack } = registerControl('duckattack', 'duckatt');
+export const { duckattack } = registerControl('duckattack', 'duckatt', 'datt');
/**
* Create byte beats with custom expressions
*
* @name byteBeatExpression
- * @synonyms bbexpr
+ * @synonyms bbexpr, bb
*
* @param {number | Pattern} byteBeatExpression bitwise expression for creating bytebeat
* @example
* s("bytebeat").bbexpr('t*(t>>15^t>>66)')
*
*/
-export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpression', 'bbexpr');
+export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpression', 'bbexpr', 'bb');
/**
* Create byte beats with custom expressions
@@ -931,24 +935,26 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
* Controls the lfo rate for the pulsewidth of the pulse oscillator
*
* @name pwrate
+ * @synonyms pwr
* @param {number | Pattern} rate
* @example
* n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>")
*
*/
-export const { pwrate } = registerControl('pwrate');
+export const { pwrate } = registerControl('pwrate', 'pwr');
/**
* Controls the lfo sweep for the pulsewidth of the pulse oscillator
*
* @name pwsweep
+ * @synonyms pws
* @param {number | Pattern} sweep
* @example
* n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>")
*
*/
-export const { pwsweep } = registerControl('pwsweep');
+export const { pwsweep } = registerControl('pwsweep', 'pws');
/**
* Phaser audio effect that approximates popular guitar pedals.
@@ -1615,12 +1621,12 @@ export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt',
*
* @name delaysync
* @param {number | Pattern} cycles delay length in cycles
- * @synonyms delayt, dt
+ * @synonyms delays, ds
* @example
* s("bd bd").delay(.25).delaysync("<1 2 3 5>".div(8))
*
*/
-export const { delaysync } = registerControl('delaysync');
+export const { delaysync } = registerControl('delaysync', 'delays', 'ds');
/**
* Specifies whether delaytime is calculated relative to cps.
From ec73c48e2e3b2de112e45b0707be1738a3f6c3cd Mon Sep 17 00:00:00 2001
From: space-shell
Date: Mon, 8 Dec 2025 09:44:02 +0100
Subject: [PATCH 023/200] feat: Add Helix keybindings and Nix development
environment
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add support for Helix editor keybindings in the REPL, providing users
with a modern, selection-first modal editing experience as an alternative
to Vim, Emacs, and VSCode keybindings.
Helix Keybindings Integration:
- Add codemirror-helix@^0.5.0 dependency to @strudel/codemirror
- Import and register helix keybindings in keybindings.mjs
- Add 'Helix' option to keybindings selector in Settings UI
- Create comprehensive user documentation at technical-manual/helix.mdx
- Update developer documentation with keybinding integration pattern
The Helix mode provides selection-first editing (select → action) which
differs from Vim's action-motion paradigm, offering an alternative
workflow for modal editing enthusiasts.
Documentation:
- Update CLAUDE.md with Helix support and keybinding addition process
- Create helix.mdx with user-facing documentation and command reference
- Update CHANGELOG.md with December 2025 entry
Testing:
Run `pnpm i` to install new dependencies, then `pnpm dev` to test.
Verify Helix mode works in Settings → Keybindings → Helix.
---
CHANGELOG.md | 4 +
packages/codemirror/keybindings.mjs | 2 +
packages/codemirror/package.json | 1 +
pnpm-lock.yaml | 24 +++++-
website/src/pages/technical-manual/helix.mdx | 84 +++++++++++++++++++
.../src/repl/components/panel/SettingsTab.jsx | 2 +-
6 files changed, 114 insertions(+), 3 deletions(-)
create mode 100644 website/src/pages/technical-manual/helix.mdx
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a13e379d..7c73c39e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
NOTE: you can generate this with `node warm.js`. it might still not be perfectly sorted...
+## december 2025
+
+- 2025-12-08 Add Helix keybindings support to REPL editor - Added `codemirror-helix` package integration, enabling Helix editor keybindings as a new option in the REPL settings alongside Vim, Emacs, and VSCode modes
+
## november 2025
- 2025-11-27T22:03:53+01:00 [perf] in `noise`, let noiseMix do the disconnect when it exists by jeromew in: [#1783](https://codeberg.org/uzu/strudel/pulls/1783)
diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs
index 81dc87a5..bc13f323 100644
--- a/packages/codemirror/keybindings.mjs
+++ b/packages/codemirror/keybindings.mjs
@@ -6,6 +6,7 @@ import { emacs } from '@replit/codemirror-emacs';
import { vim, Vim } from '@replit/codemirror-vim';
// import { vim } from './vim_test.mjs';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
+import { helix } from 'codemirror-helix';
import { logger } from '@strudel/core';
const vscodePlugin = ViewPlugin.fromClass(
@@ -127,6 +128,7 @@ try {
const keymaps = {
vim,
emacs,
+ helix,
codemirror: () => keymap.of(defaultKeymap),
vscode: vscodeExtension,
};
diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json
index a3735491..41946444 100644
--- a/packages/codemirror/package.json
+++ b/packages/codemirror/package.json
@@ -44,6 +44,7 @@
"@replit/codemirror-emacs": "^6.1.0",
"@replit/codemirror-vim": "^6.3.0",
"@replit/codemirror-vscode-keymap": "^6.0.2",
+ "codemirror-helix": "^0.5.0",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/tonal": "workspace:*",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 23903fca..709a5b66 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)
@@ -221,6 +221,9 @@ importers:
'@tonaljs/tonal':
specifier: ^4.10.0
version: 4.10.0
+ codemirror-helix:
+ specifier: ^0.5.0
+ version: 0.5.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
nanostores:
specifier: ^0.11.3
version: 0.11.3
@@ -3536,6 +3539,15 @@ packages:
resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ codemirror-helix@0.5.0:
+ resolution: {integrity: sha512-hI56hf9VGz53H1YvL6H1GC7HtP6te8vX+MsIHaE9J7Q3PQ6KFapKtIRg6lqSH898ikHWpMCPu42r6HJN0IfVLA==}
+ peerDependencies:
+ '@codemirror/commands': ^6.0.0
+ '@codemirror/language': ^6.0.0
+ '@codemirror/search': ^6.0.0
+ '@codemirror/state': ^6.0.0
+ '@codemirror/view': ^6.0.0
+
collapse-white-space@2.1.0:
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
@@ -10554,7 +10566,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
@@ -11254,6 +11266,14 @@ snapshots:
cmd-shim@6.0.3: {}
+ codemirror-helix@0.5.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2):
+ dependencies:
+ '@codemirror/commands': 6.8.0
+ '@codemirror/language': 6.10.8
+ '@codemirror/search': 6.5.8
+ '@codemirror/state': 6.5.1
+ '@codemirror/view': 6.36.2
+
collapse-white-space@2.1.0: {}
color-convert@2.0.1:
diff --git a/website/src/pages/technical-manual/helix.mdx b/website/src/pages/technical-manual/helix.mdx
new file mode 100644
index 00000000..1de42d1e
--- /dev/null
+++ b/website/src/pages/technical-manual/helix.mdx
@@ -0,0 +1,84 @@
+---
+title: Helix Keybindings
+layout: ../../layouts/MainLayout.astro
+---
+
+# Helix Keybindings in the REPL
+
+The Strudel REPL supports [Helix editor](https://helix-editor.com/) keybindings through the `codemirror-helix` extension. Helix is a post-modern modal text editor with a focus on selection-first editing and multiple cursors.
+
+## Enabling Helix Mode
+
+1. Open the Settings panel in the REPL (click the settings icon or use the keyboard shortcut)
+2. Find the "Keybindings" section
+3. Select "Helix" from the available options
+
+## Key Differences from Vim
+
+Helix uses a selection-first approach, which means:
+
+- **Selection → Action** (Helix) vs **Action → Motion** (Vim)
+- For example, to delete a word in Helix: `w` (select word) → `d` (delete)
+- In Vim, it would be: `dw` (delete word)
+
+## Common Helix Commands
+
+### Normal Mode
+
+- `i` — Enter insert mode before selection
+- `a` — Enter insert mode after selection
+- `v` — Enter visual/select mode
+- `w` — Select next word
+- `e` — Select to end of word
+- `b` — Select previous word
+- `x` — Select/extend line
+- `d` — Delete selection
+- `c` — Change selection (delete and enter insert mode)
+- `y` — Yank (copy) selection
+- `p` — Paste after selection
+- `u` — Undo
+- `U` — Redo
+- `/` — Search
+- `n` — Select next search match
+- `N` — Select previous search match
+
+### Movement
+
+- `h, j, k, l` — Move left, down, up, right
+- `gg` — Go to start of document
+- `ge` — Go to end of document
+- `{` / `}` — Move to previous/next paragraph
+- `%` — Match bracket
+
+### Multi-cursor
+
+- `C` — Duplicate cursor to line below
+- `Alt-C` — Duplicate cursor to line above
+- `,` — Remove primary cursor
+- `Alt-,` — Remove all secondary cursors
+
+## Strudel-Specific Features
+
+Currently, Helix mode in Strudel provides standard Helix keybindings. Unlike Vim mode, there are no custom Strudel-specific commands (like `:w` to evaluate) yet.
+
+To evaluate code while in Helix mode, use the standard shortcuts:
+- `Ctrl+Enter` or `Alt+Enter` — Evaluate code
+- `Alt+.` — Stop playback
+
+## Resources
+
+- [Helix Documentation](https://docs.helix-editor.com/)
+- [Helix Keymap](https://docs.helix-editor.com/keymap.html)
+- [codemirror-helix on npm](https://www.npmjs.com/package/codemirror-helix)
+- [codemirror-helix on GitLab](https://gitlab.com/_rvidal/codemirror-helix)
+
+## Notes
+
+- The `codemirror-helix` extension is described as an "initial version" and may not implement all Helix features
+- Behavior may differ slightly from the native Helix editor
+- If you encounter issues, you can switch back to other keybinding modes in Settings
+- The keybinding preference is saved in your browser's local storage
+
+## Contributing
+
+If you'd like to add Strudel-specific Helix commands (similar to Vim's `:w` for evaluate), contributions are welcome! See the implementation in `/packages/codemirror/keybindings.mjs` for reference.
diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx
index f46fa661..3cc8d639 100644
--- a/website/src/repl/components/panel/SettingsTab.jsx
+++ b/website/src/repl/components/panel/SettingsTab.jsx
@@ -220,7 +220,7 @@ export function SettingsTab({ started }) {
settingsMap.setKey('keybindings', keybindings)}
- items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs', vscode: 'VSCode' }}
+ items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs', helix: 'Helix', vscode: 'VSCode' }}
>
From a6b3b9ef5e7ca385ea39722fe861a8ed469113ac Mon Sep 17 00:00:00 2001
From: space-shell
Date: Mon, 15 Dec 2025 13:22:13 +0100
Subject: [PATCH 024/200] updates formatting
---
website/src/pages/technical-manual/helix.mdx | 1 +
1 file changed, 1 insertion(+)
diff --git a/website/src/pages/technical-manual/helix.mdx b/website/src/pages/technical-manual/helix.mdx
index 1de42d1e..36f05898 100644
--- a/website/src/pages/technical-manual/helix.mdx
+++ b/website/src/pages/technical-manual/helix.mdx
@@ -62,6 +62,7 @@ Helix uses a selection-first approach, which means:
Currently, Helix mode in Strudel provides standard Helix keybindings. Unlike Vim mode, there are no custom Strudel-specific commands (like `:w` to evaluate) yet.
To evaluate code while in Helix mode, use the standard shortcuts:
+
- `Ctrl+Enter` or `Alt+Enter` — Evaluate code
- `Alt+.` — Stop playback
From fd1e3d248aa3b2d429456c73a1de17cd19253e5c Mon Sep 17 00:00:00 2001
From: Dsm0
Date: Tue, 23 Dec 2025 01:47:06 -0800
Subject: [PATCH 025/200] block based eval
---
packages/codemirror/block_utilities.mjs | 51 +++
packages/codemirror/codemirror.mjs | 85 ++++-
packages/codemirror/flash.mjs | 8 +-
packages/codemirror/highlight.mjs | 229 ++++++++++----
packages/codemirror/slider.mjs | 123 +++++++-
packages/codemirror/widget.mjs | 240 +++++++++++---
packages/core/evaluate.mjs | 28 ++
packages/core/repl.mjs | 296 ++++++++++++++++--
packages/draw/draw.mjs | 10 +
packages/transpiler/transpiler.mjs | 209 +++++++++++--
.../src/repl/components/panel/SettingsTab.jsx | 6 +
website/src/repl/useReplContext.jsx | 19 +-
website/src/settings.mjs | 2 +
13 files changed, 1106 insertions(+), 200 deletions(-)
create mode 100644 packages/codemirror/block_utilities.mjs
diff --git a/packages/codemirror/block_utilities.mjs b/packages/codemirror/block_utilities.mjs
new file mode 100644
index 00000000..4b53755d
--- /dev/null
+++ b/packages/codemirror/block_utilities.mjs
@@ -0,0 +1,51 @@
+// Block-based evaluation utilities
+
+export function getBlockRegions(code) {
+ const chars = code.split('');
+ let i = 0,
+ blanks = [],
+ blockStart = 0,
+ regions = [];
+ while (i < chars.length) {
+ const isBlank = chars[i] === '\n';
+ if (isBlank) {
+ blanks.push(i);
+ } else if (chars[i].trim() !== '') {
+ if (blanks.length > 1) {
+ regions.push([blockStart, blanks[0]]);
+ blockStart = i;
+ }
+ blanks = [];
+ }
+ i++;
+ }
+ regions.push([blockStart, blanks.length ? blanks[0] : i]);
+ return regions;
+}
+
+export function getBlockAt(code, cursor) {
+ const regions = getBlockRegions(code);
+ for (const [start, end] of regions) {
+ if (cursor >= start && cursor <= end) {
+ return [start, end];
+ }
+ }
+ return null;
+}
+
+export const evalBlock = (strudelMirror) => {
+ const { state } = strudelMirror.editor;
+ const code = state.doc.toString();
+ const cursor = state.selection.main.head;
+ const range = getBlockAt(code, cursor);
+ if (range) {
+ const [a, b] = range;
+ const block = code.slice(a, b);
+ if (block) {
+ // Flash the block being evaluated
+ strudelMirror.flash(200, { from: a, to: b });
+ strudelMirror.repl.evaluate(block, true, true, true, { range });
+ }
+ }
+ return true;
+};
diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs
index a612ab61..71986ba4 100644
--- a/packages/codemirror/codemirror.mjs
+++ b/packages/codemirror/codemirror.mjs
@@ -13,7 +13,7 @@ import {
} from '@codemirror/view';
import { persistentAtom } from '@nanostores/persistent';
import { logger, registerControl, repl } from '@strudel/core';
-import { cleanupDraw, Drawer } from '@strudel/draw';
+import { cleanupDraw, Drawer, cleanupDrawContext } from '@strudel/draw';
import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { basicSetup } from './basicSetup.mjs';
@@ -21,9 +21,13 @@ import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs';
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
+import { widgetPlugin, updateWidgets } from './widget.mjs';
import { activateTheme, initTheme, theme } from './themes.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
-import { updateWidgets, widgetPlugin } from './widget.mjs';
+import { getActiveWidgets } from './widget.mjs';
+import { getSliderWidgets } from './slider.mjs';
+
+import { evalBlock } from './block_utilities.mjs';
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
@@ -63,6 +67,7 @@ export const defaultSettings = {
isLineWrappingEnabled: false,
isTabIndentationEnabled: false,
isMultiCursorEnabled: false,
+ isBlockBasedEvalEnabled: false,
theme: 'strudelTheme',
fontFamily: 'monospace',
fontSize: 18,
@@ -74,7 +79,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
});
// https://codemirror.net/docs/guide/
-export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) {
+export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) {
const settings = codemirrorSettings.get();
const initialSettings = Object.keys(compartments).map((key) =>
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
@@ -104,11 +109,26 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
keymap.of([
{
key: 'Ctrl-Enter',
- run: () => onEvaluate?.(),
+ run: () => {
+ // issue with referencing settings, this works more reliably
+ if (strudelMirror?.isBlockBasedEvalEnabled) {
+ evalBlock(strudelMirror);
+ return true;
+ } else {
+ return onEvaluate?.();
+ }
+ },
},
{
key: 'Alt-Enter',
- run: () => onEvaluate?.(),
+ run: () => {
+ if (strudelMirror?.isBlockBasedEvalEnabled) {
+ evalBlock(strudelMirror);
+ return true;
+ } else {
+ return onEvaluate?.();
+ }
+ },
},
{
key: 'Ctrl-.',
@@ -162,6 +182,7 @@ export class StrudelMirror {
this.onDraw = onDraw || this.draw;
this.id = id || s4();
this.solo = solo;
+ this.isBlockBasedEvalEnabled = false; // Will be updated via updateSettings()
this.drawer = new Drawer((haps, time, _, painters) => {
const currentFrame = haps.filter((hap) => hap.isActive(time));
@@ -169,6 +190,7 @@ export class StrudelMirror {
this.onDraw(haps, time, painters);
}, drawTime);
+
this.prebaked = prebake();
autodraw && this.drawFirstFrame();
this.repl = repl({
@@ -192,20 +214,28 @@ export class StrudelMirror {
cleanupDraw(true, id);
}
},
- beforeEval: async () => {
- cleanupDraw(true, id);
+ beforeEval: async ({ blockBased } = {}) => {
+ // Only clean up all drawings for full evaluation
+ // Block-based eval should preserve animations (like .scope()) from other blocks
+ if (!blockBased) {
+ cleanupDraw(true, id);
+ }
await this.prebaked;
await replOptions?.beforeEval?.();
},
afterEval: (options) => {
// remember for when highlighting is toggled on
- this.miniLocations = options.meta?.miniLocations;
+ this.miniLocations = options.meta?.miniLocations || [];
this.widgets = options.meta?.widgets;
+
const sliders = this.widgets.filter((w) => w.type === 'slider');
- updateSliderWidgets(this.editor, sliders);
const widgets = this.widgets.filter((w) => w.type !== 'slider');
- updateWidgets(this.editor, widgets);
- updateMiniLocations(this.editor, this.miniLocations);
+ // range-aware update for block-based evaluation
+ const range = options.range && options.range.length >= 2 ? options.range : null;
+
+ updateSliderWidgets(this.editor, sliders, range);
+ updateWidgets(this.editor, widgets, range);
+ updateMiniLocations(this.editor, this.miniLocations, range);
replOptions?.afterEval?.(options);
// if no painters are set (.onPaint was not called), then we only need
// the present moment (for highlighting)
@@ -213,8 +243,14 @@ export class StrudelMirror {
this.drawer.setDrawTime(drawTime);
// invalidate drawer after we've set the appropriate drawTime
this.drawer.invalidate(this.repl.scheduler);
+
+ // Clean up draw context if a non-inline widget was removed
+ if (options.widgetRemoved) {
+ cleanupDrawContext(id);
+ }
},
});
+ this.cleanupDrawContext = () => cleanupDrawContext(id);
this.editor = initEditor({
root,
initialCode,
@@ -227,7 +263,9 @@ export class StrudelMirror {
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
mondo: replOptions.mondo,
+ strudelMirror: this,
});
+
const cmEditor = this.root.querySelector('.cm-editor');
if (cmEditor) {
this.root.style.display = 'block';
@@ -297,8 +335,9 @@ export class StrudelMirror {
this.flash();
await this.repl.evaluate(this.code, autostart);
}
+
async stop() {
- this.repl.scheduler.stop();
+ this.repl.stop();
}
// Listen for global stop requests (e.g., from Vim :q)
@@ -317,8 +356,8 @@ export class StrudelMirror {
this.evaluate();
}
}
- flash(ms) {
- flash(this.editor, ms);
+ flash(ms, range) {
+ flash(this.editor, ms, range);
}
highlight(haps, time) {
highlightMiniLocations(this.editor, time, haps);
@@ -350,6 +389,10 @@ export class StrudelMirror {
setLineWrappingEnabled(enabled) {
this.reconfigureExtension('isLineWrappingEnabled', enabled);
}
+
+ setBlockBasedEvalEnabled(enabled) {
+ this.reconfigureExtension('isBlockBasedEvalEnabled', enabled);
+ }
setBracketMatchingEnabled(enabled) {
this.reconfigureExtension('isBracketMatchingEnabled', enabled);
}
@@ -371,6 +414,10 @@ export class StrudelMirror {
for (let key in extensions) {
this.reconfigureExtension(key, settings[key]);
}
+ // Update block-based eval setting on the instance
+ if (settings.isBlockBasedEvalEnabled !== undefined) {
+ this.isBlockBasedEvalEnabled = parseBooleans(settings.isBlockBasedEvalEnabled);
+ }
const updated = { ...codemirrorSettings.get(), ...settings };
codemirrorSettings.set(updated);
}
@@ -392,6 +439,16 @@ export class StrudelMirror {
};
this.editor.dispatch({ changes });
}
+ // used for debugging but could serve other purposes
+ getActiveWidgets() {
+ return getActiveWidgets(this.editor);
+ }
+ getSliderWidgets() {
+ return getSliderWidgets(this.editor);
+ }
+ getMiniLocations() {
+ return this.miniLocations;
+ }
clear() {
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest);
diff --git a/packages/codemirror/flash.mjs b/packages/codemirror/flash.mjs
index 243500bf..7b247350 100644
--- a/packages/codemirror/flash.mjs
+++ b/packages/codemirror/flash.mjs
@@ -14,7 +14,8 @@ export const flashField = StateField.define({
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)]);
+ const range = e.value.range || { from: 0, to: tr.newDoc.length };
+ flash = Decoration.set([mark.range(range.from, range.to)]);
} else {
flash = Decoration.set([]);
}
@@ -29,8 +30,9 @@ export const flashField = StateField.define({
provide: (f) => EditorView.decorations.from(f),
});
-export const flash = (view, ms = 200) => {
- view.dispatch({ effects: setFlash.of(true) });
+export const flash = (view, ms = 200, range) => {
+ const flashData = range ? { range } : true;
+ view.dispatch({ effects: setFlash.of(flashData) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, ms);
diff --git a/packages/codemirror/highlight.mjs b/packages/codemirror/highlight.mjs
index f9f977c6..5cc6d3a1 100644
--- a/packages/codemirror/highlight.mjs
+++ b/packages/codemirror/highlight.mjs
@@ -3,8 +3,9 @@ import { Decoration, EditorView } from '@codemirror/view';
export const setMiniLocations = StateEffect.define();
export const showMiniLocations = StateEffect.define();
-export const updateMiniLocations = (view, locations) => {
- view.dispatch({ effects: setMiniLocations.of(locations) });
+export const displayMiniLocations = StateEffect.define();
+export const updateMiniLocations = (view, locations, range = null) => {
+ view.dispatch({ effects: setMiniLocations.of({ locations, range }) });
};
export const highlightMiniLocations = (view, atTime, haps) => {
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
@@ -21,23 +22,56 @@ const miniLocations = StateField.define({
for (let e of tr.effects) {
if (e.is(setMiniLocations)) {
- // this is called on eval, with the mini locations obtained from the transpiler
- // codemirror will automatically remap the marks when the document is edited
- // create a mark for each mini location, adding the range to the spec to find it later
- const marks = e.value
- .filter(([from]) => from < tr.newDoc.length)
- .map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
- .map(
- (range) =>
- Decoration.mark({
- id: range.join(':'),
- // this green is only to verify that the decoration moves when the document is edited
- // it will be removed later, so the mark is not visible by default
- attributes: { style: `background-color: #00CA2880` },
- }).range(...range), // -> Decoration
- );
+ //block-based eval case
+ if (e.value.range) {
+ const stateMiniLocations = getMiniLocationsFromDecorations(locations);
+ const existingById = new Map(stateMiniLocations.map(({ id, from, to }) => [id, [from, to]]));
- locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet
+ const normalized = e.value.locations
+ .filter(([from]) => from < tr.newDoc.length)
+ .map(([from, to]) => [from, Math.min(to, tr.newDoc.length)]);
+
+ const newIds = new Set(normalized.map((r) => r.join(':')));
+
+ const marks = normalized.map((range) => {
+ const id = range.join(':');
+ const useRange = existingById.get(id) || range;
+ return Decoration.mark({
+ id,
+ // this green is only to verify that the decoration moves when the document is edited
+ // it will be removed later, so the mark is not visible by default
+ attributes: { style: `background-color: #00CA2880` },
+ }).range(...useRange); // -> Decoration
+ });
+
+ const previousMarks = stateMiniLocations
+ .filter(({ id }) => !newIds.has(id))
+ .map(({ from, to, id }) =>
+ Decoration.mark({
+ id,
+ attributes: { style: `background-color: #00CA2880` },
+ }).range(from, to),
+ );
+
+ locations = Decoration.set(previousMarks.concat(marks), true); // -> DecorationSet === RangeSet
+ } else {
+ // this is called on eval, with the mini locations obtained from the transpiler
+ // codemirror will automatically remap the marks when the document is edited
+ // create a mark for each mini location, adding the range to the spec to find it later
+ const marks = e.value.locations
+ .filter(([from]) => from < tr.newDoc.length)
+ .map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
+ .map(
+ (range) =>
+ Decoration.mark({
+ id: range.join(':'),
+ // this green is only to verify that the decoration moves when the document is edited
+ // it will be removed later, so the mark is not visible by default
+ attributes: { style: `background-color: #00CA2880` },
+ }).range(...range), // -> Decoration
+ );
+ locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet
+ }
}
}
@@ -75,12 +109,83 @@ const visibleMiniLocations = StateField.define({
},
});
-// // Derive the set of decorations from the miniLocations and visibleLocations
-const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
- const iterator = state.field(miniLocations).iter();
- const { haps } = state.field(visibleMiniLocations);
- const builder = new RangeSetBuilder();
+const displayMiniLocationsState = StateField.define({
+ create() {
+ return true; // default to showing miniLocations
+ },
+ update(display, tr) {
+ for (let e of tr.effects) {
+ if (e.is(displayMiniLocations)) {
+ display = e.value;
+ }
+ }
+ return display;
+ },
+});
+// // Derive the set of decorations from the miniLocations and visibleLocations
+const miniLocationHighlights = EditorView.decorations.compute(
+ [miniLocations, visibleMiniLocations, displayMiniLocationsState],
+ (state) => {
+ // Check if miniLocations display is disabled
+ const shouldDisplay = state.field(displayMiniLocationsState);
+ if (!shouldDisplay) {
+ return Decoration.none; // Return empty decorations if display is disabled
+ }
+
+ const iterator = state.field(miniLocations).iter();
+ const { haps } = state.field(visibleMiniLocations);
+ const builder = new RangeSetBuilder();
+
+ while (iterator.value) {
+ const {
+ from,
+ to,
+ value: {
+ spec: { id },
+ },
+ } = iterator;
+
+ if (haps.has(id)) {
+ const hap = haps.get(id);
+ const color = hap.value?.color ?? 'var(--foreground)';
+ const style = hap.value?.markcss || `outline: solid 2px ${color}`;
+ // Get explicit channels for color values
+ /*
+ const swatch = document.createElement('div');
+ swatch.style.color = color;
+ document.body.appendChild(swatch);
+ let channels = getComputedStyle(swatch)
+ .color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
+ .slice(1)
+ .map((c) => parseFloat(c || 1));
+ document.body.removeChild(swatch);
+
+ // Get percentage of event
+ const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
+ channels[3] *= percent;
+ */
+
+ builder.add(
+ from,
+ to,
+ Decoration.mark({
+ // attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
+ attributes: { style },
+ }),
+ );
+ }
+
+ iterator.next();
+ }
+
+ return builder.finish();
+ },
+);
+
+const getMiniLocationsFromDecorations = (decorations) => {
+ const iterator = decorations.iter();
+ const miniLocationsArray = [];
while (iterator.value) {
const {
from,
@@ -89,50 +194,48 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
spec: { id },
},
} = iterator;
-
- if (haps.has(id)) {
- const hap = haps.get(id);
- const color = hap.value?.color ?? 'var(--foreground)';
- const style = hap.value?.markcss || `outline: solid 2px ${color}`;
- // Get explicit channels for color values
- /*
- const swatch = document.createElement('div');
- swatch.style.color = color;
- document.body.appendChild(swatch);
- let channels = getComputedStyle(swatch)
- .color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
- .slice(1)
- .map((c) => parseFloat(c || 1));
- document.body.removeChild(swatch);
-
- // Get percentage of event
- const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
- channels[3] *= percent;
- */
-
- builder.add(
- from,
- to,
- Decoration.mark({
- // attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
- attributes: { style },
- }),
- );
- }
-
+ miniLocationsArray.push({
+ from,
+ to,
+ id,
+ });
iterator.next();
}
+ return miniLocationsArray;
+};
- return builder.finish();
-});
+export const getMiniLocations = (state) => {
+ const decorations = state.field(miniLocations);
+ return getMiniLocationsFromDecorations(decorations);
+};
-export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
+export const getActiveMiniLocations = (state) => {
+ const miniLocations = getMiniLocations(state);
+ const { haps } = state.field(visibleMiniLocations);
+
+ const activeMiniLocations = miniLocations.filter((location) => haps.has(location.id));
+ return activeMiniLocations;
+};
+
+export const highlightExtension = [
+ miniLocations,
+ visibleMiniLocations,
+ displayMiniLocationsState,
+ miniLocationHighlights,
+];
export const isPatternHighlightingEnabled = (on, config) => {
- on &&
- config &&
- setTimeout(() => {
- updateMiniLocations(config.editor, config.miniLocations);
- }, 100);
- return on ? Prec.highest(highlightExtension) : [];
+ // NOTE:
+ // Modified this function to always return the highlightExtension, and instead just toggle whether or not the miniLocations are displayed.
+ // This is because block based evaluation only updates regions of miniLocations, and those updates need to be kept track of constantly.
+ // The setTimeout was also removed because it conflicted with the range-specific updates required by
+ // block based evaluation.
+ // Not sure if this is the best approach, but for block based eval I can't think of a better way to do it.
+
+ if (config) {
+ // Toggle the display state for miniLocations
+ config.editor.dispatch({ effects: displayMiniLocations.of(on) });
+ }
+
+ return Prec.highest(highlightExtension);
};
diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs
index 72f95125..647779ba 100644
--- a/packages/codemirror/slider.mjs
+++ b/packages/codemirror/slider.mjs
@@ -2,11 +2,11 @@ import { ref, pure } from '@strudel/core';
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
import { StateEffect } from '@codemirror/state';
+// Global state storage for all widget types
export let sliderValues = {};
-const getSliderID = (from) => `slider_${from}`;
export class SliderWidget extends WidgetType {
- constructor(value, min, max, from, to, step, view) {
+ constructor(value, min, max, from, to, step, view, id) {
super();
this.value = value;
this.min = min;
@@ -16,10 +16,21 @@ export class SliderWidget extends WidgetType {
this.to = to;
this.step = step;
this.view = view;
+ this.id = id || `${from}:${to}`; // Range-based ID for stability
}
- eq() {
- return false;
+ eq(other) {
+ if (!(other instanceof SliderWidget)) {
+ return false;
+ }
+ return (
+ this.id === other.id &&
+ this.from === other.from &&
+ this.to === other.to &&
+ this.value === other.value &&
+ this.min === other.min &&
+ this.max === other.max
+ );
}
toDOM() {
@@ -38,6 +49,7 @@ export class SliderWidget extends WidgetType {
slider.from = this.from;
slider.originalFrom = this.originalFrom;
slider.to = this.to;
+ slider.id = this.id; // Store range-based ID in DOM element
slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
this.slider = slider;
slider.addEventListener('input', (e) => {
@@ -49,7 +61,7 @@ export class SliderWidget extends WidgetType {
slider.originalValue = insert;
slider.value = insert;
this.view.dispatch({ changes: change });
- const id = getSliderID(slider.originalFrom); // matches id generated in transpiler
+ const id = slider.id; // Use range-based ID
window.postMessage({ type: 'cm-slider', value: Number(next), id });
});
return wrap;
@@ -62,19 +74,60 @@ export class SliderWidget extends WidgetType {
export const setSliderWidgets = StateEffect.define();
-export const updateSliderWidgets = (view, widgets) => {
- view.dispatch({ effects: setSliderWidgets.of(widgets) });
+export const setSliderWidgetsInRange = StateEffect.define();
+
+export const updateSliderWidgets = (view, widgets, range = null) => {
+ if (range) {
+ // range argument passed for block-based evaluation
+ view.dispatch({ effects: setSliderWidgetsInRange.of({ widgets, range }) });
+ } else {
+ view.dispatch({ effects: setSliderWidgets.of(widgets) });
+ }
};
function getSliders(widgetConfigs, view) {
- return widgetConfigs
- .filter((w) => w.type === 'slider')
- .map(({ from, to, value, min, max, step }) => {
- return Decoration.widget({
- widget: new SliderWidget(value, min, max, from, to, step, view),
- side: 0,
- }).range(from /* , to */);
- });
+ return (
+ widgetConfigs
+ .filter((w) => w.type === 'slider')
+ // Deduplicate sliders that might appear multiple times (e.g., during paste operations)
+ .filter((slider, index, self) => index === self.findIndex((s) => s.from === slider.from && s.to === slider.to))
+ .sort((a, b) => a.from - b.from)
+ .map(({ from, to, value, min, max, step, id }) => {
+ return Decoration.widget({
+ widget: new SliderWidget(value, min, max, from, to, step, view, id),
+ side: 0,
+ }).range(from /* , to */);
+ })
+ );
+}
+
+export function getSliderWidgets(view) {
+ if (!view || !view.state) {
+ return [];
+ }
+
+ const sliderPluginInstance = view.plugin(sliderPlugin);
+ if (!sliderPluginInstance || !sliderPluginInstance.decorations) {
+ return [];
+ }
+
+ const sliderWidgets = [];
+
+ sliderPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => {
+ if (decoration.widget instanceof SliderWidget) {
+ sliderWidgets.push({
+ type: 'slider',
+ from: decoration.widget.from,
+ to: decoration.widget.to,
+ value: decoration.widget.value,
+ min: decoration.widget.min,
+ max: decoration.widget.max,
+ step: decoration.widget.step,
+ });
+ }
+ });
+
+ return sliderWidgets;
}
export const sliderPlugin = ViewPlugin.fromClass(
@@ -101,7 +154,42 @@ export const sliderPlugin = ViewPlugin.fromClass(
}
}
for (let e of tr.effects) {
- if (e.is(setSliderWidgets)) {
+ if (e.is(setSliderWidgetsInRange)) {
+ // Block-aware slider update logic
+ const { widgets, range } = e.value;
+ const [rangeStart, rangeEnd] = range;
+
+ // Get existing slider widgets that should be preserved
+ const existingSliders = [];
+ this.decorations.between(0, update.view.state.doc.length, (from, to, decoration) => {
+ if (decoration.widget instanceof SliderWidget) {
+ // Preserve sliders outside the evaluation range
+ // Use strict > for rangeEnd because when code is deleted, slider positions
+ // map to the deletion boundary (rangeEnd), and those should be removed, not preserved
+ if (from < rangeStart || from > rangeEnd) {
+ existingSliders.push({
+ from,
+ to,
+ value: decoration.widget.value,
+ min: decoration.widget.min,
+ max: decoration.widget.max,
+ step: decoration.widget.step,
+ id: decoration.widget.id || `${from}:${to}`,
+ type: 'slider',
+ });
+ }
+ }
+ });
+
+ // Merge preserved sliders with new widgets
+ const mergedWidgets = [...existingSliders, ...widgets]
+ .filter(
+ (slider, index, self) => index === self.findIndex((s) => s.type === 'slider' && s.id === slider.id),
+ )
+ .sort((a, b) => a.from - b.from);
+
+ this.decorations = Decoration.set(getSliders(mergedWidgets, update.view));
+ } else if (e.is(setSliderWidgets)) {
this.decorations = Decoration.set(getSliders(e.value, update.view));
}
}
@@ -131,6 +219,7 @@ export let sliderWithID = (id, value, min, max) => {
sliderValues[id] = value; // sync state at eval time (code -> state)
return ref(() => sliderValues[id]); // use state at query time
};
+
// update state when sliders are moved
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
@@ -139,7 +228,7 @@ if (typeof window !== 'undefined') {
// update state when slider is moved
sliderValues[e.data.id] = e.data.value;
} else {
- console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
+ console.error(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
}
}
});
diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs
index 42d3b151..6c40d673 100644
--- a/packages/codemirror/widget.mjs
+++ b/packages/codemirror/widget.mjs
@@ -1,55 +1,111 @@
import { StateEffect, StateField } from '@codemirror/state';
-import { Decoration, EditorView, WidgetType } from '@codemirror/view';
+import { Decoration, EditorView, WidgetType, ViewPlugin } from '@codemirror/view';
import { getWidgetID, registerWidgetType } from '@strudel/transpiler';
import { Pattern } from '@strudel/core';
-export const addWidget = StateEffect.define({
- map: ({ from, to }, change) => {
- return { from: change.mapPos(from), to: change.mapPos(to) };
- },
-});
+export const setWidgets = StateEffect.define();
-export const updateWidgets = (view, widgets) => {
- view.dispatch({ effects: addWidget.of(widgets) });
+export const setWidgetsInRange = StateEffect.define();
+
+export const updateWidgets = (view, widgets, range = null) => {
+ if (range) {
+ // range argument passed for block-based evaluation
+ view.dispatch({ effects: setWidgetsInRange.of({ widgets, range }) });
+ } else {
+ view.dispatch({ effects: setWidgets.of(widgets) });
+ }
};
-function getWidgets(widgetConfigs) {
- return (
- widgetConfigs
- // codemirror throws an error if we don't sort
- .sort((a, b) => a.to - b.to)
- .map((widgetConfig) => {
+function getWidgets(widgetConfigs, view) {
+ const filtered = widgetConfigs
+ // Filter to widget configs only (exclude sliders)
+ .filter((w) => w && w.type && w.type !== 'slider')
+ // Deduplicate widgets by ID, matching slider behavior for stable widget identity
+ .filter((widget, index, self) => index === self.findIndex((w) => w.type === widget.type && w.id === widget.id));
+
+ // Filter out widgets whose range is encompassed by another widget
+ // const nonEncompassed = filterEncompassedWidgets(filtered);
+
+ return filtered
+ .sort((a, b) => (a.to || 0) - (b.to || 0))
+ .map((widgetConfig) => {
+ try {
return Decoration.widget({
- widget: new BlockWidget(widgetConfig),
+ widget: new BlockWidget(widgetConfig, view),
side: 0,
- block: true,
- }).range(widgetConfig.to);
- })
- );
+ }).range(widgetConfig.to || widgetConfig.from || 0);
+ } catch (error) {
+ console.error('error creating widget', error);
+ return null;
+ }
+ })
+ .filter(Boolean); // Remove any null results from failed creations
}
-const widgetField = StateField.define(
- /* */ {
- create() {
- return Decoration.none;
- },
- update(widgets, tr) {
- widgets = widgets.map(tr.changes);
- for (let e of tr.effects) {
- if (e.is(addWidget)) {
- try {
- widgets = widgets.update({
- filter: () => false,
- add: getWidgets(e.value),
- });
- } catch (error) {
- console.log('err', error);
+export const widgetPlugin = ViewPlugin.fromClass(
+ class {
+ decorations; //: DecorationSet
+
+ constructor(view /* : EditorView */) {
+ this.decorations = Decoration.set([]);
+ }
+
+ update(update /* : ViewUpdate */) {
+ update.transactions.forEach((tr) => {
+ if (tr.docChanged) {
+ this.decorations = this.decorations.map(tr.changes);
+ const iterator = this.decorations.iter();
+ // Apply changes to iterator.from and iterator.to if docChanged
+ while (iterator.value) {
+ // when the widgets are moved, we need to tell the dom node the current position
+ // this is important because the widget functions have to work with the dom node
+ if (iterator.value?.widget instanceof BlockWidget) {
+ iterator.value.widget.from = iterator.from;
+ iterator.value.widget.to = iterator.to;
+ }
+ iterator.next();
}
}
- }
- return widgets;
- },
- provide: (f) => EditorView.decorations.from(f),
+ for (let e of tr.effects) {
+ if (e.is(setWidgetsInRange)) {
+ // Block-aware widget update logic
+ const { widgets, range } = e.value;
+ const [rangeStart, rangeEnd] = range;
+
+ // Get existing widget widgets that should be preserved
+ const existingWidgets = [];
+ this.decorations.between(0, update.view.state.doc.length, (from, to, decoration) => {
+ if (decoration.widget instanceof BlockWidget) {
+ // Preserve widgets outside the evaluation range
+ // Use strict > for rangeEnd because when code is deleted, widget positions
+ // map to the deletion boundary (rangeEnd), and those should be removed, not preserved
+ if (from < rangeStart || from > rangeEnd) {
+ existingWidgets.push({
+ from: decoration.widget.from,
+ to: decoration.widget.to,
+ type: decoration.widget.type,
+ index: decoration.widget.index,
+ id: decoration.widget.id,
+ });
+ }
+ }
+ });
+
+ // Merge preserved widgets with new widgets, deduplicating by ID
+ const mergedWidgets = [...existingWidgets, ...widgets].filter(
+ (widget, index, self) => index === self.findIndex((w) => w.type === widget.type && w.id === widget.id),
+ );
+
+ this.decorations = Decoration.set(getWidgets(mergedWidgets, update.view));
+ } else if (e.is(setWidgets)) {
+ this.decorations = Decoration.set(getWidgets(e.value, update.view));
+ }
+ }
+ });
+ }
+ },
+ {
+ decorations: (v) => v.decorations,
},
);
@@ -60,24 +116,116 @@ export function setWidget(id, el) {
}
export class BlockWidget extends WidgetType {
- constructor(widgetConfig) {
+ constructor(widgetConfig, view) {
super();
+
+ // Graceful handling of invalid configs like sliders
+ if (!widgetConfig || typeof widgetConfig !== 'object') {
+ widgetConfig = { type: 'unknown', from: 0, to: 0 };
+ }
+
+ this.from = widgetConfig.from || 0;
+ this.originalFrom = widgetConfig.from || 0;
+ this.to = widgetConfig.to || this.from;
+ this.originalTo = widgetConfig.to || this.from;
+ this.type = widgetConfig.type || 'unknown';
+ this.index = widgetConfig.index || 0;
+ this.view = view;
+
+ // Use range-based ID for stability, similar to sliders
+ this.id = widgetConfig.id || getWidgetID?.(widgetConfig);
this.widgetConfig = widgetConfig;
}
- eq() {
- return true;
+
+ eq(other) {
+ if (!(other instanceof BlockWidget)) {
+ return false;
+ }
+ return (
+ this.id === other.id &&
+ this.from === other.from &&
+ this.to === other.to &&
+ this.type === other.type &&
+ this.index === other.index
+ );
}
+
toDOM() {
- const id = getWidgetID(this.widgetConfig);
- const el = widgetElements[id];
- return el;
+ let wrap = document.createElement('span');
+ wrap.setAttribute('aria-hidden', 'true');
+ wrap.className = 'cm-widget-container';
+
+ let el = widgetElements[this.id];
+ if (el) {
+ // Ensure the element has the correct ID
+ el.id = this.id;
+ wrap.appendChild(el);
+ } else {
+ // Create a placeholder element if the widget element doesn't exist
+ // This prevents CodeMirror errors when widget is missing
+ const placeholder = document.createElement('span');
+ placeholder.setAttribute('aria-hidden', 'true');
+ placeholder.className = 'cm-widget-placeholder';
+ placeholder.style.cssText = 'display: none;'; // Hide placeholder
+ placeholder.id = this.id;
+ wrap.appendChild(placeholder);
+ }
+
+ return wrap;
}
+
ignoreEvent(e) {
return true;
}
}
-export const widgetPlugin = [widgetField];
+export function getActiveWidgets(view) {
+ if (!view || !view.state) {
+ return [];
+ }
+
+ const widgetPluginInstance = view.plugin(widgetPlugin);
+ if (!widgetPluginInstance || !widgetPluginInstance.decorations) {
+ return [];
+ }
+
+ const widgets = [];
+
+ widgetPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => {
+ if (decoration.widget instanceof BlockWidget) {
+ widgets.push({
+ type: decoration.widget.type,
+ from: decoration.widget.from,
+ to: decoration.widget.to,
+ index: decoration.widget.index,
+ id: decoration.widget.id,
+ });
+ }
+ });
+
+ return widgets;
+}
+
+export function getAllWidgetIds(view) {
+ if (!view || !view.state) {
+ return [];
+ }
+
+ const widgetPluginInstance = view.plugin(widgetPlugin);
+ if (!widgetPluginInstance || !widgetPluginInstance.decorations) {
+ return [];
+ }
+
+ const widgetIds = [];
+
+ widgetPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => {
+ if (decoration.widget instanceof BlockWidget) {
+ widgetIds.push(decoration.widget.id);
+ }
+ });
+
+ return widgetIds;
+}
// widget implementer API to create a new widget type
export function registerWidget(type, fn) {
diff --git a/packages/core/evaluate.mjs b/packages/core/evaluate.mjs
index 0599d866..bb7b4664 100644
--- a/packages/core/evaluate.mjs
+++ b/packages/core/evaluate.mjs
@@ -5,6 +5,34 @@ This program is free software: you can redistribute it and/or modify it under th
*/
export const strudelScope = {};
+// Make strudelScope available globally so transpiled code can access it
+globalThis.strudelScope = strudelScope;
+
+// Track user-defined keys (from block-based eval) so we can clear them without removing strudel functions
+export const userDefinedKeys = new Set();
+globalThis.userDefinedKeys = userDefinedKeys;
+
+/**
+ * Clears all user-defined variables and functions from the scope.
+ * This removes variables created during block-based evaluation.
+ * @name clearScope
+ * @example
+ * // After defining variables in blocks:
+ * // let myVar = 5
+ * // function myFunc() { return 10; }
+ * clearScope() // removes myVar and myFunc from scope
+ */
+export const clearScope = () => {
+ for (const key of userDefinedKeys) {
+ delete strudelScope[key];
+ delete globalThis[key];
+ }
+ userDefinedKeys.clear();
+ // Return silence if available (for use in pattern expressions), otherwise undefined
+ return globalThis.silence;
+};
+// Make clearScope available globally
+globalThis.clearScope = clearScope;
export const evalScope = async (...args) => {
const results = await Promise.allSettled(args);
diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs
index 470be47b..4e0af7ab 100644
--- a/packages/core/repl.mjs
+++ b/packages/core/repl.mjs
@@ -32,6 +32,7 @@ export function repl({
pattern: undefined,
miniLocations: [],
widgets: [],
+ sliders: [],
pending: false,
started: false,
};
@@ -70,11 +71,172 @@ export function repl({
let allTransform;
let eachTransform;
+ // Block-based evaluation state
+ let codeBlocks = {};
+ let lastActiveVisualizerLabel = null;
+ // Track which patterns belong to which blocks: { blockRange: [patternKeys] }
+ let blockPatterns = new Map();
+
+ // Helper function to collect properties from all code blocks (handles both labeled and anonymous blocks)
+ function collectFromBlocks(property) {
+ return Object.entries(codeBlocks).flatMap(([key, block]) => {
+ if (key === '$') {
+ // Anonymous blocks are stored as an array of block objects
+ return Array.isArray(block) ? block.flatMap((b) => b[property] || []) : [];
+ }
+ // Labeled blocks are stored as single block objects
+ return block[property] || [];
+ });
+ }
+
+ // Helper function to process a single labeled block
+ function processLabeledBlock(labels, i, code, options, meta) {
+ const label = labels[i];
+ const nextLabel = labels[i + 1] || { index: code.length, end: code.length };
+
+ const labelCode = code.slice(label.index, nextLabel.index);
+ const labelRange = [label.index + options.range[0], label.end + options.range[0]];
+
+ // Calculate the full block range (from label start to next label start)
+ const blockStart = label.index + options.range[0];
+ const blockEnd = nextLabel.index + options.range[0];
+
+ const blockWidgets = (meta?.widgets || []).filter((widget) => {
+ const widgetPos = widget.from ?? widget.index ?? 0;
+ return widgetPos >= blockStart && widgetPos < blockEnd;
+ });
+
+ const blockSliders = (meta?.sliders || []).filter((slider) => {
+ const sliderPos = slider.from ?? slider.index ?? 0;
+ return sliderPos >= blockStart && sliderPos < blockEnd;
+ });
+
+ const blockMiniLocations = (meta?.miniLocations || []).filter((loc) => {
+ // const locStart = loc.start ?? loc.from ?? 0;
+ // mini locations can be either [start, end] arrays or objects with start/from
+ const locStart = Array.isArray(loc) ? loc[0] : (loc.start ?? loc.from ?? 0);
+ return locStart >= blockStart && locStart < blockEnd;
+ });
+
+ handleSingleLabelBlock(
+ label,
+ labelCode,
+ { ...options, range: labelRange },
+ { widgets: blockWidgets, sliders: blockSliders, miniLocations: blockMiniLocations },
+ );
+ }
+
+ // // Helper function to filter meta (widgets, sliders, miniLocations) by block range
+ // function splitCodeByRange(meta, blockStart, blockEnd) {
+ // }
+
+ // Helper function to extract labels from code with their positions
+ function extractLabelsFromCode(code) {
+ const labels = [];
+ // Regex to find label patterns like "d1:" or "myLabel:"
+ const labelRegex = /^(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:)/gm;
+ let match;
+ while ((match = labelRegex.exec(code)) !== null) {
+ labels.push({
+ name: match[2],
+ index: match.index,
+ end: match.index + match[1].length,
+ fullMatch: match[1],
+ });
+ }
+
+ // Also check for 'all()' and treat it as a special label
+ // just for management purposes
+ const allRegex = /all\s*\(\s*([^)]+)\s*\)/;
+ const allMatch = allRegex.exec(code);
+ if (allMatch) {
+ // Check if the argument contains a widget call
+ const argCode = allMatch[1];
+ const activeVisualizer = detectActiveVisualizer(argCode);
+
+ labels.push({
+ name: 'all',
+ index: allMatch.index,
+ end: allMatch.index + allMatch[0].length,
+ fullMatch: allMatch[0],
+ activeVisualizer: activeVisualizer,
+ });
+ }
+
+ return labels;
+ }
+
+ // Helper function to detect non-inline widget calls in code
+ function detectActiveVisualizer(code) {
+ // List of non-inline widgets that need cleanup
+ // These are Pattern.prototype methods that create persistent visualizations
+ // I don't like this approach, feels hacky, but since these methods don't create ids that
+ // can be read through the transpiler, I'm not sure how best to detect them.
+ // Would probably be better to register these methods through some function that would
+ // tag them better
+ const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall',];
+
+ for (const widget of nonInlineWidgets) {
+ const widgetRegex = new RegExp(`\\.${widget}\\s*\\(`);
+ if (widgetRegex.test(code)) {
+ return widget;
+ }
+ }
+ return null;
+ }
+
+ // Helper function to handle single label code block storage
+ function handleSingleLabelBlock(label, code, options, meta) {
+
+ // Detect if this block contains a non-inline widget
+ // For 'all' label, widget info is already on the label object from extractLabelsFromCode
+
+ // As mentioned in detectActiveVisualizer, this is a bad approach to
+ // managing non-inline widgets (or widgets that don't have ids)
+ // and a proper solution would give them widgets in the transpiler, or at least
+ // track where they are so we don't have to futz around with regexes
+ const activeVisualizer = label.activeVisualizer !== undefined
+ ? label.activeVisualizer
+ : detectActiveVisualizer(code);
+
+ if (activeVisualizer !== null) {
+ lastActiveVisualizerLabel = label.name;
+ }
+
+ // Store the entire code block under the label name
+ codeBlocks[label.name] = {
+ code: code,
+ range: options.range,
+ labels: [label.name],
+ miniLocations: meta?.miniLocations || [],
+ widgets: meta?.widgets || [],
+ sliders: meta?.sliders || [],
+ activeVisualizer: activeVisualizer, // Store the widget type if present, null otherwise
+ };
+
+
+ // Clean up any blocks with conflicting ranges
+ for (const [existingKey, existingBlock] of Object.entries(codeBlocks)) {
+ if (existingKey !== label.name && existingBlock.range && options.range) {
+ const [existingStart, existingEnd] = existingBlock.range;
+ const [newStart, newEnd] = options.range;
+
+ // If ranges overlap (not just touch), remove the stale block
+ if (!(newEnd <= existingStart || newStart >= existingEnd)) {
+ delete codeBlocks[existingKey];
+ }
+ }
+ }
+ }
+
const hush = function () {
pPatterns = {};
anonymousIndex = 0;
allTransform = undefined;
eachTransform = undefined;
+ codeBlocks = {};
+ blockPatterns.clear();
+ lastActiveVisualizerLabel = null; // Reset 'all' visualizer tracking
return silence;
};
@@ -93,7 +255,18 @@ export function repl({
};
setTime(() => scheduler.now()); // TODO: refactor?
- const stop = () => scheduler.stop();
+ const stop = () => {
+ codeBlocks = {};
+ blockPatterns.clear();
+ pPatterns = {};
+ lastActiveVisualizerLabel = null; // Reset 'all' visualizer tracking
+ updateState({
+ miniLocations: [],
+ widgets: [],
+ sliders: [],
+ });
+ scheduler.stop();
+ };
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
@@ -201,7 +374,7 @@ export function repl({
});
};
- const evaluate = async (code, autostart = true, shouldHush = true) => {
+ const evaluate = async (code, autostart = true, shouldHush = true, blockBased = false, options = {}) => {
if (!code) {
throw new Error('no code to evaluate');
}
@@ -209,15 +382,92 @@ export function repl({
updateState({ code, pending: true });
await injectPatternMethods();
setTime(() => scheduler.now()); // TODO: refactor?
- await beforeEval?.({ code });
+ await beforeEval?.({ code, blockBased });
allTransforms = []; // reset all transforms
- shouldHush && hush();
+
+ const transpilerOptionsWithBlock = {
+ ...transpilerOptions,
+ blockBased,
+ range: options.range || [],
+ };
+
+ if (!blockBased) {
+ codeBlocks = {};
+ shouldHush && hush();
+ }
if (mondo) {
code = `mondolang\`${code}\``;
}
- let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
- if (Object.keys(pPatterns).length) {
+
+ let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptionsWithBlock);
+
+ // Track activeVisualizer cleanup: check if any block's visualizer was removed
+ let widgetRemoved = false;
+
+ if (blockBased) {
+ const labels = extractLabelsFromCode(code);
+
+ // Store code blocks in dictionary using labels as keys
+ if (labels.length > 0) {
+ for (let i = 0; i < labels.length; i++) {
+ // processLabeledBlock(labels, i, code, options, meta);
+ // processing transpiler output instead of code is simply to avoid
+ // extra regex in detecting whether or not an inline widget has been commented out
+ processLabeledBlock(labels, i, meta.output, options, meta);
+ }
+ } else if (pattern !== silence) {
+ // variable/function declarations that return silence are allowed,
+ // but anonymous pattern blocks pose an issue for block-based evaluation
+ // if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder
+
+ // it's very common for users to write code prefixed with '$'
+ // but to modify and override existing patterns, the patterns must be labeled,
+ // otherwise we'll have no idea of which pattern is being overridden
+
+ // (we probably need to update the docs on this)
+
+ // we could easily enable it, but it would confuse a lot of people
+ throw new Error(
+ 'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)',
+ );
+ }
+
+ // For block-based evaluation, collect from all blocks
+ // The highlight/widget/slider systems will handle selective updates based on the range
+ meta.miniLocations = collectFromBlocks('miniLocations');
+ meta.widgets = collectFromBlocks('widgets');
+ meta.sliders = collectFromBlocks('sliders');
+
+ // Track activeVisualizer cleanup: check if any block's visualizer was removed
+ const blocksToUpdate = labels.map((label) => label.name);
+
+ for (const [key, block] of Object.entries(codeBlocks)) {
+ if (blocksToUpdate.includes(key)) {
+ // This block was just updated
+ if (block.activeVisualizer !== null) {
+ // Block now has a visualizer, update tracking
+ lastActiveVisualizerLabel = key;
+ } else if (lastActiveVisualizerLabel === key) {
+ // This block lost its visualizer, trigger cleanup
+ widgetRemoved = true;
+ lastActiveVisualizerLabel = null;
+ }
+ }
+ }
+ }
+
+ const allPatterns = Object.values(pPatterns);
+
+ if (blockBased) {
+ pPatterns = Object.fromEntries(
+ Object.entries(pPatterns).filter(([key]) => {
+ return Object.keys(codeBlocks).includes(key);
+ }),
+ );
+ }
+
+ if (allPatterns.length) {
let patterns = [];
let soloActive = false;
for (const [key, value] of Object.entries(pPatterns)) {
@@ -250,18 +500,22 @@ export function repl({
if (!isPattern(pattern)) {
pattern = silence;
}
+
logger(`[eval] code updated`);
pattern = await setPattern(pattern, autostart);
updateState({
miniLocations: meta?.miniLocations || [],
widgets: meta?.widgets || [],
+ sliders: meta?.sliders || [],
activeCode: code,
pattern,
evalError: undefined,
schedulerError: undefined,
pending: false,
});
- afterEval?.({ code, pattern, meta });
+
+
+ afterEval?.({ code, pattern, meta, range: options.range, widgetRemoved });
return pattern;
} catch (err) {
logger(`[eval] error: ${err.message}`, 'error');
@@ -276,18 +530,18 @@ export function repl({
export const getTrigger =
({ getTime, defaultOutput }) =>
- async (hap, deadline, duration, cps, t) => {
- // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
- // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
- try {
- if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
- await defaultOutput(hap, deadline, duration, cps, t);
+ async (hap, deadline, duration, cps, t) => {
+ // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
+ // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
+ try {
+ if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
+ await defaultOutput(hap, deadline, duration, cps, t);
+ }
+ if (hap.context.onTrigger) {
+ // call signature of output / onTrigger is different...
+ await hap.context.onTrigger(hap, getTime(), cps, t);
+ }
+ } catch (err) {
+ errorLogger(err, 'getTrigger');
}
- if (hap.context.onTrigger) {
- // call signature of output / onTrigger is different...
- await hap.context.onTrigger(hap, getTime(), cps, t);
- }
- } catch (err) {
- errorLogger(err, 'getTrigger');
- }
- };
+ };
diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs
index 95f3aed5..514215f1 100644
--- a/packages/draw/draw.mjs
+++ b/packages/draw/draw.mjs
@@ -78,6 +78,16 @@ export const cleanupDraw = (clearScreen = true, id) => {
stopAllAnimations(id);
};
+export const cleanupDrawContext = (replID) => {
+ const ctx = getDrawContext();
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+
+ // clear the big canvas context, ignore inline widgets
+ Object.keys(animationFrames).forEach((id) =>
+ (!replID || id.startsWith(replID)) && !id.startsWith('_') && stopAnimationFrame(id)
+ );
+};
+
Pattern.prototype.onPaint = function (painter) {
return this.withState((state) => {
if (!state.controls.painters) {
diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs
index fea6bad4..467e6c53 100644
--- a/packages/transpiler/transpiler.mjs
+++ b/packages/transpiler/transpiler.mjs
@@ -19,7 +19,14 @@ export function registerLanguage(type, config) {
}
export function transpiler(input, options = {}) {
- const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
+ const {
+ wrapAsync = false,
+ addReturn = true,
+ emitMiniLocations = true,
+ emitWidgets = true,
+ blockBased = false,
+ range = []
+ } = options;
let ast = parse(input, {
ecmaVersion: 2022,
@@ -28,6 +35,13 @@ export function transpiler(input, options = {}) {
});
let miniLocations = [];
+
+ // Position offset for block-based evaluation
+ let nodeOffset = range && range.length > 0 ? range[0] : 0;
+
+ // Track declarations to add to strudelScope for block-based eval
+ let scopeDeclarations = [];
+
const collectMiniLocations = (value, node) => {
const minilang = languages.get('minilang');
if (minilang) {
@@ -40,9 +54,27 @@ export function transpiler(input, options = {}) {
}
};
let widgets = [];
+ let sliders = [];
walk(ast, {
enter(node, parent /* , prop, index */) {
+ // Apply position offset for block-based evaluation
+ if (blockBased && node.start !== undefined) {
+ node.start = node.start + nodeOffset;
+ node.end = node.end + nodeOffset;
+ }
+ // Collect variable and function declarations for strudelScope (block-based eval)
+ if (blockBased && parent?.type === 'Program') {
+ if (node.type === 'VariableDeclaration') {
+ for (const declarator of node.declarations) {
+ if (declarator.id?.name) {
+ scopeDeclarations.push(declarator.id.name);
+ }
+ }
+ } else if (node.type === 'FunctionDeclaration' && node.id?.name) {
+ scopeDeclarations.push(node.id.name);
+ }
+ }
if (isLanguageLiteral(node)) {
const { name } = node.tag;
const language = languages.get(name);
@@ -79,22 +111,29 @@ export function transpiler(input, options = {}) {
return this.replace(miniWithLocation(value, node));
}
if (isSliderFunction(node)) {
- emitWidgets &&
- widgets.push({
- from: node.arguments[0].start,
- to: node.arguments[0].end,
- value: node.arguments[0].raw, // don't use value!
- min: node.arguments[1]?.value ?? 0,
- max: node.arguments[2]?.value ?? 1,
- step: node.arguments[3]?.value,
- type: 'slider',
- });
- return this.replace(sliderWithLocation(node));
+ const from = node.arguments[0].start + nodeOffset;
+ const to = node.arguments[0].end + nodeOffset;
+ const id = `${from}:${to}`; // Range-based ID for stability
+
+ const sliderConfig = {
+ from,
+ to,
+ id,
+ value: node.arguments[0].raw, // don't use value!
+ min: node.arguments[1]?.value ?? 0,
+ max: node.arguments[2]?.value ?? 1,
+ step: node.arguments[3]?.value,
+ type: 'slider',
+ };
+ emitWidgets && widgets.push(sliderConfig);
+ sliders.push(sliderConfig);
+ return this.replace(sliderWithLocation(node, nodeOffset));
}
if (isWidgetMethod(node)) {
const type = node.callee.property.name;
const index = widgets.filter((w) => w.type === type).length;
const widgetConfig = {
+ from: node.start,
to: node.end,
index,
type,
@@ -115,17 +154,33 @@ export function transpiler(input, options = {}) {
let { body } = ast;
+ const silenceExpression = {
+ type: 'ExpressionStatement',
+ expression: {
+ type: 'Identifier',
+ name: 'silence',
+ },
+ };
+
if (!body.length) {
console.warn('empty body -> fallback to silence');
- body.push({
- type: 'ExpressionStatement',
- expression: {
- type: 'Identifier',
- name: 'silence',
- },
- });
+ body.push(silenceExpression);
} else if (!body?.[body.length - 1]?.expression) {
- throw new Error('unexpected ast format without body expression');
+ // Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration)
+ if (blockBased) {
+ // For block-based eval, add silence as the return value when block ends with declaration
+ body.push(silenceExpression);
+ } else {
+ throw new Error('unexpected ast format without body expression');
+ }
+ }
+
+ // For block-based eval, add scope assignments before the return statement
+ // This allows variables/functions defined in one block to be used in other blocks
+ if (blockBased && scopeDeclarations.length > 0) {
+ const scopeAssignments = scopeDeclarations.flatMap((name) => createScopeAssignment(name));
+ // Insert scope assignments before the last statement (which will become the return)
+ body.splice(body.length - 1, 0, ...scopeAssignments);
}
// add return to last statement
@@ -143,7 +198,7 @@ export function transpiler(input, options = {}) {
if (!emitMiniLocations) {
return { output };
}
- return { output, miniLocations, widgets };
+ return { output, miniLocations, widgets, sliders };
}
function isStringWithDoubleQuotes(node, locations, code) {
@@ -190,8 +245,14 @@ function isWidgetMethod(node) {
return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name);
}
-function sliderWithLocation(node) {
- const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id
+function sliderWithLocation(node, nodeOffset = 0) {
+ // Apply nodeOffset for block-based evaluation to generate correct range
+ const from = node.arguments[0].start + nodeOffset;
+ const to = node.arguments[0].end + nodeOffset;
+
+ // Use range-based ID for stability during block evaluation
+ const id = `${from}:${to}`;
+
// add loc as identifier to first argument
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
node.arguments.unshift({
@@ -206,14 +267,26 @@ function sliderWithLocation(node) {
export function getWidgetID(widgetConfig) {
// the widget id is used as id for the dom element + as key for eventual resources
// for example, for each scope widget, a new analyser + buffer (large) is created
- // that means, if we use the index index of line position as id, less garbage is generated
- // return `widget_${widgetConfig.to}`; // more gargabe
- //return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage
- return `${widgetConfig.id || ''}_widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage
+ // Update: use range-based ID generation for better stability during block evaluation
+ // When we have both from and to, use them together for stability
+ // Otherwise fall back to position-based ID for backward compatibility
+ let uniqueIdentifier;
+ if (widgetConfig.from !== undefined && widgetConfig.to !== undefined) {
+ // Use range for more stable identification
+ uniqueIdentifier = `${widgetConfig.from}-${widgetConfig.to}`;
+ } else {
+ // Fallback to single position (for backward compatibility)
+ uniqueIdentifier = widgetConfig.to || widgetConfig.from || 0;
+ }
+ const baseId = `${widgetConfig.id || ''}_widget_${widgetConfig.type}`;
+ return `${baseId}_${widgetConfig.index}_${uniqueIdentifier}`;
}
function widgetWithLocation(node, widgetConfig) {
const id = getWidgetID(widgetConfig);
+ // Store the unique ID back into the config so it's available for widget management
+ // This is critical for block-based evaluation to match existing widgets with new ones
+ widgetConfig.id = id;
// add loc as identifier to first argument
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
node.arguments.unshift({
@@ -327,3 +400,85 @@ function languageWithLocation(name, value, offset) {
optional: false,
};
}
+
+// Creates AST nodes for: userDefinedKeys.add('name'); strudelScope.name = name; globalThis.name = name;
+// Used in block-based evaluation to persist variables/functions across blocks
+// We add to both strudelScope (for internal lookups) and globalThis (for direct access)
+// We also track the key in userDefinedKeys so clearScope() can remove it later
+function createScopeAssignment(name) {
+ return [
+ // userDefinedKeys.add('name');
+ {
+ type: 'ExpressionStatement',
+ expression: {
+ type: 'CallExpression',
+ callee: {
+ type: 'MemberExpression',
+ object: {
+ type: 'Identifier',
+ name: 'userDefinedKeys',
+ },
+ property: {
+ type: 'Identifier',
+ name: 'add',
+ },
+ computed: false,
+ },
+ arguments: [
+ {
+ type: 'Literal',
+ value: name,
+ },
+ ],
+ },
+ },
+ // strudelScope.name = name;
+ {
+ type: 'ExpressionStatement',
+ expression: {
+ type: 'AssignmentExpression',
+ operator: '=',
+ left: {
+ type: 'MemberExpression',
+ object: {
+ type: 'Identifier',
+ name: 'strudelScope',
+ },
+ property: {
+ type: 'Identifier',
+ name: name,
+ },
+ computed: false,
+ },
+ right: {
+ type: 'Identifier',
+ name: name,
+ },
+ },
+ },
+ // globalThis.name = name;
+ {
+ type: 'ExpressionStatement',
+ expression: {
+ type: 'AssignmentExpression',
+ operator: '=',
+ left: {
+ type: 'MemberExpression',
+ object: {
+ type: 'Identifier',
+ name: 'globalThis',
+ },
+ property: {
+ type: 'Identifier',
+ name: name,
+ },
+ computed: false,
+ },
+ right: {
+ type: 'Identifier',
+ name: name,
+ },
+ },
+ },
+ ];
+}
diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx
index f46fa661..4e81aa00 100644
--- a/website/src/repl/components/panel/SettingsTab.jsx
+++ b/website/src/repl/components/panel/SettingsTab.jsx
@@ -116,6 +116,7 @@ export function SettingsTab({ started }) {
isMultiCursorEnabled,
patternAutoStart,
includePrebakeScriptInShare,
+ isBlockBasedEvalEnabled,
} = useSettings();
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
@@ -288,6 +289,11 @@ export function SettingsTab({ started }) {
onChange={(cbEvent) => settingsMap.setKey('isMultiCursorEnabled', cbEvent.target.checked)}
value={isMultiCursorEnabled}
/>
+ settingsMap.setKey('isBlockBasedEvalEnabled', cbEvent.target.checked)}
+ value={isBlockBasedEvalEnabled}
+ />
settingsMap.setKey('isFlashEnabled', cbEvent.target.checked)}
diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx
index 0fbd89e3..0d44eedf 100644
--- a/website/src/repl/useReplContext.jsx
+++ b/website/src/repl/useReplContext.jsx
@@ -106,17 +106,19 @@ export function useReplContext() {
//post to iframe parent (like Udels) if it exists...
window.parent?.postMessage(code);
- setLatestCode(code);
- window.location.hash = '#' + code2hash(code);
- setDocumentTitle(code);
+ // Get the full buffer content from the editor instead of just the evaluated block
+ const fullBufferCode = editorRef.current?.code || code;
+ setLatestCode(fullBufferCode);
+ window.location.hash = '#' + code2hash(fullBufferCode);
+ setDocumentTitle(fullBufferCode);
const viewingPatternData = getViewingPatternData();
- setVersionDefaultsFrom(code);
- const data = { ...viewingPatternData, code };
+ setVersionDefaultsFrom(fullBufferCode);
+ const data = { ...viewingPatternData, code: fullBufferCode };
let id = data.id;
const isExamplePattern = viewingPatternData.collection !== userPattern.collection;
if (isExamplePattern) {
- const codeHasChanged = code !== viewingPatternData.code;
+ const codeHasChanged = fullBufferCode !== viewingPatternData.code;
if (codeHasChanged) {
// fork example
const newPattern = userPattern.duplicate(data);
@@ -168,9 +170,8 @@ export function useReplContext() {
useEffect(() => {
let editorSettings = {};
Object.keys(defaultSettings).forEach((key) => {
- if (Object.prototype.hasOwnProperty.call(_settings, key)) {
- editorSettings[key] = _settings[key];
- }
+ // Don't use hasOwnProperty - nanostore uses proxies so values may not be own properties
+ editorSettings[key] = _settings[key];
});
editorRef.current?.updateSettings(editorSettings);
}, [_settings]);
diff --git a/website/src/settings.mjs b/website/src/settings.mjs
index 8ec7d455..9094cba9 100644
--- a/website/src/settings.mjs
+++ b/website/src/settings.mjs
@@ -32,6 +32,7 @@ export const defaultSettings = {
isPatternHighlightingEnabled: true,
isTabIndentationEnabled: false,
isMultiCursorEnabled: false,
+ isBlockBasedEvalEnabled: false,
theme: 'strudelTheme',
fontFamily: 'monospace',
fontSize: 18,
@@ -92,6 +93,7 @@ export function useSettings() {
isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled),
isTabIndentationEnabled: parseBoolean(state.isTabIndentationEnabled),
isMultiCursorEnabled: parseBoolean(state.isMultiCursorEnabled),
+ isBlockBasedEvalEnabled: parseBoolean(state.isBlockBasedEvalEnabled),
fontSize: Number(state.fontSize),
panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is!
isPanelPinned: parseBoolean(state.isPanelPinned),
From ba7721f53c47644368fc7d17801cd60fa817c190 Mon Sep 17 00:00:00 2001
From: Dsm0
Date: Tue, 23 Dec 2025 02:25:18 -0800
Subject: [PATCH 026/200] code formatting
---
packages/codemirror/codemirror.mjs | 1 -
packages/core/repl.mjs | 44 ++++++++++++++----------------
packages/draw/draw.mjs | 4 +--
packages/transpiler/transpiler.mjs | 2 +-
website/astro.config.mjs | 9 ++++--
5 files changed, 30 insertions(+), 30 deletions(-)
diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs
index 71986ba4..d0115e13 100644
--- a/packages/codemirror/codemirror.mjs
+++ b/packages/codemirror/codemirror.mjs
@@ -190,7 +190,6 @@ export class StrudelMirror {
this.onDraw(haps, time, painters);
}, drawTime);
-
this.prebaked = prebake();
autodraw && this.drawFirstFrame();
this.repl = repl({
diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs
index 4e0af7ab..4c730515 100644
--- a/packages/core/repl.mjs
+++ b/packages/core/repl.mjs
@@ -172,9 +172,9 @@ export function repl({
// These are Pattern.prototype methods that create persistent visualizations
// I don't like this approach, feels hacky, but since these methods don't create ids that
// can be read through the transpiler, I'm not sure how best to detect them.
- // Would probably be better to register these methods through some function that would
+ // Would probably be better to register these methods through some function that would
// tag them better
- const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall',];
+ const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall'];
for (const widget of nonInlineWidgets) {
const widgetRegex = new RegExp(`\\.${widget}\\s*\\(`);
@@ -187,17 +187,15 @@ export function repl({
// Helper function to handle single label code block storage
function handleSingleLabelBlock(label, code, options, meta) {
-
// Detect if this block contains a non-inline widget
// For 'all' label, widget info is already on the label object from extractLabelsFromCode
- // As mentioned in detectActiveVisualizer, this is a bad approach to
+ // As mentioned in detectActiveVisualizer, this is a bad approach to
// managing non-inline widgets (or widgets that don't have ids)
// and a proper solution would give them widgets in the transpiler, or at least
// track where they are so we don't have to futz around with regexes
- const activeVisualizer = label.activeVisualizer !== undefined
- ? label.activeVisualizer
- : detectActiveVisualizer(code);
+ const activeVisualizer =
+ label.activeVisualizer !== undefined ? label.activeVisualizer : detectActiveVisualizer(code);
if (activeVisualizer !== null) {
lastActiveVisualizerLabel = label.name;
@@ -214,7 +212,6 @@ export function repl({
activeVisualizer: activeVisualizer, // Store the widget type if present, null otherwise
};
-
// Clean up any blocks with conflicting ranges
for (const [existingKey, existingBlock] of Object.entries(codeBlocks)) {
if (existingKey !== label.name && existingBlock.range && options.range) {
@@ -412,7 +409,7 @@ export function repl({
if (labels.length > 0) {
for (let i = 0; i < labels.length; i++) {
// processLabeledBlock(labels, i, code, options, meta);
- // processing transpiler output instead of code is simply to avoid
+ // processing transpiler output instead of code is simply to avoid
// extra regex in detecting whether or not an inline widget has been commented out
processLabeledBlock(labels, i, meta.output, options, meta);
}
@@ -514,7 +511,6 @@ export function repl({
pending: false,
});
-
afterEval?.({ code, pattern, meta, range: options.range, widgetRemoved });
return pattern;
} catch (err) {
@@ -530,18 +526,18 @@ export function repl({
export const getTrigger =
({ getTime, defaultOutput }) =>
- async (hap, deadline, duration, cps, t) => {
- // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
- // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
- try {
- if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
- await defaultOutput(hap, deadline, duration, cps, t);
- }
- if (hap.context.onTrigger) {
- // call signature of output / onTrigger is different...
- await hap.context.onTrigger(hap, getTime(), cps, t);
- }
- } catch (err) {
- errorLogger(err, 'getTrigger');
+ async (hap, deadline, duration, cps, t) => {
+ // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
+ // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
+ try {
+ if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
+ await defaultOutput(hap, deadline, duration, cps, t);
}
- };
+ if (hap.context.onTrigger) {
+ // call signature of output / onTrigger is different...
+ await hap.context.onTrigger(hap, getTime(), cps, t);
+ }
+ } catch (err) {
+ errorLogger(err, 'getTrigger');
+ }
+ };
diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs
index 514215f1..f915e911 100644
--- a/packages/draw/draw.mjs
+++ b/packages/draw/draw.mjs
@@ -83,8 +83,8 @@ export const cleanupDrawContext = (replID) => {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// clear the big canvas context, ignore inline widgets
- Object.keys(animationFrames).forEach((id) =>
- (!replID || id.startsWith(replID)) && !id.startsWith('_') && stopAnimationFrame(id)
+ Object.keys(animationFrames).forEach(
+ (id) => (!replID || id.startsWith(replID)) && !id.startsWith('_') && stopAnimationFrame(id),
);
};
diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs
index 467e6c53..24dd3f1f 100644
--- a/packages/transpiler/transpiler.mjs
+++ b/packages/transpiler/transpiler.mjs
@@ -25,7 +25,7 @@ export function transpiler(input, options = {}) {
emitMiniLocations = true,
emitWidgets = true,
blockBased = false,
- range = []
+ range = [],
} = options;
let ast = parse(input, {
diff --git a/website/astro.config.mjs b/website/astro.config.mjs
index 151c48fc..b5921d55 100644
--- a/website/astro.config.mjs
+++ b/website/astro.config.mjs
@@ -10,8 +10,13 @@ import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
import tailwind from '@astrojs/tailwind';
import AstroPWA from '@vite-pwa/astro';
-const site = `https://strudel.cc/`; // root url without a path
-const base = '/'; // base path of the strudel site
+import process from 'node:process';
+
+const site = process.env.SITE_URL || `https://strudel.cc/`; // root url without a path
+const base = process.env.BASE_PATH || ''; // base path of the strudel site
+
+// const site = `https://strudel.cc/`; // root url without a path
+// const base = '/'; // base path of the strudel site
const baseNoTrailing = base.endsWith('/') ? base.slice(0, -1) : base;
// this rehype plugin fixes relative links
From 4df19f9ff846c9bd62e640688cd51b71eb50632b Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Wed, 24 Dec 2025 16:05:46 -0500
Subject: [PATCH 027/200] Save MIDI-in CC state
---
packages/midi/midi.mjs | 47 ++++++++++++++++++++++++++++++++++++++++--
1 file changed, 45 insertions(+), 2 deletions(-)
diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs
index cd38d358..8abfe6cd 100644
--- a/packages/midi/midi.mjs
+++ b/packages/midi/midi.mjs
@@ -481,6 +481,43 @@ let listeners = {};
const refs = {};
const refsByChan = {};
+/**
+ *
+ * @param {string} input
+ * @param {number|undefined} chan
+ * @returns {Object}
+ */
+function getMidinState(input, chan) {
+ const initialDataRaw = localStorage.getItem(`strudel-midin-${input}-chan${chan !== undefined ? chan : 'all'}`);
+ if (!initialDataRaw) {
+ return {};
+ }
+
+ try {
+ return JSON.parse(initialDataRaw);
+ } catch (err) {
+ console.warn(
+ `Failed to parse MIDI state from localStorage for input "${input}" and channel "${chan}"`,
+ initialDataRaw,
+ err,
+ );
+ return {};
+ }
+}
+
+/**
+ *
+ * @param {string} input
+ * @param {number|undefined} chan
+ * @param {number} cc
+ * @param {number} value
+ */
+function saveMidinState(input, chan, cc, value) {
+ const state = getMidinState(input, chan);
+ state[cc] = value;
+ localStorage.setItem(`strudel-midin-${input}-chan${chan !== undefined ? chan : 'all'}`, JSON.stringify(state));
+}
+
/**
* MIDI input: Opens a MIDI input port to receive MIDI control change messages.
*
@@ -524,10 +561,13 @@ export async function midin(input) {
refs[input] ??= {};
refsByChan[input] ??= {};
const cc = (cc, chan) => {
+ const initialState = getMidinState(input, chan);
+ const initialValue = initialState[cc] || 0;
+
if (chan !== undefined) {
- return ref(() => refsByChan[input][cc]?.[chan] || 0);
+ return ref(() => refsByChan[input][cc]?.[chan] || initialValue);
}
- return ref(() => refs[input][cc] || 0);
+ return ref(() => refs[input][cc] || initialValue);
};
listeners[input] && device.removeListener('midimessage', listeners[input]);
@@ -539,6 +579,9 @@ export async function midin(input) {
refsByChan[input][ccNum] ??= {};
refsByChan[input][ccNum][chan] = scaled;
refs[input][ccNum] = scaled;
+
+ saveMidinState(input, undefined, ccNum, scaled);
+ saveMidinState(input, chan, ccNum, scaled);
};
device.addListener('midimessage', listeners[input]);
return cc;
From 84e59645fd630f13ccb478c24876becd5e7257dd Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Thu, 25 Dec 2025 18:27:28 -0500
Subject: [PATCH 028/200] Encapsulate MIDI input handling
---
packages/midi/midi.mjs | 80 ++++++++++++++++++++++++++----------------
1 file changed, 50 insertions(+), 30 deletions(-)
diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs
index 8abfe6cd..8d54d768 100644
--- a/packages/midi/midi.mjs
+++ b/packages/midi/midi.mjs
@@ -477,10 +477,6 @@ Pattern.prototype.midi = function (midiport, options = {}) {
});
};
-let listeners = {};
-const refs = {};
-const refsByChan = {};
-
/**
*
* @param {string} input
@@ -518,6 +514,51 @@ function saveMidinState(input, chan, cc, value) {
localStorage.setItem(`strudel-midin-${input}-chan${chan !== undefined ? chan : 'all'}`, JSON.stringify(state));
}
+class MidiInput {
+ /**
+ *
+ * @param {string | number} input MIDI device name or index defaulting to 0
+ * @param {WebMidi.MIDIInput | undefined} device MIDI input device instance (or empty to use as placeholder)
+ */
+ constructor(device) {
+ this.id = device.id;
+ this.name = device.name;
+
+ this._refs = {};
+ this._refsByChan = {};
+
+ this.cc = (cc, chan) => {
+ const initialState = getMidinState(this.name, chan);
+ const initialValue = initialState[cc] || 0;
+
+ if (chan !== undefined) {
+ return ref(() => this._refsByChan[cc]?.[chan] || initialValue);
+ }
+ return ref(() => this._refs[cc] || initialValue);
+ };
+
+ const midiListener = this._onMidiMessage.bind(this);
+ device.addListener('midimessage', midiListener);
+ }
+
+ _onMidiMessage(e) {
+ const ccNum = e.dataBytes[0];
+ const v = e.dataBytes[1];
+ const chan = e.message.channel;
+ const scaled = v / 127;
+
+ this._refs[ccNum] = scaled;
+ this._refsByChan[ccNum] ??= {};
+ this._refsByChan[ccNum][chan] = scaled;
+
+ saveMidinState(this.name, undefined, ccNum, scaled);
+ saveMidinState(this.name, chan, ccNum, scaled);
+ }
+}
+
+// MIDI input wrappers, by device ID
+const midiInputs = {};
+
/**
* MIDI input: Opens a MIDI input port to receive MIDI control change messages.
*
@@ -550,6 +591,10 @@ export async function midin(input) {
`midiin: device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
);
}
+
+ const instance = midiInputs[device.id] || new MidiInput(device);
+ midiInputs[device.id] = instance;
+
if (initial) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
@@ -558,31 +603,6 @@ export async function midin(input) {
}`,
);
}
- refs[input] ??= {};
- refsByChan[input] ??= {};
- const cc = (cc, chan) => {
- const initialState = getMidinState(input, chan);
- const initialValue = initialState[cc] || 0;
- if (chan !== undefined) {
- return ref(() => refsByChan[input][cc]?.[chan] || initialValue);
- }
- return ref(() => refs[input][cc] || initialValue);
- };
-
- listeners[input] && device.removeListener('midimessage', listeners[input]);
- listeners[input] = (e) => {
- const ccNum = e.dataBytes[0];
- const v = e.dataBytes[1];
- const chan = e.message.channel;
- const scaled = v / 127;
- refsByChan[input][ccNum] ??= {};
- refsByChan[input][ccNum][chan] = scaled;
- refs[input][ccNum] = scaled;
-
- saveMidinState(input, undefined, ccNum, scaled);
- saveMidinState(input, chan, ccNum, scaled);
- };
- device.addListener('midimessage', listeners[input]);
- return cc;
+ return instance.cc;
}
From 213edfb211e08cc7ea9fd2649b50b875ad61fc5c Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Thu, 25 Dec 2025 18:32:53 -0500
Subject: [PATCH 029/200] Roll up save/load state into MIDI wrapper
---
packages/midi/midi.mjs | 67 +++++++++++++++++-------------------------
1 file changed, 27 insertions(+), 40 deletions(-)
diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs
index 8d54d768..27c865e7 100644
--- a/packages/midi/midi.mjs
+++ b/packages/midi/midi.mjs
@@ -477,43 +477,6 @@ Pattern.prototype.midi = function (midiport, options = {}) {
});
};
-/**
- *
- * @param {string} input
- * @param {number|undefined} chan
- * @returns {Object}
- */
-function getMidinState(input, chan) {
- const initialDataRaw = localStorage.getItem(`strudel-midin-${input}-chan${chan !== undefined ? chan : 'all'}`);
- if (!initialDataRaw) {
- return {};
- }
-
- try {
- return JSON.parse(initialDataRaw);
- } catch (err) {
- console.warn(
- `Failed to parse MIDI state from localStorage for input "${input}" and channel "${chan}"`,
- initialDataRaw,
- err,
- );
- return {};
- }
-}
-
-/**
- *
- * @param {string} input
- * @param {number|undefined} chan
- * @param {number} cc
- * @param {number} value
- */
-function saveMidinState(input, chan, cc, value) {
- const state = getMidinState(input, chan);
- state[cc] = value;
- localStorage.setItem(`strudel-midin-${input}-chan${chan !== undefined ? chan : 'all'}`, JSON.stringify(state));
-}
-
class MidiInput {
/**
*
@@ -528,7 +491,7 @@ class MidiInput {
this._refsByChan = {};
this.cc = (cc, chan) => {
- const initialState = getMidinState(this.name, chan);
+ const initialState = this._loadState(chan);
const initialValue = initialState[cc] || 0;
if (chan !== undefined) {
@@ -551,8 +514,32 @@ class MidiInput {
this._refsByChan[ccNum] ??= {};
this._refsByChan[ccNum][chan] = scaled;
- saveMidinState(this.name, undefined, ccNum, scaled);
- saveMidinState(this.name, chan, ccNum, scaled);
+ this._saveState(undefined, ccNum, scaled);
+ this._saveState(chan, ccNum, scaled);
+ }
+
+ _loadState(chan) {
+ const initialDataRaw = localStorage.getItem(`strudel-midin-${this.name}-chan${chan !== undefined ? chan : 'all'}`);
+ if (!initialDataRaw) {
+ return {};
+ }
+
+ try {
+ return JSON.parse(initialDataRaw);
+ } catch (err) {
+ console.warn(
+ `Failed to parse MIDI state from localStorage for input "${this.name}" and channel "${chan}"`,
+ initialDataRaw,
+ err,
+ );
+ return {};
+ }
+ }
+
+ _saveState(chan, cc, value) {
+ const state = this._loadState(chan);
+ state[cc] = value;
+ localStorage.setItem(`strudel-midin-${this.name}-chan${chan !== undefined ? chan : 'all'}`, JSON.stringify(state));
}
}
From 47557cf06ff06e94e6407ba62955a7c5768c71ec Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Thu, 25 Dec 2025 18:38:55 -0500
Subject: [PATCH 030/200] Move device lookup inside input wrapper
---
packages/midi/midi.mjs | 22 ++++++++++++----------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs
index 27c865e7..97226dc5 100644
--- a/packages/midi/midi.mjs
+++ b/packages/midi/midi.mjs
@@ -481,11 +481,18 @@ class MidiInput {
/**
*
* @param {string | number} input MIDI device name or index defaulting to 0
- * @param {WebMidi.MIDIInput | undefined} device MIDI input device instance (or empty to use as placeholder)
*/
- constructor(device) {
+ constructor(input) {
+ const device = getDevice(input, WebMidi.inputs);
+ if (!device) {
+ throw new Error(
+ `midiin: device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
+ );
+ }
+
this.id = device.id;
this.name = device.name;
+ this.device = device;
this._refs = {};
this._refsByChan = {};
@@ -572,15 +579,10 @@ export async function midin(input) {
);
}
const initial = await enableWebMidi(); // only returns on first init
- const device = getDevice(input, WebMidi.inputs);
- if (!device) {
- throw new Error(
- `midiin: device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
- );
- }
- const instance = midiInputs[device.id] || new MidiInput(device);
- midiInputs[device.id] = instance;
+ const instance = midiInputs[input] || new MidiInput(input);
+ midiInputs[input] = instance;
+ const device = instance.device;
if (initial) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
From 1860d31624d0cb1e7186bf273acb09cedb5ee184 Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Thu, 25 Dec 2025 18:46:39 -0500
Subject: [PATCH 031/200] Tweak cc init
---
packages/midi/midi.mjs | 26 +++++++++++++++-----------
1 file changed, 15 insertions(+), 11 deletions(-)
diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs
index 97226dc5..c2c08c07 100644
--- a/packages/midi/midi.mjs
+++ b/packages/midi/midi.mjs
@@ -497,20 +497,24 @@ class MidiInput {
this._refs = {};
this._refsByChan = {};
- this.cc = (cc, chan) => {
- const initialState = this._loadState(chan);
- const initialValue = initialState[cc] || 0;
-
- if (chan !== undefined) {
- return ref(() => this._refsByChan[cc]?.[chan] || initialValue);
- }
- return ref(() => this._refs[cc] || initialValue);
- };
-
const midiListener = this._onMidiMessage.bind(this);
device.addListener('midimessage', midiListener);
}
+ createCC(cc, chan) {
+ if (chan !== undefined && !(chan in this._refsByChan)) {
+ this._refsByChan[chan] = {};
+ }
+
+ const lookupMap = chan === undefined ? this._refs : this._refsByChan[chan];
+ if (!(cc in lookupMap)) {
+ const initialState = this._loadState(chan);
+ lookupMap[cc] = initialState[cc] || 0;
+ }
+
+ return ref(() => lookupMap[cc]);
+ }
+
_onMidiMessage(e) {
const ccNum = e.dataBytes[0];
const v = e.dataBytes[1];
@@ -593,5 +597,5 @@ export async function midin(input) {
);
}
- return instance.cc;
+ return instance.createCC.bind(instance);
}
From 1aaa04ab50af665a612ca0f295a4153a2054724d Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Thu, 25 Dec 2025 18:58:13 -0500
Subject: [PATCH 032/200] Send stored CC values to controller
---
packages/midi/midi.mjs | 31 +++++++++++++++++++++++++++----
1 file changed, 27 insertions(+), 4 deletions(-)
diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs
index c2c08c07..859c54f9 100644
--- a/packages/midi/midi.mjs
+++ b/packages/midi/midi.mjs
@@ -499,13 +499,16 @@ class MidiInput {
const midiListener = this._onMidiMessage.bind(this);
device.addListener('midimessage', midiListener);
+
+ this._loadAllStates();
+
+ const output = WebMidi.outputs.find((o) => o.name === this.name);
+ if (output) {
+ this._sendAllStates(output);
+ }
}
createCC(cc, chan) {
- if (chan !== undefined && !(chan in this._refsByChan)) {
- this._refsByChan[chan] = {};
- }
-
const lookupMap = chan === undefined ? this._refs : this._refsByChan[chan];
if (!(cc in lookupMap)) {
const initialState = this._loadState(chan);
@@ -529,6 +532,15 @@ class MidiInput {
this._saveState(chan, ccNum, scaled);
}
+ _loadAllStates() {
+ Object.assign(this._refs, this._loadState(undefined));
+
+ for (let chan = 1; chan <= 16; chan++) {
+ this._refsByChan[chan] ??= {};
+ Object.assign(this._refsByChan[chan], this._loadState(chan));
+ }
+ }
+
_loadState(chan) {
const initialDataRaw = localStorage.getItem(`strudel-midin-${this.name}-chan${chan !== undefined ? chan : 'all'}`);
if (!initialDataRaw) {
@@ -552,6 +564,17 @@ class MidiInput {
state[cc] = value;
localStorage.setItem(`strudel-midin-${this.name}-chan${chan !== undefined ? chan : 'all'}`, JSON.stringify(state));
}
+
+ _sendAllStates(output) {
+ for (const [chan, refs] of Object.entries(this._refsByChan)) {
+ const channel = Number(chan);
+ for (const [cc, value] of Object.entries(refs)) {
+ const ccn = Number(cc);
+ const scaled = Math.round(value * 127);
+ output.sendControlChange(ccn, scaled, channel);
+ }
+ }
+ }
}
// MIDI input wrappers, by device ID
From b8dc50267e44ccaa39ddd2b638ccc34420476450 Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Thu, 25 Dec 2025 19:02:06 -0500
Subject: [PATCH 033/200] Clean up unused state fields
---
packages/midi/midi.mjs | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs
index 859c54f9..49a87ac1 100644
--- a/packages/midi/midi.mjs
+++ b/packages/midi/midi.mjs
@@ -490,9 +490,7 @@ class MidiInput {
);
}
- this.id = device.id;
this.name = device.name;
- this.device = device;
this._refs = {};
this._refsByChan = {};
@@ -502,7 +500,7 @@ class MidiInput {
this._loadAllStates();
- const output = WebMidi.outputs.find((o) => o.name === this.name);
+ const output = WebMidi.outputs.find((o) => o.name === device.name);
if (output) {
this._sendAllStates(output);
}
@@ -577,7 +575,7 @@ class MidiInput {
}
}
-// MIDI input wrappers, by device ID
+// MIDI input wrappers, by specified input string/index
const midiInputs = {};
/**
@@ -609,12 +607,11 @@ export async function midin(input) {
const instance = midiInputs[input] || new MidiInput(input);
midiInputs[input] = instance;
- const device = instance.device;
if (initial) {
- const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
+ const otherInputs = WebMidi.inputs.filter((o) => o.name !== instance.name);
logger(
- `Midi enabled! Using "${device.name}". ${
+ `Midi enabled! Using "${instance.name}". ${
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`,
);
From 0c01a997ac3e21227ac092248c4d20e06e422017 Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Thu, 25 Dec 2025 19:06:19 -0500
Subject: [PATCH 034/200] Avoid referencing found MIDI device name
---
packages/midi/midi.mjs | 23 ++++++++++++++++++-----
1 file changed, 18 insertions(+), 5 deletions(-)
diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs
index 49a87ac1..eb6ca12e 100644
--- a/packages/midi/midi.mjs
+++ b/packages/midi/midi.mjs
@@ -483,6 +483,8 @@ class MidiInput {
* @param {string | number} input MIDI device name or index defaulting to 0
*/
constructor(input) {
+ this.stateKey = typeof input === 'string' ? input : undefined;
+
const device = getDevice(input, WebMidi.inputs);
if (!device) {
throw new Error(
@@ -490,8 +492,6 @@ class MidiInput {
);
}
- this.name = device.name;
-
this._refs = {};
this._refsByChan = {};
@@ -540,7 +540,13 @@ class MidiInput {
}
_loadState(chan) {
- const initialDataRaw = localStorage.getItem(`strudel-midin-${this.name}-chan${chan !== undefined ? chan : 'all'}`);
+ if (!this.stateKey) {
+ return {};
+ }
+
+ const initialDataRaw = localStorage.getItem(
+ `strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`,
+ );
if (!initialDataRaw) {
return {};
}
@@ -549,7 +555,7 @@ class MidiInput {
return JSON.parse(initialDataRaw);
} catch (err) {
console.warn(
- `Failed to parse MIDI state from localStorage for input "${this.name}" and channel "${chan}"`,
+ `Failed to parse MIDI state from localStorage for input "${this.stateKey}" and channel "${chan}"`,
initialDataRaw,
err,
);
@@ -558,9 +564,16 @@ class MidiInput {
}
_saveState(chan, cc, value) {
+ if (!this.stateKey) {
+ return;
+ }
+
const state = this._loadState(chan);
state[cc] = value;
- localStorage.setItem(`strudel-midin-${this.name}-chan${chan !== undefined ? chan : 'all'}`, JSON.stringify(state));
+ localStorage.setItem(
+ `strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`,
+ JSON.stringify(state),
+ );
}
_sendAllStates(output) {
From dab1a018e4cf0e7c35ea3c1317c303df1d4c1b5e Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Thu, 25 Dec 2025 19:52:07 -0500
Subject: [PATCH 035/200] Support delayed connection and reconnection of MIDI
inputs
---
packages/midi/midi.mjs | 105 +++++++++++++++++++++++++++++++----------
1 file changed, 80 insertions(+), 25 deletions(-)
diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs
index eb6ca12e..56e7f005 100644
--- a/packages/midi/midi.mjs
+++ b/packages/midi/midi.mjs
@@ -59,9 +59,6 @@ export function enableWebMidi(options = {}) {
}
function getDevice(indexOrName, devices) {
- if (!devices.length) {
- throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
- }
if (typeof indexOrName === 'number') {
return devices[indexOrName];
}
@@ -73,12 +70,13 @@ function getDevice(indexOrName, devices) {
const IACOutput = byName('IAC');
const device = IACOutput ?? devices[0];
if (!device) {
- throw new Error(
- `🔌 MIDI device '${device ? device : ''}' not found. Use one of ${getMidiDeviceNamesString(devices)}`,
- );
+ if (!devices.length) {
+ throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
+ }
+ throw new Error(`🔌 Default MIDI device not found. Use one of ${getMidiDeviceNamesString(devices)}`);
}
- return IACOutput ?? devices[0];
+ return device;
}
// send start/stop messages to outputs when repl starts/stops
@@ -483,27 +481,15 @@ class MidiInput {
* @param {string | number} input MIDI device name or index defaulting to 0
*/
constructor(input) {
+ this.input = input;
this.stateKey = typeof input === 'string' ? input : undefined;
- const device = getDevice(input, WebMidi.inputs);
- if (!device) {
- throw new Error(
- `midiin: device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
- );
- }
-
this._refs = {};
this._refsByChan = {};
- const midiListener = this._onMidiMessage.bind(this);
- device.addListener('midimessage', midiListener);
-
this._loadAllStates();
- const output = WebMidi.outputs.find((o) => o.name === device.name);
- if (output) {
- this._sendAllStates(output);
- }
+ this.initialDevice = this._startDeviceListener();
}
createCC(cc, chan) {
@@ -516,6 +502,71 @@ class MidiInput {
return ref(() => lookupMap[cc]);
}
+ _startDeviceListener() {
+ const initialDevice = getDevice(this.input, WebMidi.inputs);
+ if (!initialDevice) {
+ console.warn(`midiin: device "${this.input}" not found`);
+ }
+
+ // Background connection loop
+ (async () => {
+ const midiListener = this._onMidiMessage.bind(this);
+ let device = initialDevice;
+
+ while (true) {
+ if (!device) {
+ device = await this._waitForDevice();
+ }
+
+ console.log('midiin: device connected:', device.name);
+
+ // Wait a bit for device to be ready to receive last state
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+
+ try {
+ const output = WebMidi.outputs.find((o) => o.name === device.name);
+ if (output) {
+ this._sendAllStates(output);
+ }
+ } catch (err) {
+ console.error('midiin: failed to send last state on connect:', device.name, err);
+ }
+
+ // Listen for incoming MIDI messages and for disconnection
+ device.addListener('midimessage', midiListener);
+
+ await new Promise((resolve) => {
+ const disconnectListener = (e) => {
+ if (e.port.name === device.name) {
+ console.warn('midiin: device disconnected:', device.name);
+ WebMidi.removeListener('disconnected', disconnectListener);
+ resolve();
+ }
+ };
+ WebMidi.addListener('disconnected', disconnectListener);
+ });
+
+ device = null; // Clear var to trigger wait for connection
+ }
+ })();
+
+ return initialDevice;
+ }
+
+ _waitForDevice() {
+ return new Promise((resolve) => {
+ const connListener = () => {
+ const device = getDevice(this.input, WebMidi.inputs);
+ if (device) {
+ WebMidi.removeListener('connected', connListener);
+ resolve(device);
+ }
+ };
+
+ WebMidi.addListener('connected', connListener);
+ });
+ }
+
_onMidiMessage(e) {
const ccNum = e.dataBytes[0];
const v = e.dataBytes[1];
@@ -622,11 +673,15 @@ export async function midin(input) {
midiInputs[input] = instance;
if (initial) {
- const otherInputs = WebMidi.inputs.filter((o) => o.name !== instance.name);
+ const device = instance.initialDevice;
+
+ const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
- `Midi enabled! Using "${instance.name}". ${
- otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
- }`,
+ device
+ ? `Midi enabled! Using "${device.name}". ${
+ otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
+ }`
+ : `Midi enabled! Waiting for device "${input}"... Currently connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
);
}
From bb7f587025c071bc13e13904eb0084345d807b77 Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Thu, 25 Dec 2025 19:56:15 -0500
Subject: [PATCH 036/200] Encapsulate more logic
---
packages/midi/midi.mjs | 40 +++++++++++++++++++++++++---------------
1 file changed, 25 insertions(+), 15 deletions(-)
diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs
index 56e7f005..08defe7e 100644
--- a/packages/midi/midi.mjs
+++ b/packages/midi/midi.mjs
@@ -524,10 +524,8 @@ class MidiInput {
await new Promise((resolve) => setTimeout(resolve, 2000));
try {
- const output = WebMidi.outputs.find((o) => o.name === device.name);
- if (output) {
- this._sendAllStates(output);
- }
+ // Still continue if sending did not work
+ this._sendAllStates(device);
} catch (err) {
console.error('midiin: failed to send last state on connect:', device.name, err);
}
@@ -535,16 +533,10 @@ class MidiInput {
// Listen for incoming MIDI messages and for disconnection
device.addListener('midimessage', midiListener);
- await new Promise((resolve) => {
- const disconnectListener = (e) => {
- if (e.port.name === device.name) {
- console.warn('midiin: device disconnected:', device.name);
- WebMidi.removeListener('disconnected', disconnectListener);
- resolve();
- }
- };
- WebMidi.addListener('disconnected', disconnectListener);
- });
+ await this._waitForDeviceDisconnect(device);
+
+ console.warn('midiin: device disconnected:', device.name);
+ device.removeListener('midimessage', midiListener);
device = null; // Clear var to trigger wait for connection
}
@@ -567,6 +559,19 @@ class MidiInput {
});
}
+ _waitForDeviceDisconnect(device) {
+ return new Promise((resolve) => {
+ const disconnListener = (e) => {
+ if (e.port.name === device.name) {
+ WebMidi.removeListener('disconnected', disconnListener);
+ resolve();
+ }
+ };
+
+ WebMidi.addListener('disconnected', disconnListener);
+ });
+ }
+
_onMidiMessage(e) {
const ccNum = e.dataBytes[0];
const v = e.dataBytes[1];
@@ -627,7 +632,12 @@ class MidiInput {
);
}
- _sendAllStates(output) {
+ _sendAllStates(device) {
+ const output = WebMidi.outputs.find((o) => o.name === device.name);
+ if (!output) {
+ return;
+ }
+
for (const [chan, refs] of Object.entries(this._refsByChan)) {
const channel = Number(chan);
for (const [cc, value] of Object.entries(refs)) {
From ecada58edc9999f3dce3f23c5b8ec445243866fd Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Thu, 25 Dec 2025 20:59:50 -0500
Subject: [PATCH 037/200] Carry out MIDI input wrapper into dedicated file
---
packages/midi/input.mjs | 183 ++++++++++++++++++++++++++++++++++++
packages/midi/midi.mjs | 203 +---------------------------------------
packages/midi/util.mjs | 38 ++++++++
3 files changed, 224 insertions(+), 200 deletions(-)
create mode 100644 packages/midi/input.mjs
create mode 100644 packages/midi/util.mjs
diff --git a/packages/midi/input.mjs b/packages/midi/input.mjs
new file mode 100644
index 00000000..aa16cfea
--- /dev/null
+++ b/packages/midi/input.mjs
@@ -0,0 +1,183 @@
+/*
+midi.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 { WebMidi } from 'webmidi';
+import { ref } from '@strudel/core';
+import { getDevice } from './util.mjs';
+
+export class MidiInput {
+ /**
+ *
+ * @param {string | number} input MIDI device name or index defaulting to 0
+ */
+ constructor(input) {
+ this.input = input;
+ this.stateKey = typeof input === 'string' ? input : undefined;
+
+ this._refs = {};
+ this._refsByChan = {};
+
+ this._loadAllStates();
+
+ this.initialDevice = this._startDeviceListener();
+ }
+
+ createCC(cc, chan) {
+ const lookupMap = chan === undefined ? this._refs : this._refsByChan[chan];
+ if (!(cc in lookupMap)) {
+ const initialState = this._loadState(chan);
+ lookupMap[cc] = initialState[cc] || 0;
+ }
+
+ return ref(() => lookupMap[cc]);
+ }
+
+ _startDeviceListener() {
+ const initialDevice = getDevice(this.input, WebMidi.inputs);
+ if (!initialDevice) {
+ console.warn(`midiin: device "${this.input}" not found`);
+ }
+
+ // Background connection loop
+ (async () => {
+ const midiListener = this._onMidiMessage.bind(this);
+ let device = initialDevice;
+
+ while (true) {
+ if (!device) {
+ device = await this._waitForDevice();
+ }
+
+ console.log('midiin: device connected:', device.name);
+
+ // Wait a bit for device to be ready to receive last state
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+
+ try {
+ // Still continue if sending did not work
+ this._sendAllStates(device);
+ } catch (err) {
+ console.error('midiin: failed to send last state on connect:', device.name, err);
+ }
+
+ // Listen for incoming MIDI messages and for disconnection
+ device.addListener('midimessage', midiListener);
+
+ await this._waitForDeviceDisconnect(device);
+
+ console.warn('midiin: device disconnected:', device.name);
+ device.removeListener('midimessage', midiListener);
+
+ device = null; // Clear var to trigger wait for connection
+ }
+ })();
+
+ return initialDevice;
+ }
+
+ _waitForDevice() {
+ return new Promise((resolve) => {
+ const connListener = () => {
+ const device = getDevice(this.input, WebMidi.inputs);
+ if (device) {
+ WebMidi.removeListener('connected', connListener);
+ resolve(device);
+ }
+ };
+
+ WebMidi.addListener('connected', connListener);
+ });
+ }
+
+ _waitForDeviceDisconnect(device) {
+ return new Promise((resolve) => {
+ const disconnListener = (e) => {
+ if (e.port.name === device.name) {
+ WebMidi.removeListener('disconnected', disconnListener);
+ resolve();
+ }
+ };
+
+ WebMidi.addListener('disconnected', disconnListener);
+ });
+ }
+
+ _onMidiMessage(e) {
+ const ccNum = e.dataBytes[0];
+ const v = e.dataBytes[1];
+ const chan = e.message.channel;
+ const scaled = v / 127;
+
+ this._refs[ccNum] = scaled;
+ this._refsByChan[ccNum] ??= {};
+ this._refsByChan[ccNum][chan] = scaled;
+
+ this._saveState(undefined, ccNum, scaled);
+ this._saveState(chan, ccNum, scaled);
+ }
+
+ _loadAllStates() {
+ Object.assign(this._refs, this._loadState(undefined));
+
+ for (let chan = 1; chan <= 16; chan++) {
+ this._refsByChan[chan] ??= {};
+ Object.assign(this._refsByChan[chan], this._loadState(chan));
+ }
+ }
+
+ _loadState(chan) {
+ if (!this.stateKey) {
+ return {};
+ }
+
+ const initialDataRaw = localStorage.getItem(
+ `strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`,
+ );
+ if (!initialDataRaw) {
+ return {};
+ }
+
+ try {
+ return JSON.parse(initialDataRaw);
+ } catch (err) {
+ console.warn(
+ `Failed to parse MIDI state from localStorage for input "${this.stateKey}" and channel "${chan}"`,
+ initialDataRaw,
+ err,
+ );
+ return {};
+ }
+ }
+
+ _saveState(chan, cc, value) {
+ if (!this.stateKey) {
+ return;
+ }
+
+ const state = this._loadState(chan);
+ state[cc] = value;
+ localStorage.setItem(
+ `strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`,
+ JSON.stringify(state),
+ );
+ }
+
+ _sendAllStates(device) {
+ const output = WebMidi.outputs.find((o) => o.name === device.name);
+ if (!output) {
+ return;
+ }
+
+ for (const [chan, refs] of Object.entries(this._refsByChan)) {
+ const channel = Number(chan);
+ for (const [cc, value] of Object.entries(refs)) {
+ const ccn = Number(cc);
+ const scaled = Math.round(value * 127);
+ output.sendControlChange(ccn, scaled, channel);
+ }
+ }
+ }
+}
diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs
index 08defe7e..1e106a89 100644
--- a/packages/midi/midi.mjs
+++ b/packages/midi/midi.mjs
@@ -5,10 +5,12 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
-import { Pattern, isPattern, logger, ref } from '@strudel/core';
+import { Pattern, isPattern, logger } from '@strudel/core';
import { noteToMidi, getControlName } from '@strudel/core';
import { Note } from 'webmidi';
import { scheduleAtTime } from '../superdough/helpers.mjs';
+import { getMidiDeviceNamesString, getDevice } from './util.mjs';
+import { MidiInput } from './input.mjs';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -17,10 +19,6 @@ function supportsMidi() {
return typeof navigator.requestMIDIAccess === 'function';
}
-function getMidiDeviceNamesString(devices) {
- return devices.map((o) => `'${o.name}'`).join(' | ');
-}
-
export function enableWebMidi(options = {}) {
const { onReady, onConnected, onDisconnected, onEnabled } = options;
if (WebMidi.enabled) {
@@ -58,27 +56,6 @@ export function enableWebMidi(options = {}) {
});
}
-function getDevice(indexOrName, devices) {
- if (typeof indexOrName === 'number') {
- return devices[indexOrName];
- }
- const byName = (name) => devices.find((output) => output.name.includes(name));
- if (typeof indexOrName === 'string') {
- return byName(indexOrName);
- }
- // attempt to default to first IAC device if none is specified
- const IACOutput = byName('IAC');
- const device = IACOutput ?? devices[0];
- if (!device) {
- if (!devices.length) {
- throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
- }
- throw new Error(`🔌 Default MIDI device not found. Use one of ${getMidiDeviceNamesString(devices)}`);
- }
-
- return device;
-}
-
// send start/stop messages to outputs when repl starts/stops
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
@@ -475,180 +452,6 @@ Pattern.prototype.midi = function (midiport, options = {}) {
});
};
-class MidiInput {
- /**
- *
- * @param {string | number} input MIDI device name or index defaulting to 0
- */
- constructor(input) {
- this.input = input;
- this.stateKey = typeof input === 'string' ? input : undefined;
-
- this._refs = {};
- this._refsByChan = {};
-
- this._loadAllStates();
-
- this.initialDevice = this._startDeviceListener();
- }
-
- createCC(cc, chan) {
- const lookupMap = chan === undefined ? this._refs : this._refsByChan[chan];
- if (!(cc in lookupMap)) {
- const initialState = this._loadState(chan);
- lookupMap[cc] = initialState[cc] || 0;
- }
-
- return ref(() => lookupMap[cc]);
- }
-
- _startDeviceListener() {
- const initialDevice = getDevice(this.input, WebMidi.inputs);
- if (!initialDevice) {
- console.warn(`midiin: device "${this.input}" not found`);
- }
-
- // Background connection loop
- (async () => {
- const midiListener = this._onMidiMessage.bind(this);
- let device = initialDevice;
-
- while (true) {
- if (!device) {
- device = await this._waitForDevice();
- }
-
- console.log('midiin: device connected:', device.name);
-
- // Wait a bit for device to be ready to receive last state
- await new Promise((resolve) => setTimeout(resolve, 2000));
-
- try {
- // Still continue if sending did not work
- this._sendAllStates(device);
- } catch (err) {
- console.error('midiin: failed to send last state on connect:', device.name, err);
- }
-
- // Listen for incoming MIDI messages and for disconnection
- device.addListener('midimessage', midiListener);
-
- await this._waitForDeviceDisconnect(device);
-
- console.warn('midiin: device disconnected:', device.name);
- device.removeListener('midimessage', midiListener);
-
- device = null; // Clear var to trigger wait for connection
- }
- })();
-
- return initialDevice;
- }
-
- _waitForDevice() {
- return new Promise((resolve) => {
- const connListener = () => {
- const device = getDevice(this.input, WebMidi.inputs);
- if (device) {
- WebMidi.removeListener('connected', connListener);
- resolve(device);
- }
- };
-
- WebMidi.addListener('connected', connListener);
- });
- }
-
- _waitForDeviceDisconnect(device) {
- return new Promise((resolve) => {
- const disconnListener = (e) => {
- if (e.port.name === device.name) {
- WebMidi.removeListener('disconnected', disconnListener);
- resolve();
- }
- };
-
- WebMidi.addListener('disconnected', disconnListener);
- });
- }
-
- _onMidiMessage(e) {
- const ccNum = e.dataBytes[0];
- const v = e.dataBytes[1];
- const chan = e.message.channel;
- const scaled = v / 127;
-
- this._refs[ccNum] = scaled;
- this._refsByChan[ccNum] ??= {};
- this._refsByChan[ccNum][chan] = scaled;
-
- this._saveState(undefined, ccNum, scaled);
- this._saveState(chan, ccNum, scaled);
- }
-
- _loadAllStates() {
- Object.assign(this._refs, this._loadState(undefined));
-
- for (let chan = 1; chan <= 16; chan++) {
- this._refsByChan[chan] ??= {};
- Object.assign(this._refsByChan[chan], this._loadState(chan));
- }
- }
-
- _loadState(chan) {
- if (!this.stateKey) {
- return {};
- }
-
- const initialDataRaw = localStorage.getItem(
- `strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`,
- );
- if (!initialDataRaw) {
- return {};
- }
-
- try {
- return JSON.parse(initialDataRaw);
- } catch (err) {
- console.warn(
- `Failed to parse MIDI state from localStorage for input "${this.stateKey}" and channel "${chan}"`,
- initialDataRaw,
- err,
- );
- return {};
- }
- }
-
- _saveState(chan, cc, value) {
- if (!this.stateKey) {
- return;
- }
-
- const state = this._loadState(chan);
- state[cc] = value;
- localStorage.setItem(
- `strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`,
- JSON.stringify(state),
- );
- }
-
- _sendAllStates(device) {
- const output = WebMidi.outputs.find((o) => o.name === device.name);
- if (!output) {
- return;
- }
-
- for (const [chan, refs] of Object.entries(this._refsByChan)) {
- const channel = Number(chan);
- for (const [cc, value] of Object.entries(refs)) {
- const ccn = Number(cc);
- const scaled = Math.round(value * 127);
- output.sendControlChange(ccn, scaled, channel);
- }
- }
- }
-}
-
// MIDI input wrappers, by specified input string/index
const midiInputs = {};
diff --git a/packages/midi/util.mjs b/packages/midi/util.mjs
new file mode 100644
index 00000000..f2e7e822
--- /dev/null
+++ b/packages/midi/util.mjs
@@ -0,0 +1,38 @@
+import { Input, Output } from 'webmidi';
+
+/**
+ * Get a string listing device names for error messages.
+ * @param {Input[] | Output[]} devices
+ * @returns {string}
+ */
+export function getMidiDeviceNamesString(devices) {
+ return devices.map((o) => `'${o.name}'`).join(' | ');
+}
+
+/**
+ * Look up a device by index or name. Otherwise return a default device, or fail if none are connected.
+ *
+ * @param {string | number} indexOrName
+ * @param {Input[] | Output[]} devices
+ * @returns {Input | Output | undefined}
+ */
+export function getDevice(indexOrName, devices) {
+ if (typeof indexOrName === 'number') {
+ return devices[indexOrName];
+ }
+ const byName = (name) => devices.find((output) => output.name.includes(name));
+ if (typeof indexOrName === 'string') {
+ return byName(indexOrName);
+ }
+ // attempt to default to first IAC device if none is specified
+ const IACOutput = byName('IAC');
+ const device = IACOutput ?? devices[0];
+ if (!device) {
+ if (!devices.length) {
+ throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
+ }
+ throw new Error(`🔌 Default MIDI device not found. Use one of ${getMidiDeviceNamesString(devices)}`);
+ }
+
+ return device;
+}
From a0291e8a428d6dbf96f6e1c93b1a31423f1172f6 Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Thu, 25 Dec 2025 21:07:39 -0500
Subject: [PATCH 038/200] Fix comments
---
packages/midi/input.mjs | 2 +-
packages/midi/util.mjs | 6 ++++++
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/packages/midi/input.mjs b/packages/midi/input.mjs
index aa16cfea..943916e5 100644
--- a/packages/midi/input.mjs
+++ b/packages/midi/input.mjs
@@ -1,5 +1,5 @@
/*
-midi.mjs -
+input.mjs - MIDI input wrapper
Copyright (C) 2022 Strudel contributors - see
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see .
*/
diff --git a/packages/midi/util.mjs b/packages/midi/util.mjs
index f2e7e822..99158e68 100644
--- a/packages/midi/util.mjs
+++ b/packages/midi/util.mjs
@@ -1,3 +1,9 @@
+/*
+util.mjs - MIDI utility functions
+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 { Input, Output } from 'webmidi';
/**
From ee326c709e201c55f0d61b421f9cf1e90565f9d8 Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Thu, 25 Dec 2025 21:42:58 -0500
Subject: [PATCH 039/200] Fix MIDI input logging
---
packages/midi/input.mjs | 15 ++++++---------
1 file changed, 6 insertions(+), 9 deletions(-)
diff --git a/packages/midi/input.mjs b/packages/midi/input.mjs
index 943916e5..8e488942 100644
--- a/packages/midi/input.mjs
+++ b/packages/midi/input.mjs
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { WebMidi } from 'webmidi';
-import { ref } from '@strudel/core';
+import { logger, ref } from '@strudel/core';
import { getDevice } from './util.mjs';
export class MidiInput {
@@ -15,7 +15,7 @@ export class MidiInput {
*/
constructor(input) {
this.input = input;
- this.stateKey = typeof input === 'string' ? input : undefined;
+ this.stateKey = typeof input === 'string' ? input : undefined; // Saved state is not tracked for numeric index inputs
this._refs = {};
this._refsByChan = {};
@@ -37,9 +37,6 @@ export class MidiInput {
_startDeviceListener() {
const initialDevice = getDevice(this.input, WebMidi.inputs);
- if (!initialDevice) {
- console.warn(`midiin: device "${this.input}" not found`);
- }
// Background connection loop
(async () => {
@@ -51,8 +48,6 @@ export class MidiInput {
device = await this._waitForDevice();
}
- console.log('midiin: device connected:', device.name);
-
// Wait a bit for device to be ready to receive last state
await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -68,9 +63,7 @@ export class MidiInput {
await this._waitForDeviceDisconnect(device);
- console.warn('midiin: device disconnected:', device.name);
device.removeListener('midimessage', midiListener);
-
device = null; // Clear var to trigger wait for connection
}
})();
@@ -83,6 +76,8 @@ export class MidiInput {
const connListener = () => {
const device = getDevice(this.input, WebMidi.inputs);
if (device) {
+ logger(`[midi] device reconnected: ${device.name}`);
+
WebMidi.removeListener('connected', connListener);
resolve(device);
}
@@ -96,6 +91,8 @@ export class MidiInput {
return new Promise((resolve) => {
const disconnListener = (e) => {
if (e.port.name === device.name) {
+ logger(`[midi] device disconnected: ${device.name}`);
+
WebMidi.removeListener('disconnected', disconnListener);
resolve();
}
From 1a7f464998e3dd797ff821c89a5754133f4c93b1 Mon Sep 17 00:00:00 2001
From: Dsm0
Date: Tue, 30 Dec 2025 18:34:26 -0600
Subject: [PATCH 040/200] requested revisions 1 (and fix failing test)
---
packages/codemirror/block_utilities.mjs | 2 +-
packages/core/repl.mjs | 103 +++++-------------------
packages/transpiler/transpiler.mjs | 71 +++++++++++++++-
test/examples.test.mjs | 1 +
4 files changed, 90 insertions(+), 87 deletions(-)
diff --git a/packages/codemirror/block_utilities.mjs b/packages/codemirror/block_utilities.mjs
index 4b53755d..0d13259a 100644
--- a/packages/codemirror/block_utilities.mjs
+++ b/packages/codemirror/block_utilities.mjs
@@ -44,7 +44,7 @@ export const evalBlock = (strudelMirror) => {
if (block) {
// Flash the block being evaluated
strudelMirror.flash(200, { from: a, to: b });
- strudelMirror.repl.evaluate(block, true, true, true, { range });
+ strudelMirror.repl.evaluate(block, true, true, { range });
}
}
return true;
diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs
index 4c730515..d1bb1b1a 100644
--- a/packages/core/repl.mjs
+++ b/packages/core/repl.mjs
@@ -126,76 +126,11 @@ export function repl({
);
}
- // // Helper function to filter meta (widgets, sliders, miniLocations) by block range
- // function splitCodeByRange(meta, blockStart, blockEnd) {
- // }
-
- // Helper function to extract labels from code with their positions
- function extractLabelsFromCode(code) {
- const labels = [];
- // Regex to find label patterns like "d1:" or "myLabel:"
- const labelRegex = /^(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:)/gm;
- let match;
- while ((match = labelRegex.exec(code)) !== null) {
- labels.push({
- name: match[2],
- index: match.index,
- end: match.index + match[1].length,
- fullMatch: match[1],
- });
- }
-
- // Also check for 'all()' and treat it as a special label
- // just for management purposes
- const allRegex = /all\s*\(\s*([^)]+)\s*\)/;
- const allMatch = allRegex.exec(code);
- if (allMatch) {
- // Check if the argument contains a widget call
- const argCode = allMatch[1];
- const activeVisualizer = detectActiveVisualizer(argCode);
-
- labels.push({
- name: 'all',
- index: allMatch.index,
- end: allMatch.index + allMatch[0].length,
- fullMatch: allMatch[0],
- activeVisualizer: activeVisualizer,
- });
- }
-
- return labels;
- }
-
- // Helper function to detect non-inline widget calls in code
- function detectActiveVisualizer(code) {
- // List of non-inline widgets that need cleanup
- // These are Pattern.prototype methods that create persistent visualizations
- // I don't like this approach, feels hacky, but since these methods don't create ids that
- // can be read through the transpiler, I'm not sure how best to detect them.
- // Would probably be better to register these methods through some function that would
- // tag them better
- const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall'];
-
- for (const widget of nonInlineWidgets) {
- const widgetRegex = new RegExp(`\\.${widget}\\s*\\(`);
- if (widgetRegex.test(code)) {
- return widget;
- }
- }
- return null;
- }
-
// Helper function to handle single label code block storage
function handleSingleLabelBlock(label, code, options, meta) {
// Detect if this block contains a non-inline widget
- // For 'all' label, widget info is already on the label object from extractLabelsFromCode
-
- // As mentioned in detectActiveVisualizer, this is a bad approach to
- // managing non-inline widgets (or widgets that don't have ids)
- // and a proper solution would give them widgets in the transpiler, or at least
- // track where they are so we don't have to futz around with regexes
- const activeVisualizer =
- label.activeVisualizer !== undefined ? label.activeVisualizer : detectActiveVisualizer(code);
+ // The activeVisualizer is now provided by the transpiler for all labels
+ const activeVisualizer = label.activeVisualizer || null;
if (activeVisualizer !== null) {
lastActiveVisualizerLabel = label.name;
@@ -371,7 +306,7 @@ export function repl({
});
};
- const evaluate = async (code, autostart = true, shouldHush = true, blockBased = false, options = {}) => {
+ const evaluate = async (code, autostart = true, blockBased = false, options = {}) => {
if (!code) {
throw new Error('no code to evaluate');
}
@@ -390,7 +325,7 @@ export function repl({
if (!blockBased) {
codeBlocks = {};
- shouldHush && hush();
+ hush();
}
if (mondo) {
@@ -403,7 +338,7 @@ export function repl({
let widgetRemoved = false;
if (blockBased) {
- const labels = extractLabelsFromCode(code);
+ const labels = meta.labels || [];
// Store code blocks in dictionary using labels as keys
if (labels.length > 0) {
@@ -526,18 +461,18 @@ export function repl({
export const getTrigger =
({ getTime, defaultOutput }) =>
- async (hap, deadline, duration, cps, t) => {
- // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
- // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
- try {
- if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
- await defaultOutput(hap, deadline, duration, cps, t);
+ async (hap, deadline, duration, cps, t) => {
+ // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
+ // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
+ try {
+ if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
+ await defaultOutput(hap, deadline, duration, cps, t);
+ }
+ if (hap.context.onTrigger) {
+ // call signature of output / onTrigger is different...
+ await hap.context.onTrigger(hap, getTime(), cps, t);
+ }
+ } catch (err) {
+ errorLogger(err, 'getTrigger');
}
- if (hap.context.onTrigger) {
- // call signature of output / onTrigger is different...
- await hap.context.onTrigger(hap, getTime(), cps, t);
- }
- } catch (err) {
- errorLogger(err, 'getTrigger');
- }
- };
+ };
diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs
index 24dd3f1f..e7d4acfa 100644
--- a/packages/transpiler/transpiler.mjs
+++ b/packages/transpiler/transpiler.mjs
@@ -55,6 +55,7 @@ export function transpiler(input, options = {}) {
};
let widgets = [];
let sliders = [];
+ let labels = [];
walk(ast, {
enter(node, parent /* , prop, index */) {
@@ -146,10 +147,32 @@ export function transpiler(input, options = {}) {
return this.replace(withAwait(node));
}
if (isLabelStatement(node)) {
+ // Collect label info for block-based evaluation
+ // Store positions WITHOUT offset so repl can slice the transpiler output correctly
+ if (blockBased) {
+ labels.push({
+ name: node.label.name,
+ index: node.start - nodeOffset,
+ end: node.label.end - nodeOffset,
+ fullMatch: input.slice(node.start - nodeOffset, node.label.end - nodeOffset),
+ activeVisualizer: findVisualizerInSubtree(node.body),
+ });
+ }
return this.replace(labelToP(node));
}
+ // Detect all() calls as special labels for block management
+ // Store positions WITHOUT offset so repl can slice the transpiler output correctly
+ if (blockBased && isAllCall(node)) {
+ labels.push({
+ name: 'all',
+ index: node.start - nodeOffset,
+ end: node.end - nodeOffset,
+ fullMatch: input.slice(node.start - nodeOffset, node.end - nodeOffset),
+ activeVisualizer: node.arguments[0] ? findVisualizerInSubtree(node.arguments[0]) : null,
+ });
+ }
},
- leave(node, parent, prop, index) {},
+ leave(node, parent, prop, index) { },
});
let { body } = ast;
@@ -198,7 +221,7 @@ export function transpiler(input, options = {}) {
if (!emitMiniLocations) {
return { output };
}
- return { output, miniLocations, widgets, sliders };
+ return { output, miniLocations, widgets, sliders, labels };
}
function isStringWithDoubleQuotes(node, locations, code) {
@@ -301,6 +324,10 @@ function isBareSamplesCall(node, parent) {
return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression';
}
+function isAllCall(node) {
+ return node.type === 'CallExpression' && node.callee.name === 'all';
+}
+
function withAwait(node) {
return {
type: 'AwaitExpression',
@@ -401,6 +428,46 @@ function languageWithLocation(name, value, offset) {
};
}
+// List of non-inline widgets that need cleanup
+// These are Pattern.prototype methods that create persistent visualizations
+// (should be repalced by a function call producing an actual list of registered widgets)
+const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall'];
+
+function isVisualizerCall(node) {
+ if (
+ node.type === 'CallExpression' &&
+ node.callee.type === 'MemberExpression' &&
+ nonInlineWidgets.includes(node.callee.property?.name)
+ ) {
+ return node.callee.property.name;
+ }
+ return null;
+}
+
+function findVisualizerInSubtree(node) {
+ if (!node || typeof node !== 'object') return null;
+
+ // Check if this node is a visualizer call
+ const viz = isVisualizerCall(node);
+ if (viz) return viz;
+
+ // Recursively search children
+ for (const key of Object.keys(node)) {
+ if (key === 'parent') continue; // Skip parent references to avoid cycles
+ const child = node[key];
+ if (Array.isArray(child)) {
+ for (const item of child) {
+ const found = findVisualizerInSubtree(item);
+ if (found) return found;
+ }
+ } else if (child && typeof child === 'object' && child.type) {
+ const found = findVisualizerInSubtree(child);
+ if (found) return found;
+ }
+ }
+ return null;
+}
+
// Creates AST nodes for: userDefinedKeys.add('name'); strudelScope.name = name; globalThis.name = name;
// Used in block-based evaluation to persist variables/functions across blocks
// We add to both strudelScope (for internal lookups) and globalThis (for direct access)
diff --git a/test/examples.test.mjs b/test/examples.test.mjs
index 896a02f8..18f12bb4 100644
--- a/test/examples.test.mjs
+++ b/test/examples.test.mjs
@@ -20,6 +20,7 @@ const skippedExamples = [
'accelerationX',
'defaultmidimap',
'midimaps',
+ 'clearScope'
];
describe('runs examples', () => {
From 160ef84b47be62bb8181cd63858c2d83ff451cb4 Mon Sep 17 00:00:00 2001
From: Dsm0
Date: Tue, 30 Dec 2025 20:14:32 -0600
Subject: [PATCH 041/200] move evaluate/evaluateBlock to different functions
---
packages/codemirror/block_utilities.mjs | 2 +-
packages/core/repl.mjs | 255 ++++++++++++++----------
packages/transpiler/transpiler.mjs | 2 +-
test/examples.test.mjs | 2 +-
4 files changed, 152 insertions(+), 109 deletions(-)
diff --git a/packages/codemirror/block_utilities.mjs b/packages/codemirror/block_utilities.mjs
index 0d13259a..fd3dbedc 100644
--- a/packages/codemirror/block_utilities.mjs
+++ b/packages/codemirror/block_utilities.mjs
@@ -44,7 +44,7 @@ export const evalBlock = (strudelMirror) => {
if (block) {
// Flash the block being evaluated
strudelMirror.flash(200, { from: a, to: b });
- strudelMirror.repl.evaluate(block, true, true, { range });
+ strudelMirror.repl.evaluateBlock(block, true, { range });
}
}
return true;
diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs
index d1bb1b1a..e3efdd3e 100644
--- a/packages/core/repl.mjs
+++ b/packages/core/repl.mjs
@@ -187,6 +187,48 @@ export function repl({
};
setTime(() => scheduler.now()); // TODO: refactor?
+ // Helper function to apply pattern transformations (solo, each, all)
+ // this should be abstracted more
+ function applyPatternTransforms(pattern) {
+ const allPatterns = Object.values(pPatterns);
+
+ if (allPatterns.length) {
+ let patterns = [];
+ let soloActive = false;
+ for (const [key, value] of Object.entries(pPatterns)) {
+ // handle soloed patterns ex: S$: s("bd!4")
+ const isSolod = key.length > 1 && key.startsWith('S');
+ if (isSolod && soloActive === false) {
+ // first time we see a soloed pattern, clear existing patterns
+ patterns = [];
+ soloActive = true;
+ }
+ if (!soloActive || (soloActive && isSolod)) {
+ const valWithState = value.withState((state) => state.setControls({ id: key }));
+ patterns.push(valWithState);
+ }
+ }
+ if (eachTransform) {
+ // Explicit lambda so only element (not index and array) are passed
+ patterns = patterns.map((x) => eachTransform(x));
+ }
+ pattern = stack(...patterns);
+ } else if (eachTransform) {
+ pattern = eachTransform(pattern);
+ }
+ if (allTransforms.length) {
+ for (const transform of allTransforms) {
+ pattern = transform(pattern);
+ }
+ }
+
+ if (!isPattern(pattern)) {
+ pattern = silence;
+ }
+
+ return pattern;
+ }
+
const stop = () => {
codeBlocks = {};
blockPatterns.clear();
@@ -306,7 +348,7 @@ export function repl({
});
};
- const evaluate = async (code, autostart = true, blockBased = false, options = {}) => {
+ const evaluate = async (code, autostart = true) => {
if (!code) {
throw new Error('no code to evaluate');
}
@@ -314,20 +356,60 @@ export function repl({
updateState({ code, pending: true });
await injectPatternMethods();
setTime(() => scheduler.now()); // TODO: refactor?
- await beforeEval?.({ code, blockBased });
+ await beforeEval?.({ code, blockBased: false });
+ allTransforms = []; // reset all transforms
+
+ codeBlocks = {};
+ hush();
+
+ if (mondo) {
+ code = `mondolang\`${code}\``;
+ }
+
+ let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
+
+ pattern = applyPatternTransforms(pattern);
+
+ logger(`[eval] code updated`);
+ pattern = await setPattern(pattern, autostart);
+ updateState({
+ miniLocations: meta?.miniLocations || [],
+ widgets: meta?.widgets || [],
+ sliders: meta?.sliders || [],
+ activeCode: code,
+ pattern,
+ evalError: undefined,
+ schedulerError: undefined,
+ pending: false,
+ });
+
+ afterEval?.({ code, pattern, meta, range: undefined, widgetRemoved: false });
+ return pattern;
+ } catch (err) {
+ logger(`[eval] error: ${err.message}`, 'error');
+ console.error(err);
+ updateState({ evalError: err, pending: false });
+ onEvalError?.(err);
+ }
+ };
+
+ const evaluateBlock = async (code, autostart = true, options = {}) => {
+ if (!code) {
+ throw new Error('no code to evaluate');
+ }
+ try {
+ updateState({ code, pending: true });
+ await injectPatternMethods();
+ setTime(() => scheduler.now()); // TODO: refactor?
+ await beforeEval?.({ code, blockBased: true });
allTransforms = []; // reset all transforms
const transpilerOptionsWithBlock = {
...transpilerOptions,
- blockBased,
+ blockBased: true,
range: options.range || [],
};
- if (!blockBased) {
- codeBlocks = {};
- hush();
- }
-
if (mondo) {
code = `mondolang\`${code}\``;
}
@@ -337,101 +419,61 @@ export function repl({
// Track activeVisualizer cleanup: check if any block's visualizer was removed
let widgetRemoved = false;
- if (blockBased) {
- const labels = meta.labels || [];
+ const labels = meta.labels || [];
- // Store code blocks in dictionary using labels as keys
- if (labels.length > 0) {
- for (let i = 0; i < labels.length; i++) {
- // processLabeledBlock(labels, i, code, options, meta);
- // processing transpiler output instead of code is simply to avoid
- // extra regex in detecting whether or not an inline widget has been commented out
- processLabeledBlock(labels, i, meta.output, options, meta);
- }
- } else if (pattern !== silence) {
- // variable/function declarations that return silence are allowed,
- // but anonymous pattern blocks pose an issue for block-based evaluation
- // if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder
-
- // it's very common for users to write code prefixed with '$'
- // but to modify and override existing patterns, the patterns must be labeled,
- // otherwise we'll have no idea of which pattern is being overridden
-
- // (we probably need to update the docs on this)
-
- // we could easily enable it, but it would confuse a lot of people
- throw new Error(
- 'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)',
- );
+ // Store code blocks in dictionary using labels as keys
+ if (labels.length > 0) {
+ for (let i = 0; i < labels.length; i++) {
+ // processing transpiler output instead of code is simply to avoid
+ // extra regex in detecting whether or not an inline widget has been commented out
+ processLabeledBlock(labels, i, meta.output, options, meta);
}
+ } else if (pattern !== silence) {
+ // variable/function declarations that return silence are allowed,
+ // but anonymous pattern blocks pose an issue for block-based evaluation
+ // if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder
- // For block-based evaluation, collect from all blocks
- // The highlight/widget/slider systems will handle selective updates based on the range
- meta.miniLocations = collectFromBlocks('miniLocations');
- meta.widgets = collectFromBlocks('widgets');
- meta.sliders = collectFromBlocks('sliders');
+ // it's very common for users to write code prefixed with '$'
+ // but to modify and override existing patterns, the patterns must be labeled,
+ // otherwise we'll have no idea of which pattern is being overridden
- // Track activeVisualizer cleanup: check if any block's visualizer was removed
- const blocksToUpdate = labels.map((label) => label.name);
+ // (we probably need to update the docs on this)
- for (const [key, block] of Object.entries(codeBlocks)) {
- if (blocksToUpdate.includes(key)) {
- // This block was just updated
- if (block.activeVisualizer !== null) {
- // Block now has a visualizer, update tracking
- lastActiveVisualizerLabel = key;
- } else if (lastActiveVisualizerLabel === key) {
- // This block lost its visualizer, trigger cleanup
- widgetRemoved = true;
- lastActiveVisualizerLabel = null;
- }
- }
- }
- }
-
- const allPatterns = Object.values(pPatterns);
-
- if (blockBased) {
- pPatterns = Object.fromEntries(
- Object.entries(pPatterns).filter(([key]) => {
- return Object.keys(codeBlocks).includes(key);
- }),
+ // we could easily enable it, but it would confuse a lot of people
+ throw new Error(
+ 'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)',
);
}
- if (allPatterns.length) {
- let patterns = [];
- let soloActive = false;
- for (const [key, value] of Object.entries(pPatterns)) {
- // handle soloed patterns ex: S$: s("bd!4")
- const isSolod = key.length > 1 && key.startsWith('S');
- if (isSolod && soloActive === false) {
- // first time we see a soloed pattern, clear existing patterns
- patterns = [];
- soloActive = true;
+ meta.miniLocations = collectFromBlocks('miniLocations');
+ meta.widgets = collectFromBlocks('widgets');
+ meta.sliders = collectFromBlocks('sliders');
+
+ // Track activeVisualizer cleanup: check if any block's visualizer was removed
+ const blocksToUpdate = labels.map((label) => label.name);
+
+ // this is the hackiest bit
+ for (const [key, block] of Object.entries(codeBlocks)) {
+ if (blocksToUpdate.includes(key)) {
+ // This block was just updated
+ if (block.activeVisualizer !== null) {
+ // Block now has a visualizer, update tracking
+ lastActiveVisualizerLabel = key;
+ } else if (lastActiveVisualizerLabel === key) {
+ // This block lost its visualizer, trigger cleanup
+ widgetRemoved = true;
+ lastActiveVisualizerLabel = null;
}
- if (!soloActive || (soloActive && isSolod)) {
- const valWithState = value.withState((state) => state.setControls({ id: key }));
- patterns.push(valWithState);
- }
- }
- if (eachTransform) {
- // Explicit lambda so only element (not index and array) are passed
- patterns = patterns.map((x) => eachTransform(x));
- }
- pattern = stack(...patterns);
- } else if (eachTransform) {
- pattern = eachTransform(pattern);
- }
- if (allTransforms.length) {
- for (const transform of allTransforms) {
- pattern = transform(pattern);
}
}
- if (!isPattern(pattern)) {
- pattern = silence;
- }
+ pPatterns = Object.fromEntries(
+ Object.entries(pPatterns).filter(([key]) => {
+ return Object.keys(codeBlocks).includes(key);
+ }),
+ );
+
+ pattern = applyPatternTransforms(pattern);
logger(`[eval] code updated`);
pattern = await setPattern(pattern, autostart);
@@ -455,24 +497,25 @@ export function repl({
onEvalError?.(err);
}
};
+
const setCode = (code) => updateState({ code });
- return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state };
+ return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state };
}
export const getTrigger =
({ getTime, defaultOutput }) =>
- async (hap, deadline, duration, cps, t) => {
- // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
- // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
- try {
- if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
- await defaultOutput(hap, deadline, duration, cps, t);
- }
- if (hap.context.onTrigger) {
- // call signature of output / onTrigger is different...
- await hap.context.onTrigger(hap, getTime(), cps, t);
- }
- } catch (err) {
- errorLogger(err, 'getTrigger');
+ async (hap, deadline, duration, cps, t) => {
+ // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
+ // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
+ try {
+ if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
+ await defaultOutput(hap, deadline, duration, cps, t);
}
- };
+ if (hap.context.onTrigger) {
+ // call signature of output / onTrigger is different...
+ await hap.context.onTrigger(hap, getTime(), cps, t);
+ }
+ } catch (err) {
+ errorLogger(err, 'getTrigger');
+ }
+ };
diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs
index e7d4acfa..db08c60b 100644
--- a/packages/transpiler/transpiler.mjs
+++ b/packages/transpiler/transpiler.mjs
@@ -172,7 +172,7 @@ export function transpiler(input, options = {}) {
});
}
},
- leave(node, parent, prop, index) { },
+ leave(node, parent, prop, index) {},
});
let { body } = ast;
diff --git a/test/examples.test.mjs b/test/examples.test.mjs
index 18f12bb4..a3dec47a 100644
--- a/test/examples.test.mjs
+++ b/test/examples.test.mjs
@@ -20,7 +20,7 @@ const skippedExamples = [
'accelerationX',
'defaultmidimap',
'midimaps',
- 'clearScope'
+ 'clearScope',
];
describe('runs examples', () => {
From 726e5ef14a6de2aab36202434ac52ae45c5e551f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sat, 10 Jan 2026 07:18:29 -0800
Subject: [PATCH 042/200] Fix formatting
---
packages/midi/midi.mjs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs
index 9b32f6de..63faeaf4 100644
--- a/packages/midi/midi.mjs
+++ b/packages/midi/midi.mjs
@@ -488,7 +488,7 @@ const refsByChan = {};
* MIDI input: Opens a MIDI input port to receive MIDI control change messages.
*
* The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel
- * @tags external_io
+ * @tags external_io
* @param {string | number} input MIDI device name or index defaulting to 0
* @returns {function(number, number=): Pattern} A function from (cc, channel?) to a pattern.
* When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1)
From 550d7ff6cb84fe3273256c2153cb869bc27d1263 Mon Sep 17 00:00:00 2001
From: Felix Roos
Date: Sun, 11 Jan 2026 12:32:54 +0100
Subject: [PATCH 043/200] fix: react error
---
website/src/repl/components/panel/Reference.jsx | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx
index 3053f03d..34701036 100644
--- a/website/src/repl/components/panel/Reference.jsx
+++ b/website/src/repl/components/panel/Reference.jsx
@@ -1,4 +1,4 @@
-import { useMemo, useState } from 'react';
+import { useMemo, useState, Fragment } from 'react';
import jsdocJson from '../../../../../doc.json';
import { Textbox } from '../textbox/Textbox';
@@ -109,9 +109,8 @@ export function Reference() {
From 9ffd0e496fcb71fcbc46add25eef7e40f56d4fb6 Mon Sep 17 00:00:00 2001
From: Felix Roos
Date: Sun, 11 Jan 2026 12:33:03 +0100
Subject: [PATCH 044/200] fix: all doc
---
packages/core/repl.mjs | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs
index 047989f7..9fe8eef8 100644
--- a/packages/core/repl.mjs
+++ b/packages/core/repl.mjs
@@ -120,7 +120,9 @@ export function repl({
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
- /** Applies a function to all the running patterns. Note that the patterns are groups together into a single `stack` before the function is applied. This is probably what you want, but see `each` for
+ let allTransforms = [];
+ /**
+ * Applies a function to all the running patterns. Note that the patterns are groups together into a single `stack` before the function is applied. This is probably what you want, but see `each` for
* a version that applies the function to each pattern separately.
* ```
* $: sound("bd - cp sd")
@@ -135,7 +137,6 @@ export function repl({
*
* @tags combiners
*/
- let allTransforms = [];
const all = function (transform) {
allTransforms.push(transform);
return silence;
From 055f009357f0b8b7438e9b7b06945c04c65167cd Mon Sep 17 00:00:00 2001
From: space-shell
Date: Tue, 13 Jan 2026 19:47:12 +0100
Subject: [PATCH 045/200] feat: Refactor Vim/Helix keybindings with shared
helper functions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Factor out the repl-eval and repl-stop event dispatching code into
replEval() and replStop() helper functions that are shared between
Vim and Helix keybindings.
This refactoring:
- Reduces code duplication between Vim :w/:q and Helix :w/:q commands
- Makes the keybinding handlers more maintainable
- Properly implements Helix commands using the commands.of() API
- Ensures consistent behavior across both keybinding modes
The Helix keybindings now properly integrate with the REPL using the
same event dispatch mechanism as Vim, dispatching custom repl-evaluate
and repl-stop events with fallbacks to keyboard events.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
packages/codemirror/keybindings.mjs | 163 ++++++++++++++++++----------
1 file changed, 106 insertions(+), 57 deletions(-)
diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs
index bc13f323..3e660630 100644
--- a/packages/codemirror/keybindings.mjs
+++ b/packages/codemirror/keybindings.mjs
@@ -6,7 +6,7 @@ import { emacs } from '@replit/codemirror-emacs';
import { vim, Vim } from '@replit/codemirror-vim';
// import { vim } from './vim_test.mjs';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
-import { helix } from 'codemirror-helix';
+import { helix, commands } from 'codemirror-helix';
import { logger } from '@strudel/core';
const vscodePlugin = ViewPlugin.fromClass(
@@ -21,6 +21,70 @@ const vscodePlugin = ViewPlugin.fromClass(
);
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
+function replEval(view) {
+ try {
+ // Dispatch a dedicated evaluate event first
+ let handled = false;
+ try {
+ const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true });
+ handled = document.dispatchEvent(ev) === false; // false means preventDefault was called
+ } catch (e) {
+ console.error('Error dispatching repl-evaluate event', e);
+ }
+ if (handled) {
+ return;
+ }
+ // Try Ctrl+Enter first if not handled by custom event
+ const ctrlEnter = new KeyboardEvent('keydown', {
+ key: 'Enter',
+ code: 'Enter',
+ ctrlKey: true,
+ bubbles: true,
+ cancelable: true,
+ });
+ view?.dom?.dispatchEvent?.(ctrlEnter);
+ // If not handled (no handler called preventDefault), try Alt+Enter as
+ // fallback
+ if (!ctrlEnter.defaultPrevented) {
+ const altEnter = new KeyboardEvent('keydown', {
+ key: 'Enter',
+ code: 'Enter',
+ altKey: true,
+ bubbles: true,
+ cancelable: true,
+ });
+ view?.dom?.dispatchEvent?.(altEnter);
+ }
+ } catch (e) {
+ console.error('Error dispatching repl evaluation event', e);
+ }
+}
+
+function replStop(view) {
+ try {
+ // First try dispatching our custom stop event, then fallback to Alt+.
+ let handled = false;
+ try {
+ const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true });
+ handled = document.dispatchEvent(ev) === false;
+ } catch (e) {
+ console.error('Error dispatching repl-stop event', e);
+ }
+ if (!handled) {
+ const altDot = new KeyboardEvent('keydown', {
+ key: '.',
+ code: 'Period',
+ altKey: true,
+ bubbles: true,
+ cancelable: true,
+ });
+ view?.dom?.dispatchEvent?.(altDot);
+ }
+ } catch (e) {
+ console.error('Error dispatching repl stop event', e);
+ }
+}
+
// Map Vim :w to trigger the same action as evaluation. We dispatch a custom
// event 'repl-evaluate' that the editor listens for, and also simulate
// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
@@ -48,29 +112,8 @@ try {
// :q to pause/stop
Vim.defineEx('quit', 'q', (cm) => {
- try {
- const view = cm?.view || cm;
- // First try dispatching our custom stop event, then fallback to Alt+.
- let handled = false;
- try {
- const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true });
- handled = document.dispatchEvent(ev) === false;
- } catch (e) {
- console.error('Error dispatching repl-stop event', e);
- }
- if (!handled) {
- const altDot = new KeyboardEvent('keydown', {
- key: '.',
- code: 'Period',
- altKey: true,
- bubbles: true,
- cancelable: true,
- });
- view?.dom?.dispatchEvent?.(altDot);
- }
- } catch (e) {
- console.error('Error dispatching :q stop event', e);
- }
+ const view = cm?.view || cm;
+ replStop(view);
});
// :w to evaluate
@@ -84,38 +127,7 @@ try {
} catch (e) {
console.error('Error logging Vim :w evaluation', e);
}
- // Dispatch a dedicated evaluate event first
- let handled = false;
- try {
- const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true });
- handled = document.dispatchEvent(ev) === false; // false means preventDefault was called
- } catch (e) {
- console.error('Error dispatching repl-evaluate event', e);
- }
- if (handled) {
- return;
- }
- // Try Ctrl+Enter first if not handled by custom event
- const ctrlEnter = new KeyboardEvent('keydown', {
- key: 'Enter',
- code: 'Enter',
- ctrlKey: true,
- bubbles: true,
- cancelable: true,
- });
- view?.dom?.dispatchEvent?.(ctrlEnter);
- // If not handled (no handler called preventDefault), try Alt+Enter as
- // fallback
- if (!ctrlEnter.defaultPrevented) {
- const altEnter = new KeyboardEvent('keydown', {
- key: 'Enter',
- code: 'Enter',
- altKey: true,
- bubbles: true,
- cancelable: true,
- });
- view?.dom?.dispatchEvent?.(altEnter);
- }
+ replEval(view);
} catch (e) {
console.error('Error dispatching :w evaluation event', e);
}
@@ -125,12 +137,49 @@ try {
console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e);
}
+// Map Helix :w to trigger the same action as evaluation. We dispatch a custom
+// event 'repl-evaluate' that the editor listens for, and also simulate
+// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
+// appears in the Console panel.
+const helixCommands = commands.of([
+ {
+ // :w to evaluate
+ name: 'write',
+ aliases: ['w'],
+ help: 'Repl-eval',
+ handler(view, args) {
+ try {
+ view?.focus?.(); // Let the app know this came from Helix :w
+ logger('[helix] :w — evaluating code');
+ replEval(view);
+ } catch (e) {
+ console.error('Error dispatching helix :w evaluation event', e);
+ }
+ },
+ },
+ {
+ // :q to pause/stop
+ name: 'quit',
+ aliases: ['q'],
+ help: 'Repl-stop',
+ handler(view, args) {
+ try {
+ view?.focus?.(); // Let the app know this came from Helix :q
+ logger('[helix] :q — stopping repl');
+ replStop(view);
+ } catch (e) {
+ console.error('Error dispatching helix :q stop event', e);
+ }
+ },
+ },
+]);
+
const keymaps = {
vim,
emacs,
- helix,
codemirror: () => keymap.of(defaultKeymap),
vscode: vscodeExtension,
+ helix: () => [helix(), helixCommands],
};
export function keybindings(name) {
From 44ad05fb203368f6cc1fa17144b7b527bdf0c453 Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Wed, 14 Jan 2026 23:37:26 -0500
Subject: [PATCH 046/200] Whitespace fixes
---
packages/midi/input.mjs | 360 ++++++++++++++++++++--------------------
packages/midi/util.mjs | 88 +++++-----
2 files changed, 224 insertions(+), 224 deletions(-)
diff --git a/packages/midi/input.mjs b/packages/midi/input.mjs
index 8e488942..498e6fe2 100644
--- a/packages/midi/input.mjs
+++ b/packages/midi/input.mjs
@@ -1,180 +1,180 @@
-/*
-input.mjs - MIDI input wrapper
-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 { WebMidi } from 'webmidi';
-import { logger, ref } from '@strudel/core';
-import { getDevice } from './util.mjs';
-
-export class MidiInput {
- /**
- *
- * @param {string | number} input MIDI device name or index defaulting to 0
- */
- constructor(input) {
- this.input = input;
- this.stateKey = typeof input === 'string' ? input : undefined; // Saved state is not tracked for numeric index inputs
-
- this._refs = {};
- this._refsByChan = {};
-
- this._loadAllStates();
-
- this.initialDevice = this._startDeviceListener();
- }
-
- createCC(cc, chan) {
- const lookupMap = chan === undefined ? this._refs : this._refsByChan[chan];
- if (!(cc in lookupMap)) {
- const initialState = this._loadState(chan);
- lookupMap[cc] = initialState[cc] || 0;
- }
-
- return ref(() => lookupMap[cc]);
- }
-
- _startDeviceListener() {
- const initialDevice = getDevice(this.input, WebMidi.inputs);
-
- // Background connection loop
- (async () => {
- const midiListener = this._onMidiMessage.bind(this);
- let device = initialDevice;
-
- while (true) {
- if (!device) {
- device = await this._waitForDevice();
- }
-
- // Wait a bit for device to be ready to receive last state
- await new Promise((resolve) => setTimeout(resolve, 2000));
-
- try {
- // Still continue if sending did not work
- this._sendAllStates(device);
- } catch (err) {
- console.error('midiin: failed to send last state on connect:', device.name, err);
- }
-
- // Listen for incoming MIDI messages and for disconnection
- device.addListener('midimessage', midiListener);
-
- await this._waitForDeviceDisconnect(device);
-
- device.removeListener('midimessage', midiListener);
- device = null; // Clear var to trigger wait for connection
- }
- })();
-
- return initialDevice;
- }
-
- _waitForDevice() {
- return new Promise((resolve) => {
- const connListener = () => {
- const device = getDevice(this.input, WebMidi.inputs);
- if (device) {
- logger(`[midi] device reconnected: ${device.name}`);
-
- WebMidi.removeListener('connected', connListener);
- resolve(device);
- }
- };
-
- WebMidi.addListener('connected', connListener);
- });
- }
-
- _waitForDeviceDisconnect(device) {
- return new Promise((resolve) => {
- const disconnListener = (e) => {
- if (e.port.name === device.name) {
- logger(`[midi] device disconnected: ${device.name}`);
-
- WebMidi.removeListener('disconnected', disconnListener);
- resolve();
- }
- };
-
- WebMidi.addListener('disconnected', disconnListener);
- });
- }
-
- _onMidiMessage(e) {
- const ccNum = e.dataBytes[0];
- const v = e.dataBytes[1];
- const chan = e.message.channel;
- const scaled = v / 127;
-
- this._refs[ccNum] = scaled;
- this._refsByChan[ccNum] ??= {};
- this._refsByChan[ccNum][chan] = scaled;
-
- this._saveState(undefined, ccNum, scaled);
- this._saveState(chan, ccNum, scaled);
- }
-
- _loadAllStates() {
- Object.assign(this._refs, this._loadState(undefined));
-
- for (let chan = 1; chan <= 16; chan++) {
- this._refsByChan[chan] ??= {};
- Object.assign(this._refsByChan[chan], this._loadState(chan));
- }
- }
-
- _loadState(chan) {
- if (!this.stateKey) {
- return {};
- }
-
- const initialDataRaw = localStorage.getItem(
- `strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`,
- );
- if (!initialDataRaw) {
- return {};
- }
-
- try {
- return JSON.parse(initialDataRaw);
- } catch (err) {
- console.warn(
- `Failed to parse MIDI state from localStorage for input "${this.stateKey}" and channel "${chan}"`,
- initialDataRaw,
- err,
- );
- return {};
- }
- }
-
- _saveState(chan, cc, value) {
- if (!this.stateKey) {
- return;
- }
-
- const state = this._loadState(chan);
- state[cc] = value;
- localStorage.setItem(
- `strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`,
- JSON.stringify(state),
- );
- }
-
- _sendAllStates(device) {
- const output = WebMidi.outputs.find((o) => o.name === device.name);
- if (!output) {
- return;
- }
-
- for (const [chan, refs] of Object.entries(this._refsByChan)) {
- const channel = Number(chan);
- for (const [cc, value] of Object.entries(refs)) {
- const ccn = Number(cc);
- const scaled = Math.round(value * 127);
- output.sendControlChange(ccn, scaled, channel);
- }
- }
- }
-}
+/*
+input.mjs - MIDI input wrapper
+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 { WebMidi } from 'webmidi';
+import { logger, ref } from '@strudel/core';
+import { getDevice } from './util.mjs';
+
+export class MidiInput {
+ /**
+ *
+ * @param {string | number} input MIDI device name or index defaulting to 0
+ */
+ constructor(input) {
+ this.input = input;
+ this.stateKey = typeof input === 'string' ? input : undefined; // Saved state is not tracked for numeric index inputs
+
+ this._refs = {};
+ this._refsByChan = {};
+
+ this._loadAllStates();
+
+ this.initialDevice = this._startDeviceListener();
+ }
+
+ createCC(cc, chan) {
+ const lookupMap = chan === undefined ? this._refs : this._refsByChan[chan];
+ if (!(cc in lookupMap)) {
+ const initialState = this._loadState(chan);
+ lookupMap[cc] = initialState[cc] || 0;
+ }
+
+ return ref(() => lookupMap[cc]);
+ }
+
+ _startDeviceListener() {
+ const initialDevice = getDevice(this.input, WebMidi.inputs);
+
+ // Background connection loop
+ (async () => {
+ const midiListener = this._onMidiMessage.bind(this);
+ let device = initialDevice;
+
+ while (true) {
+ if (!device) {
+ device = await this._waitForDevice();
+ }
+
+ // Wait a bit for device to be ready to receive last state
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+
+ try {
+ // Still continue if sending did not work
+ this._sendAllStates(device);
+ } catch (err) {
+ console.error('midiin: failed to send last state on connect:', device.name, err);
+ }
+
+ // Listen for incoming MIDI messages and for disconnection
+ device.addListener('midimessage', midiListener);
+
+ await this._waitForDeviceDisconnect(device);
+
+ device.removeListener('midimessage', midiListener);
+ device = null; // Clear var to trigger wait for connection
+ }
+ })();
+
+ return initialDevice;
+ }
+
+ _waitForDevice() {
+ return new Promise((resolve) => {
+ const connListener = () => {
+ const device = getDevice(this.input, WebMidi.inputs);
+ if (device) {
+ logger(`[midi] device reconnected: ${device.name}`);
+
+ WebMidi.removeListener('connected', connListener);
+ resolve(device);
+ }
+ };
+
+ WebMidi.addListener('connected', connListener);
+ });
+ }
+
+ _waitForDeviceDisconnect(device) {
+ return new Promise((resolve) => {
+ const disconnListener = (e) => {
+ if (e.port.name === device.name) {
+ logger(`[midi] device disconnected: ${device.name}`);
+
+ WebMidi.removeListener('disconnected', disconnListener);
+ resolve();
+ }
+ };
+
+ WebMidi.addListener('disconnected', disconnListener);
+ });
+ }
+
+ _onMidiMessage(e) {
+ const ccNum = e.dataBytes[0];
+ const v = e.dataBytes[1];
+ const chan = e.message.channel;
+ const scaled = v / 127;
+
+ this._refs[ccNum] = scaled;
+ this._refsByChan[ccNum] ??= {};
+ this._refsByChan[ccNum][chan] = scaled;
+
+ this._saveState(undefined, ccNum, scaled);
+ this._saveState(chan, ccNum, scaled);
+ }
+
+ _loadAllStates() {
+ Object.assign(this._refs, this._loadState(undefined));
+
+ for (let chan = 1; chan <= 16; chan++) {
+ this._refsByChan[chan] ??= {};
+ Object.assign(this._refsByChan[chan], this._loadState(chan));
+ }
+ }
+
+ _loadState(chan) {
+ if (!this.stateKey) {
+ return {};
+ }
+
+ const initialDataRaw = localStorage.getItem(
+ `strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`,
+ );
+ if (!initialDataRaw) {
+ return {};
+ }
+
+ try {
+ return JSON.parse(initialDataRaw);
+ } catch (err) {
+ console.warn(
+ `Failed to parse MIDI state from localStorage for input "${this.stateKey}" and channel "${chan}"`,
+ initialDataRaw,
+ err,
+ );
+ return {};
+ }
+ }
+
+ _saveState(chan, cc, value) {
+ if (!this.stateKey) {
+ return;
+ }
+
+ const state = this._loadState(chan);
+ state[cc] = value;
+ localStorage.setItem(
+ `strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`,
+ JSON.stringify(state),
+ );
+ }
+
+ _sendAllStates(device) {
+ const output = WebMidi.outputs.find((o) => o.name === device.name);
+ if (!output) {
+ return;
+ }
+
+ for (const [chan, refs] of Object.entries(this._refsByChan)) {
+ const channel = Number(chan);
+ for (const [cc, value] of Object.entries(refs)) {
+ const ccn = Number(cc);
+ const scaled = Math.round(value * 127);
+ output.sendControlChange(ccn, scaled, channel);
+ }
+ }
+ }
+}
diff --git a/packages/midi/util.mjs b/packages/midi/util.mjs
index 99158e68..f02f1f64 100644
--- a/packages/midi/util.mjs
+++ b/packages/midi/util.mjs
@@ -1,44 +1,44 @@
-/*
-util.mjs - MIDI utility functions
-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 { Input, Output } from 'webmidi';
-
-/**
- * Get a string listing device names for error messages.
- * @param {Input[] | Output[]} devices
- * @returns {string}
- */
-export function getMidiDeviceNamesString(devices) {
- return devices.map((o) => `'${o.name}'`).join(' | ');
-}
-
-/**
- * Look up a device by index or name. Otherwise return a default device, or fail if none are connected.
- *
- * @param {string | number} indexOrName
- * @param {Input[] | Output[]} devices
- * @returns {Input | Output | undefined}
- */
-export function getDevice(indexOrName, devices) {
- if (typeof indexOrName === 'number') {
- return devices[indexOrName];
- }
- const byName = (name) => devices.find((output) => output.name.includes(name));
- if (typeof indexOrName === 'string') {
- return byName(indexOrName);
- }
- // attempt to default to first IAC device if none is specified
- const IACOutput = byName('IAC');
- const device = IACOutput ?? devices[0];
- if (!device) {
- if (!devices.length) {
- throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
- }
- throw new Error(`🔌 Default MIDI device not found. Use one of ${getMidiDeviceNamesString(devices)}`);
- }
-
- return device;
-}
+/*
+util.mjs - MIDI utility functions
+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 { Input, Output } from 'webmidi';
+
+/**
+ * Get a string listing device names for error messages.
+ * @param {Input[] | Output[]} devices
+ * @returns {string}
+ */
+export function getMidiDeviceNamesString(devices) {
+ return devices.map((o) => `'${o.name}'`).join(' | ');
+}
+
+/**
+ * Look up a device by index or name. Otherwise return a default device, or fail if none are connected.
+ *
+ * @param {string | number} indexOrName
+ * @param {Input[] | Output[]} devices
+ * @returns {Input | Output | undefined}
+ */
+export function getDevice(indexOrName, devices) {
+ if (typeof indexOrName === 'number') {
+ return devices[indexOrName];
+ }
+ const byName = (name) => devices.find((output) => output.name.includes(name));
+ if (typeof indexOrName === 'string') {
+ return byName(indexOrName);
+ }
+ // attempt to default to first IAC device if none is specified
+ const IACOutput = byName('IAC');
+ const device = IACOutput ?? devices[0];
+ if (!device) {
+ if (!devices.length) {
+ throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
+ }
+ throw new Error(`🔌 Default MIDI device not found. Use one of ${getMidiDeviceNamesString(devices)}`);
+ }
+
+ return device;
+}
From bc9e97b9ac2473095d6b807297f530269b512ae8 Mon Sep 17 00:00:00 2001
From: Nick Matantsev
Date: Thu, 15 Jan 2026 00:13:49 -0500
Subject: [PATCH 047/200] Add comments
---
packages/midi/input.mjs | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/packages/midi/input.mjs b/packages/midi/input.mjs
index 498e6fe2..b9f07581 100644
--- a/packages/midi/input.mjs
+++ b/packages/midi/input.mjs
@@ -8,6 +8,11 @@ import { WebMidi } from 'webmidi';
import { logger, ref } from '@strudel/core';
import { getDevice } from './util.mjs';
+/**
+ * MIDI input device wrapper that manages connection and reconnection, tracks
+ * persisted CC states, etc. These instances are long-lived and are maintained as singletons
+ * (keyed globally by input string/number).
+ */
export class MidiInput {
/**
*
@@ -25,6 +30,11 @@ export class MidiInput {
this.initialDevice = this._startDeviceListener();
}
+ /**
+ * Implementation for the cc() factory function tied to this specific input.
+ * @param {number} cc MIDI CC number
+ * @param {number | undefined} chan MIDI channel (1-16) or undefined for all channels
+ */
createCC(cc, chan) {
const lookupMap = chan === undefined ? this._refs : this._refsByChan[chan];
if (!(cc in lookupMap)) {
@@ -71,6 +81,7 @@ export class MidiInput {
return initialDevice;
}
+ // Returns a promise that resolves when the specified device is connected
_waitForDevice() {
return new Promise((resolve) => {
const connListener = () => {
@@ -87,6 +98,7 @@ export class MidiInput {
});
}
+ // Returns a promise that resolves when the specified device is disconnected
_waitForDeviceDisconnect(device) {
return new Promise((resolve) => {
const disconnListener = (e) => {
@@ -162,6 +174,7 @@ export class MidiInput {
);
}
+ // Send CC values back to device to restore encoders and motorized sliders
_sendAllStates(device) {
const output = WebMidi.outputs.find((o) => o.name === device.name);
if (!output) {
From 2e7ec9ea272b368e4c9b4fe98990d0e662c0f57a Mon Sep 17 00:00:00 2001
From: Dsm0
Date: Thu, 15 Jan 2026 19:27:54 -0800
Subject: [PATCH 048/200] variable declarations with active patterns now have
highlighting
---
packages/core/repl.mjs | 56 +++++++++++++++++++++++++++++++++---------
1 file changed, 44 insertions(+), 12 deletions(-)
diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs
index e3efdd3e..09c76d60 100644
--- a/packages/core/repl.mjs
+++ b/packages/core/repl.mjs
@@ -126,7 +126,23 @@ export function repl({
);
}
- // Helper function to handle single label code block storage
+ // helper
+ function cleanupConflictingRanges(codeBlocks, currentKey, newRange) {
+ for (const [existingKey, existingBlock] of Object.entries(codeBlocks)) {
+ if (existingKey === currentKey) continue;
+ if (!existingBlock.range) continue;
+
+ const [existingStart, existingEnd] = existingBlock.range;
+ const [newStart, newEnd] = newRange;
+
+ // If ranges overlap (not just touch), remove the stale block
+ if (!(newEnd <= existingStart || newStart >= existingEnd)) {
+ delete codeBlocks[existingKey];
+ }
+ }
+ }
+
+ // helper
function handleSingleLabelBlock(label, code, options, meta) {
// Detect if this block contains a non-inline widget
// The activeVisualizer is now provided by the transpiler for all labels
@@ -147,18 +163,30 @@ export function repl({
activeVisualizer: activeVisualizer, // Store the widget type if present, null otherwise
};
- // Clean up any blocks with conflicting ranges
- for (const [existingKey, existingBlock] of Object.entries(codeBlocks)) {
- if (existingKey !== label.name && existingBlock.range && options.range) {
- const [existingStart, existingEnd] = existingBlock.range;
- const [newStart, newEnd] = options.range;
+ // Clean up any blocks with conflicting ranges (including declaration blocks)
+ cleanupConflictingRanges(codeBlocks, label.name, options.range);
+ }
- // If ranges overlap (not just touch), remove the stale block
- if (!(newEnd <= existingStart || newStart >= existingEnd)) {
- delete codeBlocks[existingKey];
- }
- }
- }
+ // helper
+ // These blocks return silence but may contain mini notation strings that need highlighting
+ function handleDeclarationBlock(code, options, meta) {
+ const range = options.range || [];
+ if (range.length < 2) return;
+
+ const blockKey = `_decl:${range[0]}:${range[1]}`;
+
+ codeBlocks[blockKey] = {
+ code: code,
+ range: range,
+ labels: [],
+ miniLocations: meta?.miniLocations || [],
+ widgets: meta?.widgets || [],
+ sliders: meta?.sliders || [],
+ activeVisualizer: null,
+ };
+
+ // Clean up any overlapping declaration blocks
+ cleanupConflictingRanges(codeBlocks, blockKey, range);
}
const hush = function () {
@@ -443,6 +471,10 @@ export function repl({
throw new Error(
'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)',
);
+ } else {
+ // Declaration block (variable/function that returns silence)
+ // Store it so its miniLocations are preserved for highlighting patterns stored in variables
+ handleDeclarationBlock(code, options, meta);
}
meta.miniLocations = collectFromBlocks('miniLocations');
From f9d0e154e841997f4260b9ceb81faa58bff5fee2 Mon Sep 17 00:00:00 2001
From: robojumper
Date: Sat, 17 Jan 2026 17:47:48 +0100
Subject: [PATCH 049/200] fix: performance issues when reference is open
---
website/src/repl/components/panel/Reference.jsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx
index c03ac0cf..4c48b6a3 100644
--- a/website/src/repl/components/panel/Reference.jsx
+++ b/website/src/repl/components/panel/Reference.jsx
@@ -1,4 +1,4 @@
-import { useMemo, useState } from 'react';
+import { memo, useMemo, useState } from 'react';
import jsdocJson from '../../../../../doc.json';
import { Textbox } from '../textbox/Textbox';
@@ -35,7 +35,7 @@ const getInnerText = (html) => {
return div.textContent || div.innerText || '';
};
-export function Reference() {
+export const Reference = memo(function Reference() {
const [search, setSearch] = useState('');
const visibleFunctions = useMemo(() => {
@@ -112,4 +112,4 @@ export function Reference() {
);
-}
+});
From 77076a1c3eeed4a1d9f070336c8805ed9d6349b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sat, 17 Jan 2026 19:12:23 +0100
Subject: [PATCH 050/200] Improve reference search/filtering semantics
---
.../src/repl/components/panel/Reference.jsx | 125 +++++++++++++-----
1 file changed, 92 insertions(+), 33 deletions(-)
diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx
index c943db14..24d4a677 100644
--- a/website/src/repl/components/panel/Reference.jsx
+++ b/website/src/repl/components/panel/Reference.jsx
@@ -1,4 +1,4 @@
-import { useMemo, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import jsdocJson from '../../../../../doc.json';
import { Textbox } from '../textbox/Textbox';
@@ -46,6 +46,7 @@ const getInnerText = (html) => {
export function Reference() {
const [search, setSearch] = useState('');
const [selectedTag, setSelectedTag] = useState(null);
+ const [selectedFunction, setSelectedFunction] = useState(null);
const toggleTag = (tag) => {
if (selectedTag === tag) {
@@ -55,7 +56,7 @@ export function Reference() {
}
};
- const visibleFunctions = useMemo(() => {
+ const searchVisibleFunctions = useMemo(() => {
return availableFunctions.filter((entry) => {
if (selectedTag) {
if (!(entry.tags || ['untagged']).includes(selectedTag)) {
@@ -63,6 +64,14 @@ export function Reference() {
}
}
+ // if (entry.name === selectedFunction) {
+ // return true;
+ // }
+
+ // if (!selectedTag) {
+ // return false;
+ // }
+
if (!search) {
return true;
}
@@ -75,6 +84,19 @@ export function Reference() {
});
}, [search, selectedTag]);
+ const detailVisibleFunctions = useMemo(() => {
+ return searchVisibleFunctions.filter((x) => {
+ if (selectedTag === null) {
+ if (search) {
+ return true;
+ }
+ return x.name === selectedFunction;
+ } else {
+ return true;
+ }
+ });
+ }, [searchVisibleFunctions, selectedFunction, selectedTag]);
+
const tagCounts = {};
for (const doc of availableFunctions) {
(doc.tags || ['untagged']).forEach((t) => {
@@ -84,42 +106,60 @@ export function Reference() {
});
}
+ const onSearchTagFilterClick = () => {
+ const container = document.getElementById('reference-container');
+ container.scrollTo(0, 0);
+ setSelectedTag(null);
+ setSelectedFunction(null);
+ };
+
+ useEffect(() => {
+ if (selectedFunction) {
+ const el = document.getElementById(`doc-${selectedFunction}`);
+ const container = document.getElementById('reference-container');
+ container.scrollTo(0, el.offsetTop);
+ }
+ }, [selectedFunction]);
+
return (
- //
- //
- //
-
+
-
-
+
{
+ setSelectedFunction(null);
+ setSearch(e);
+ }}
+ />
-
- {visibleFunctions.map((entry, i) => (
+ {selectedTag && (
+
+
+ {selectedTag ? selectedTag + ' x' : 'Filter by tag'}
+
+
+ )}
+
From 42d0bfadceba9cae8b276eba55aea578ddc28436 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sat, 17 Jan 2026 20:08:23 +0100
Subject: [PATCH 051/200] Clean up
---
.../src/repl/components/panel/Reference.jsx | 23 ++++++-------------
1 file changed, 7 insertions(+), 16 deletions(-)
diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx
index 8a018ff9..950949b9 100644
--- a/website/src/repl/components/panel/Reference.jsx
+++ b/website/src/repl/components/panel/Reference.jsx
@@ -25,13 +25,13 @@ const availableFunctions = (() => {
seen.add(s);
// Swap `doc.name` in for `s` in the list of synonyms
const synonymsWithDoc = [doc.name, ...synonyms].filter((x) => x && x !== s);
- // functions.push({
- // ...doc,
- // name: s, // update names for the synonym
- // longname: s,
- // synonyms: synonymsWithDoc,
- // synonyms_text: synonymsWithDoc.join(', '),
- // });
+ functions.push({
+ ...doc,
+ name: s, // update names for the synonym
+ longname: s,
+ synonyms: synonymsWithDoc,
+ synonyms_text: synonymsWithDoc.join(', '),
+ });
}
}
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
@@ -64,14 +64,6 @@ export function Reference() {
}
}
- // if (entry.name === selectedFunction) {
- // return true;
- // }
-
- // if (!selectedTag) {
- // return false;
- // }
-
if (!search) {
return true;
}
@@ -228,7 +220,6 @@ export function Reference() {
)) || Searcb or select a tag to get started.
}
{detailVisibleFunctions.length > 0 &&
}
- {/* {!selectedTag && Select a tag to see the functions.
} */}
From 99cf256ee253b12057775e689dfd3c73c943b504 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sat, 17 Jan 2026 20:11:26 +0100
Subject: [PATCH 052/200] Clean up 2
---
website/src/repl/components/panel/Reference.jsx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx
index 950949b9..e5bb5600 100644
--- a/website/src/repl/components/panel/Reference.jsx
+++ b/website/src/repl/components/panel/Reference.jsx
@@ -99,8 +99,6 @@ export function Reference() {
}
const onSearchTagFilterClick = () => {
- const container = document.getElementById('reference-container');
- container.scrollTo(0, 0);
setSelectedTag(null);
setSelectedFunction(null);
};
@@ -133,7 +131,7 @@ export function Reference() {
className="text-foreground border-2 border-gray-500 px-1 py-0.5 my-2 rounded-md cursor-pointer font-sans"
onClick={onSearchTagFilterClick}
>
- {selectedTag ? selectedTag + ' x' : 'Filter by tag'}
+ {selectedTag}
)}
From 7bc8576a68ed959dd563776ed54c47b053d73dd7 Mon Sep 17 00:00:00 2001
From: Aria
Date: Sat, 17 Jan 2026 11:16:58 -0800
Subject: [PATCH 053/200] Make kabelsalat stereo out
---
packages/superdough/superdough.mjs | 2 +-
packages/superdough/worklets.mjs | 16 ++++++++++++----
2 files changed, 13 insertions(+), 5 deletions(-)
diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs
index ec44e8af..03a4c3da 100644
--- a/packages/superdough/superdough.mjs
+++ b/packages/superdough/superdough.mjs
@@ -613,7 +613,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// Kabelsalat
if (fx.workletSrc !== undefined) {
- const workletNode = getWorklet(ac, 'generic-processor', {});
+ const workletNode = getWorklet(ac, 'generic-processor', {}, { outputChannelCount: [2] });
chain.connect(workletNode);
const workletSrc = fx.workletSrc
.replace(/\bpat\[(\d+)\]/g, (_, i) => fx.workletInputs[i])
diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs
index 6969ee2c..26a0fa66 100644
--- a/packages/superdough/worklets.mjs
+++ b/packages/superdough/worklets.mjs
@@ -1540,12 +1540,20 @@ class GenericProcessor extends AudioWorkletProcessor {
this.gateNode?.setValue(0);
this.gateEnded = true;
}
- const outL = outputs[0][0];
- const outR = outputs[0][1] ?? outputs[0][0];
+ const output = outputs[0];
+ const outL = output[0];
+ const outR = output[1];
for (let n = 0; n < blockSize; n++) {
this.genSample(this.playPos, this.nodes, input ? input[n] : 0, this.registers, this.outputs, this.sources);
- outL[n] = this.outputs[0];
- outR[n] = this.outputs[1];
+ const left = this.outputs[0];
+ const right = this.outputs[1];
+ // Spread to stereo if possible; else mixdown to mono
+ if (outR) {
+ outL[n] = left;
+ outR[n] = right;
+ } else {
+ outL[n] = 0.5 * (left + right);
+ }
this.playPos += 1 / sampleRate;
}
return true;
From d198910a86fcaecd658ef528bfa6d7884e93490b Mon Sep 17 00:00:00 2001
From: "Jade (Rose) Rowland"
Date: Sat, 17 Jan 2026 15:26:28 -0800
Subject: [PATCH 054/200] Publish
- @strudel/codemirror@1.2.7
- @strudel/core@1.2.6
- @strudel/csound@1.2.7
- @strudel/draw@1.2.6
- @strudel/embed@1.1.2
- @strudel/gamepad@1.2.6
- @strudel/hydra@1.2.6
- @strudel/midi@1.2.7
- @strudel/mini@1.2.6
- mondolang@1.1.2
- @strudel/mondo@1.1.6
- @strudel/motion@1.2.6
- @strudel/mqtt@1.2.6
- @strudel/osc@1.3.1
- @strudel/reference@1.2.2
- @strudel/repl@1.2.8
- @strudel/sampler@0.2.4
- @strudel/serial@1.2.6
- @strudel/soundfonts@1.2.7
- superdough@1.2.7
- supradough@1.2.5
- @strudel/tonal@1.2.6
- @strudel/transpiler@1.2.6
- vite-plugin-bundle-audioworklet@0.1.2
- @strudel/web@1.2.7
- @strudel/webaudio@1.2.7
- @strudel/xen@1.2.6
---
packages/codemirror/package.json | 2 +-
packages/core/package.json | 2 +-
packages/csound/package.json | 2 +-
packages/draw/package.json | 2 +-
packages/embed/package.json | 2 +-
packages/gamepad/package.json | 2 +-
packages/hydra/package.json | 2 +-
packages/midi/package.json | 2 +-
packages/mini/package.json | 2 +-
packages/mondo/package.json | 2 +-
packages/mondough/package.json | 2 +-
packages/motion/package.json | 2 +-
packages/mqtt/package.json | 2 +-
packages/osc/package.json | 2 +-
packages/reference/package.json | 2 +-
packages/repl/package.json | 2 +-
packages/sampler/package.json | 2 +-
packages/serial/package.json | 2 +-
packages/soundfonts/package.json | 2 +-
packages/superdough/package.json | 2 +-
packages/supradough/package.json | 2 +-
packages/tonal/package.json | 2 +-
packages/transpiler/package.json | 2 +-
packages/vite-plugin-bundle-audioworklet/package.json | 2 +-
packages/web/package.json | 2 +-
packages/webaudio/package.json | 2 +-
packages/xen/package.json | 2 +-
27 files changed, 27 insertions(+), 27 deletions(-)
diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json
index a3735491..2f87124a 100644
--- a/packages/codemirror/package.json
+++ b/packages/codemirror/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
- "version": "1.2.6",
+ "version": "1.2.7",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
diff --git a/packages/core/package.json b/packages/core/package.json
index abb54e9c..e558c574 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
diff --git a/packages/csound/package.json b/packages/csound/package.json
index 762131d6..03975275 100644
--- a/packages/csound/package.json
+++ b/packages/csound/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
- "version": "1.2.6",
+ "version": "1.2.7",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/draw/package.json b/packages/draw/package.json
index b0af8418..4a6f9a5a 100644
--- a/packages/draw/package.json
+++ b/packages/draw/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/embed/package.json b/packages/embed/package.json
index ffafdc8b..84a8cd6f 100644
--- a/packages/embed/package.json
+++ b/packages/embed/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/embed",
- "version": "1.1.1",
+ "version": "1.1.2",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js",
"type": "module",
diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json
index 6a07df96..21ad8eef 100644
--- a/packages/gamepad/package.json
+++ b/packages/gamepad/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/gamepad",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/hydra/package.json b/packages/hydra/package.json
index 16d6f0f4..67419ce0 100644
--- a/packages/hydra/package.json
+++ b/packages/hydra/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
diff --git a/packages/midi/package.json b/packages/midi/package.json
index 11fe9a3f..36891127 100644
--- a/packages/midi/package.json
+++ b/packages/midi/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
- "version": "1.2.6",
+ "version": "1.2.7",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/mini/package.json b/packages/mini/package.json
index b9a50fb6..43230655 100644
--- a/packages/mini/package.json
+++ b/packages/mini/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/mondo/package.json b/packages/mondo/package.json
index ebdbd329..1dba882e 100644
--- a/packages/mondo/package.json
+++ b/packages/mondo/package.json
@@ -1,6 +1,6 @@
{
"name": "mondolang",
- "version": "1.1.1",
+ "version": "1.1.2",
"description": "a language for functional composition that translates to js",
"main": "mondo.mjs",
"type": "module",
diff --git a/packages/mondough/package.json b/packages/mondough/package.json
index 9b170fde..919c5da2 100644
--- a/packages/mondough/package.json
+++ b/packages/mondough/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/mondo",
- "version": "1.1.5",
+ "version": "1.1.6",
"description": "mondo notation for strudel",
"main": "mondough.mjs",
"type": "module",
diff --git a/packages/motion/package.json b/packages/motion/package.json
index fd1fae89..dda3c368 100644
--- a/packages/motion/package.json
+++ b/packages/motion/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/motion",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json
index 492d915d..9fa9da9b 100644
--- a/packages/mqtt/package.json
+++ b/packages/mqtt/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/mqtt",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
diff --git a/packages/osc/package.json b/packages/osc/package.json
index 00faffbd..f1bbbdd5 100644
--- a/packages/osc/package.json
+++ b/packages/osc/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
- "version": "1.3.0",
+ "version": "1.3.1",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"bin": "./server.js",
diff --git a/packages/reference/package.json b/packages/reference/package.json
index 6d1fa180..783c1bbc 100644
--- a/packages/reference/package.json
+++ b/packages/reference/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/reference",
- "version": "1.2.1",
+ "version": "1.2.2",
"description": "Headless reference of all strudel functions",
"main": "index.mjs",
"type": "module",
diff --git a/packages/repl/package.json b/packages/repl/package.json
index e5216316..003086a9 100644
--- a/packages/repl/package.json
+++ b/packages/repl/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
- "version": "1.2.7",
+ "version": "1.2.8",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
diff --git a/packages/sampler/package.json b/packages/sampler/package.json
index b9ea4c2a..a960fbbe 100644
--- a/packages/sampler/package.json
+++ b/packages/sampler/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/sampler",
- "version": "0.2.3",
+ "version": "0.2.4",
"description": "",
"keywords": [
"tidalcycles",
diff --git a/packages/serial/package.json b/packages/serial/package.json
index 9dc6c899..e98a897e 100644
--- a/packages/serial/package.json
+++ b/packages/serial/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json
index 9bf0c400..dfdce782 100644
--- a/packages/soundfonts/package.json
+++ b/packages/soundfonts/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
- "version": "1.2.6",
+ "version": "1.2.7",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
diff --git a/packages/superdough/package.json b/packages/superdough/package.json
index 78db8338..c6ed7c64 100644
--- a/packages/superdough/package.json
+++ b/packages/superdough/package.json
@@ -1,6 +1,6 @@
{
"name": "superdough",
- "version": "1.2.6",
+ "version": "1.2.7",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
diff --git a/packages/supradough/package.json b/packages/supradough/package.json
index ee3869d0..10cf8df5 100644
--- a/packages/supradough/package.json
+++ b/packages/supradough/package.json
@@ -1,6 +1,6 @@
{
"name": "supradough",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
"main": "index.mjs",
"type": "module",
diff --git a/packages/tonal/package.json b/packages/tonal/package.json
index 14ffabc7..a6a539c9 100644
--- a/packages/tonal/package.json
+++ b/packages/tonal/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json
index 51355044..7b2a053f 100644
--- a/packages/transpiler/package.json
+++ b/packages/transpiler/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/transpiler",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"type": "module",
diff --git a/packages/vite-plugin-bundle-audioworklet/package.json b/packages/vite-plugin-bundle-audioworklet/package.json
index 198a4f9c..524af470 100644
--- a/packages/vite-plugin-bundle-audioworklet/package.json
+++ b/packages/vite-plugin-bundle-audioworklet/package.json
@@ -1,7 +1,7 @@
{
"name": "vite-plugin-bundle-audioworklet",
"main": "./vite-plugin-bundle-audioworklet.js",
- "version": "0.1.1",
+ "version": "0.1.2",
"description": "",
"keywords": [
"vite",
diff --git a/packages/web/package.json b/packages/web/package.json
index f02d8613..40ba83c5 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
- "version": "1.2.6",
+ "version": "1.2.7",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs",
"publishConfig": {
diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json
index f0bc4043..2815c739 100644
--- a/packages/webaudio/package.json
+++ b/packages/webaudio/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
- "version": "1.2.6",
+ "version": "1.2.7",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/xen/package.json b/packages/xen/package.json
index c178c5de..e7d9d203 100644
--- a/packages/xen/package.json
+++ b/packages/xen/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/xen",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"type": "module",
From 85e787214b476f0f6f03578e210269711a0175f6 Mon Sep 17 00:00:00 2001
From: "Jade (Rose) Rowland"
Date: Sat, 17 Jan 2026 15:59:38 -0800
Subject: [PATCH 055/200] rollback_supradough
---
packages/superdough/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/superdough/package.json b/packages/superdough/package.json
index c6ed7c64..23b64b29 100644
--- a/packages/superdough/package.json
+++ b/packages/superdough/package.json
@@ -1,6 +1,6 @@
{
"name": "superdough",
- "version": "1.2.7",
+ "version": "1.2.4",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
From 166985a05576a5cb88f2965748f0ef61d3fadedd Mon Sep 17 00:00:00 2001
From: "Jade (Rose) Rowland"
Date: Sat, 17 Jan 2026 16:00:34 -0800
Subject: [PATCH 056/200] spr
---
packages/osc/server.js | 0
1 file changed, 0 insertions(+), 0 deletions(-)
mode change 100644 => 100755 packages/osc/server.js
diff --git a/packages/osc/server.js b/packages/osc/server.js
old mode 100644
new mode 100755
From 12b89f9f88b01de01de8a18e13f110cea499bcfa Mon Sep 17 00:00:00 2001
From: "Jade (Rose) Rowland"
Date: Sat, 17 Jan 2026 16:12:51 -0800
Subject: [PATCH 057/200] rollback supradough
---
packages/supradough/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/supradough/package.json b/packages/supradough/package.json
index 10cf8df5..ee3869d0 100644
--- a/packages/supradough/package.json
+++ b/packages/supradough/package.json
@@ -1,6 +1,6 @@
{
"name": "supradough",
- "version": "1.2.5",
+ "version": "1.2.4",
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
"main": "index.mjs",
"type": "module",
From a3ac9646f5cb86958bb6193cddd5f30f78a57368 Mon Sep 17 00:00:00 2001
From: "Jade (Rose) Rowland"
Date: Sat, 17 Jan 2026 18:54:39 -0800
Subject: [PATCH 058/200] inc webaudio
---
packages/webaudio/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json
index 2815c739..18bd3290 100644
--- a/packages/webaudio/package.json
+++ b/packages/webaudio/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
- "version": "1.2.7",
+ "version": "1.2.8",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
From f4f04a7d574684aea78a757ef0d92370225cb27d Mon Sep 17 00:00:00 2001
From: "Jade (Rose) Rowland"
Date: Sat, 17 Jan 2026 19:01:10 -0800
Subject: [PATCH 059/200] Publish
- @strudel/codemirror@1.2.8
- @strudel/csound@1.2.8
- @strudel/midi@1.2.8
- @strudel/osc@1.3.2
- @strudel/repl@1.2.9
- @strudel/soundfonts@1.2.8
- superdough@1.2.5
- supradough@1.2.4
- @strudel/web@1.2.8
- @strudel/webaudio@1.2.9
---
packages/codemirror/package.json | 2 +-
packages/csound/package.json | 2 +-
packages/midi/package.json | 2 +-
packages/osc/package.json | 2 +-
packages/repl/package.json | 2 +-
packages/soundfonts/package.json | 2 +-
packages/superdough/package.json | 2 +-
packages/web/package.json | 2 +-
packages/webaudio/package.json | 2 +-
9 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json
index 2f87124a..9ad04097 100644
--- a/packages/codemirror/package.json
+++ b/packages/codemirror/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
- "version": "1.2.7",
+ "version": "1.2.8",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
diff --git a/packages/csound/package.json b/packages/csound/package.json
index 03975275..19ba6819 100644
--- a/packages/csound/package.json
+++ b/packages/csound/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
- "version": "1.2.7",
+ "version": "1.2.8",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/midi/package.json b/packages/midi/package.json
index 36891127..38d0f0ca 100644
--- a/packages/midi/package.json
+++ b/packages/midi/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
- "version": "1.2.7",
+ "version": "1.2.8",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/osc/package.json b/packages/osc/package.json
index f1bbbdd5..56924225 100644
--- a/packages/osc/package.json
+++ b/packages/osc/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
- "version": "1.3.1",
+ "version": "1.3.2",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"bin": "./server.js",
diff --git a/packages/repl/package.json b/packages/repl/package.json
index 003086a9..d2803492 100644
--- a/packages/repl/package.json
+++ b/packages/repl/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
- "version": "1.2.8",
+ "version": "1.2.9",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json
index dfdce782..2403adfc 100644
--- a/packages/soundfonts/package.json
+++ b/packages/soundfonts/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
- "version": "1.2.7",
+ "version": "1.2.8",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
diff --git a/packages/superdough/package.json b/packages/superdough/package.json
index 23b64b29..d1e9b45f 100644
--- a/packages/superdough/package.json
+++ b/packages/superdough/package.json
@@ -1,6 +1,6 @@
{
"name": "superdough",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
diff --git a/packages/web/package.json b/packages/web/package.json
index 40ba83c5..8be1f89d 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
- "version": "1.2.7",
+ "version": "1.2.8",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs",
"publishConfig": {
diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json
index 18bd3290..6b6980e9 100644
--- a/packages/webaudio/package.json
+++ b/packages/webaudio/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
- "version": "1.2.8",
+ "version": "1.2.9",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
From f0cb96dcad64e98cb7c692fc3949464317db2611 Mon Sep 17 00:00:00 2001
From: "Jade (Rose) Rowland"
Date: Sat, 17 Jan 2026 19:02:25 -0800
Subject: [PATCH 060/200] patch
---
lerna-debug.log | 211 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 211 insertions(+)
create mode 100644 lerna-debug.log
diff --git a/lerna-debug.log b/lerna-debug.log
new file mode 100644
index 00000000..3c15de50
--- /dev/null
+++ b/lerna-debug.log
@@ -0,0 +1,211 @@
+0 silly argv {
+0 silly argv _: [ 'version' ],
+0 silly argv private: false,
+0 silly argv lernaVersion: '8.1.9',
+0 silly argv '$0': 'node_modules/lerna/dist/cli.js'
+0 silly argv }
+1 notice cli v8.1.9
+2 verbose packageConfigs Explicit "packages" configuration found in lerna.json. Resolving packages using the configured glob(s): ["packages/*"]
+3 verbose rootPath /Users/jaderose/Documents/Github/cstrudel/strudel
+4 info versioning independent
+5 silly isAnythingCommitted
+6 verbose isAnythingCommitted 1
+7 silly getCurrentBranch
+8 verbose currentBranch main
+9 silly remoteBranchExists
+10 silly isBehindUpstream
+11 silly isBehindUpstream main is behind origin/main by 0 commit(s) and ahead by 4
+12 silly hasTags
+13 verbose hasTags true
+14 silly git-describe.sync "@strudel/codemirror@1.2.7-4-ga3ac9646" => {"lastTagName":"@strudel/codemirror@1.2.7","lastVersion":"1.2.7","refCount":"4","sha":"a3ac9646","isDirty":false}
+15 info Looking for changed packages since @strudel/codemirror@1.2.7
+16 silly checking diff packages/codemirror
+17 silly no diff found in @strudel/codemirror
+18 silly checking diff packages/core
+19 silly no diff found in @strudel/core
+20 silly checking diff packages/csound
+21 silly no diff found in @strudel/csound
+22 silly checking diff packages/desktopbridge
+23 silly no diff found in @strudel/desktopbridge
+24 silly checking diff packages/draw
+25 silly no diff found in @strudel/draw
+26 silly checking diff packages/embed
+27 silly no diff found in @strudel/embed
+28 silly checking diff packages/gamepad
+29 silly no diff found in @strudel/gamepad
+30 silly checking diff packages/hs2js
+31 silly no diff found in hs2js
+32 silly checking diff packages/hydra
+33 silly no diff found in @strudel/hydra
+34 silly checking diff packages/midi
+35 silly no diff found in @strudel/midi
+36 silly checking diff packages/mini
+37 silly no diff found in @strudel/mini
+38 silly checking diff packages/mondo
+39 silly no diff found in mondolang
+40 silly checking diff packages/mondough
+41 silly no diff found in @strudel/mondo
+42 silly checking diff packages/motion
+43 silly no diff found in @strudel/motion
+44 silly checking diff packages/mqtt
+45 silly no diff found in @strudel/mqtt
+46 silly checking diff packages/osc
+47 silly found diff in packages/osc/server.js
+48 verbose filtered diff [ 'packages/osc/server.js' ]
+49 silly checking diff packages/reference
+50 silly no diff found in @strudel/reference
+51 silly checking diff packages/repl
+52 silly no diff found in @strudel/repl
+53 silly checking diff packages/sampler
+54 silly no diff found in @strudel/sampler
+55 silly checking diff packages/serial
+56 silly no diff found in @strudel/serial
+57 silly checking diff packages/soundfonts
+58 silly no diff found in @strudel/soundfonts
+59 silly checking diff packages/superdough
+60 silly found diff in packages/superdough/package.json
+61 verbose filtered diff [ 'packages/superdough/package.json' ]
+62 silly checking diff packages/supradough
+63 silly found diff in packages/supradough/package.json
+64 verbose filtered diff [ 'packages/supradough/package.json' ]
+65 silly checking diff packages/tidal
+66 silly no diff found in @strudel/tidal
+67 silly checking diff packages/tonal
+68 silly no diff found in @strudel/tonal
+69 silly checking diff packages/transpiler
+70 silly no diff found in @strudel/transpiler
+71 silly checking diff packages/vite-plugin-bundle-audioworklet
+72 silly no diff found in vite-plugin-bundle-audioworklet
+73 silly checking diff packages/web
+74 silly no diff found in @strudel/web
+75 silly checking diff packages/webaudio
+76 silly found diff in packages/webaudio/package.json
+77 verbose filtered diff [ 'packages/webaudio/package.json' ]
+78 silly checking diff packages/xen
+79 silly no diff found in @strudel/xen
+80 verbose updated @strudel/codemirror
+81 verbose updated @strudel/csound
+82 verbose updated @strudel/midi
+83 verbose updated @strudel/osc
+84 verbose updated @strudel/repl
+85 verbose updated @strudel/soundfonts
+86 verbose updated superdough
+87 verbose updated supradough
+88 verbose updated @strudel/web
+89 verbose updated @strudel/webaudio
+90 verbose git-describe undefined => "@strudel/codemirror@1.2.7-4-ga3ac9646"
+91 silly git-describe parsed => {"lastTagName":"@strudel/codemirror@1.2.7","lastVersion":"1.2.7","refCount":"4","sha":"a3ac9646","isDirty":false}
+92 info execute Skipping releases
+93 silly lifecycle No script for "preversion" in "@strudel/monorepo", continuing
+94 silly lifecycle No script for "preversion" in "@strudel/osc", continuing
+95 silly lifecycle No script for "preversion" in "superdough", continuing
+96 silly lifecycle No script for "preversion" in "supradough", continuing
+97 verbose version supradough has no lockfile. Skipping lockfile update.
+98 verbose version @strudel/osc has no lockfile. Skipping lockfile update.
+99 verbose version superdough has no lockfile. Skipping lockfile update.
+100 silly lifecycle No script for "version" in "supradough", continuing
+101 silly lifecycle No script for "version" in "superdough", continuing
+102 silly lifecycle No script for "preversion" in "@strudel/codemirror", continuing
+103 silly lifecycle No script for "preversion" in "@strudel/webaudio", continuing
+104 silly lifecycle No script for "version" in "@strudel/osc", continuing
+105 verbose version @strudel/webaudio has no lockfile. Skipping lockfile update.
+106 verbose version @strudel/codemirror has no lockfile. Skipping lockfile update.
+107 silly lifecycle No script for "version" in "@strudel/codemirror", continuing
+108 silly lifecycle No script for "version" in "@strudel/webaudio", continuing
+109 silly lifecycle No script for "preversion" in "@strudel/csound", continuing
+110 silly lifecycle No script for "preversion" in "@strudel/midi", continuing
+111 silly lifecycle No script for "preversion" in "@strudel/soundfonts", continuing
+112 silly lifecycle No script for "preversion" in "@strudel/web", continuing
+113 verbose version @strudel/csound has no lockfile. Skipping lockfile update.
+114 verbose version @strudel/midi has no lockfile. Skipping lockfile update.
+115 verbose version @strudel/soundfonts has no lockfile. Skipping lockfile update.
+116 verbose version @strudel/web has no lockfile. Skipping lockfile update.
+117 silly lifecycle No script for "version" in "@strudel/midi", continuing
+118 silly lifecycle No script for "version" in "@strudel/csound", continuing
+119 silly lifecycle No script for "version" in "@strudel/soundfonts", continuing
+120 silly lifecycle No script for "preversion" in "@strudel/repl", continuing
+121 verbose version @strudel/repl has no lockfile. Skipping lockfile update.
+122 silly lifecycle No script for "version" in "@strudel/web", continuing
+123 silly lifecycle No script for "version" in "@strudel/repl", continuing
+124 silly lifecycle No script for "version" in "@strudel/monorepo", continuing
+125 verbose version Updating root pnpm-lock.yaml
+126 silly version Skipped applying prettier to ignored file: packages/supradough/package.json
+127 silly version Skipped applying prettier to ignored file: packages/superdough/package.json
+128 silly version Skipped applying prettier to ignored file: packages/osc/package.json
+129 silly version Skipped applying prettier to ignored file: packages/codemirror/package.json
+130 silly version Skipped applying prettier to ignored file: packages/webaudio/package.json
+131 silly version Skipped applying prettier to ignored file: packages/midi/package.json
+132 silly version Skipped applying prettier to ignored file: packages/csound/package.json
+133 silly version Skipped applying prettier to ignored file: packages/soundfonts/package.json
+134 silly version Skipped applying prettier to ignored file: packages/web/package.json
+135 silly version Skipped applying prettier to ignored file: packages/repl/package.json
+136 silly version Skipped applying prettier to ignored file: pnpm-lock.yaml
+137 silly gitAdd [
+137 silly gitAdd 'packages/supradough/package.json',
+137 silly gitAdd 'packages/superdough/package.json',
+137 silly gitAdd 'packages/osc/package.json',
+137 silly gitAdd 'packages/codemirror/package.json',
+137 silly gitAdd 'packages/webaudio/package.json',
+137 silly gitAdd 'packages/midi/package.json',
+137 silly gitAdd 'packages/csound/package.json',
+137 silly gitAdd 'packages/soundfonts/package.json',
+137 silly gitAdd 'packages/web/package.json',
+137 silly gitAdd 'packages/repl/package.json',
+137 silly gitAdd 'pnpm-lock.yaml'
+137 silly gitAdd ]
+138 silly gitCommit Publish
+138 silly gitCommit
+138 silly gitCommit - @strudel/codemirror@1.2.8
+138 silly gitCommit - @strudel/csound@1.2.8
+138 silly gitCommit - @strudel/midi@1.2.8
+138 silly gitCommit - @strudel/osc@1.3.2
+138 silly gitCommit - @strudel/repl@1.2.9
+138 silly gitCommit - @strudel/soundfonts@1.2.8
+138 silly gitCommit - superdough@1.2.5
+138 silly gitCommit - supradough@1.2.4
+138 silly gitCommit - @strudel/web@1.2.8
+138 silly gitCommit - @strudel/webaudio@1.2.9
+139 verbose git [
+139 verbose git 'commit',
+139 verbose git '-F',
+139 verbose git '/private/var/folders/hc/yf_zr55547sbcpz5rj7q85sc0000gn/T/3c85b5d4-56ae-40e9-8df7-db2bc1048402/lerna-commit.txt'
+139 verbose git ]
+140 silly gitTag @strudel/codemirror@1.2.8 git tag %s -m %s
+141 verbose git [
+141 verbose git 'tag',
+141 verbose git '@strudel/codemirror@1.2.8',
+141 verbose git '-m',
+141 verbose git '@strudel/codemirror@1.2.8'
+141 verbose git ]
+142 silly gitTag @strudel/csound@1.2.8 git tag %s -m %s
+143 verbose git [ 'tag', '@strudel/csound@1.2.8', '-m', '@strudel/csound@1.2.8' ]
+144 silly gitTag @strudel/midi@1.2.8 git tag %s -m %s
+145 verbose git [ 'tag', '@strudel/midi@1.2.8', '-m', '@strudel/midi@1.2.8' ]
+146 silly gitTag @strudel/osc@1.3.2 git tag %s -m %s
+147 verbose git [ 'tag', '@strudel/osc@1.3.2', '-m', '@strudel/osc@1.3.2' ]
+148 silly gitTag @strudel/repl@1.2.9 git tag %s -m %s
+149 verbose git [ 'tag', '@strudel/repl@1.2.9', '-m', '@strudel/repl@1.2.9' ]
+150 silly gitTag @strudel/soundfonts@1.2.8 git tag %s -m %s
+151 verbose git [
+151 verbose git 'tag',
+151 verbose git '@strudel/soundfonts@1.2.8',
+151 verbose git '-m',
+151 verbose git '@strudel/soundfonts@1.2.8'
+151 verbose git ]
+152 silly gitTag superdough@1.2.5 git tag %s -m %s
+153 verbose git [ 'tag', 'superdough@1.2.5', '-m', 'superdough@1.2.5' ]
+154 silly gitTag supradough@1.2.4 git tag %s -m %s
+155 verbose git [ 'tag', 'supradough@1.2.4', '-m', 'supradough@1.2.4' ]
+156 silly gitTag @strudel/web@1.2.8 git tag %s -m %s
+157 verbose git [ 'tag', '@strudel/web@1.2.8', '-m', '@strudel/web@1.2.8' ]
+158 silly gitTag @strudel/webaudio@1.2.9 git tag %s -m %s
+159 verbose git [ 'tag', '@strudel/webaudio@1.2.9', '-m', '@strudel/webaudio@1.2.9' ]
+160 error Error: Command failed with exit code 128: git tag superdough@1.2.5 -m superdough@1.2.5
+160 error fatal: tag 'superdough@1.2.5' already exists
+160 error at makeError (/Users/jaderose/Documents/Github/cstrudel/strudel/node_modules/.pnpm/execa@5.0.0/node_modules/execa/lib/error.js:59:11)
+160 error at handlePromise (/Users/jaderose/Documents/Github/cstrudel/strudel/node_modules/.pnpm/execa@5.0.0/node_modules/execa/index.js:114:26)
+160 error at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
+160 error at async Promise.all (index 6)
+160 error at async VersionCommand.gitCommitAndTagVersionForUpdates (/Users/jaderose/Documents/Github/cstrudel/strudel/node_modules/.pnpm/lerna@8.1.9_encoding@0.1.13/node_modules/lerna/dist/index.js:9957:11)
+160 error at async VersionCommand.commitAndTagUpdates (/Users/jaderose/Documents/Github/cstrudel/strudel/node_modules/.pnpm/lerna@8.1.9_encoding@0.1.13/node_modules/lerna/dist/index.js:9933:18)
+160 error at async Promise.all (index 0)
From 17ef84ed708fde6ea4df17aa3a142e79ca119bf4 Mon Sep 17 00:00:00 2001
From: "Jade (Rose) Rowland"
Date: Sat, 17 Jan 2026 19:11:08 -0800
Subject: [PATCH 061/200] sp
---
packages/superdough/helpers.mjs | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs
index 584b1967..07199aff 100644
--- a/packages/superdough/helpers.mjs
+++ b/packages/superdough/helpers.mjs
@@ -172,6 +172,7 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
if (a == null && d == null && s == null && r == null) {
return defaultValues ?? [envmin, envmin, envmax, releaseMin];
}
+
const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin;
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
};
From f610965f4332837febe45743105da170e8b331ed Mon Sep 17 00:00:00 2001
From: "Jade (Rose) Rowland"
Date: Sat, 17 Jan 2026 19:12:20 -0800
Subject: [PATCH 062/200] Publish
- @strudel/codemirror@1.3.0
- @strudel/csound@1.3.0
- @strudel/midi@1.3.0
- @strudel/repl@1.3.0
- @strudel/soundfonts@1.3.0
- superdough@1.3.0
- @strudel/web@1.3.0
- @strudel/webaudio@1.3.0
---
packages/codemirror/package.json | 2 +-
packages/csound/package.json | 2 +-
packages/midi/package.json | 2 +-
packages/repl/package.json | 2 +-
packages/soundfonts/package.json | 2 +-
packages/superdough/package.json | 2 +-
packages/web/package.json | 2 +-
packages/webaudio/package.json | 2 +-
8 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json
index 9ad04097..c8cd4ae7 100644
--- a/packages/codemirror/package.json
+++ b/packages/codemirror/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
- "version": "1.2.8",
+ "version": "1.3.0",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
diff --git a/packages/csound/package.json b/packages/csound/package.json
index 19ba6819..4af015bb 100644
--- a/packages/csound/package.json
+++ b/packages/csound/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
- "version": "1.2.8",
+ "version": "1.3.0",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/midi/package.json b/packages/midi/package.json
index 38d0f0ca..1881e024 100644
--- a/packages/midi/package.json
+++ b/packages/midi/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
- "version": "1.2.8",
+ "version": "1.3.0",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/repl/package.json b/packages/repl/package.json
index d2803492..1aff2bbe 100644
--- a/packages/repl/package.json
+++ b/packages/repl/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
- "version": "1.2.9",
+ "version": "1.3.0",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json
index 2403adfc..a904942a 100644
--- a/packages/soundfonts/package.json
+++ b/packages/soundfonts/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
- "version": "1.2.8",
+ "version": "1.3.0",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
diff --git a/packages/superdough/package.json b/packages/superdough/package.json
index d1e9b45f..8e2ffad6 100644
--- a/packages/superdough/package.json
+++ b/packages/superdough/package.json
@@ -1,6 +1,6 @@
{
"name": "superdough",
- "version": "1.2.5",
+ "version": "1.3.0",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
diff --git a/packages/web/package.json b/packages/web/package.json
index 8be1f89d..29c478c5 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
- "version": "1.2.8",
+ "version": "1.3.0",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs",
"publishConfig": {
diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json
index 6b6980e9..48b40470 100644
--- a/packages/webaudio/package.json
+++ b/packages/webaudio/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
- "version": "1.2.9",
+ "version": "1.3.0",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
From 6e36bdfff7995fcd93c3b3575b00003f6a60958a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sun, 18 Jan 2026 12:08:12 +0100
Subject: [PATCH 063/200] Add tags for some of the new functions
---
packages/core/controls.mjs | 34 ++++++++++++++++++++++++++++++
packages/core/pattern.mjs | 8 ++++++-
packages/core/signal.mjs | 3 +++
packages/midi/midi.mjs | 1 +
packages/superdough/superdough.mjs | 2 ++
5 files changed, 47 insertions(+), 1 deletion(-)
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index 59d9f1aa..834d963c 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -702,6 +702,7 @@ export const {
* any of the 8 individual FMs (e.g. `fmrel8`)
*
* @name fmrelease
+ * @tags fx, superdough, supradough
* @synonyms fmrel
* @param {number | Pattern} time release time
*
@@ -1553,6 +1554,7 @@ export const { fanchor } = registerControl('fanchor');
* Rate of the LFO for the lowpass filter
*
* @name lprate
+ * @tags fx, superdough
* @param {number | Pattern} rate rate in hertz
* @example
* note("*16").s("sawtooth").lpf(600).lprate("<4 8 2 1>")
@@ -1563,6 +1565,7 @@ export const { lprate } = registerControl('lprate');
* Cycle-synced rate of the LFO for the lowpass filter
*
* @name lpsync
+ * @tags fx, superdough
* @param {number | Pattern} rate rate in cycles
* @example
* note("*16").s("sawtooth").lpf(600).lpsync("<4 8 2 1>")
@@ -1573,6 +1576,7 @@ export const { lpsync } = registerControl('lpsync');
* Depth of the LFO for the lowpass filter
*
* @name lpdepth
+ * @tags fx, superdough
* @param {number | Pattern} depth depth of modulation
* @example
* note("*16").s("sawtooth").lpf(600).lpdepth("<1 .5 1.8 0>")
@@ -1583,6 +1587,7 @@ export const { lpdepth } = registerControl('lpdepth');
* Depth of the LFO for the lowpass filter, in HZ
*
* @name lpdepthfrequency
+ * @tags fx, superdough
* @synonyms lpdepthfreq
* @param {number | Pattern} depth depth of modulation
* @example
@@ -1595,6 +1600,7 @@ export const { lpdepthfrequency, lpdepthfreq } = registerControl('lpdepthfrequen
* Shape of the LFO for the lowpass filter
*
* @name lpshape
+ * @tags fx, superdough
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { lpshape } = registerControl('lpshape');
@@ -1603,6 +1609,7 @@ export const { lpshape } = registerControl('lpshape');
* DC offset of the LFO for the lowpass filter
*
* @name lpdc
+ * @tags fx, superdough
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { lpdc } = registerControl('lpdc');
@@ -1611,6 +1618,7 @@ export const { lpdc } = registerControl('lpdc');
* Skew of the LFO for the lowpass filter
*
* @name lpskew
+ * @tags fx, superdough
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { lpskew } = registerControl('lpskew');
@@ -1619,6 +1627,7 @@ export const { lpskew } = registerControl('lpskew');
* Rate of the LFO for the bandpass filter
*
* @name bprate
+ * @tags fx, superdough
* @param {number | Pattern} rate rate in hertz
*/
export const { bprate } = registerControl('bprate');
@@ -1627,6 +1636,7 @@ export const { bprate } = registerControl('bprate');
* Cycle-synced rate of the LFO for the bandpass filter
*
* @name bpsync
+ * @tags fx, superdough
* @param {number | Pattern} rate rate in cycles
*/
export const { bpsync } = registerControl('bpsync');
@@ -1635,6 +1645,7 @@ export const { bpsync } = registerControl('bpsync');
* Depth of the LFO for the bandpass filter
*
* @name bpdepth
+ * @tags fx, superdough
* @param {number | Pattern} depth depth of modulation
*/
export const { bpdepth } = registerControl('bpdepth');
@@ -1643,6 +1654,7 @@ export const { bpdepth } = registerControl('bpdepth');
* Depth of the LFO for the bandpass filter, in HZ
*
* @name bpdepthfrequency
+ * @tags fx, superdough
* @synonyms bpdepthfreq
* @param {number | Pattern} depth depth of modulation
* @example
@@ -1655,6 +1667,7 @@ export const { bpdepthfrequency, bpdepthfreq } = registerControl('bpdepthfrequen
* Shape of the LFO for the bandpass filter
*
* @name bpshape
+ * @tags fx, superdough
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { bpshape } = registerControl('bpshape');
@@ -1663,6 +1676,7 @@ export const { bpshape } = registerControl('bpshape');
* DC offset of the LFO for the bandpass filter
*
* @name bpdc
+ * @tags fx, superdough
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { bpdc } = registerControl('bpdc');
@@ -1671,6 +1685,7 @@ export const { bpdc } = registerControl('bpdc');
* Skew of the LFO for the bandpass filter
*
* @name bpskew
+ * @tags fx, superdough
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { bpskew } = registerControl('bpskew');
@@ -1679,6 +1694,7 @@ export const { bpskew } = registerControl('bpskew');
* Rate of the LFO for the highpass filter
*
* @name hprate
+ * @tags fx, superdough
* @param {number | Pattern} rate rate in hertz
*/
export const { hprate } = registerControl('hprate');
@@ -1687,6 +1703,7 @@ export const { hprate } = registerControl('hprate');
* Cycle-synced rate of the LFO for the highpass filter
*
* @name hpsync
+ * @tags fx, superdough
* @param {number | Pattern} rate rate in cycles
*/
export const { hpsync } = registerControl('hpsync');
@@ -1695,6 +1712,7 @@ export const { hpsync } = registerControl('hpsync');
* Depth of the LFO for the highpass filter
*
* @name hpdepth
+ * @tags fx, superdough
* @param {number | Pattern} depth depth of modulation
*/
export const { hpdepth } = registerControl('hpdepth');
@@ -1703,6 +1721,7 @@ export const { hpdepth } = registerControl('hpdepth');
* Depth of the LFO for the hipass filter, in hz
*
* @name hpdepthfrequency
+ * @tags fx, superdough
* @synonyms hpdepthfreq
* @param {number | Pattern} depth depth of modulation
* @example
@@ -1715,6 +1734,7 @@ export const { hpdepthfrequency, hpdepthfreq } = registerControl('hpdepthfrequen
* Shape of the LFO for the highpass filter
*
* @name hpshape
+ * @tags fx, superdough
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { hpshape } = registerControl('hpshape');
@@ -1723,6 +1743,7 @@ export const { hpshape } = registerControl('hpshape');
* DC offset of the LFO for the highpass filter
*
* @name hpdc
+ * @tags fx, superdough
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { hpdc } = registerControl('hpdc');
@@ -1731,6 +1752,7 @@ export const { hpdc } = registerControl('hpdc');
* Skew of the LFO for the highpass filter
*
* @name hpskew
+ * @tags fx, superdough
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { hpskew } = registerControl('hpskew');
@@ -2182,6 +2204,7 @@ export const { orbit } = registerControl('orbit', 'o');
* otherPat.bmod(..) (to modulate another pattern with the bus)
*
* @name bus
+ * @tags fx, superdough
* @param {number | Pattern} number
*/
export const { bus } = registerControl('bus');
@@ -2190,6 +2213,7 @@ export const { bus } = registerControl('bus');
* Postgain multiplier prior to sending the signal to the audio bus.
*
* @name busgain
+ * @tags fx, superdough
* @synonyms bgain
* @param {number | Pattern} number
*/
@@ -2252,6 +2276,7 @@ export const { voice } = registerControl('voice');
/**
* The chord to voice
* @name chord
+ * @tags music_theory
* @param {string | Pattern} symbols chord symbols to voice e.g., C, Eb, Fm7, G7. The symbols can be defined via addVoicings
* @example
* chord("").voicing()
@@ -2261,6 +2286,7 @@ export const { chord } = registerControl('chord');
* Which dictionary to use for the voicings. This falls back to the default dictionary if not provided
*
* @name dictionary
+ * @tags music_theory
* @param {string} dictionaryName which dictionary (having been defined with `addVoicings`) to use
* @example
* addVoicings('house', {
@@ -2275,6 +2301,7 @@ export const { dictionary, dict } = registerControl('dictionary', 'dict');
/** The top note to align the voicing to. Defaults to c5
*
* @name anchor
+ * @tags music_theory
* @param {string | Pattern} anchorNote the note to align the voicings to
* @example
* anchor("").chord("C").voicing()
@@ -2284,6 +2311,7 @@ export const { anchor } = registerControl('anchor');
* Sets how the voicing is offset from the anchored position
*
* @name offset
+ * @tags music_theory
* @param {number | Pattern} shift the amount to shift the voicing up or down
* @example
* chord("").offset("<0 1 2 3 4 5>") // alter the voicing each time
@@ -2293,6 +2321,7 @@ export const { offset } = registerControl('offset');
* How many octaves are voicing steps spread apart, defaults to 1
*
* @name octaves
+ * @tags music_theory
* @param {number | Pattern} count the number of octaves
* @example
* chord("").octaves("<2 4>").voicing()
@@ -2302,6 +2331,7 @@ export const { octaves } = registerControl('octaves');
* Remove anchor note from the voicing. Useful for melody harmonization
*
* @name mode
+ * @tags music_theory
* @param {string | Pattern} modeName one of {below | above | duck | root}
* @example
* mode("").chord("C").voicing()
@@ -3097,6 +3127,7 @@ Pattern.prototype.modulate = function (type, config, idPat) {
* a `sometimes`. See example below.
*
* @name lfo
+ * @tags fx, superdough
* @param {Object} config LFO configuration.
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
@@ -3150,6 +3181,7 @@ export const lfo = (config) => pure({}).lfo(config);
* a `sometimes`. See example below.
*
* @name env
+ * @tags fx, superdough
* @param {Object} config Envelope configuration.
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
@@ -3210,6 +3242,7 @@ export const env = (config) => pure({}).env(config);
* a `sometimes`. See example below.
*
* @name bmod
+ * @tags fx, superdough
* @param {Object} config Bus modulation configuration.
* @param {string | Pattern} [config.bus] Bus to get modulation signal from
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c
@@ -3236,6 +3269,7 @@ export const bmod = (config) => pure({}).bmod(config);
* and sustains
*
* @name transient
+ * @tags fx, superdough
* @param {number | Pattern} attack Emphasis on transients; between -1 (deaccentuate) and 1 (accentuate)
* @param {number | Pattern} sustain Emphasis on the sustains; between -1 (deaccentuate) and 1 (accentuate)
* @example
diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs
index f22b29ff..2b78a68f 100644
--- a/packages/core/pattern.mjs
+++ b/packages/core/pattern.mjs
@@ -1572,7 +1572,10 @@ export function fastcat(...pats) {
return result;
}
-/** See `fastcat` */
+/** See `fastcat`
+ * @name sequence
+ * @tags combiners
+ */
export function sequence(...pats) {
return fastcat(...pats);
}
@@ -2377,6 +2380,7 @@ export const rev = register(
* Reverse a whole pattern. See also `rev` for reversing each cycle.
*
* @name revv
+ * @tags temporal
* @memberof Pattern
* @returns Pattern
* @example
@@ -3903,6 +3907,7 @@ export const phases = (list) => {
* calls and/or in a single .FX(fx1, fx2, ..) call. The fx1, .. are _patterns_ which
* establish the controls of the given effect. See examples.
* @name FX
+ * @tags fx, superdough
* @memberof Pattern
* @returns Pattern
* @example
@@ -3953,6 +3958,7 @@ const _asArrayPattern = (pats) => {
* by wrapping them inside a function in K (see example).
*
* @name K
+ * @tags generators, fx, superdough
* @param {KabelsalatExpression | Function} expr Kabelsalat graph definition
* @memberof Pattern
* @returns Pattern
diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs
index 18090bbd..9827ddd2 100644
--- a/packages/core/signal.mjs
+++ b/packages/core/signal.mjs
@@ -286,6 +286,7 @@ export const getRandsAtTime = (t, n = 1, seed = 0) => {
* precise RNG, try `useRNG('precise')`.
*
* @name useRNG
+ * @tags generators, internals
* @param {string} mod - Mode. One of 'legacy', 'precise'
* @example
* useRNG('legacy')
@@ -341,6 +342,7 @@ export const binaryN = (n, nBits = 16) => {
* Creates a binary list pattern from a number.
*
* @name binaryL
+ * @tags generators
* @param {number} n - input number to convert to binary
* s("saw").seg(8)
* .partials(binaryL(irand(4096).add(1)))
@@ -354,6 +356,7 @@ export const binaryL = (n) => {
* Creates a binary list pattern from a number, padded to n bits long.
*
* @name binaryNL
+ * @tags generators
* @param {number} n - input number to convert to binary
* @param {number} nBits - pattern length, defaults to 16
*/
diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs
index e03dcbd0..8ce26c2a 100644
--- a/packages/midi/midi.mjs
+++ b/packages/midi/midi.mjs
@@ -577,6 +577,7 @@ export async function midin(input) {
* note durations
*
* @name midikeys
+ * @tags external_io
* @param {string | number} input MIDI device name or index defaulting to 0
* @returns {function((number | Pattern)=): Pattern} A function that produces a pattern.
* When queried, the pattern will produces the most recently played midi notes and velocities,
diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs
index 45c77d67..bcf235f3 100644
--- a/packages/superdough/superdough.mjs
+++ b/packages/superdough/superdough.mjs
@@ -40,6 +40,7 @@ export let maxPolyphony = DEFAULT_MAX_POLYPHONY;
* start to die out in first-in-first-out order once the max polyphony has been hit
*
* @name setMaxPolyphony
+ * @tags fx, superdough
* @param {number} Max polyphony. Defaults to 128
* @example
* setMaxPolyphony(4)
@@ -73,6 +74,7 @@ export function applyGainCurve(val) {
* quadratic, exponential, etc. rather than linear
*
* @name setGainCurve
+ * @tags fx, superdough
* @param {Function} function to apply to all gain values
* @example
* setGainCurve((x) => x * x) // quadratic gain
From f8750901f0e08d7c7ff43daed5efa04daa47344f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Va=CC=81clav=20Volhejn?=
Date: Sun, 18 Jan 2026 12:16:50 +0100
Subject: [PATCH 064/200] Tag the remaining functions
---
packages/core/pattern.mjs | 4 ++++
packages/core/signal.mjs | 9 ++++++++-
packages/superdough/util.mjs | 4 +++-
3 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs
index 2b78a68f..b7a047bf 100644
--- a/packages/core/pattern.mjs
+++ b/packages/core/pattern.mjs
@@ -1653,6 +1653,7 @@ export const func = curry((a, b) => reify(b).func(a));
/**
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
*
+ * @tags functional
* @param {string | string[]} name name of the function, or an array of names to be used as synonyms
* @param {function} func function with 1 or more params, where last is the current pattern
* @param {bool} patternify defaults to true; if set to false, you will have more control over the arguments to `func` as they will be
@@ -3843,6 +3844,7 @@ export const chebyshev = _distortWithAlg('chebyshev');
* Turns a list of patterns into a single pattern which outputs list-values
*
* @name parray
+ * @tags combiners
* @returns Pattern
*/
export const parray = (pats) => {
@@ -3865,6 +3867,7 @@ const _ensureListPattern = (list) => {
* Can also be used to create a new synth via `s('user').partials(...)`
*
* @name partials
+ * @tags fx, superdough
* @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped)
* @example
* s("user").seg(16).n(irand(8)).scale("A:major")
@@ -3886,6 +3889,7 @@ export const partials = (list) => {
* Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases
*
* @name phases
+ * @tags fx, superdough
* @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped)
* @example
* // Phase cancellation
diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs
index 9827ddd2..2f9fe4b1 100644
--- a/packages/core/signal.mjs
+++ b/packages/core/signal.mjs
@@ -286,7 +286,7 @@ export const getRandsAtTime = (t, n = 1, seed = 0) => {
* precise RNG, try `useRNG('precise')`.
*
* @name useRNG
- * @tags generators, internals
+ * @tags generators, math
* @param {string} mod - Mode. One of 'legacy', 'precise'
* @example
* useRNG('legacy')
@@ -376,6 +376,7 @@ export const binaryNL = (n, nBits = 16) => {
* Creates a list of random numbers of the given length
*
* @name randL
+ * @tags generators
* @param {number} n Number of random numbers to sample
* @example
* s("saw").seg(16).n(irand(12)).scale("F1:minor")
@@ -434,6 +435,7 @@ export const scramble = register('scramble', (n, pat) => {
/**
* Modify a pattern by applying a function to the `randomSeed` control if present
*
+ * @tags math
* @param {Function} func Function from seed (or undefined) to seed (or undefined)
* @param {Pattern} pat Pattern to update
* @returns Pattern
@@ -453,6 +455,7 @@ export const withSeed = (func, pat) => {
* that use randomness, like `shuffle` and `sometimes`.
*
* @name seed
+ * @tags math
* @param {number} n A new seed. Can be any number.
* @example
* $: s("hh*4").degrade();
@@ -1031,6 +1034,8 @@ export const keyDown = register('keyDown', function (pat) {
* event durations, from the pattern that it is combined with.
* For example `cyclesPer.struct("1 1 [1 1] 1")` would give the same as `"0.25 0.25 [0.125 0.125] 0.25"`.
* See also its reciprocal, `per`, also known as `perCycle`.
+ *
+ * @tags temporal
* @example
* // Shorter events are lower in pitch
* sound("saw saw [saw saw] saw")
@@ -1049,6 +1054,7 @@ export const cyclesPer = new Pattern(function (state) {
* event durations, from the pattern that it is combined with.
* For example `per.struct("1 1 [1 1] 1")` would give the same as `"4 4 [8 8] 4"`.
* See also its reciprocal, `cyclesPer`.
+ * @tags temporal
* @synonyms perCycle
* @example
* // Shorter events are more distorted
@@ -1066,6 +1072,7 @@ export const perCycle = per;
* particular, where the event duration halves, the
* returned value increases by one. `perx.struct("1 1 [1 [1 1]] 1")` would therefore be
* the same as `"3 3 [4 [5 5]] 3"`.
+ * @tags temporal
*/
export const perx = new Pattern(function (state) {
const n = Fraction(1).div(state.span.duration);
diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs
index 48830541..caa58fa8 100644
--- a/packages/superdough/util.mjs
+++ b/packages/superdough/util.mjs
@@ -110,7 +110,9 @@ export function getCommonSampleInfo(hapValue, bank) {
return { transpose, url, index, midi, label };
}
-/** Selects entries from `source` and renames them via `map` */
+/** Selects entries from `source` and renames them via `map`
+ * @tags internals
+ */
export const pickAndRename = (source, map) => {
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
};
From 6981638f706edd5ab68144bead3599852b562966 Mon Sep 17 00:00:00 2001
From: Felix Roos
Date: Sun, 18 Jan 2026 22:04:49 +0100
Subject: [PATCH 065/200] fix: move kabelsalat web to superdough
---
packages/core/package.json | 1 -
packages/core/repl.mjs | 8 --------
packages/superdough/package.json | 1 +
packages/superdough/superdough.mjs | 17 +++++++++++++++++
pnpm-lock.yaml | 6 +++---
5 files changed, 21 insertions(+), 12 deletions(-)
diff --git a/packages/core/package.json b/packages/core/package.json
index e558c574..9316ec49 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -31,7 +31,6 @@
},
"homepage": "https://strudel.cc",
"dependencies": {
- "@kabelsalat/web": "^0.4.1",
"fraction.js": "^5.2.1"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs
index 58a3532c..bf86ba49 100644
--- a/packages/core/repl.mjs
+++ b/packages/core/repl.mjs
@@ -12,7 +12,6 @@ import {
import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
import { reset_state } from './impure.mjs';
-import { SalatRepl } from '@kabelsalat/web';
export function repl({
defaultOutput,
@@ -31,7 +30,6 @@ export function repl({
id,
mondo = false,
}) {
- const kabel = new SalatRepl({ localScope: true });
const state = {
schedulerError: undefined,
evalError: undefined,
@@ -89,11 +87,6 @@ export function repl({
return silence;
};
- const compileKabel = (code) => {
- const node = kabel.evaluate(code);
- return node.compile({ log: false });
- };
-
// helper to get a patternified pure value out
function unpure(pat) {
if (pat._Pattern) {
@@ -221,7 +214,6 @@ export function repl({
setcps: setCps,
setCpm,
setcpm: setCpm,
- compileKabel,
});
};
diff --git a/packages/superdough/package.json b/packages/superdough/package.json
index 8e2ffad6..ce17ab5e 100644
--- a/packages/superdough/package.json
+++ b/packages/superdough/package.json
@@ -37,6 +37,7 @@
},
"dependencies": {
"@kabelsalat/lib": "^0.4.1",
+ "@kabelsalat/web": "^0.4.1",
"nanostores": "^0.11.3"
},
"engines": {
diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs
index 529392e9..79da4f91 100644
--- a/packages/superdough/superdough.mjs
+++ b/packages/superdough/superdough.mjs
@@ -260,6 +260,14 @@ export function loadWorklets() {
return workletsLoading;
}
+let kabel;
+async function initKabelsalat() {
+ const { SalatRepl } = await import('@kabelsalat/web');
+ logger('[kabelsalat] ready');
+ kabel = new SalatRepl({ localScope: true });
+ return kabel;
+}
+
// this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) {
const {
@@ -306,6 +314,7 @@ export async function initAudio(options = {}) {
} catch (err) {
console.warn('could not load AudioWorklet effects', err);
}
+ await initKabelsalat();
logger('[superdough] ready');
}
let audioReady;
@@ -441,6 +450,14 @@ class Chain {
}
}
+const compileKabel = (code) => {
+ if (!kabel) {
+ throw new Error('kabelsalat not loaded');
+ }
+ const node = kabel.evaluate(code);
+ return node.compile({ log: false });
+};
+
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
// mapping from main FX and numbered FX chains to nodes
const nodes = { main: {} };
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 53437f8f..468ceacf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -234,9 +234,6 @@ importers:
packages/core:
dependencies:
- '@kabelsalat/web':
- specifier: ^0.4.1
- version: 0.4.1
fraction.js:
specifier: ^5.2.1
version: 5.2.1
@@ -524,6 +521,9 @@ importers:
'@kabelsalat/lib':
specifier: ^0.4.1
version: 0.4.1
+ '@kabelsalat/web':
+ specifier: ^0.4.1
+ version: 0.4.1
nanostores:
specifier: ^0.11.3
version: 0.11.3
From 4b8d80016b0b4cabdd1231c7295023c05e5fe263 Mon Sep 17 00:00:00 2001
From: space-shell
Date: Mon, 19 Jan 2026 10:13:48 +0100
Subject: [PATCH 066/200] Remove trailin whitespace from CHANGELOG.md
---
CHANGELOG.md | 1073 +-------------------------------------------------
1 file changed, 9 insertions(+), 1064 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11edc7e4..2007ee11 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,10 @@
-# changelog
-
-NOTE: you can generate this with `node warm.js`. it might still not be perfectly sorted...
-
-## january 2026
-
-
+load page 1/1
+-------------
+- 2026-01-18T22:27:46+01:00 fix: move kabelsalat web to superdough by @froos in: [#1932](https://codeberg.org/uzu/strudel/pulls/1932)
+- 2026-01-18T12:36:50+01:00 Add tags to reference by @vvolhejn in: [#1645](https://codeberg.org/uzu/strudel/pulls/1645)
+- 2026-01-18T10:36:02+01:00 fix: performance issues when reference is open by @robojumper in: [#1926](https://codeberg.org/uzu/strudel/pulls/1926)
+- 2026-01-17T21:00:43+01:00 Make kabelsalat stereo out by @glossing in: [#1927](https://codeberg.org/uzu/strudel/pulls/1927)
+- 2026-01-16T10:38:04+01:00 update changelog + fix script to not miss entries by @froos in: [#1916](https://codeberg.org/uzu/strudel/pulls/1916)
- 2026-01-15T16:59:14+01:00 mondo fix: add registered functions to scope automatically by @daslyfe in: [#1896](https://codeberg.org/uzu/strudel/pulls/1896)
- 2026-01-15T16:58:14+01:00 Added docs for pattern search by @JohnBjrk in: [#1905](https://codeberg.org/uzu/strudel/pulls/1905)
- 2026-01-15T16:57:19+01:00 Update kabelsalat dependency by @jeromew in: [#1911](https://codeberg.org/uzu/strudel/pulls/1911)
@@ -16,1067 +16,12 @@ NOTE: you can generate this with `node warm.js`. it might still not be perfectly
- 2026-01-13T00:18:10+01:00 Add shortcut for navigating through labels! by @daslyfe in: [#1807](https://codeberg.org/uzu/strudel/pulls/1807)
- 2026-01-12T00:17:37+01:00 add warm.strudel.cc to faq by @yaxu in: [#1891](https://codeberg.org/uzu/strudel/pulls/1891)
- 2026-01-11T19:00:25+01:00 Fix sounds example to work in the REPL by @JesCoding in: [#1851](https://codeberg.org/uzu/strudel/pulls/1851)
-- 2026-01-11T13:52:02+01:00 Improve hint text when sound search has no results by @floy in: [#1883](https://codeberg.org/uzu/strudel/pulls/1883)
-- 2026-01-11T13:19:46+01:00 New page FAQ in "More" by @scrappy_fiddler in: [#1753](https://codeberg.org/uzu/strudel/pulls/1753)
-- 2026-01-11T12:45:07+01:00 fix: remove faulty default readme by @froos in: [#1889](https://codeberg.org/uzu/strudel/pulls/1889)
-- 2026-01-11T12:15:34+01:00 fix/self-hosted-config by @alienmind in: [#1880](https://codeberg.org/uzu/strudel/pulls/1880)
- 2026-01-11T12:07:48+01:00 Feat: Support External AudioContext Injection by @1d10t in: [#1833](https://codeberg.org/uzu/strudel/pulls/1833)
-- 2026-01-11T11:37:37+01:00 fix: add trem to top level by @froos in: [#1887](https://codeberg.org/uzu/strudel/pulls/1887)
-- 2026-01-11T11:37:18+01:00 fix: export start cycle min 0 by @froos in: [#1888](https://codeberg.org/uzu/strudel/pulls/1888)
-- 2026-01-11T11:05:44+01:00 fix: repl package init audio properly by @froos in: [#1836](https://codeberg.org/uzu/strudel/pulls/1836)
-- 2026-01-11T06:51:24+01:00 Fix: show reload dialog when uploading prebake script by @daslyfe in: [#1886](https://codeberg.org/uzu/strudel/pulls/1886)
-- 2026-01-11T06:01:42+01:00 fixes Serial onTrigger() params #1633 by @gueejla in: [#1885](https://codeberg.org/uzu/strudel/pulls/1885)
- 2026-01-10T23:12:48+01:00 Perf: Targeted node pools by @glossing in: [#1810](https://codeberg.org/uzu/strudel/pulls/1810)
- 2026-01-10T20:52:40+01:00 Allow top level distortions for the purpose of FX by @glossing in: [#1884](https://codeberg.org/uzu/strudel/pulls/1884)
-- 2026-01-09T03:43:37+01:00 Bake in scaling by `freq` for FM with a gain node by @glossing in: [#1878](https://codeberg.org/uzu/strudel/pulls/1878)
-- 2026-01-09T02:53:38+01:00 Bug fix: Properly handle subcontrols by @glossing in: [#1877](https://codeberg.org/uzu/strudel/pulls/1877)
- 2026-01-07T20:06:18+01:00 Feat: Add ability to turn mini parsing off with mini-off decorator by @glossing in: [#1786](https://codeberg.org/uzu/strudel/pulls/1786)
-- 2026-01-07T19:22:24+01:00 Bug Fix: Update loopStart/End to not be offset by @glossing in: [#1826](https://codeberg.org/uzu/strudel/pulls/1826)
-- 2026-01-06T06:35:49+01:00 Update modulator docstrings and allow ids to be patterns by @glossing in: [#1874](https://codeberg.org/uzu/strudel/pulls/1874)
-- 2026-01-04T17:14:11+01:00 Fix doc link in @strudel/osc README.md by @forrcaho in: [#1872](https://codeberg.org/uzu/strudel/pulls/1872)
-- 2026-01-04T02:01:07+01:00 Make stretch modulatable by @glossing in: [#1870](https://codeberg.org/uzu/strudel/pulls/1870)
-- 2026-01-02T18:44:53+01:00 Make pan modulatable by @glossing in: [#1865](https://codeberg.org/uzu/strudel/pulls/1865)
-- 2026-01-01T21:42:23+01:00 Fix transpilation example to have same mini-notation by @JesCoding in: [#1850](https://codeberg.org/uzu/strudel/pulls/1850)
-- 2026-01-01T21:39:05+01:00 fix: missing punctuation by @eddyflux in: [#1860](https://codeberg.org/uzu/strudel/pulls/1860)
-- 2026-01-01T20:20:34+01:00 Fix formatting of docstring by @glossing in: [#1864](https://codeberg.org/uzu/strudel/pulls/1864)
- 2026-01-01T19:52:15+01:00 Feat: FX Chains by @glossing in: [#1861](https://codeberg.org/uzu/strudel/pulls/1861)
-
-## december 2025
-
-- 2025-12-29T21:59:11+01:00 Bugfix: Fix modulator clamping when min/max not specified by @glossing in: [#1859](https://codeberg.org/uzu/strudel/pulls/1859)
- 2025-12-29T20:54:11+01:00 Feature: LFOs and Envelopes by @glossing in: [#1507](https://codeberg.org/uzu/strudel/pulls/1507)
-- 2025-12-29T16:07:18+01:00 Fix AudioContext change detection. Use AudioNode.context by @jeromew in: [#1858](https://codeberg.org/uzu/strudel/pulls/1858)
-- 2025-12-28T22:58:38+01:00 Say that @license should use SPDX identifier by @Wuzzy in: [#1817](https://codeberg.org/uzu/strudel/pulls/1817)
-- 2025-12-28T22:56:07+01:00 Expose Vim object in order to create custom keybindings by @JohnBjrk in: [#1816](https://codeberg.org/uzu/strudel/pulls/1816)
-- 2025-12-28T14:20:03+01:00 dough repl fixes by @froos in: [#1855](https://codeberg.org/uzu/strudel/pulls/1855)
-- 2025-12-28T13:40:29+01:00 add basic dough repl by @froos in: [#1749](https://codeberg.org/uzu/strudel/pulls/1749)
-- 2025-12-20T22:27:52+01:00 Document "-" in mini-notation by @Wuzzy in: [#1818](https://codeberg.org/uzu/strudel/pulls/1818)
-- 2025-12-20T22:20:27+01:00 simplify envValAtTime and remove asymmetric behavior (fix #1653) by @pulu in: [#1815](https://codeberg.org/uzu/strudel/pulls/1815)
-- 2025-12-19T08:11:50+01:00 fix: visual block selection mode for vim bindings by @Dsm0 in: [#1839](https://codeberg.org/uzu/strudel/pulls/1839)
-- 2025-12-19T01:03:53+01:00 [perf] Add audiograph `await debugAudiograph()` feature by @jeromew in: [#1763](https://codeberg.org/uzu/strudel/pulls/1763)
-- 2025-12-19T00:45:41+01:00 Feature: non-realtime exporting by @Ghost in: [#1674](https://codeberg.org/uzu/strudel/pulls/1674)
-- 2025-12-14T19:33:37+01:00 Fix: wrong warning in build environments by @jeromew in: [#1835](https://codeberg.org/uzu/strudel/pulls/1835)
-- 2025-12-14T15:09:12+01:00 delta -> per / perx / cyclesPer refinements by @yaxu in: [#1832](https://codeberg.org/uzu/strudel/pulls/1832)
-- 2025-12-14T01:07:36+01:00 Improved randomness by @glossing in: [#1505](https://codeberg.org/uzu/strudel/pulls/1505)
-- 2025-12-12T10:28:27+01:00 Add delta signal for representing the duration of events in patterns that are combined with it by @yaxu in: [#1831](https://codeberg.org/uzu/strudel/pulls/1831)
- 2025-12-11T18:00:49+01:00 Feature: stateful timeline function for jumping between timelines by @yaxu in: [#1669](https://codeberg.org/uzu/strudel/pulls/1669)
-- 2025-12-11T16:37:58+01:00 Updates relating to LLM, github, etc by @yaxu in: [#1830](https://codeberg.org/uzu/strudel/pulls/1830)
-- 2025-12-10T15:32:44+01:00 fix .as so it doesn't set undefined values by @yaxu in: [#1827](https://codeberg.org/uzu/strudel/pulls/1827)
-- 2025-12-08T19:27:31+01:00 [perf] propagate `onceEnded` and `releaseAudioNode` by @jeromew in: [#1809](https://codeberg.org/uzu/strudel/pulls/1809)
-- 2025-12-08 Add Helix keybindings support to REPL editor - Added `codemirror-helix` package integration, enabling Helix editor keybindings as a new option in the REPL settings alongside Vim, Emacs, and VSCode modes
-- 2025-12-07T21:51:40+01:00 Feat: Add channel support to midi in by @glossing in: [#1775](https://codeberg.org/uzu/strudel/pulls/1775)
-- 2025-12-07T21:23:43+01:00 Feat: Transient shaper by @glossing in: [#1777](https://codeberg.org/uzu/strudel/pulls/1777)
-- 2025-12-07T19:15:43+01:00 Add vel as a synonym for velocity & update a few docstrings by @glossing in: [#1781](https://codeberg.org/uzu/strudel/pulls/1781)
-- 2025-12-07T18:53:04+01:00 [perf] fix phaser leak of unused biquads by @jeromew in: [#1800](https://codeberg.org/uzu/strudel/pulls/1800)
-- 2025-12-07T18:42:50+01:00 Bug fix: Remove failing tests due to shabda removal by @glossing in: [#1820](https://codeberg.org/uzu/strudel/pulls/1820)
-- 2025-12-03T18:27:28+01:00 Feature: Eight FMs by @glossing in: [#1628](https://codeberg.org/uzu/strudel/pulls/1628)
-- 2025-12-03T17:35:05+01:00 [perf] release unused AudioBufferSourceNode + releaseAudioNode by @jeromew in: [#1805](https://codeberg.org/uzu/strudel/pulls/1805)
-- **2025-12-01 strudel.cc deployed**
-
-## november 2025
-
-- 2025-11-29T01:00:42+01:00 added export to getSuperdoughAudioController() so that its possible to route superdough audio through other webaudio applications. by @ndr0n in: [#1796](https://codeberg.org/uzu/strudel/pulls/1796)
-- 2025-11-28T23:26:20+01:00 add revv() for reversing whole patterns by @yaxu in: [#1791](https://codeberg.org/uzu/strudel/pulls/1791)
-- 2025-11-28T20:19:16+01:00 [perf] Disconnect lfos for phaser and filters by @glossing in: [#1787](https://codeberg.org/uzu/strudel/pulls/1787)
-- 2025-11-27T23:08:52+01:00 fix: return silence when no pattern is returned by @froos in: [#1795](https://codeberg.org/uzu/strudel/pulls/1795)
-- 2025-11-27T22:47:03+01:00 fix(tool/dbpatch): add missing package name by @peterpf in: [#1765](https://codeberg.org/uzu/strudel/pulls/1765)
-- 2025-11-27T22:38:05+01:00 add CHANGELOG.md + basic script to generate by @froos in: [#1794](https://codeberg.org/uzu/strudel/pulls/1794)
-- 2025-11-27T22:03:53+01:00 [perf] in `noise`, let noiseMix do the disconnect when it exists by @jeromew in: [#1783](https://codeberg.org/uzu/strudel/pulls/1783)
-- 2025-11-27T22:01:01+01:00 Bug Fix: Retries for sounds tab by @glossing in: [#1754](https://codeberg.org/uzu/strudel/pulls/1754)
-- 2025-11-27T20:36:46+01:00 [hydra] return the hydra object when await initHydra(..) is called by @jeromew in: [#1784](https://codeberg.org/uzu/strudel/pulls/1784)
-- 2025-11-27T20:36:14+01:00 Use errorLogger for query and tonal errors by @glossing in: [#1782](https://codeberg.org/uzu/strudel/pulls/1782)
-- 2025-11-27T20:27:10+01:00 [perf] level 5 `connect-leak` on `vowel` by @jeromew in: [#1779](https://codeberg.org/uzu/strudel/pulls/1779)
-- 2025-11-26T08:23:38+01:00 feat: add prebake script import button for loading .strudel files by @daslyfe in: [#1774](https://codeberg.org/uzu/strudel/pulls/1774)
-- 2025-11-26T07:39:28+01:00 Fix Sampler port trampling by @Dayglo in: [#1717](https://codeberg.org/uzu/strudel/pulls/1717)
-- 2025-11-25T20:59:09+01:00 Feat: Hook up octave and fix example by @glossing in: [#1773](https://codeberg.org/uzu/strudel/pulls/1773)
-- 2025-11-25T20:48:09+01:00 [perf] fix `connect-leak` in `tremolo` param by @jeromew in: [#1780](https://codeberg.org/uzu/strudel/pulls/1780)
-- 2025-11-24T17:51:20+01:00 filter modulation improvements! by @daslyfe in: [#1769](https://codeberg.org/uzu/strudel/pulls/1769)
-- 2025-11-24T01:20:53+01:00 Add o as a synonym for orbit by @daslyfe in: [#1766](https://codeberg.org/uzu/strudel/pulls/1766)
-- 2025-11-23T22:23:26+01:00 [perf] fix `connect-leak` in `fm` modulation by @jeromew in: [#1758](https://codeberg.org/uzu/strudel/pulls/1758)
-- 2025-11-23T09:47:24+01:00 Fix interoperability issue between `all` and `await initHydra()` by @jeromew in: [#1663](https://codeberg.org/uzu/strudel/pulls/1663)
-- 2025-11-23T04:23:06+01:00 Bug fix: Swap l/r gains with temp variable by @glossing in: [#1768](https://codeberg.org/uzu/strudel/pulls/1768)
-- 2025-11-23T00:52:28+01:00 FIX: prevent LFO filter modulation from popping from negative values by @daslyfe in: [#1767](https://codeberg.org/uzu/strudel/pulls/1767)
-- 2025-11-23T00:03:32+01:00 prefix "S" for solo by @froos in: [#1481](https://codeberg.org/uzu/strudel/pulls/1481)
-- 2025-11-21T01:56:11+01:00 Bug Fix: Use frac due to negative frequencies from FM by @glossing in: [#1759](https://codeberg.org/uzu/strudel/pulls/1759)
-- 2025-11-20T19:48:16+01:00 [perf] fix `connect-leak` added by #1742 when noise() is not used by @jeromew in: [#1757](https://codeberg.org/uzu/strudel/pulls/1757)
-- 2025-11-20T14:51:00+01:00 [perf] fix `connect-leak` in `delay` effect by @jeromew in: [#1755](https://codeberg.org/uzu/strudel/pulls/1755)
-- 2025-11-20T14:49:41+01:00 [perf] fix `connect leak` when .noise() is in the mix by @jeromew in: [#1742](https://codeberg.org/uzu/strudel/pulls/1742)
-- 2025-11-18T23:52:45+01:00 wchooseCycles has now notes in an example by @scrappy_fiddler in: [#1748](https://codeberg.org/uzu/strudel/pulls/1748)
-- 2025-11-17T05:31:54+01:00 Feature: Partials by @glossing in: [#1659](https://codeberg.org/uzu/strudel/pulls/1659)
-- 2025-11-16T22:06:14+01:00 README: update superdough documentation by @TristanMlct in: [#1741](https://codeberg.org/uzu/strudel/pulls/1741)
-- 2025-11-16T22:00:43+01:00 Feature: Envelope worklet by @glossing in: [#1524](https://codeberg.org/uzu/strudel/pulls/1524)
-- 2025-11-16T21:08:54+01:00 Worklet optimizations by @glossing in: [#1730](https://codeberg.org/uzu/strudel/pulls/1730)
-- 2025-11-15T14:59:30+01:00 [perf] disconnect Offline AudioNode connections in generateReverb by @jeromew in: [#1740](https://codeberg.org/uzu/strudel/pulls/1740)
-- 2025-11-15T14:57:47+01:00 [perf] disconnect `send` effect AudioNode when `room` is used by @jeromew in: [#1736](https://codeberg.org/uzu/strudel/pulls/1736)
-- 2025-11-12T21:22:34+01:00 [supradough] fix: unstable filter by @froos in: [#1593](https://codeberg.org/uzu/strudel/pulls/1593)
-- 2025-11-12T21:12:07+01:00 Fix link syntax in `project-start` by @Kissaki in: [#1724](https://codeberg.org/uzu/strudel/pulls/1724)
-- 2025-11-12T21:06:13+01:00 Fix web README sample code by @Kissaki in: [#1725](https://codeberg.org/uzu/strudel/pulls/1725)
-- 2025-11-12T20:59:39+01:00 Fix typo: 'studel' -> 'strudel' by @drhayes in: [#1726](https://codeberg.org/uzu/strudel/pulls/1726)
-- 2025-11-12T15:23:41+01:00 fix for node 24 support in tests - #1718 by @ausav in: [#1719](https://codeberg.org/uzu/strudel/pulls/1719)
-- 2025-11-11T09:00:49+01:00 soundAlias example fix by @PepsiiMan in: [#1720](https://codeberg.org/uzu/strudel/pulls/1720)
-- 2025-11-10T08:18:36+01:00 fix: Make Supradough package build by @munshkr in: [#1711](https://codeberg.org/uzu/strudel/pulls/1711)
-- 2025-11-10T08:18:00+01:00 added docs for spaces in scale names by @ondras in: [#1715](https://codeberg.org/uzu/strudel/pulls/1715)
-- 2025-11-10T02:44:01+01:00 Feature: LFOs for Filters by @glossing in: [#1636](https://codeberg.org/uzu/strudel/pulls/1636)
-- 2025-11-05T17:07:00+01:00 improvement: sync midi messages to audio context clock by @daslyfe in: [#1708](https://codeberg.org/uzu/strudel/pulls/1708)
-- 2025-11-04T19:36:14+01:00 Add stick button mappings to gamepad implementation and improve docs by @kaosuryoko in: [#1696](https://codeberg.org/uzu/strudel/pulls/1696)
-- 2025-11-04T19:29:35+01:00 Refactor sound stopping and triggering logic in SoundsTab component by @IJOL in: [#1688](https://codeberg.org/uzu/strudel/pulls/1688)
-- 2025-11-04T19:13:21+01:00 Add setting to toggle pattern auto-start on pattern change by @moumar in: [#1690](https://codeberg.org/uzu/strudel/pulls/1690)
-- 2025-11-04T18:58:50+01:00 Voicings JSDoc by @sharkeys_lunchbox in: [#1686](https://codeberg.org/uzu/strudel/pulls/1686)
-- 2025-11-01T07:50:40+01:00 FIX: Loading local samples uses too much memory by @daslyfe in: [#1706](https://codeberg.org/uzu/strudel/pulls/1706)
-
-
-## october 2025
-
-- 2025-10-28T22:58:24+01:00 Bug Fix: Handle scale-for-notes when n also supplied by @glossing in: [#1625](https://codeberg.org/uzu/strudel/pulls/1625)
-- 2025-10-28T22:51:16+01:00 Repurpose vim shortcuts for usability by @dtricks in: [#1624](https://codeberg.org/uzu/strudel/pulls/1624)
-- 2025-10-28T22:35:21+01:00 Add support for euclidian in mondo with `bd&3:8` by @TristanCacqueray in: [#1630](https://codeberg.org/uzu/strudel/pulls/1630)
-- 2025-10-27T10:42:07+01:00 Use bunny cdn for all samples and json files by @yaxu in: [#1701](https://codeberg.org/uzu/strudel/pulls/1701)
-- **2025-10-27 @strudel/core@1.2.5**
-- 2025-10-26T22:52:54+01:00 degithub - switch some samples to bunnycdn by @yaxu in: [#1697](https://codeberg.org/uzu/strudel/pulls/1697)
-- 2025-10-26T17:09:22+01:00 Fix ZZFX example by @moumar in: [#1685](https://codeberg.org/uzu/strudel/pulls/1685)
-- 2025-10-23T15:56:04+02:00 Make osc port and host configurable. Changes dependencies. by @yaxu in: [#1682](https://codeberg.org/uzu/strudel/pulls/1682)
-- 2025-10-23T03:47:31+02:00 Adds back shape to superdough by @glossing in: [#1672](https://codeberg.org/uzu/strudel/pulls/1672)
-- 2025-10-22T22:15:19+02:00 Fix sampler.mjs githubPath by @jeromew in: [#1684](https://codeberg.org/uzu/strudel/pulls/1684)
-- 2025-10-22T16:20:50+02:00 Fix onPaint for widgets by @yaxu in: [#1658](https://codeberg.org/uzu/strudel/pulls/1658)
-- 2025-10-22T15:19:12+02:00 Fix a bug introduced by #4e17cfbdd6 by @milliganf in: [#1679](https://codeberg.org/uzu/strudel/pulls/1679)
-- 2025-10-20T22:37:15+02:00 feature/autocomplete-sound-names by @drdozer in: [#1564](https://codeberg.org/uzu/strudel/pulls/1564)
-- 2025-10-20T21:58:12+02:00 add replicate + use it for ! in mondo by @JoStro in: [#1436](https://codeberg.org/uzu/strudel/pulls/1436)
-- 2025-10-18T04:29:20+02:00 Bug Fix: Wavetable: phase wrapping at 1 and detune by @glossing in: [#1620](https://codeberg.org/uzu/strudel/pulls/1620)
-- 2025-10-16T20:26:28+02:00 github samples: default to "samples" if repository is not specified by @prezmop in: [#1644](https://codeberg.org/uzu/strudel/pulls/1644)
-- 2025-10-16T17:33:39+02:00 Docs: add example of custom chained function by @dariusk in: [#1642](https://codeberg.org/uzu/strudel/pulls/1642)
-- 2025-10-14T12:39:48+02:00 textbox by @yaxu in: [#1650](https://codeberg.org/uzu/strudel/pulls/1650)
-- 2025-10-13T07:26:51+02:00 Feature: Distortion Modes by @glossing in: [#1561](https://codeberg.org/uzu/strudel/pulls/1561)
-- 2025-10-13T07:05:57+02:00 Feature: Wavetable FM by @glossing in: [#1623](https://codeberg.org/uzu/strudel/pulls/1623)
-- 2025-10-12T20:43:42+02:00 fix: repair REPL sample sources and URL concat (#1640) by @erikfox in: [#1646](https://codeberg.org/uzu/strudel/pulls/1646)
-- 2025-10-10T11:25:02+02:00 Add control-metadata by @yaxu in: [#1634](https://codeberg.org/uzu/strudel/pulls/1634)
-- 2025-10-07T22:59:39+02:00 Fix MQTT: change the trigger handler to match new hap by @jurrchen in: [#1629](https://codeberg.org/uzu/strudel/pulls/1629)
-- 2025-10-05T21:42:19+02:00 Fix case sensitivity for synonym search by @vvolhejn in: [#1618](https://codeberg.org/uzu/strudel/pulls/1618)
-- 2025-10-01T08:56:35+02:00 Optimize wavetable synth by @glossing in: [#1613](https://codeberg.org/uzu/strudel/pulls/1613)
-- 2025-10-01T02:02:33+02:00 fix: pattern switching by @daslyfe in: [#1616](https://codeberg.org/uzu/strudel/pulls/1616)
-
-## september 2025
-
-- 2025-09-29T02:10:54+02:00 fix wavetable JSON by @daslyfe in: [#1611](https://codeberg.org/uzu/strudel/pulls/1611)
-- 2025-09-28T21:28:16+02:00 rename wavetable controls to match existing naming conventions by @daslyfe in: [#1610](https://codeberg.org/uzu/strudel/pulls/1610)
-- 2025-09-27T23:28:13+02:00 Feature: Wavetable synth improvements by @glossing in: [#1607](https://codeberg.org/uzu/strudel/pulls/1607)
-- 2025-09-27T21:43:10+02:00 wavetable improvements by @daslyfe in: [#1608](https://codeberg.org/uzu/strudel/pulls/1608)
-- 2025-09-26T08:48:46+02:00 create orbit based DJ filter by @daslyfe in: [#1603](https://codeberg.org/uzu/strudel/pulls/1603)
-- 2025-09-26T08:15:49+02:00 Feature: Wavetable synth by @glossing in: [#1525](https://codeberg.org/uzu/strudel/pulls/1525)
-- 2025-09-23T13:28:20+02:00 fix: time signal by @daslyfe in: [#1583](https://codeberg.org/uzu/strudel/pulls/1583)
-- 2025-09-21T16:16:06+02:00 mondo: fix all + setcpm + setcps by @froos in: [#1600](https://codeberg.org/uzu/strudel/pulls/1600)
-- 2025-09-19T21:59:43+02:00 fix: osc error message by @froos in: [#1597](https://codeberg.org/uzu/strudel/pulls/1597)
-- 2025-09-18T21:09:56+02:00 osc: --debug flag to see incoming messages by @froos in: [#1595](https://codeberg.org/uzu/strudel/pulls/1595)
-- 2025-09-17T14:12:10+02:00 simplify osc usage by @froos in: [#1588](https://codeberg.org/uzu/strudel/pulls/1588)
-- 2025-09-16T16:59:58+02:00 dev: logger errors to console in dev mode by @daslyfe in: [#1585](https://codeberg.org/uzu/strudel/pulls/1585)
-- 2025-09-16T03:19:37+02:00 Bug Fix / Feature: Updates to `duck` to avoid clicks and allow configuration over release/attack by @glossing in: [#1514](https://codeberg.org/uzu/strudel/pulls/1514)
-- 2025-09-15T23:36:31+02:00 Feat: Enhance `scale` function to quantize notes to a named scale by @glossing in: [#1492](https://codeberg.org/uzu/strudel/pulls/1492)
-- 2025-09-15T22:52:14+02:00 add tic80 font by @froos in: [#1579](https://codeberg.org/uzu/strudel/pulls/1579)
-- 2025-09-15T00:00:19+02:00 supradough: fix delay + add some jsdoc types by @froos in: [#1578](https://codeberg.org/uzu/strudel/pulls/1578)
-- 2025-09-14T12:42:58+02:00 added plyWith/plyWithClassic functions by @Dsm0 in: [#1571](https://codeberg.org/uzu/strudel/pulls/1571)
-- 2025-09-14T10:39:12+02:00 feat: sound alias by @Options in: [#1494](https://codeberg.org/uzu/strudel/pulls/1494)
-- 2025-09-14T01:20:15+02:00 Bug Fix: Allow penv values to be falsy by @glossing in: [#1559](https://codeberg.org/uzu/strudel/pulls/1559)
-- 2025-09-14T01:10:48+02:00 Update website/src/pages/workshop/first-sounds.mdx by @fesmith in: [#1566](https://codeberg.org/uzu/strudel/pulls/1566)
-- 2025-09-14T01:07:47+02:00 fix: autocomplete-styles + html rendering by @froos in: [#1570](https://codeberg.org/uzu/strudel/pulls/1570)
-- 2025-09-14T00:49:17+02:00 Feature: Synonyms in autocomplete and reference by @glossing in: [#1535](https://codeberg.org/uzu/strudel/pulls/1535)
-- 2025-09-13T20:01:48+02:00 Docs: add xen package examples by @dudymas in: [#1446](https://codeberg.org/uzu/strudel/pulls/1446)
-- 2025-09-13T19:51:10+02:00 Update website/src/pages/learn/code.mdx by @anecondev in: [#1391](https://codeberg.org/uzu/strudel/pulls/1391)
-- 2025-09-11T22:52:30+02:00 supradough poc by @froos in: [#1362](https://codeberg.org/uzu/strudel/pulls/1362)
-- 2025-09-11T17:29:35+02:00 fix: exclude mondough dependencies by @froos in: [#1563](https://codeberg.org/uzu/strudel/pulls/1563)
-- 2025-09-11T00:27:21+02:00 add basicSetup for keybindings by @Dsm0 in: [#1462](https://codeberg.org/uzu/strudel/pulls/1462)
-- 2025-09-10T17:25:26+02:00 feat: add delete user samples button for convenience by @daslyfe in: [#1556](https://codeberg.org/uzu/strudel/pulls/1556)
-- 2025-09-08T08:54:01+02:00 Fix formatting of REPL footnote by @ddbeck in: [#1547](https://codeberg.org/uzu/strudel/pulls/1547)
-- 2025-09-08T05:38:08+02:00 fix: OSC by @daslyfe in: [#1557](https://codeberg.org/uzu/strudel/pulls/1557)
-- 2025-09-07T22:47:13+02:00 Add examples for ? and | operators to documentation by james-@collapse in: [#1532](https://codeberg.org/uzu/strudel/pulls/1532)
-- 2025-09-07T21:48:19+02:00 Signal flow documentation by @glossing in: [#1504](https://codeberg.org/uzu/strudel/pulls/1504)
-- 2025-09-07T21:27:29+02:00 Allow flattening of dirs with sample server by @glossing in: [#1529](https://codeberg.org/uzu/strudel/pulls/1529)
-- 2025-09-07T21:05:24+02:00 fix: prevent orbit clicking by defining max number of channels in an orbit by @daslyfe in: [#1552](https://codeberg.org/uzu/strudel/pulls/1552)
-- 2025-09-02T08:42:42+02:00 Padding for tooltips, preserving linebreaks in descriptions, not closing autocompletes on click by @glossing in: [#1521](https://codeberg.org/uzu/strudel/pulls/1521)
-- 2025-09-01T17:50:11+02:00 Update "restore defaults" to not delete patterns by @glossing in: [#1519](https://codeberg.org/uzu/strudel/pulls/1519)
-
-## august 2025
-
-- 2025-08-31T19:10:04+02:00 feat: add the ability to control the speed and start time of the reverb IR by @daslyfe in: [#1501](https://codeberg.org/uzu/strudel/pulls/1501)
-- 2025-08-25T02:31:22+02:00 Bug Fix: Duck on Mobile / Safari by @glossing in: [#1503](https://codeberg.org/uzu/strudel/pulls/1503)
-- 2025-08-23T18:39:53+02:00 fix benchmarks by @yaxu in: [#1517](https://codeberg.org/uzu/strudel/pulls/1517)
-- 2025-08-21T20:57:31+02:00 add --json flag to @strudel/sampler to generate a strudel.json by @froos in: [#1513](https://codeberg.org/uzu/strudel/pulls/1513)
-- 2025-08-21T10:04:28+02:00 remove hs2js postinstall by @froos in: [#1510](https://codeberg.org/uzu/strudel/pulls/1510)
-- 2025-08-20T15:15:34+02:00 doc: scrub, duckorbit, duckattack, duckdepth + fix: arp, arpWidth, hush by @fyynn in: [#1502](https://codeberg.org/uzu/strudel/pulls/1502)
-- 2025-08-20T12:31:37+02:00 fix: repl autocomplete not rendering correctly by @robase in: [#1480](https://codeberg.org/uzu/strudel/pulls/1480)
-- 2025-08-20T12:10:02+02:00 BUG FIX: make 'midin' initialization work with multiple controllers by @cbabraham in: [#1469](https://codeberg.org/uzu/strudel/pulls/1469)
-- 2025-08-17T23:18:46+02:00 euclidish by @yaxu in: [#1482](https://codeberg.org/uzu/strudel/pulls/1482)
-- 2025-08-17T06:37:23+02:00 feat: Create a duck (sidechain) orbit effect by @daslyfe in: [#1470](https://codeberg.org/uzu/strudel/pulls/1470)
-- 2025-08-17T04:40:15+02:00 Bug Fix: Restore `log` functionality after onTrigger arg removal by @glossing in: [#1491](https://codeberg.org/uzu/strudel/pulls/1491)
-- 2025-08-17T04:33:39+02:00 Bug Fix: Handle zero appearing first in FM's ADSR by @glossing in: [#1496](https://codeberg.org/uzu/strudel/pulls/1496)
-- 2025-08-17T04:27:19+02:00 Bug Fix: FM (and vibrato) for Supersaws by @glossing in: [#1495](https://codeberg.org/uzu/strudel/pulls/1495)
-- 2025-08-04T07:51:31+02:00 Fix incorrect stack Mini Notation by @samyk in: [#1484](https://codeberg.org/uzu/strudel/pulls/1484)
-- 2025-08-03T18:57:45+02:00 fix: regression caused by incorrectly exported alias for transpose and scaleTranspose by @daslyfe in: [#1489](https://codeberg.org/uzu/strudel/pulls/1489)
-- 2025-08-03T18:34:13+02:00 Add `strans` and `scaleTrans` as synonyms of `scaleTranspose` by @TodePond in: [#1488](https://codeberg.org/uzu/strudel/pulls/1488)
-- 2025-08-02T18:18:28+02:00 Add `trans` alias for `transpose` by @TodePond in: [#1487](https://codeberg.org/uzu/strudel/pulls/1487)
-- 2025-08-02T03:27:26+02:00 feat: Create Accurate 909 bass drum synthesizer by @daslyfe in: [#1478](https://codeberg.org/uzu/strudel/pulls/1478)
-- 2025-08-02T03:23:49+02:00 feat: Add fmwave control and ability to fm with noise by @daslyfe in: [#1472](https://codeberg.org/uzu/strudel/pulls/1472)
-
-## july 2025
-
-- 2025-07-30T00:22:41+02:00 hotfix: uzu kit JSON should be in the repo to avoid offline loading errors by @daslyfe in: [#1485](https://codeberg.org/uzu/strudel/pulls/1485)
-- 2025-07-24T05:28:23+02:00 feat: make uzu-drumkit the default drumkit by @daslyfe in: [#1422](https://codeberg.org/uzu/strudel/pulls/1422)
-- 2025-07-21T07:16:19+02:00 FIX: PWM modulation by @daslyfe in: [#1467](https://codeberg.org/uzu/strudel/pulls/1467)
-- 2025-07-19T17:16:22+02:00 Tremolo modulation by @Ghost in: [#1095](https://codeberg.org/uzu/strudel/pulls/1095)
-- 2025-07-15T09:39:33+02:00 fix: can now use def in mondough by @froos in: [#1461](https://codeberg.org/uzu/strudel/pulls/1461)
-- 2025-07-11T11:10:24+02:00 fixed keybinding presedence issue by @Dsm0 in: [#1456](https://codeberg.org/uzu/strudel/pulls/1456)
-- 2025-07-11T11:09:05+02:00 added multicursor support on ctrl/cmd + click by @Dsm0 in: [#1457](https://codeberg.org/uzu/strudel/pulls/1457)
-- 2025-07-09T09:39:46+02:00 added tab-indent setting by @Dsm0 in: [#1454](https://codeberg.org/uzu/strudel/pulls/1454)
-- 2025-07-08T15:48:41+02:00 ontrigger-refactoring by @froos in: [#1442](https://codeberg.org/uzu/strudel/pulls/1442)
-- 2025-07-07T23:14:24+02:00 fix: remove first gm_synth_bass_1, as it doesn't work in safari by @froos in: [#1452](https://codeberg.org/uzu/strudel/pulls/1452)
-- 2025-07-07T00:06:16+02:00 fix: minor doc changes by @Aurel300 in: [#1435](https://codeberg.org/uzu/strudel/pulls/1435)
-- 2025-07-05T12:53:45+02:00 Fix randrun and deps including shuffle. Fixes #1441 by @yaxu in: [#1447](https://codeberg.org/uzu/strudel/pulls/1447)
-- 2025-07-05T12:15:52+02:00 add setDefault + resetDefaults to superdough by @froos in: [#1433](https://codeberg.org/uzu/strudel/pulls/1433)
-- 2025-07-04T22:36:19+02:00 disable fm for supersaw by @froos in: [#1443](https://codeberg.org/uzu/strudel/pulls/1443)
-
-## june 2025
-
-- 2025-06-30T13:28:10+02:00 corrected minor spelling mistake (#1425) by tj-@mueller in: [#1434](https://codeberg.org/uzu/strudel/pulls/1434)
-- 2025-06-30T05:26:11+02:00 Add browser cache explanation in samples.mdx by @Ghost in: [#1369](https://codeberg.org/uzu/strudel/pulls/1369)
-- 2025-06-30T05:18:42+02:00 add-release-notes by @froos in: [#1432](https://codeberg.org/uzu/strudel/pulls/1432)
-- 2025-06-28T20:30:49+02:00 feat: delaytime in cycles by @Ghost in: [#1341](https://codeberg.org/uzu/strudel/pulls/1341)
-- 2025-06-28T13:44:49+02:00 Case insensitive search in the reference tab by @kdiab in: [#1420](https://codeberg.org/uzu/strudel/pulls/1420)
-- 2025-06-28T00:04:13+02:00 add unjoin, into and chunkinto by @yaxu in: [#1418](https://codeberg.org/uzu/strudel/pulls/1418)
-- 2025-06-26T15:29:46+02:00 mondo notation by @froos in: [#1311](https://codeberg.org/uzu/strudel/pulls/1311)
-- 2025-06-24T06:14:15+02:00 allow calling `all` multiple times by @froos in: [#1417](https://codeberg.org/uzu/strudel/pulls/1417)
-- 2025-06-24T05:53:22+02:00 tonal: allow scales without tonic (default to C) by @Ghost in: [#1092](https://codeberg.org/uzu/strudel/pulls/1092)
-- 2025-06-20T20:00:12+02:00 website intro: fix whitespace and code hosting name by @trofi in: [#1400](https://codeberg.org/uzu/strudel/pulls/1400)
-- 2025-06-19T10:15:07+02:00 fix stepcat #1396 by @yaxu in: [#1398](https://codeberg.org/uzu/strudel/pulls/1398)
-- 2025-06-19T09:52:38+02:00 deprecate cpm + refactor docs to use setcpm by @froos in: [#1397](https://codeberg.org/uzu/strudel/pulls/1397)
-- 2025-06-19T02:49:09+02:00 feat: 3 new codemirror themes by @daslyfe in: [#1394](https://codeberg.org/uzu/strudel/pulls/1394)
-- 2025-06-14T16:23:00+02:00 link back to tech manual in wiki by @yaxu in: [#1382](https://codeberg.org/uzu/strudel/pulls/1382)
-- 2025-06-14T14:55:37+02:00 degithub-links by @froos in: [#1380](https://codeberg.org/uzu/strudel/pulls/1380)
-- 2025-06-13T15:24:42+02:00 fix euclidLegatoRot by @bwagner in: [#1378](https://codeberg.org/uzu/strudel/pulls/1378)
-- 2025-06-13T10:07:51+02:00 remove-ms by @yaxu in: [#1377](https://codeberg.org/uzu/strudel/pulls/1377)
-- 2025-06-12T10:25:48+02:00 Share fat URL by @Ghost in: [#1375](https://codeberg.org/uzu/strudel/pulls/1375)
-- 2025-06-05T20:39:16+02:00 feat: onTriggerTime for interfacing with random APIs and doing illegal things on event time by @Ghost in: [#1364](https://codeberg.org/uzu/strudel/pulls/1364)
-- 2025-06-05T01:08:18+02:00 feat: berlin noise by @Ghost in: [#1363](https://codeberg.org/uzu/strudel/pulls/1363)
-
-## may 2025
-
-- 2025-05-29T14:36:43+02:00 Fix bytebeat sample offset by @Ghost in: [#1359](https://codeberg.org/uzu/strudel/pulls/1359)
-- 2025-05-29T12:14:18+02:00 preserve stepcount in chunks by @yaxu in: [#1358](https://codeberg.org/uzu/strudel/pulls/1358)
-- 2025-05-29T02:34:30+02:00 Byte Beat improvements -> 2 by @Ghost in: [#1357](https://codeberg.org/uzu/strudel/pulls/1357)
-- 2025-05-28T16:40:32+02:00 Bytebeat improvements by @Ghost in: [#1356](https://codeberg.org/uzu/strudel/pulls/1356)
-- 2025-05-27T17:42:39+02:00 feat: Byte Beats! by @Ghost in: [#1354](https://codeberg.org/uzu/strudel/pulls/1354)
-- 2025-05-18T21:43:05+02:00 fix: typo on docs causing problems with autocompletion. by @Ghost in: [#1350](https://codeberg.org/uzu/strudel/pulls/1350)
-- 2025-05-15T01:12:07+02:00 feat: Create pulsewidth (pw) and pulsewidth lfo parameters by @Ghost in: [#1343](https://codeberg.org/uzu/strudel/pulls/1343)
-- 2025-05-14T18:25:17+02:00 feat: Multi Channel Orbits by @Ghost in: [#1344](https://codeberg.org/uzu/strudel/pulls/1344)
-- 2025-05-13T17:45:29+02:00 Fix typo by @Ghost in: [#1346](https://codeberg.org/uzu/strudel/pulls/1346)
-- 2025-05-02T00:01:27+02:00 Fix web + repl package builds by @froos in: [#1339](https://codeberg.org/uzu/strudel/pulls/1339)
-- 2025-05-01T23:50:57+02:00 fix: superdough worklets bundling by @froos in: [#1338](https://codeberg.org/uzu/strudel/pulls/1338)
-
-## april 2025
-
-- 2025-04-27T22:25:54+02:00 FIX: sound import order by @Ghost in: [#1333](https://codeberg.org/uzu/strudel/pulls/1333)
-- 2025-04-27T18:39:49+02:00 update docs to reflect import sounds tab change by @hpunq in: [#1332](https://codeberg.org/uzu/strudel/pulls/1332)
-- 2025-04-27T18:38:26+02:00 feat: Improve gain curve by @Ghost in: [#1318](https://codeberg.org/uzu/strudel/pulls/1318)
-- 2025-04-27T07:54:49+02:00 Add Icon to import sample button by @Ghost in: [#1331](https://codeberg.org/uzu/strudel/pulls/1331)
-- 2025-04-27T07:53:41+02:00 Add new "import-sounds" tab with explanation on folder import by @hpunq in: [#1329](https://codeberg.org/uzu/strudel/pulls/1329)
-- 2025-04-22T00:04:38+02:00 feat: new themes + theme improvements by @Ghost in: [#1326](https://codeberg.org/uzu/strudel/pulls/1326)
-- 2025-04-20T18:24:15+02:00 feat: Create scrub function for scrubbing an audio file by @Ghost in: [#1321](https://codeberg.org/uzu/strudel/pulls/1321)
-- 2025-04-18T07:17:39+02:00 fix: disable astro toolbar by default by @Ghost in: [#1324](https://codeberg.org/uzu/strudel/pulls/1324)
-- 2025-04-18T07:17:38+02:00 fix: udels header by @Ghost in: [#1325](https://codeberg.org/uzu/strudel/pulls/1325)
-- 2025-04-11T09:18:58+02:00 Send delta in OSC message in seconds, to match tidal/superdirt by @yaxu in: [#1323](https://codeberg.org/uzu/strudel/pulls/1323)
-- 2025-04-08T06:08:57+02:00 FIX: Multichannel Audio by @Ghost in: [#1322](https://codeberg.org/uzu/strudel/pulls/1322)
-- 2025-04-08T04:18:03+02:00 feat: add max polyphony feature for superdough by @Ghost in: [#1317](https://codeberg.org/uzu/strudel/pulls/1317)
-- 2025-04-05T21:12:05+02:00 enhancement: make error messages easier to read by @Ghost in: [#1315](https://codeberg.org/uzu/strudel/pulls/1315)
-- 2025-04-04T06:26:57+02:00 fix: replace empty spaces in registered sound keys by @Ghost in: [#1319](https://codeberg.org/uzu/strudel/pulls/1319)
-
-## march 2025
-
-- 2025-03-26T17:01:05+01:00 small feat: Add alias for segment and ribbon by @Ghost in: [#1314](https://codeberg.org/uzu/strudel/pulls/1314)
-- 2025-03-26T14:54:18+01:00 Fix typo pattnr by @Ghost in: [#1316](https://codeberg.org/uzu/strudel/pulls/1316)
-- 2025-03-23T20:05:31+01:00 bugfix: Allow single param to be used in the as function by @Ghost in: [#1312](https://codeberg.org/uzu/strudel/pulls/1312)
-- 2025-03-20T23:35:18+01:00 Add MIDI Program Change, SysEx, NRPN, PitchBend and AfterTouch Output by @nkymut in: [#1244](https://codeberg.org/uzu/strudel/pulls/1244)
-- 2025-03-19T17:32:30+01:00 make soundfont base url configurable by @Ghost in: [#1040](https://codeberg.org/uzu/strudel/pulls/1040)
-- 2025-03-18T20:07:08+01:00 Add num samples from 0 up to 20 by @yaxu in: [#1310](https://codeberg.org/uzu/strudel/pulls/1310)
-- 2025-03-17T10:38:54+01:00 add num samples (edited numbers) by @yaxu in: [#1309](https://codeberg.org/uzu/strudel/pulls/1309)
-- 2025-03-08T22:17:47+01:00 @strudel/sampler improvements by @froos in: [#1288](https://codeberg.org/uzu/strudel/pulls/1288)
-- 2025-03-08T21:56:50+01:00 Add Gamepad module by @nkymut in: [#1223](https://codeberg.org/uzu/strudel/pulls/1223)
-- 2025-03-04T19:17:37+01:00 change behaviour of polymeter, and remove polymeterSteps by @yaxu in: [#1302](https://codeberg.org/uzu/strudel/pulls/1302)
-- 2025-03-04T03:36:57+01:00 feat: Create Pulse Oscillator with variable PWM by @Ghost in: [#1304](https://codeberg.org/uzu/strudel/pulls/1304)
-- 2025-03-02T17:08:31+01:00 feat: Theme improvements by @Ghost in: [#1295](https://codeberg.org/uzu/strudel/pulls/1295)
-- 2025-03-02T11:58:46+01:00 bugfix zoom stepcount by @yaxu in: [#1301](https://codeberg.org/uzu/strudel/pulls/1301)
-
-## february 2025
-
-- 2025-02-28T16:01:20+01:00 Fix test error #1297 by @nkymut in: [#1298](https://codeberg.org/uzu/strudel/pulls/1298)
-- 2025-02-28T02:53:42+01:00 Hotfix: prevent undefined pattern code from crashing strudel on load by @Ghost in: [#1297](https://codeberg.org/uzu/strudel/pulls/1297)
-- 2025-02-27T08:24:58+01:00 Fix misplaced ending sentence by @Ghost in: [#1296](https://codeberg.org/uzu/strudel/pulls/1296)
-- 2025-02-23T10:54:13+01:00 Signpost licenses for source code and samples a bit more, ref #1277 by @yaxu in: [#1289](https://codeberg.org/uzu/strudel/pulls/1289)
-- 2025-02-23T10:52:27+01:00 Allow wchooseCycles probabilities to be patterned by @yaxu in: [#1292](https://codeberg.org/uzu/strudel/pulls/1292)
-- 2025-02-22T02:18:59+01:00 Create Pattern Page Pagination by @Ghost in: [#1287](https://codeberg.org/uzu/strudel/pulls/1287)
-- 2025-02-21T22:38:10+01:00 showcase tweaks by @yaxu in: [#1291](https://codeberg.org/uzu/strudel/pulls/1291)
-- 2025-02-11T16:34:24+01:00 Fixes inverted triangle wave by renaming it to `itri`, making non-inverted `tri` by @yaxu in: [#1283](https://codeberg.org/uzu/strudel/pulls/1283)
-- 2025-02-11T10:02:34+01:00 Fix `squeezejoin` and functions using it, including `bite` by @yaxu in: [#1286](https://codeberg.org/uzu/strudel/pulls/1286)
-- 2025-02-09T16:18:28+01:00 Rename repeat back to extend by @yaxu in: [#1285](https://codeberg.org/uzu/strudel/pulls/1285)
-- 2025-02-07T18:11:52+01:00 mqtt bugfix - connection check by @yaxu in: [#1282](https://codeberg.org/uzu/strudel/pulls/1282)
-- 2025-02-07T17:38:29+01:00 Bugfix: update mqtt connections dictionary by @yaxu in: [#1281](https://codeberg.org/uzu/strudel/pulls/1281)
-- 2025-02-07T11:39:54+01:00 make mqtt topic patternable by @yaxu in: [#1280](https://codeberg.org/uzu/strudel/pulls/1280)
-- 2025-02-07T11:07:32+01:00 midimaps by @froos in: [#1274](https://codeberg.org/uzu/strudel/pulls/1274)
-- 2025-02-06T15:59:03+01:00 MQTT - support adding hap duration and cps metadata to JSON messages by @yaxu in: [#1279](https://codeberg.org/uzu/strudel/pulls/1279)
-- 2025-02-05T16:10:54+01:00 [breaking change] Sample signals from query onset, rather than midpoint by @yaxu in: [#1278](https://codeberg.org/uzu/strudel/pulls/1278)
-- 2025-02-03T00:55:36+01:00 Stepwise documentation tweaks, with mridangam samples by @yaxu in: [#1275](https://codeberg.org/uzu/strudel/pulls/1275)
-- 2025-02-02T21:26:44+01:00 Polish, rename, and document stepwise functions by @yaxu in: [#1262](https://codeberg.org/uzu/strudel/pulls/1262)
-- 2025-02-01T22:57:00+01:00 Fix sf2 timing by @froos in: [#1272](https://codeberg.org/uzu/strudel/pulls/1272)
-- 2025-02-01T22:32:59+01:00 Add bank aliasing and case insensitivity by @TodePond in: [#1245](https://codeberg.org/uzu/strudel/pulls/1245)
-
-## january 2025
-
-- 2025-01-31T09:39:54+01:00 Add Device Motion module by @nkymut in: [#1217](https://codeberg.org/uzu/strudel/pulls/1217)
-- 2025-01-29T15:40:01+01:00 add "as" function + getControlName by @froos in: [#1247](https://codeberg.org/uzu/strudel/pulls/1247)
-- 2025-01-29T15:30:04+01:00 Theme glowup by @froos in: [#1268](https://codeberg.org/uzu/strudel/pulls/1268)
-- 2025-01-29T15:22:18+01:00 patchday by @froos in: [#1264](https://codeberg.org/uzu/strudel/pulls/1264)
-- 2025-01-28T00:00:03+01:00 Revert "Fix sometimes" by @yaxu in: [#1267](https://codeberg.org/uzu/strudel/pulls/1267)
-- 2025-01-24T15:32:48+01:00 add reference package by @froos in: [#1252](https://codeberg.org/uzu/strudel/pulls/1252)
-- 2025-01-24T15:16:55+01:00 support `all(pianoroll)` and `all(pianoroll({labels: true}))` by @yaxu in: [#1234](https://codeberg.org/uzu/strudel/pulls/1234)
-- 2025-01-24T12:13:49+01:00 MQTT - if password isn't provided, prompt for one by @yaxu in: [#1249](https://codeberg.org/uzu/strudel/pulls/1249)
-- 2025-01-21T06:24:03+01:00 fix docs for beat function by @Ghost in: [#1248](https://codeberg.org/uzu/strudel/pulls/1248)
-- 2025-01-18T23:27:51+01:00 understand voicings page by @froos in: [#1230](https://codeberg.org/uzu/strudel/pulls/1230)
-- 2025-01-17T19:27:00+01:00 Add binary and binaryN by @Ghost in: [#1226](https://codeberg.org/uzu/strudel/pulls/1226)
-- 2025-01-16T12:18:33+01:00 Fix sometimes by @yaxu in: [#1243](https://codeberg.org/uzu/strudel/pulls/1243)
-- 2025-01-16T05:55:46+01:00 "beat" function for "step sequencer" style rhythm notation by @Ghost in: [#1237](https://codeberg.org/uzu/strudel/pulls/1237)
-- 2025-01-14T14:39:15+01:00 Add stepBind, and some toplevel aliases for binds and withValue by @yaxu in: [#1241](https://codeberg.org/uzu/strudel/pulls/1241)
-- 2025-01-14T06:11:10+01:00 Add onKey function for custom key commands for patterns by @Ghost in: [#1235](https://codeberg.org/uzu/strudel/pulls/1235)
-- 2025-01-12T11:32:24+01:00 Update documentation for param value modification by @Ghost in: [#1238](https://codeberg.org/uzu/strudel/pulls/1238)
-
-## december 2024
-
-- 2024-12-29T13:16:08+01:00 Documentation for all/each, and bugfix for each by @yaxu in: [#1233](https://codeberg.org/uzu/strudel/pulls/1233)
-- 2024-12-29T11:29:52+01:00 Make `all()` post-stack again, and add `each()` for pre-stack by @yaxu in: [#1229](https://codeberg.org/uzu/strudel/pulls/1229)
-- 2024-12-27T22:16:54+01:00 suggested changes to voicings.mdx by @Ghost in: [#1231](https://codeberg.org/uzu/strudel/pulls/1231)
-- 2024-12-23T11:53:11+01:00 add basic spectrum function by @froos in: [#1213](https://codeberg.org/uzu/strudel/pulls/1213)
-- 2024-12-22T21:04:45+01:00 Fix regression for d1, p1, p(n) by @yaxu in: [#1227](https://codeberg.org/uzu/strudel/pulls/1227)
-
-## november 2024
-
-- 2024-11-30T10:09:36+01:00 Make cps patternable by @eefano in: [#1001](https://codeberg.org/uzu/strudel/pulls/1001)
-- 2024-11-30T10:07:56+01:00 MQTT support by @yaxu in: [#1224](https://codeberg.org/uzu/strudel/pulls/1224)
-- 2024-11-30T09:46:14+01:00 Apply `all` function to individual patterns rather than final stack by @yaxu in: [#1209](https://codeberg.org/uzu/strudel/pulls/1209)
-- 2024-11-20T16:32:51+01:00 REPL: solo and sync configuration by @Ghost in: [#1214](https://codeberg.org/uzu/strudel/pulls/1214)
-
-## october 2024
-
-- 2024-10-30T21:29:43+01:00 Add s_zip for 'cat'-ing patterns together step-by-step, bugfix `steps` by @yaxu in: [#1208](https://codeberg.org/uzu/strudel/pulls/1208)
-- 2024-10-30T21:28:32+01:00 Preserve tactus for 'degrade' and friends, and tidy up 'pick' and friends by @yaxu in: [#1205](https://codeberg.org/uzu/strudel/pulls/1205)
-- 2024-10-30T17:11:53+01:00 chore: Edit run locally instructions in README.md by @Ghost in: [#1206](https://codeberg.org/uzu/strudel/pulls/1206)
-- 2024-10-23T23:08:14+02:00 colorize console + tweak header by @froos in: [#1203](https://codeberg.org/uzu/strudel/pulls/1203)
-- 2024-10-22T22:55:00+02:00 markcss by @froos in: [#1202](https://codeberg.org/uzu/strudel/pulls/1202)
-- 2024-10-21T22:56:37+02:00 add 2 new ui settings by @froos in: [#1200](https://codeberg.org/uzu/strudel/pulls/1200)
-- 2024-10-21T20:22:52+02:00 Make panel hover behavior optional by @Ghost in: [#1199](https://codeberg.org/uzu/strudel/pulls/1199)
-- 2024-10-19T04:28:36+02:00 Menu Panel Improvements! by @Ghost in: [#1193](https://codeberg.org/uzu/strudel/pulls/1193)
-- 2024-10-19T01:10:06+02:00 update lockfile + minor versions by @froos in: [#1198](https://codeberg.org/uzu/strudel/pulls/1198)
-
-## september 2024
-
-- 2024-09-27T00:17:00+02:00 remove redundant example for cat, update snapshot by @Ghost in: [#1189](https://codeberg.org/uzu/strudel/pulls/1189)
-- 2024-09-25T13:18:19+02:00 Adding search bar (soundtab.jsx) by @Ghost in: [#1185](https://codeberg.org/uzu/strudel/pulls/1185)
-- 2024-09-23T17:18:34+02:00 Screenreader improvements by @yaxu in: [#1158](https://codeberg.org/uzu/strudel/pulls/1158)
-- 2024-09-20T22:26:41+02:00 Fix serial timing by @yaxu in: [#1188](https://codeberg.org/uzu/strudel/pulls/1188)
-- 2024-09-20T00:26:30+02:00 Add bite function by @yaxu in: [#1187](https://codeberg.org/uzu/strudel/pulls/1187)
-- 2024-09-14T13:30:53+02:00 better spacing in zen mode by @froos in: [#1147](https://codeberg.org/uzu/strudel/pulls/1147)
-- 2024-09-14T10:50:24+02:00 add filter + filterWhen + within by @froos in: [#1039](https://codeberg.org/uzu/strudel/pulls/1039)
-- 2024-09-14T10:49:21+02:00 refactor sampler by @froos in: [#1101](https://codeberg.org/uzu/strudel/pulls/1101)
-- 2024-09-14T10:43:17+02:00 handle midin device not found error by @froos in: [#1146](https://codeberg.org/uzu/strudel/pulls/1146)
-- 2024-09-13T21:59:23+02:00 Add a search bar to the REPL Reference tab by @Ghost in: [#1165](https://codeberg.org/uzu/strudel/pulls/1165)
-- 2024-09-13T21:58:47+02:00 Correct spelling mistakes by @Ghost in: [#1183](https://codeberg.org/uzu/strudel/pulls/1183)
-- 2024-09-07T23:41:29+02:00 Add seqPLoop from Tidal by @yaxu in: [#1182](https://codeberg.org/uzu/strudel/pulls/1182)
-- 2024-09-05T05:52:50+02:00 make phaser control match superdirt by @Ghost in: [#1180](https://codeberg.org/uzu/strudel/pulls/1180)
-- 2024-09-05T05:33:41+02:00 Revert "Make phaser control consistent with superdirt" by @Ghost in: [#1179](https://codeberg.org/uzu/strudel/pulls/1179)
-- 2024-09-05T05:29:12+02:00 Make phaser control consistent with superdirt by @Ghost in: [#1178](https://codeberg.org/uzu/strudel/pulls/1178)
-- 2024-09-03T04:37:15+02:00 fix sample speed when using splice and fit with superdirt by @Ghost in: [#1172](https://codeberg.org/uzu/strudel/pulls/1172)
-- 2024-09-01T16:02:24+02:00 Create audio target selector for OSC/Superdirt by @Ghost in: [#1160](https://codeberg.org/uzu/strudel/pulls/1160)
-- 2024-09-01T15:03:47+02:00 Fixes fit so it works after a chop or slice by @yaxu in: [#1171](https://codeberg.org/uzu/strudel/pulls/1171)
-
-## august 2024
-
-- 2024-08-30T14:24:08+02:00 polyJoin by @yaxu in: [#1168](https://codeberg.org/uzu/strudel/pulls/1168)
-- 2024-08-23T17:05:10+02:00 Add scramble and shuffle by @yaxu in: [#1167](https://codeberg.org/uzu/strudel/pulls/1167)
-- 2024-08-18T18:22:20+02:00 Improve + simplify neocyclist timing by @Ghost in: [#1164](https://codeberg.org/uzu/strudel/pulls/1164)
-- 2024-08-15T05:18:33+02:00 containerize/seperate out boolean checks for repl types/Repl logic into bespoke components. by @Ghost in: [#1163](https://codeberg.org/uzu/strudel/pulls/1163)
-- 2024-08-12T18:57:21+02:00 [CORS HOTFIX] by @Ghost in: [#1162](https://codeberg.org/uzu/strudel/pulls/1162)
-- 2024-08-09T05:11:28+02:00 Fix OSC clock jitter by @Ghost in: [#1157](https://codeberg.org/uzu/strudel/pulls/1157)
-
-## july 2024
-
-- 2024-07-27T11:02:38+02:00 Fix loopAt tactus by @yaxu in: [#1145](https://codeberg.org/uzu/strudel/pulls/1145)
-- 2024-07-26T04:37:34+02:00 "stretch" function (phase vocoder) by @Ghost in: [#1130](https://codeberg.org/uzu/strudel/pulls/1130)
-- 2024-07-26T04:37:17+02:00 Udels (MultiFrame Strudel) Revisited by @Ghost in: [#1132](https://codeberg.org/uzu/strudel/pulls/1132)
-- 2024-07-24T11:40:28+02:00 Fix tactus marking in mininotation by @yaxu in: [#1144](https://codeberg.org/uzu/strudel/pulls/1144)
-
-## june 2024
-
-- 2024-06-25T18:13:28+02:00 export comment commands by @froos in: [#1136](https://codeberg.org/uzu/strudel/pulls/1136)
-- 2024-06-24T18:19:22+02:00 Chop chop by @yaxu in: [#1078](https://codeberg.org/uzu/strudel/pulls/1078)
-- 2024-06-18T23:58:08+02:00 Fix bug in Fraction.lcm by @yaxu in: [#1133](https://codeberg.org/uzu/strudel/pulls/1133)
-- 2024-06-18T05:32:12+02:00 Fix clock worker dependency path in module builds by @Ghost in: [#1129](https://codeberg.org/uzu/strudel/pulls/1129)
-- 2024-06-04T00:26:48+02:00 Labeled statements doc by @froos in: [#1126](https://codeberg.org/uzu/strudel/pulls/1126)
-- 2024-06-03T22:40:21+02:00 doc: visual functions + refactor onPaint by @froos in: [#1125](https://codeberg.org/uzu/strudel/pulls/1125)
-- 2024-06-02T18:36:29+02:00 Fix indexDB failing with large amount of files by @Ghost in: [#1124](https://codeberg.org/uzu/strudel/pulls/1124)
-
-## may 2024
-
-- 2024-05-31T12:17:34+02:00 Migrate tutorial fanchor by @froos in: [#1122](https://codeberg.org/uzu/strudel/pulls/1122)
-- 2024-05-31T10:46:47+02:00 [BUG FIX] Audio worklets sometimes dont load by @Ghost in: [#1121](https://codeberg.org/uzu/strudel/pulls/1121)
-- 2024-05-31T10:25:24+02:00 change fanchor to 0 by @Ghost in: [#1107](https://codeberg.org/uzu/strudel/pulls/1107)
-- 2024-05-30T14:39:30+02:00 fix: use full repl in web package by @froos in: [#1119](https://codeberg.org/uzu/strudel/pulls/1119)
-- 2024-05-30T09:36:32+02:00 can now access strudelMirror from repl by @froos in: [#1117](https://codeberg.org/uzu/strudel/pulls/1117)
-- 2024-05-29T13:06:34+02:00 Add the mousex and mousey signal by @Ghost in: [#1112](https://codeberg.org/uzu/strudel/pulls/1112)
-- 2024-05-29T13:02:00+02:00 Fixes drawPianoroll import in codemirror example by @Ghost in: [#1116](https://codeberg.org/uzu/strudel/pulls/1116)
-- 2024-05-28T22:43:46+02:00 clarify `off` in pattern-effects.mdx by @Ghost in: [#1074](https://codeberg.org/uzu/strudel/pulls/1074)
-- 2024-05-26T17:33:07+02:00 rollback phaser by @Ghost in: [#1113](https://codeberg.org/uzu/strudel/pulls/1113)
-- 2024-05-26T17:33:06+02:00 Fix audio worklets by @Ghost in: [#1114](https://codeberg.org/uzu/strudel/pulls/1114)
-- 2024-05-26T16:53:56+02:00 Calculate phaser modulation phase based on time by @Ghost in: [#1110](https://codeberg.org/uzu/strudel/pulls/1110)
-- 2024-05-23T15:06:01+02:00 Add analog-style ladder filter by @Ghost in: [#1103](https://codeberg.org/uzu/strudel/pulls/1103)
-- 2024-05-22T12:53:20+02:00 fix sampler on windows by @Ghost in: [#1109](https://codeberg.org/uzu/strudel/pulls/1109)
-- 2024-05-20T23:26:20+02:00 web package fixes by @froos in: [#1044](https://codeberg.org/uzu/strudel/pulls/1044)
-- 2024-05-20T22:41:28+02:00 Fix sampler windows by @froos in: [#1108](https://codeberg.org/uzu/strudel/pulls/1108)
-- 2024-05-19T12:07:26+02:00 hs2js package / tidal parser by @froos in: [#870](https://codeberg.org/uzu/strudel/pulls/870)
-- 2024-05-18T10:49:08+02:00 fix little dub tune example by @Ghost in: [#1104](https://codeberg.org/uzu/strudel/pulls/1104)
-- 2024-05-17T14:43:58+02:00 Samples tab improvements by @Ghost in: [#1102](https://codeberg.org/uzu/strudel/pulls/1102)
-- 2024-05-13T10:10:34+02:00 osc: couple of fixes by @Ghost in: [#1093](https://codeberg.org/uzu/strudel/pulls/1093)
-- 2024-05-13T06:31:20+02:00 Use sessionStorage for viewingPatternData and activePattern by @Ghost in: [#1091](https://codeberg.org/uzu/strudel/pulls/1091)
-- 2024-05-12T18:23:09+02:00 repl: set document.title from @title by @Ghost in: [#1090](https://codeberg.org/uzu/strudel/pulls/1090)
-- 2024-05-07T14:29:22+02:00 fix: missing events due to premature worklet cleanup by @froos in: [#1089](https://codeberg.org/uzu/strudel/pulls/1089)
-- 2024-05-03T11:52:57+02:00 Benchmarks by @yaxu in: [#1079](https://codeberg.org/uzu/strudel/pulls/1079)
-- 2024-05-03T08:46:52+02:00 fix: csound + dough timing by @froos in: [#1086](https://codeberg.org/uzu/strudel/pulls/1086)
-- 2024-05-03T00:37:32+02:00 Improve performance of ! (replicate) by @yaxu in: [#1084](https://codeberg.org/uzu/strudel/pulls/1084)
-- 2024-05-02T23:40:22+02:00 fix: url parsing with extra params by @froos in: [#1083](https://codeberg.org/uzu/strudel/pulls/1083)
-
-## april 2024
-
-- 2024-04-29T12:36:11+02:00 Tactus calculation toggle and breaking change to tactus calculation in fast/slow/hurry by @yaxu in: [#1081](https://codeberg.org/uzu/strudel/pulls/1081)
-- 2024-04-28T20:56:21+02:00 fix docs on alignment.mdx by @Ghost in: [#1076](https://codeberg.org/uzu/strudel/pulls/1076)
-- 2024-04-28T20:49:18+02:00 add signals to recap in first-effects.mdx by @Ghost in: [#1073](https://codeberg.org/uzu/strudel/pulls/1073)
-- 2024-04-28T20:44:57+02:00 fix translation issue in first-effects.mdx by @Ghost in: [#1072](https://codeberg.org/uzu/strudel/pulls/1072)
-- 2024-04-28T20:44:29+02:00 fix failing format test by @Ghost in: [#1077](https://codeberg.org/uzu/strudel/pulls/1077)
-- 2024-04-27T22:50:17+02:00 add nesting to `off` example variation in pattern-effects.mdx by @Ghost in: [#1075](https://codeberg.org/uzu/strudel/pulls/1075)
-- 2024-04-27T22:48:58+02:00 add `<...>` to first-sounds.mdx recap by @Ghost in: [#1070](https://codeberg.org/uzu/strudel/pulls/1070)
-- 2024-04-27T22:48:07+02:00 fix first sounds typo by @Ghost in: [#1069](https://codeberg.org/uzu/strudel/pulls/1069)
-- 2024-04-27T22:47:44+02:00 fix cr typo on first-sounds.mdx by @Ghost in: [#1068](https://codeberg.org/uzu/strudel/pulls/1068)
-- 2024-04-26T15:12:30+02:00 More tactus tidying by @yaxu in: [#1071](https://codeberg.org/uzu/strudel/pulls/1071)
-- 2024-04-23T23:37:21+02:00 Fix stepjoin by @yaxu in: [#1067](https://codeberg.org/uzu/strudel/pulls/1067)
-- 2024-04-23T23:32:00+02:00 clarify license by @yaxu in: [#1064](https://codeberg.org/uzu/strudel/pulls/1064)
-- 2024-04-23T15:14:30+02:00 Tactus tweaks - fixes for maintaining tactus and highlight locations by @yaxu in: [#1065](https://codeberg.org/uzu/strudel/pulls/1065)
-- 2024-04-21T23:57:57+02:00 fix OSC timing for recent scheduler updates by @Ghost in: [#1062](https://codeberg.org/uzu/strudel/pulls/1062)
-- 2024-04-21T22:17:07+02:00 Stepwise functions from Tidal by @yaxu in: [#1060](https://codeberg.org/uzu/strudel/pulls/1060)
-- 2024-04-21T11:03:55+02:00 Fix wchooseCycles not picking the whole pattern by @Ghost in: [#1061](https://codeberg.org/uzu/strudel/pulls/1061)
-- 2024-04-19T00:05:52+02:00 add swing + swingBy by @froos in: [#1038](https://codeberg.org/uzu/strudel/pulls/1038)
-- 2024-04-19T00:05:08+02:00 anonymous patterns + muting by @froos in: [#1059](https://codeberg.org/uzu/strudel/pulls/1059)
-- 2024-04-12T12:34:27+02:00 improve tutorial + custom samples doc by @froos in: [#1053](https://codeberg.org/uzu/strudel/pulls/1053)
-- 2024-04-12T12:31:49+02:00 fix: do not reset cc input values on each eval by @froos in: [#1054](https://codeberg.org/uzu/strudel/pulls/1054)
-- 2024-04-08T17:25:27+02:00 transpose: support all combinations of numbers and strings for notes and intervals by @froos in: [#1048](https://codeberg.org/uzu/strudel/pulls/1048)
-- 2024-04-08T10:46:18+02:00 Wax, wane, taper and taperlist by @yaxu in: [#1042](https://codeberg.org/uzu/strudel/pulls/1042)
-- 2024-04-06T23:44:57+02:00 Midi Time hotfix for scheduler updates by @Ghost in: [#1047](https://codeberg.org/uzu/strudel/pulls/1047)
-- 2024-04-05T12:48:03+02:00 pitchwheel visual by @froos in: [#1041](https://codeberg.org/uzu/strudel/pulls/1041)
-- 2024-04-05T12:47:19+02:00 fix cyclist fizzling out by @froos in: [#1046](https://codeberg.org/uzu/strudel/pulls/1046)
-
-## march 2024
-
-- 2024-03-30T16:05:59+01:00 remove dangerous arithmetic feature by @froos in: [#1030](https://codeberg.org/uzu/strudel/pulls/1030)
-- 2024-03-30T16:05:27+01:00 Fix sampler paths by @froos in: [#1034](https://codeberg.org/uzu/strudel/pulls/1034)
-- 2024-03-30T14:43:08+01:00 local sample server cli by @froos in: [#1033](https://codeberg.org/uzu/strudel/pulls/1033)
-- 2024-03-29T17:14:28+01:00 add font file types to offline cache by @froos in: [#1032](https://codeberg.org/uzu/strudel/pulls/1032)
-- 2024-03-29T17:10:16+01:00 add closeBrackets setting by @froos in: [#1031](https://codeberg.org/uzu/strudel/pulls/1031)
-- 2024-03-29T14:55:06+01:00 Tactus tidy by @yaxu in: [#1027](https://codeberg.org/uzu/strudel/pulls/1027)
-- 2024-03-28T17:06:44+01:00 add setting for sync flag by @froos in: [#1025](https://codeberg.org/uzu/strudel/pulls/1025)
-- 2024-03-28T11:37:57+01:00 better theme integration for visuals + various fixes by @froos in: [#1024](https://codeberg.org/uzu/strudel/pulls/1024)
-- 2024-03-28T11:33:25+01:00 More fonts by @froos in: [#1023](https://codeberg.org/uzu/strudel/pulls/1023)
-- 2024-03-27T13:06:05+01:00 Feature: tactus marking by @yaxu in: [#1021](https://codeberg.org/uzu/strudel/pulls/1021)
-- 2024-03-25T06:02:02+01:00 hotfix for 1017 by @Ghost in: [#1020](https://codeberg.org/uzu/strudel/pulls/1020)
-- 2024-03-24T15:06:33+01:00 Document signals by @Ghost in: [#1015](https://codeberg.org/uzu/strudel/pulls/1015)
-- 2024-03-24T10:16:11+01:00 Repl sync fixes by @Ghost in: [#1014](https://codeberg.org/uzu/strudel/pulls/1014)
-- 2024-03-23T20:21:51+01:00 eliminate chromium clock jitter by @froos in: [#1004](https://codeberg.org/uzu/strudel/pulls/1004)
-- 2024-03-23T15:51:07+01:00 update undocumented script by @froos in: [#1013](https://codeberg.org/uzu/strudel/pulls/1013)
-- 2024-03-23T15:26:52+01:00 fix: await injectPatternMethods by @froos in: [#1012](https://codeberg.org/uzu/strudel/pulls/1012)
-- 2024-03-23T15:18:12+01:00 rename trig -> reset, trigzero -> restart by @froos in: [#1010](https://codeberg.org/uzu/strudel/pulls/1010)
-- 2024-03-23T12:30:03+01:00 Inline punchcard + spiral by @froos in: [#1008](https://codeberg.org/uzu/strudel/pulls/1008)
-- 2024-03-23T12:27:22+01:00 Color in hap value by @froos in: [#1007](https://codeberg.org/uzu/strudel/pulls/1007)
-- 2024-03-22T23:53:51+01:00 using strudel in your project guide + cleanup examples by @froos in: [#1006](https://codeberg.org/uzu/strudel/pulls/1006)
-- 2024-03-22T23:41:50+01:00 accidentals in scale degrees by @eefano in: [#1000](https://codeberg.org/uzu/strudel/pulls/1000)
-- 2024-03-21T22:53:55+01:00 supersaw oscillator by @Ghost in: [#978](https://codeberg.org/uzu/strudel/pulls/978)
-- 2024-03-21T13:00:04+01:00 remove canvas, externalize samples, delete junk by @froos in: [#1003](https://codeberg.org/uzu/strudel/pulls/1003)
-- 2024-03-18T11:37:55+01:00 Fix pure mini highlight by @yaxu in: [#994](https://codeberg.org/uzu/strudel/pulls/994)
-- 2024-03-18T07:12:14+01:00 inline viz / widgets package by @froos in: [#989](https://codeberg.org/uzu/strudel/pulls/989)
-- 2024-03-17T04:07:00+01:00 Labeled statements by @froos in: [#991](https://codeberg.org/uzu/strudel/pulls/991)
-- 2024-03-16T18:24:38+01:00 Beat-oriented functionality by @yaxu in: [#976](https://codeberg.org/uzu/strudel/pulls/976)
-- 2024-03-15T01:47:35+01:00 REPL sync between windows by @Ghost in: [#900](https://codeberg.org/uzu/strudel/pulls/900)
-- 2024-03-14T00:20:07+01:00 Update synths.mdx by @Ghost in: [#984](https://codeberg.org/uzu/strudel/pulls/984)
-- 2024-03-13T00:36:22+01:00 fix: share now shares what's visible instead of active by @froos in: [#985](https://codeberg.org/uzu/strudel/pulls/985)
-- 2024-03-10T01:18:57+01:00 use ireal as default voicing dict by @froos in: [#967](https://codeberg.org/uzu/strudel/pulls/967)
-- 2024-03-10T00:50:23+01:00 little fix for withVal by @eefano in: [#980](https://codeberg.org/uzu/strudel/pulls/980)
-- 2024-03-10T00:46:51+01:00 Velocity in value by @froos in: [#974](https://codeberg.org/uzu/strudel/pulls/974)
-- 2024-03-10T00:42:50+01:00 fix: clear hydra on reset by @froos in: [#983](https://codeberg.org/uzu/strudel/pulls/983)
-- 2024-03-10T00:38:43+01:00 move canvas related helpers from core to new draw package by @froos in: [#971](https://codeberg.org/uzu/strudel/pulls/971)
-- 2024-03-08T05:38:07+01:00 replace shape with distort in learn doc by @Ghost in: [#982](https://codeberg.org/uzu/strudel/pulls/982)
-- 2024-03-06T12:14:49+01:00 alias - for ~ by @yaxu in: [#981](https://codeberg.org/uzu/strudel/pulls/981)
-- 2024-03-04T16:04:23+01:00 Worklet Improvents / fixes by @Ghost in: [#963](https://codeberg.org/uzu/strudel/pulls/963)
-- 2024-03-01T17:30:19+01:00 Nested controls by @froos in: [#973](https://codeberg.org/uzu/strudel/pulls/973)
-
-## february 2024
-
-- 2024-02-29T15:33:12+01:00 feat: can now invert euclid pulses with negative numbers by @froos in: [#959](https://codeberg.org/uzu/strudel/pulls/959)
-- 2024-02-29T15:31:23+01:00 pickOut(), pickRestart(), pickReset() by @eefano in: [#950](https://codeberg.org/uzu/strudel/pulls/950)
-- 2024-02-29T15:12:45+01:00 remove legacy legato + duration implementations by @froos in: [#965](https://codeberg.org/uzu/strudel/pulls/965)
-- 2024-02-29T08:47:53+01:00 fix for transpose(): preserve hap value object structure by @eefano in: [#966](https://codeberg.org/uzu/strudel/pulls/966)
-- 2024-02-29T08:32:00+01:00 add debounce to logger by @froos in: [#968](https://codeberg.org/uzu/strudel/pulls/968)
-- 2024-02-28T18:43:52+01:00 controls refactoring: simplify exports by @froos in: [#962](https://codeberg.org/uzu/strudel/pulls/962)
-- 2024-02-25T19:17:00+01:00 fix: reset global fx on pattern change by @froos in: [#960](https://codeberg.org/uzu/strudel/pulls/960)
-- 2024-02-25T14:15:30+01:00 fix midi issue on firefox and added quote error by @Ghost in: [#936](https://codeberg.org/uzu/strudel/pulls/936)
-- 2024-02-25T13:19:47+01:00 'Enable Bracket Matching' option in Codemirror by @eefano in: [#956](https://codeberg.org/uzu/strudel/pulls/956)
-- 2024-02-23T14:37:46+01:00 fix script importable packages (web + repl) by @froos in: [#957](https://codeberg.org/uzu/strudel/pulls/957)
-- 2024-02-21T16:17:37+01:00 account for cps in midi time duration by @Ghost in: [#954](https://codeberg.org/uzu/strudel/pulls/954)
-- 2024-02-21T10:27:12+01:00 Auto await samples by @froos in: [#955](https://codeberg.org/uzu/strudel/pulls/955)
-- 2024-02-08T13:16:15+01:00 remove cjs builds by @froos in: [#945](https://codeberg.org/uzu/strudel/pulls/945)
-- 2024-02-04T23:15:37+01:00 Minor documentation error: Update first-sounds.mdx by @Ghost in: [#941](https://codeberg.org/uzu/strudel/pulls/941)
-
-
-## january 2024
-
-- 2024-01-24T16:48:57+01:00 fix: pianoroll sorting by @froos in: [#938](https://codeberg.org/uzu/strudel/pulls/938)
-- 2024-01-23T00:04:03+01:00 V1 release notes by @froos in: [#935](https://codeberg.org/uzu/strudel/pulls/935)
-- 2024-01-22T20:20:53+01:00 2 years blog post by @froos in: [#929](https://codeberg.org/uzu/strudel/pulls/929)
-- 2024-01-22T20:02:34+01:00 make 0.5hz cps the default by @yaxu in: [#931](https://codeberg.org/uzu/strudel/pulls/931)
-- 2024-01-22T00:52:01+01:00 Fix pattern tab not showing patterns without created date by @Ghost in: [#934](https://codeberg.org/uzu/strudel/pulls/934)
-- 2024-01-21T20:46:28+01:00 Add useful pattern selection behavior for performing. by @Ghost in: [#897](https://codeberg.org/uzu/strudel/pulls/897)
-- 2024-01-21T01:30:28+01:00 Refactor cps functions by @froos in: [#933](https://codeberg.org/uzu/strudel/pulls/933)
-- 2024-01-20T23:47:31+01:00 Make splice cps-aware by @yaxu in: [#932](https://codeberg.org/uzu/strudel/pulls/932)
-- 2024-01-19T18:50:57+01:00 community bakery by @froos in: [#923](https://codeberg.org/uzu/strudel/pulls/923)
-- 2024-01-19T18:49:54+01:00 add pickF and pickmodF by @Ghost in: [#924](https://codeberg.org/uzu/strudel/pulls/924)
-- 2024-01-19T15:10:48+01:00 Mini-notation additions towards tidal compatibility by @yaxu in: [#926](https://codeberg.org/uzu/strudel/pulls/926)
-- 2024-01-18T23:34:11+01:00 Blog improvements by @froos in: [#919](https://codeberg.org/uzu/strudel/pulls/919)
-- 2024-01-18T18:08:29+01:00 pick, pickmod, inhabit, inhabitmod by @yaxu in: [#921](https://codeberg.org/uzu/strudel/pulls/921)
-- 2024-01-18T18:04:27+01:00 Revert "`pick` now accepts lookup tables, with alternate cycle squeezing behaviour as new `inhabit` function" by @yaxu in: [#920](https://codeberg.org/uzu/strudel/pulls/920)
-- 2024-01-18T17:45:39+01:00 `pick` now accepts lookup tables, with alternate cycle squeezing behaviour as new `inhabit` function by @yaxu in: [#918](https://codeberg.org/uzu/strudel/pulls/918)
-- 2024-01-18T10:30:08+01:00 rename @strudel.cycles/* packages to @strudel/* by @froos in: [#917](https://codeberg.org/uzu/strudel/pulls/917)
-- 2024-01-18T06:54:33+01:00 pitch envelopes by @froos in: [#913](https://codeberg.org/uzu/strudel/pulls/913)
-- 2024-01-18T05:09:38+01:00 Fix: swatch/[name].png.js static path by @Ghost in: [#916](https://codeberg.org/uzu/strudel/pulls/916)
-- 2024-01-16T08:22:28+01:00 Add more vowel qualities for the vowels function by @Ghost in: [#907](https://codeberg.org/uzu/strudel/pulls/907)
-- 2024-01-14T23:56:36+01:00 adds a blog by @froos in: [#911](https://codeberg.org/uzu/strudel/pulls/911)
-- 2024-01-14T23:52:41+01:00 Remove hideHeader for better mobile UI and consistency by @Ghost in: [#894](https://codeberg.org/uzu/strudel/pulls/894)
-- 2024-01-14T22:43:06+01:00 Further Envelope improvements by @Ghost in: [#868](https://codeberg.org/uzu/strudel/pulls/868)
-- 2024-01-14T00:48:04+01:00 public sharing by @froos in: [#910](https://codeberg.org/uzu/strudel/pulls/910)
-- 2024-01-12T18:31:41+01:00 fix some build warnings by @froos in: [#902](https://codeberg.org/uzu/strudel/pulls/902)
-- 2024-01-12T18:01:55+01:00 prevent vite from complaining about additional exports in jsx files by @Ghost in: [#891](https://codeberg.org/uzu/strudel/pulls/891)
-- 2024-01-12T17:05:47+01:00 Update Vite version so hot reload works properly with newest pnpm version by @Ghost in: [#892](https://codeberg.org/uzu/strudel/pulls/892)
-- 2024-01-12T15:09:41+01:00 support , in < > by @froos in: [#886](https://codeberg.org/uzu/strudel/pulls/886)
-- 2024-01-10T13:55:39+01:00 fix: autocomplete / tooltip code example bug by @froos in: [#898](https://codeberg.org/uzu/strudel/pulls/898)
-- 2024-01-06T15:23:55+01:00 fix: invisible selection on vim + emacs mode by @froos in: [#889](https://codeberg.org/uzu/strudel/pulls/889)
-- 2024-01-05T22:29:20+01:00 scales can now be anchored by @froos in: [#888](https://codeberg.org/uzu/strudel/pulls/888)
-- 2024-01-04T11:21:42+01:00 add root mode for voicings by @froos in: [#887](https://codeberg.org/uzu/strudel/pulls/887)
-- 2024-01-01T18:32:17+01:00 Showcase by @froos in: [#885](https://codeberg.org/uzu/strudel/pulls/885)
-- 2024-01-01T14:22:11+01:00 bugfix: suspend and close exisiting audio context when changing interface by @Ghost in: [#882](https://codeberg.org/uzu/strudel/pulls/882)
-- 2024-01-01T14:21:52+01:00 add mastodon link by @froos in: [#884](https://codeberg.org/uzu/strudel/pulls/884)
-
-## december 2023
-
-- 2023-12-31T17:02:57+01:00 fix: make sure n is never undefined before nanFallback by @froos in: [#881](https://codeberg.org/uzu/strudel/pulls/881)
-- 2023-12-31T16:47:22+01:00 Error tolerance by @froos in: [#880](https://codeberg.org/uzu/strudel/pulls/880)
-- 2023-12-31T10:06:24+01:00 bugfix: sound select indexes out of bounds by @Ghost in: [#871](https://codeberg.org/uzu/strudel/pulls/871)
-- 2023-12-31T10:03:01+01:00 Audio device selection by @Ghost in: [#854](https://codeberg.org/uzu/strudel/pulls/854)
-- 2023-12-31T00:59:49+01:00 Dependency update by @froos in: [#879](https://codeberg.org/uzu/strudel/pulls/879)
-- 2023-12-29T16:53:41+01:00 move all examples to separate examples folder by @froos in: [#878](https://codeberg.org/uzu/strudel/pulls/878)
-- 2023-12-29T15:29:17+01:00 final vanillification by @froos in: [#876](https://codeberg.org/uzu/strudel/pulls/876)
-- 2023-12-27T18:38:09+01:00 Bug Fix #119: Clock drift by @Ghost in: [#874](https://codeberg.org/uzu/strudel/pulls/874)
-- 2023-12-27T13:17:03+01:00 main repl vanillification by @froos in: [#873](https://codeberg.org/uzu/strudel/pulls/873)
-- 2023-12-25T17:47:25+01:00 more work on vanilla repl: repl web component + package + MicroRepl by @froos in: [#866](https://codeberg.org/uzu/strudel/pulls/866)
-- 2023-12-15T00:11:35+01:00 Vanilla repl 3 by @froos in: [#865](https://codeberg.org/uzu/strudel/pulls/865)
-- 2023-12-14T21:28:49+01:00 Vanilla repl 2 by @froos in: [#863](https://codeberg.org/uzu/strudel/pulls/863)
-- 2023-12-12T21:49:23+01:00 fix: finally repair envelopes by @froos in: [#861](https://codeberg.org/uzu/strudel/pulls/861)
-- 2023-12-12T21:20:00+01:00 add missing trailing slashes by @froos in: [#860](https://codeberg.org/uzu/strudel/pulls/860)
-- 2023-12-12T19:08:31+01:00 Sound Import from local file system by @Ghost in: [#839](https://codeberg.org/uzu/strudel/pulls/839)
-- 2023-12-11T22:48:35+01:00 Pattern organization by @froos in: [#858](https://codeberg.org/uzu/strudel/pulls/858)
-- 2023-12-09T17:25:11+01:00 Export patterns + ui tweaks by @froos in: [#855](https://codeberg.org/uzu/strudel/pulls/855)
-- 2023-12-08T09:41:10+01:00 patterns tab: import patterns + style by @froos in: [#852](https://codeberg.org/uzu/strudel/pulls/852)
-- 2023-12-07T20:42:00+01:00 Patterns tab + Refactor Panel by @froos in: [#769](https://codeberg.org/uzu/strudel/pulls/769)
-- 2023-12-07T10:25:16+01:00 CHANGES: pnpm 8.1.3 to 8.11.0 by @Ghost in: [#850](https://codeberg.org/uzu/strudel/pulls/850)
-- 2023-12-07T09:35:14+01:00 Fix edge case with rehype-urls and trailing slashes in image file paths by @Ghost in: [#849](https://codeberg.org/uzu/strudel/pulls/849)
-- 2023-12-06T23:02:31+01:00 Fix examples page, piano() and a few workshop imgs by @Ghost in: [#848](https://codeberg.org/uzu/strudel/pulls/848)
-- 2023-12-06T22:30:59+01:00 fix: swatch png src by @froos in: [#846](https://codeberg.org/uzu/strudel/pulls/846)
-- 2023-12-06T22:19:30+01:00 fix: missing hash for links starting with / by @froos in: [#845](https://codeberg.org/uzu/strudel/pulls/845)
-- 2023-12-06T22:05:05+01:00 improve slashing + base href behavior by @froos in: [#842](https://codeberg.org/uzu/strudel/pulls/842)
-- 2023-12-06T21:26:49+01:00 Add in fixes from my fork to slashocalypse branch by @Ghost in: [#843](https://codeberg.org/uzu/strudel/pulls/843)
-- 2023-12-05T18:43:58+01:00 Prevent 404 on Algolia crawls by @Ghost in: [#838](https://codeberg.org/uzu/strudel/pulls/838)
-- 2023-12-05T18:26:47+01:00 Multichannel audio by @Ghost in: [#820](https://codeberg.org/uzu/strudel/pulls/820)
-- 2023-12-05T12:19:58+01:00 ADDS: JetBrains IDE files and directories to .gitignore by @Ghost in: [#840](https://codeberg.org/uzu/strudel/pulls/840)
-- 2023-12-05T12:19:27+01:00 CHANGES: github action pnpm version from 7 to 8.3.1 by @Ghost in: [#835](https://codeberg.org/uzu/strudel/pulls/835)
-- 2023-12-05T12:19:17+01:00 CHANGES: pin pnpm to version 8.3.1 by @Ghost in: [#834](https://codeberg.org/uzu/strudel/pulls/834)
-- 2023-12-05T12:19:06+01:00 CHANGES: github action checkout v2 -> v4 by @Ghost in: [#837](https://codeberg.org/uzu/strudel/pulls/837)
-- 2023-12-05T11:15:14+01:00 FIXES: palindrome abc -> abccba by @Ghost in: [#831](https://codeberg.org/uzu/strudel/pulls/831)
-- 2023-12-02T09:43:50+01:00 Fix a typo by @Ghost in: [#830](https://codeberg.org/uzu/strudel/pulls/830)
-
-## november 2023
-
-- 2023-11-30T10:42:41+01:00 Add and style algolia search by @Ghost in: [#827](https://codeberg.org/uzu/strudel/pulls/827)
-- 2023-11-25T15:50:44+01:00 Hydra fixes and improvements by @Ghost in: [#818](https://codeberg.org/uzu/strudel/pulls/818)
-- 2023-11-24T10:07:17+01:00 add options param to initHydra by @Ghost in: [#808](https://codeberg.org/uzu/strudel/pulls/808)
-- 2023-11-24T09:57:02+01:00 Improve documentation for synonym functions by @Ghost in: [#800](https://codeberg.org/uzu/strudel/pulls/800)
-- 2023-11-24T09:00:54+01:00 New noise type: "crackle" by @Ghost in: [#806](https://codeberg.org/uzu/strudel/pulls/806)
-- 2023-11-18T21:18:16+01:00 Color hsl by @froos in: [#815](https://codeberg.org/uzu/strudel/pulls/815)
-- 2023-11-17T23:18:23+01:00 fix: multiple repls by @froos in: [#813](https://codeberg.org/uzu/strudel/pulls/813)
-- 2023-11-17T20:52:25+01:00 upstream changes by @yaxu in: [#809](https://codeberg.org/uzu/strudel/pulls/809)
-- 2023-11-17T16:04:10+01:00 Add doc for euclidLegatoRot, wordfall and slider by @Ghost in: [#801](https://codeberg.org/uzu/strudel/pulls/801)
-- 2023-11-17T14:42:58+01:00 add option to disable active line highlighting in Code Settings by @Ghost in: [#804](https://codeberg.org/uzu/strudel/pulls/804)
-- 2023-11-17T14:37:52+01:00 tidal style d1 ... d9 functions + more by @froos in: [#805](https://codeberg.org/uzu/strudel/pulls/805)
-- 2023-11-15T20:12:23+01:00 remove unwanted cm6 outline for strudelTheme by @Ghost in: [#802](https://codeberg.org/uzu/strudel/pulls/802)
-- 2023-11-13T23:30:15+01:00 Create phaser effect by @Ghost in: [#798](https://codeberg.org/uzu/strudel/pulls/798)
-- 2023-11-10T12:17:35+01:00 support multiple named serial connections, change default baudrate by @yaxu in: [#551](https://codeberg.org/uzu/strudel/pulls/551)
-- 2023-11-09T09:27:57+01:00 Fix for #1. Enables named instruments for csoundm. by @gogins in: [#662](https://codeberg.org/uzu/strudel/pulls/662)
-- 2023-11-09T09:26:45+01:00 Adding vibrato to Superdough sampler by @Ghost in: [#706](https://codeberg.org/uzu/strudel/pulls/706)
-- 2023-11-09T08:46:03+01:00 Document pianoroll by @Ghost in: [#784](https://codeberg.org/uzu/strudel/pulls/784)
-- 2023-11-09T08:45:29+01:00 Update first-effects.mdx by @Ghost in: [#795](https://codeberg.org/uzu/strudel/pulls/795)
-- 2023-11-09T08:44:43+01:00 Update pattern-effects.mdx by @Ghost in: [#796](https://codeberg.org/uzu/strudel/pulls/796)
-- 2023-11-09T08:43:50+01:00 Update recap.mdx by @Ghost in: [#797](https://codeberg.org/uzu/strudel/pulls/797)
-- 2023-11-07T11:53:49+01:00 Update first-sounds.mdx by @Ghost in: [#794](https://codeberg.org/uzu/strudel/pulls/794)
-- 2023-11-06T23:17:32+01:00 don't use anchor links for reference by @froos in: [#791](https://codeberg.org/uzu/strudel/pulls/791)
-- 2023-11-06T22:40:44+01:00 samples loading shortcuts: by @froos in: [#788](https://codeberg.org/uzu/strudel/pulls/788)
-- 2023-11-05T22:47:48+01:00 Fix scope pos + document by @froos in: [#786](https://codeberg.org/uzu/strudel/pulls/786)
-- 2023-11-05T22:10:25+01:00 Implement optional hover tooltip with function documentation by @Ghost in: [#783](https://codeberg.org/uzu/strudel/pulls/783)
-- 2023-11-05T16:46:06+01:00 Add function params in reference tab by @Ghost in: [#785](https://codeberg.org/uzu/strudel/pulls/785)
-- 2023-11-05T12:42:00+01:00 fix: style issues by @froos in: [#781](https://codeberg.org/uzu/strudel/pulls/781)
-- 2023-11-05T12:21:28+01:00 add xfade by @froos in: [#780](https://codeberg.org/uzu/strudel/pulls/780)
-- 2023-11-02T09:30:26+01:00 Update to Astro 3 by @froos in: [#775](https://codeberg.org/uzu/strudel/pulls/775)
-- 2023-11-02T08:57:14+01:00 add vscode bindings by @Ghost in: [#773](https://codeberg.org/uzu/strudel/pulls/773)
-- 2023-11-02T08:31:30+01:00 fix: share copy to clipboard + alert by @froos in: [#774](https://codeberg.org/uzu/strudel/pulls/774)
-- 2023-11-01T22:22:34+01:00 Fix chunk, add fastChunk and repeatCycles by @yaxu in: [#712](https://codeberg.org/uzu/strudel/pulls/712)
-- 2023-11-01T22:12:49+01:00 Document adsr function by @Ghost in: [#767](https://codeberg.org/uzu/strudel/pulls/767)
-- 2023-11-01T22:11:49+01:00 Add pick and squeeze functions by @Ghost in: [#771](https://codeberg.org/uzu/strudel/pulls/771)
-- 2023-11-01T22:04:00+01:00 Update vite pwa by @froos in: [#772](https://codeberg.org/uzu/strudel/pulls/772)
-
-## october 2023
-
-- 2023-10-28T23:55:07+02:00 replace strudel.tidalcycles.org with strudel.cc by @froos in: [#768](https://codeberg.org/uzu/strudel/pulls/768)
-- 2023-10-28T12:52:37+02:00 Document striate function by @Ghost in: [#766](https://codeberg.org/uzu/strudel/pulls/766)
-- 2023-10-27T23:06:20+02:00 fix zen mode logo overlap by @froos in: [#760](https://codeberg.org/uzu/strudel/pulls/760)
-- 2023-10-27T23:01:18+02:00 fix: scale offset by @froos in: [#764](https://codeberg.org/uzu/strudel/pulls/764)
-- 2023-10-27T21:59:35+02:00 Fix addivite synthesis phases by @froos in: [#762](https://codeberg.org/uzu/strudel/pulls/762)
-- 2023-10-26T16:28:16+02:00 Hydra integration by @froos in: [#759](https://codeberg.org/uzu/strudel/pulls/759)
-- 2023-10-26T14:14:27+02:00 add play function by @froos in: [#758](https://codeberg.org/uzu/strudel/pulls/758)
-- 2023-10-26T13:11:00+02:00 Add shabda shortcut by @Ghost in: [#740](https://codeberg.org/uzu/strudel/pulls/740)
-- 2023-10-26T13:07:23+02:00 mini notation: international alphabets support by @Ghost in: [#751](https://codeberg.org/uzu/strudel/pulls/751)
-- 2023-10-22T23:00:52+02:00 hopefully fix trainling slashes bug by @froos in: [#753](https://codeberg.org/uzu/strudel/pulls/753)
-- 2023-10-21T23:38:50+02:00 Fix krill build command in README by @Ghost in: [#748](https://codeberg.org/uzu/strudel/pulls/748)
-- 2023-10-21T00:20:50+02:00 completely revert config mess by @froos in: [#745](https://codeberg.org/uzu/strudel/pulls/745)
-- 2023-10-20T22:41:39+02:00 fix: try different trailing slash behavior by @froos in: [#744](https://codeberg.org/uzu/strudel/pulls/744)
-- 2023-10-20T22:34:03+02:00 fix: trailing slash confusion by @froos in: [#743](https://codeberg.org/uzu/strudel/pulls/743)
-- 2023-10-20T12:07:04+02:00 [Bug Fix] chooseWith: prevent pattern from stopping audio when selection is >= 1 or < 0 by @Ghost in: [#741](https://codeberg.org/uzu/strudel/pulls/741)
-- 2023-10-20T11:34:26+02:00 Recipes by @froos in: [#742](https://codeberg.org/uzu/strudel/pulls/742)
-- 2023-10-13T12:57:24+02:00 vite-vanilla-repl readme fix by @froos in: [#737](https://codeberg.org/uzu/strudel/pulls/737)
-- 2023-10-10T00:17:59+02:00 Add support for using samples as impulse response buffers for the reverb by @Ghost in: [#717](https://codeberg.org/uzu/strudel/pulls/717)
-- 2023-10-09T21:43:42+02:00 fix: reverb sampleRate by @froos in: [#732](https://codeberg.org/uzu/strudel/pulls/732)
-- 2023-10-09T21:34:33+02:00 fix: reverb roomsize not required by @froos in: [#731](https://codeberg.org/uzu/strudel/pulls/731)
-- 2023-10-08T13:54:52+02:00 Compressor by @froos in: [#729](https://codeberg.org/uzu/strudel/pulls/729)
-- 2023-10-08T13:25:57+02:00 fix: hashes in urls by @froos in: [#728](https://codeberg.org/uzu/strudel/pulls/728)
-- 2023-10-07T15:48:06+02:00 consume n with scale by @froos in: [#727](https://codeberg.org/uzu/strudel/pulls/727)
-- 2023-10-07T00:27:21+02:00 fix: reverb regenerate loophole by @froos in: [#726](https://codeberg.org/uzu/strudel/pulls/726)
-- 2023-10-05T00:04:17+02:00 Better convolution reverb by generating impulse responses by @Ghost in: [#718](https://codeberg.org/uzu/strudel/pulls/718)
-- 2023-10-04T10:31:53+02:00 Slider afterthoughts by @froos in: [#723](https://codeberg.org/uzu/strudel/pulls/723)
-- 2023-10-03T16:39:06+02:00 Add 'white', 'pink' and 'brown' oscillators + refactor synth by @Ghost in: [#713](https://codeberg.org/uzu/strudel/pulls/713)
-- 2023-10-01T14:20:50+02:00 support mininotation '..' range operator, fixes #715 by @yaxu in: [#716](https://codeberg.org/uzu/strudel/pulls/716)
-- 2023-10-01T14:15:09+02:00 widgets by @froos in: [#714](https://codeberg.org/uzu/strudel/pulls/714)
-
-## september 2023
-
-- 2023-09-28T11:03:09+02:00 Midi in by @froos in: [#699](https://codeberg.org/uzu/strudel/pulls/699)
-- 2023-09-27T22:53:49+02:00 add midi clock support by @froos in: [#710](https://codeberg.org/uzu/strudel/pulls/710)
-- 2023-09-25T23:36:10+02:00 Update bournemouth.mdx by @Ghost in: [#708](https://codeberg.org/uzu/strudel/pulls/708)
-- 2023-09-25T23:06:30+02:00 add dough function for raw dsp by @froos in: [#707](https://codeberg.org/uzu/strudel/pulls/707)
-- 2023-09-17T15:53:27+02:00 Update tauri.yml workflow file by @Ghost in: [#705](https://codeberg.org/uzu/strudel/pulls/705)
-- 2023-09-17T11:05:06+02:00 Adding vibrato to base oscillators by @Ghost in: [#693](https://codeberg.org/uzu/strudel/pulls/693)
-- 2023-09-17T08:27:36+02:00 Adding loop points and thus wavetable synthesis by @Ghost in: [#698](https://codeberg.org/uzu/strudel/pulls/698)
-- 2023-09-16T02:03:49+02:00 Adding filter envelopes and filter order selection by @Ghost in: [#692](https://codeberg.org/uzu/strudel/pulls/692)
-- 2023-09-09T11:19:58+02:00 Add logging from tauri by @Ghost in: [#697](https://codeberg.org/uzu/strudel/pulls/697)
-- 2023-09-05T00:33:54+02:00 Direct OSC Support in Tauri by @Ghost in: [#694](https://codeberg.org/uzu/strudel/pulls/694)
-- 2023-09-03T22:19:22+02:00 fix MIDI CC messages by @Ghost in: [#690](https://codeberg.org/uzu/strudel/pulls/690)
-- 2023-09-03T10:22:15+02:00 add sleep timer + improve message iterating by @Ghost in: [#688](https://codeberg.org/uzu/strudel/pulls/688)
-
-## august 2023
-
-- 2023-08-31T13:00:31+02:00 ZZFX Synth support by @Ghost in: [#684](https://codeberg.org/uzu/strudel/pulls/684)
-- 2023-08-31T12:36:37+02:00 Wave Selection and Global Envelope on the FM Synth Modulator by @Ghost in: [#683](https://codeberg.org/uzu/strudel/pulls/683)
-- 2023-08-31T05:52:17+02:00 Create Midi Integration for Tauri Desktop app by @Ghost in: [#685](https://codeberg.org/uzu/strudel/pulls/685)
-- 2023-08-31T05:32:16+02:00 teletext theme + fonts by @froos in: [#681](https://codeberg.org/uzu/strudel/pulls/681)
-- 2023-08-31T05:20:55+02:00 control osc partial count with n by @froos in: [#674](https://codeberg.org/uzu/strudel/pulls/674)
-- 2023-08-27T22:18:06+02:00 add emoji support by @froos in: [#680](https://codeberg.org/uzu/strudel/pulls/680)
-- 2023-08-27T16:12:32+02:00 Pianoroll improvements by @froos in: [#679](https://codeberg.org/uzu/strudel/pulls/679)
-- 2023-08-26T21:22:17+02:00 Scope by @froos in: [#677](https://codeberg.org/uzu/strudel/pulls/677)
-- 2023-08-23T21:50:50+02:00 Midi time fixes by @Ghost in: [#668](https://codeberg.org/uzu/strudel/pulls/668)
-- 2023-08-20T23:19:08+02:00 basic fm by @froos in: [#669](https://codeberg.org/uzu/strudel/pulls/669)
-- 2023-08-18T23:56:20+02:00 togglable panel position by @froos in: [#667](https://codeberg.org/uzu/strudel/pulls/667)
-- 2023-08-18T15:59:20+02:00 fix osc bundle timestamp glitches caused by drifting clock by @Ghost in: [#666](https://codeberg.org/uzu/strudel/pulls/666)
-- 2023-08-17T11:36:07+02:00 superdough: encapsulates web audio output by @froos in: [#664](https://codeberg.org/uzu/strudel/pulls/664)
-- 2023-08-11T00:06:21+02:00 fix: always run previous trigger by @froos in: [#660](https://codeberg.org/uzu/strudel/pulls/660)
-- 2023-08-10T23:54:58+02:00 fix: welcome message for latestCode by @froos in: [#659](https://codeberg.org/uzu/strudel/pulls/659)
-- 2023-08-07T07:45:26+02:00 [Bug Fix] Midi: Don't treat note 0 as false by @Ghost in: [#657](https://codeberg.org/uzu/strudel/pulls/657)
-
-## july 2023
-
-- 2023-07-29T09:06:18+02:00 [Bug Fix] Account for numeral notation when converting to midi by @Ghost in: [#656](https://codeberg.org/uzu/strudel/pulls/656)
-- 2023-07-23T22:18:49+02:00 ireal voicings by @froos in: [#653](https://codeberg.org/uzu/strudel/pulls/653)
-- 2023-07-22T09:30:21+02:00 Understand pitch by @froos in: [#652](https://codeberg.org/uzu/strudel/pulls/652)
-- 2023-07-17T23:42:59+02:00 update vitest by @froos in: [#651](https://codeberg.org/uzu/strudel/pulls/651)
-- 2023-07-17T23:34:33+02:00 stateless voicings + tonleiter lib by @froos in: [#647](https://codeberg.org/uzu/strudel/pulls/647)
-- 2023-07-17T17:57:17+02:00 FIXES: TODO in rotateChroma by @Ghost in: [#650](https://codeberg.org/uzu/strudel/pulls/650)
-- 2023-07-10T19:07:45+02:00 slice: list mode by @froos in: [#645](https://codeberg.org/uzu/strudel/pulls/645)
-- 2023-07-04T23:50:30+02:00 Delete old packages by @froos in: [#639](https://codeberg.org/uzu/strudel/pulls/639)
-- 2023-07-04T23:38:42+02:00 Adaptive Highlighting by @froos in: [#634](https://codeberg.org/uzu/strudel/pulls/634)
-- 2023-07-04T21:55:57+02:00 More work on highlight IDs by @Ghost in: [#636](https://codeberg.org/uzu/strudel/pulls/636)
-- 2023-07-04T18:44:48+02:00 snapshot tests: sort haps by part by @froos in: [#637](https://codeberg.org/uzu/strudel/pulls/637)
-
-## juny 2023
-
-- 2023-06-30T22:47:40+02:00 fix: update canvas size on window resize by @froos in: [#631](https://codeberg.org/uzu/strudel/pulls/631)
-- 2023-06-30T22:45:41+02:00 fix: out of range error by @froos in: [#630](https://codeberg.org/uzu/strudel/pulls/630)
-- 2023-06-30T08:14:40+02:00 fix: midi clock drift by @froos in: [#627](https://codeberg.org/uzu/strudel/pulls/627)
-- 2023-06-29T22:02:28+02:00 desktop: play samples from disk by @froos in: [#621](https://codeberg.org/uzu/strudel/pulls/621)
-- 2023-06-29T21:59:43+02:00 cps dependent functions by @froos in: [#620](https://codeberg.org/uzu/strudel/pulls/620)
-- 2023-06-26T22:45:04+02:00 Fix typo on packages.mdx by @Ghost in: [#520](https://codeberg.org/uzu/strudel/pulls/520)
-- 2023-06-26T22:35:02+02:00 patterning ui settings by @froos in: [#606](https://codeberg.org/uzu/strudel/pulls/606)
-- 2023-06-26T22:32:46+02:00 add spiral viz by @froos in: [#614](https://codeberg.org/uzu/strudel/pulls/614)
-- 2023-06-26T22:17:46+02:00 tauri desktop app by @Ghost in: [#613](https://codeberg.org/uzu/strudel/pulls/613)
-- 2023-06-23T09:59:43+02:00 fix: doc links by @froos in: [#612](https://codeberg.org/uzu/strudel/pulls/612)
-- 2023-06-23T09:55:18+02:00 clip now works like legato in tidal by @froos in: [#598](https://codeberg.org/uzu/strudel/pulls/598)
-- 2023-06-18T10:36:19+02:00 fix: flatten scale lists by @froos in: [#605](https://codeberg.org/uzu/strudel/pulls/605)
-- 2023-06-18T10:36:17+02:00 tonal fixes by @froos in: [#607](https://codeberg.org/uzu/strudel/pulls/607)
-- 2023-06-15T12:51:40+02:00 editor: enable line wrapping by @Ghost in: [#581](https://codeberg.org/uzu/strudel/pulls/581)
-- 2023-06-15T10:22:46+02:00 add ratio function by @froos in: [#602](https://codeberg.org/uzu/strudel/pulls/602)
-- 2023-06-12T23:24:29+02:00 enable auto-completion by @Ghost in: [#588](https://codeberg.org/uzu/strudel/pulls/588)
-- 2023-06-11T22:04:17+02:00 improve cursor by @froos in: [#597](https://codeberg.org/uzu/strudel/pulls/597)
-- 2023-06-11T20:00:45+02:00 Solmization added by @Ghost in: [#570](https://codeberg.org/uzu/strudel/pulls/570)
-- 2023-06-11T19:45:00+02:00 fix: division by zero by @froos in: [#591](https://codeberg.org/uzu/strudel/pulls/591)
-- 2023-06-11T19:44:43+02:00 fix: allow f for flat notes like tidal by @froos in: [#593](https://codeberg.org/uzu/strudel/pulls/593)
-- 2023-06-11T19:44:41+02:00 Fix option dot by @froos in: [#596](https://codeberg.org/uzu/strudel/pulls/596)
-- 2023-06-09T21:02:12+02:00 New Workshop by @froos in: [#587](https://codeberg.org/uzu/strudel/pulls/587)
-- 2023-06-09T17:31:58+02:00 Music metadata by @Ghost in: [#580](https://codeberg.org/uzu/strudel/pulls/580)
-- 2023-06-08T09:33:34+02:00 learn/tonal: fix typo in "scaleTran[s]pose" by @Ghost in: [#585](https://codeberg.org/uzu/strudel/pulls/585)
-- 2023-06-07T20:19:11+02:00 repl: add option to display line numbers by @Ghost in: [#582](https://codeberg.org/uzu/strudel/pulls/582)
-- 2023-06-05T21:35:49+02:00 Vanilla JS Refactoring by @froos in: [#563](https://codeberg.org/uzu/strudel/pulls/563)
-
-## may 2023
-
-- 2023-05-05T08:34:37+02:00 Patchday by @froos in: [#559](https://codeberg.org/uzu/strudel/pulls/559)
-- 2023-05-02T21:48:33+02:00 add basic triads and guidetone voicings by @froos in: [#557](https://codeberg.org/uzu/strudel/pulls/557)
-
-## april 2023
-
-- 2023-04-28T12:46:56+02:00 fix: make soundfonts import dynamic by @froos in: [#556](https://codeberg.org/uzu/strudel/pulls/556)
-- 2023-04-22T15:43:22+02:00 fix: colorable highlighting by @froos in: [#553](https://codeberg.org/uzu/strudel/pulls/553)
-- 2023-04-06T00:11:02+02:00 fix: load soundfonts in prebake by @froos in: [#550](https://codeberg.org/uzu/strudel/pulls/550)
-
-## march 2023
-
-- 2023-03-29T22:23:07+02:00 fix: reset time on stop by @froos in: [#548](https://codeberg.org/uzu/strudel/pulls/548)
-- 2023-03-29T22:16:38+02:00 fix: allow whitespace at the end of a mini pattern by @froos in: [#547](https://codeberg.org/uzu/strudel/pulls/547)
-- 2023-03-24T22:10:56+01:00 add firacode font by @froos in: [#544](https://codeberg.org/uzu/strudel/pulls/544)
-- 2023-03-24T12:54:20+01:00 feat: add loader bar to animate loading state by @froos in: [#542](https://codeberg.org/uzu/strudel/pulls/542)
-- 2023-03-23T22:39:27+01:00 do not reset cps before eval #517 by @froos in: [#539](https://codeberg.org/uzu/strudel/pulls/539)
-- 2023-03-23T22:27:31+01:00 improve initial loading + wait before eval by @froos in: [#538](https://codeberg.org/uzu/strudel/pulls/538)
-- 2023-03-23T21:40:20+01:00 fix period key for dvorak + remove duplicated code by @froos in: [#537](https://codeberg.org/uzu/strudel/pulls/537)
-- 2023-03-23T11:44:56+01:00 Update lerna by @froos in: [#535](https://codeberg.org/uzu/strudel/pulls/535)
-- 2023-03-23T10:21:55+01:00 feat: add freq support to gm soundfonts by @froos in: [#534](https://codeberg.org/uzu/strudel/pulls/534)
-- 2023-03-23T10:18:58+01:00 Maintain random seed state in parser, not globally by @Ghost in: [#531](https://codeberg.org/uzu/strudel/pulls/531)
-- 2023-03-21T22:35:18+01:00 FIXES: alias pm for polymeter by @Ghost in: [#527](https://codeberg.org/uzu/strudel/pulls/527)
-- 2023-03-21T22:29:07+01:00 fix(footer): fix link to tidalcycles by @julienbouquillon in: [#529](https://codeberg.org/uzu/strudel/pulls/529)
-- 2023-03-18T20:25:21+01:00 Update intro.mdx by @Ghost in: [#525](https://codeberg.org/uzu/strudel/pulls/525)
-- 2023-03-18T20:24:38+01:00 Update samples.mdx by @Ghost in: [#524](https://codeberg.org/uzu/strudel/pulls/524)
-- 2023-03-17T09:03:11+01:00 fix: envelopes in chrome by @froos in: [#521](https://codeberg.org/uzu/strudel/pulls/521)
-- 2023-03-16T16:13:30+01:00 registerSound API + improved sounds tab + regroup soundfonts by @froos in: [#516](https://codeberg.org/uzu/strudel/pulls/516)
-- 2023-03-14T21:54:53+01:00 add 2 illegible fonts by @froos in: [#518](https://codeberg.org/uzu/strudel/pulls/518)
-- 2023-03-06T22:55:00+01:00 Update README.md by @Ghost in: [#474](https://codeberg.org/uzu/strudel/pulls/474)
-- 2023-03-05T14:52:30+01:00 add arrange function by @froos in: [#508](https://codeberg.org/uzu/strudel/pulls/508)
-- 2023-03-05T14:29:08+01:00 update react to 18 by @froos in: [#514](https://codeberg.org/uzu/strudel/pulls/514)
-- 2023-03-04T19:06:18+01:00 Support list syntax in mininotation by @yaxu in: [#512](https://codeberg.org/uzu/strudel/pulls/512)
-- 2023-03-03T12:40:23+01:00 can now use : as a replacement for space in scales by @froos in: [#502](https://codeberg.org/uzu/strudel/pulls/502)
-- 2023-03-02T15:44:41+01:00 Reinstate slice and splice by @yaxu in: [#500](https://codeberg.org/uzu/strudel/pulls/500)
-- 2023-03-02T14:49:30+01:00 fix: nano-repl highlighting by @froos in: [#501](https://codeberg.org/uzu/strudel/pulls/501)
-- 2023-03-02T14:17:13+01:00 Add control aliases by @yaxu in: [#497](https://codeberg.org/uzu/strudel/pulls/497)
-- 2023-03-01T09:27:27+01:00 implement cps in scheduler by @froos in: [#493](https://codeberg.org/uzu/strudel/pulls/493)
-
-## february 2023
-
-- 2023-02-28T23:06:29+01:00 react style fixes by @froos in: [#491](https://codeberg.org/uzu/strudel/pulls/491)
-- 2023-02-28T22:57:20+01:00 refactor react package by @froos in: [#490](https://codeberg.org/uzu/strudel/pulls/490)
-- 2023-02-27T23:47:34+01:00 add algolia creds + optimize sidebar for crawling by @froos in: [#488](https://codeberg.org/uzu/strudel/pulls/488)
-- 2023-02-27T19:28:10+01:00 fix app height by @froos in: [#485](https://codeberg.org/uzu/strudel/pulls/485)
-- 2023-02-27T16:04:21+01:00 Revert "Another attempt at composable functions - WIP (#390)" by @froos in: [#484](https://codeberg.org/uzu/strudel/pulls/484)
-- 2023-02-27T15:45:20+01:00 Update mini-notation.mdx by @yaxu in: [#365](https://codeberg.org/uzu/strudel/pulls/365)
-- 2023-02-27T12:52:09+01:00 docs: packages + offline by @froos in: [#482](https://codeberg.org/uzu/strudel/pulls/482)
-- 2023-02-25T14:26:49+01:00 Fix array args by @froos in: [#480](https://codeberg.org/uzu/strudel/pulls/480)
-- 2023-02-25T12:33:22+01:00 midi cc support by @froos in: [#478](https://codeberg.org/uzu/strudel/pulls/478)
-- 2023-02-23T00:11:05+01:00 fix: hash links by @froos in: [#473](https://codeberg.org/uzu/strudel/pulls/473)
-- 2023-02-22T22:54:39+01:00 settings tab with vim / emacs modes + additional themes and fonts by @froos in: [#467](https://codeberg.org/uzu/strudel/pulls/467)
-- 2023-02-22T22:53:22+01:00 Update input-output.mdx by @Ghost in: [#471](https://codeberg.org/uzu/strudel/pulls/471)
-- 2023-02-22T22:52:50+01:00 FIXES: freqs instead of pitches by @Ghost in: [#464](https://codeberg.org/uzu/strudel/pulls/464)
-- 2023-02-22T20:01:00+01:00 fix: osc should not return a promise by @froos in: [#472](https://codeberg.org/uzu/strudel/pulls/472)
-- 2023-02-22T12:51:31+01:00 slice and splice by @yaxu in: [#466](https://codeberg.org/uzu/strudel/pulls/466)
-- 2023-02-18T01:00:19+01:00 weave and weaveWith by @yaxu in: [#465](https://codeberg.org/uzu/strudel/pulls/465)
-- 2023-02-17T00:15:21+01:00 Composable functions by @yaxu in: [#390](https://codeberg.org/uzu/strudel/pulls/390)
-- 2023-02-16T20:38:45+01:00 FIXES: Warning about jsxBracketSameLine deprecation by @Ghost in: [#461](https://codeberg.org/uzu/strudel/pulls/461)
-- 2023-02-14T22:11:02+01:00 Update synths.mdx by @Ghost in: [#438](https://codeberg.org/uzu/strudel/pulls/438)
-- 2023-02-14T19:54:42+01:00 Update mini-notation.mdx by @Ghost in: [#437](https://codeberg.org/uzu/strudel/pulls/437)
-- 2023-02-14T19:54:25+01:00 Update code.mdx by @Ghost in: [#436](https://codeberg.org/uzu/strudel/pulls/436)
-- 2023-02-13T00:43:29+01:00 Fix anchors by @froos in: [#433](https://codeberg.org/uzu/strudel/pulls/433)
-- 2023-02-11T21:02:11+01:00 autocomplete preparations by @froos in: [#427](https://codeberg.org/uzu/strudel/pulls/427)
-- 2023-02-10T23:14:48+01:00 Themes by @froos in: [#431](https://codeberg.org/uzu/strudel/pulls/431)
-- 2023-02-09T19:22:56+01:00 minirepl: add keyboard shortcuts by @froos in: [#429](https://codeberg.org/uzu/strudel/pulls/429)
-- 2023-02-09T08:56:42+01:00 add cdn.freesound to cache list by @froos in: [#425](https://codeberg.org/uzu/strudel/pulls/425)
-- 2023-02-08T20:36:00+01:00 add more offline caching by @froos in: [#421](https://codeberg.org/uzu/strudel/pulls/421)
-- 2023-02-07T22:08:01+01:00 add caching strategy for missing file types + cache all samples loaded from github by @froos in: [#419](https://codeberg.org/uzu/strudel/pulls/419)
-- 2023-02-06T23:29:57+01:00 PWA with offline support by @froos in: [#417](https://codeberg.org/uzu/strudel/pulls/417)
-- 2023-02-05T17:27:32+01:00 improve samples doc by @froos in: [#411](https://codeberg.org/uzu/strudel/pulls/411)
-- 2023-02-05T17:27:10+01:00 google gtfo by @froos in: [#413](https://codeberg.org/uzu/strudel/pulls/413)
-- 2023-02-05T16:27:59+01:00 improve effects doc by @froos in: [#409](https://codeberg.org/uzu/strudel/pulls/409)
-- 2023-02-05T14:56:03+01:00 Update effects.mdx by @Ghost in: [#410](https://codeberg.org/uzu/strudel/pulls/410)
-- 2023-02-03T19:54:47+01:00 add shabda doc by @froos in: [#407](https://codeberg.org/uzu/strudel/pulls/407)
-- 2023-02-02T21:45:56+01:00 fix: share url on subpath by @froos in: [#405](https://codeberg.org/uzu/strudel/pulls/405)
-- 2023-02-02T21:35:45+01:00 update csound + fix sound output by @froos in: [#404](https://codeberg.org/uzu/strudel/pulls/404)
-- 2023-02-02T19:53:36+01:00 pin @csound/browser to 6.18.3 + bump by @froos in: [#403](https://codeberg.org/uzu/strudel/pulls/403)
-- 2023-02-01T22:47:27+01:00 release webaudio by @froos in: [#400](https://codeberg.org/uzu/strudel/pulls/400)
-- 2023-02-01T22:45:16+01:00 can now await initAudio + initAudioOnFirstClick by @froos in: [#399](https://codeberg.org/uzu/strudel/pulls/399)
-- 2023-02-01T22:32:18+01:00 fix: minirepl styles by @froos in: [#398](https://codeberg.org/uzu/strudel/pulls/398)
-- 2023-02-01T22:17:19+01:00 proper builds + use pnpm workspaces by @froos in: [#396](https://codeberg.org/uzu/strudel/pulls/396)
-- 2023-02-01T16:49:55+01:00 add pattern methods hurry, press and pressBy by @yaxu in: [#397](https://codeberg.org/uzu/strudel/pulls/397)
-
-## january 2023
-
-- 2023-01-27T14:40:40+01:00 Add tidal-drum-patterns to examples by @Ghost in: [#379](https://codeberg.org/uzu/strudel/pulls/379)
-- 2023-01-27T14:39:56+01:00 add ribbon + test + docs by @froos in: [#388](https://codeberg.org/uzu/strudel/pulls/388)
-- 2023-01-27T12:10:37+01:00 Notes are not essential :) by @yaxu in: [#393](https://codeberg.org/uzu/strudel/pulls/393)
-- 2023-01-21T17:18:13+01:00 document csound by @froos in: [#391](https://codeberg.org/uzu/strudel/pulls/391)
-- 2023-01-19T12:04:51+01:00 Rename a to angle by @froos in: [#387](https://codeberg.org/uzu/strudel/pulls/387)
-- 2023-01-19T11:49:39+01:00 add run + test + docs by @froos in: [#386](https://codeberg.org/uzu/strudel/pulls/386)
-- 2023-01-19T11:32:56+01:00 docs: use note instead of n to mitigate confusion by @froos in: [#385](https://codeberg.org/uzu/strudel/pulls/385)
-- 2023-01-18T17:10:09+01:00 update my-patterns instructions by @froos in: [#384](https://codeberg.org/uzu/strudel/pulls/384)
-- 2023-01-15T23:19:43+01:00 Draw fixes by @froos in: [#377](https://codeberg.org/uzu/strudel/pulls/377)
-- 2023-01-14T11:07:08+01:00 improve new draw logic by @froos in: [#372](https://codeberg.org/uzu/strudel/pulls/372)
-- 2023-01-12T14:40:59+01:00 document more functions + change arp join by @froos in: [#369](https://codeberg.org/uzu/strudel/pulls/369)
-- 2023-01-10T00:03:47+01:00 add https to url by @Ghost in: [#364](https://codeberg.org/uzu/strudel/pulls/364)
-- 2023-01-09T23:37:34+01:00 doc structuring by @froos in: [#360](https://codeberg.org/uzu/strudel/pulls/360)
-- 2023-01-09T23:23:28+01:00 Support for multiple mininotation operators by @yaxu in: [#350](https://codeberg.org/uzu/strudel/pulls/350)
-- 2023-01-09T00:40:15+01:00 Fix .out(), renaming webaudio's out() to webaudio() by @yaxu in: [#361](https://codeberg.org/uzu/strudel/pulls/361)
-- 2023-01-06T22:02:31+01:00 docs: tidal comparison + add global fx + add missing sampler fx by @froos in: [#356](https://codeberg.org/uzu/strudel/pulls/356)
-- 2023-01-06T12:31:32+01:00 Fix Bjorklund by @yaxu in: [#343](https://codeberg.org/uzu/strudel/pulls/343)
-- 2023-01-04T20:29:35+01:00 Fix prebake base path by @froos in: [#345](https://codeberg.org/uzu/strudel/pulls/345)
-- 2023-01-04T19:55:49+01:00 fixes #346 by @froos in: [#347](https://codeberg.org/uzu/strudel/pulls/347)
-- 2023-01-02T21:28:07+01:00 Patternify euclid, fast, slow and polymeter step parameters in mininotation by @yaxu in: [#341](https://codeberg.org/uzu/strudel/pulls/341)
-- 2023-01-02T00:30:16+01:00 more animate functions + mini repl fix by @froos in: [#340](https://codeberg.org/uzu/strudel/pulls/340)
-- 2023-01-01T12:34:11+01:00 move /my-patterns to /swatch by @yaxu in: [#338](https://codeberg.org/uzu/strudel/pulls/338)
-- 2023-01-01T12:22:55+01:00 animation options by @froos in: [#337](https://codeberg.org/uzu/strudel/pulls/337)
-
-## december 2022
-
-- 2022-12-31T22:42:49+01:00 Tidy parser, implement polymeters by @yaxu in: [#336](https://codeberg.org/uzu/strudel/pulls/336)
-- 2022-12-31T16:51:53+01:00 animate mvp by @froos in: [#335](https://codeberg.org/uzu/strudel/pulls/335)
-- 2022-12-30T20:16:10+01:00 testing + docs docs by @froos in: [#334](https://codeberg.org/uzu/strudel/pulls/334)
-- 2022-12-29T21:11:16+01:00 Embed mode improvements by @froos in: [#333](https://codeberg.org/uzu/strudel/pulls/333)
-- 2022-12-29T14:06:20+01:00 fix: can now multiply floats in mini notation by @froos in: [#332](https://codeberg.org/uzu/strudel/pulls/332)
-- 2022-12-29T13:50:07+01:00 improve displaying 's' in pianoroll by @froos in: [#331](https://codeberg.org/uzu/strudel/pulls/331)
-- 2022-12-28T17:35:35+01:00 my-patterns: fix paths + update readme by @froos in: [#330](https://codeberg.org/uzu/strudel/pulls/330)
-- 2022-12-28T17:11:40+01:00 my-patterns build + deploy by @froos in: [#329](https://codeberg.org/uzu/strudel/pulls/329)
-- 2022-12-28T15:43:31+01:00 add my-patterns by @froos in: [#328](https://codeberg.org/uzu/strudel/pulls/328)
-- 2022-12-28T14:47:14+01:00 add examples route by @froos in: [#327](https://codeberg.org/uzu/strudel/pulls/327)
-- 2022-12-28T14:44:31+01:00 fix: workaround Object.assign globalThis by @froos in: [#326](https://codeberg.org/uzu/strudel/pulls/326)
-- 2022-12-26T23:29:47+01:00 mini repl improvements by @froos in: [#324](https://codeberg.org/uzu/strudel/pulls/324)
-- 2022-12-26T23:00:34+01:00 support notes without octave by @froos in: [#323](https://codeberg.org/uzu/strudel/pulls/323)
-- 2022-12-26T12:37:36+01:00 tutorial updates by @Ghost in: [#320](https://codeberg.org/uzu/strudel/pulls/320)
-- 2022-12-23T18:26:30+01:00 Reference tab sort by @froos in: [#318](https://codeberg.org/uzu/strudel/pulls/318)
-- 2022-12-23T00:49:56+01:00 Astro build by @froos in: [#315](https://codeberg.org/uzu/strudel/pulls/315)
-- 2022-12-19T21:02:37+01:00 object support for .scale by @froos in: [#307](https://codeberg.org/uzu/strudel/pulls/307)
-- 2022-12-19T21:02:22+01:00 Jsdoc component by @froos in: [#312](https://codeberg.org/uzu/strudel/pulls/312)
-- 2022-12-19T20:31:46+01:00 fix: copy share link to clipboard was broken for some browers by @froos in: [#311](https://codeberg.org/uzu/strudel/pulls/311)
-- 2022-12-19T12:11:24+01:00 ICLC2023 paper WIP by @yaxu in: [#306](https://codeberg.org/uzu/strudel/pulls/306)
-- 2022-12-15T21:23:22+01:00 support freq in pianoroll by @froos in: [#308](https://codeberg.org/uzu/strudel/pulls/308)
-- 2022-12-13T22:07:27+01:00 Updated csoundm to use the register facility . by @gogins in: [#303](https://codeberg.org/uzu/strudel/pulls/303)
-- 2022-12-13T21:57:03+01:00 add lint + prettier check before test by @froos in: [#305](https://codeberg.org/uzu/strudel/pulls/305)
-- 2022-12-13T21:21:44+01:00 add freq support to sampler by @froos in: [#301](https://codeberg.org/uzu/strudel/pulls/301)
-- 2022-12-12T00:48:41+01:00 fix whitespace trimming by @froos in: [#300](https://codeberg.org/uzu/strudel/pulls/300)
-- 2022-12-12T00:21:53+01:00 .defragmentHaps() for merging touching haps that share a whole and value by @yaxu in: [#299](https://codeberg.org/uzu/strudel/pulls/299)
-- 2022-12-11T23:17:49+01:00 remove whitespace from highlighted region by @froos in: [#298](https://codeberg.org/uzu/strudel/pulls/298)
-- 2022-12-11T22:03:26+01:00 update vitest by @froos in: [#297](https://codeberg.org/uzu/strudel/pulls/297)
-- 2022-12-11T21:44:16+01:00 can now add bare numbers to numeral object props by @froos in: [#287](https://codeberg.org/uzu/strudel/pulls/287)
-- 2022-12-11T21:27:58+01:00 Move stuff to new register function by @froos in: [#295](https://codeberg.org/uzu/strudel/pulls/295)
-- 2022-12-11T20:07:12+01:00 add prettier task by @froos in: [#296](https://codeberg.org/uzu/strudel/pulls/296)
-- 2022-12-10T15:39:04+01:00 Reorganise pattern.mjs with a 'toplevel first' regime by @yaxu in: [#286](https://codeberg.org/uzu/strudel/pulls/286)
-- 2022-12-10T11:56:16+01:00 Fancy hap show, include part in snapshots by @yaxu in: [#291](https://codeberg.org/uzu/strudel/pulls/291)
-- 2022-12-07T20:07:55+01:00 Switch 'operators' from .whatHow to .what.how by @yaxu in: [#285](https://codeberg.org/uzu/strudel/pulls/285)
-- 2022-12-04T12:51:01+01:00 implement collect + arp function by @froos in: [#281](https://codeberg.org/uzu/strudel/pulls/281)
-- 2022-12-02T13:38:27+01:00 do not recompile orc by @froos in: [#278](https://codeberg.org/uzu/strudel/pulls/278)
-- 2022-12-02T12:49:26+01:00 add basic csound output by @froos in: [#275](https://codeberg.org/uzu/strudel/pulls/275)
-- 2022-12-02T12:18:41+01:00 add licenses / credits to all tunes + remove some by @froos in: [#277](https://codeberg.org/uzu/strudel/pulls/277)
-- 2022-12-02T11:45:02+01:00 Support sending CRC16 bytes with serial messages by @yaxu in: [#276](https://codeberg.org/uzu/strudel/pulls/276)
-
-## november 2022
-
-- 2022-11-29T23:41:39+01:00 release version bumps by @froos in: [#273](https://codeberg.org/uzu/strudel/pulls/273)
-- 2022-11-29T23:34:50+01:00 add eslint by @froos in: [#271](https://codeberg.org/uzu/strudel/pulls/271)
-- 2022-11-29T23:34:26+01:00 tonal update with fixed memory leak by @froos in: [#272](https://codeberg.org/uzu/strudel/pulls/272)
-- 2022-11-22T09:51:26+01:00 Tidying up core by @yaxu in: [#256](https://codeberg.org/uzu/strudel/pulls/256)
-- 2022-11-21T22:15:48+01:00 fix performance bottleneck by @froos in: [#266](https://codeberg.org/uzu/strudel/pulls/266)
-- 2022-11-17T11:08:38+01:00 fix tutorial bugs by @froos in: [#263](https://codeberg.org/uzu/strudel/pulls/263)
-- 2022-11-16T13:15:28+01:00 Binaries by @froos in: [#254](https://codeberg.org/uzu/strudel/pulls/254)
-- 2022-11-13T20:20:32+01:00 Repl refactoring by @froos in: [#255](https://codeberg.org/uzu/strudel/pulls/255)
-- 2022-11-08T23:35:11+01:00 Webaudio build by @froos in: [#250](https://codeberg.org/uzu/strudel/pulls/250)
-- 2022-11-08T22:43:32+01:00 new transpiler based on acorn by @froos in: [#249](https://codeberg.org/uzu/strudel/pulls/249)
-- 2022-11-06T19:03:19+01:00 General purpose scheduler by @froos in: [#248](https://codeberg.org/uzu/strudel/pulls/248)
-- 2022-11-06T11:05:23+01:00 snapshot tests on shared snippets by @froos in: [#243](https://codeberg.org/uzu/strudel/pulls/243)
-- 2022-11-06T11:03:42+01:00 Some tunes by @froos in: [#247](https://codeberg.org/uzu/strudel/pulls/247)
-- 2022-11-06T00:37:11+01:00 patchday by @froos in: [#246](https://codeberg.org/uzu/strudel/pulls/246)
-- 2022-11-04T20:28:23+01:00 Readme + TLC by @froos in: [#244](https://codeberg.org/uzu/strudel/pulls/244)
-- 2022-11-03T15:16:04+01:00 in source example tests by @froos in: [#242](https://codeberg.org/uzu/strudel/pulls/242)
-- 2022-11-02T23:16:43+01:00 feat: support github: links by @froos in: [#240](https://codeberg.org/uzu/strudel/pulls/240)
-- 2022-11-02T21:26:44+01:00 Load samples from url by @froos in: [#239](https://codeberg.org/uzu/strudel/pulls/239)
-- 2022-11-02T11:42:32+01:00 Object arithmetic by @froos in: [#238](https://codeberg.org/uzu/strudel/pulls/238)
-- 2022-11-01T00:36:14+01:00 Tidal drum machines by @froos in: [#237](https://codeberg.org/uzu/strudel/pulls/237)
-- 2022-10-31T21:39:23+01:00 fx on stereo speakers by @froos in: [#236](https://codeberg.org/uzu/strudel/pulls/236)
-
-## october 2022
-
-- 2022-10-30T19:10:33+01:00 add vcsl sample library by @froos in: [#235](https://codeberg.org/uzu/strudel/pulls/235)
-- 2022-10-30T00:23:10+02:00 Fix zero length queries WIP by @yaxu in: [#234](https://codeberg.org/uzu/strudel/pulls/234)
-- 2022-10-29T17:54:05+02:00 Out by default by @froos in: [#232](https://codeberg.org/uzu/strudel/pulls/232)
-- 2022-10-26T23:53:49+02:00 Patternify range by @yaxu in: [#231](https://codeberg.org/uzu/strudel/pulls/231)
-- 2022-10-26T21:42:12+02:00 Just another docs branch by @froos in: [#228](https://codeberg.org/uzu/strudel/pulls/228)
-- 2022-10-26T21:41:12+02:00 Refactor tunes away from tone by @froos in: [#230](https://codeberg.org/uzu/strudel/pulls/230)
-- 2022-10-20T09:26:28+02:00 Core util tests by @Ghost in: [#226](https://codeberg.org/uzu/strudel/pulls/226)
-- 2022-10-06T22:35:45+02:00 fix fastgap for events that go across cycle boundaries by @yaxu in: [#225](https://codeberg.org/uzu/strudel/pulls/225)
-
-## september 2022
-
-- 2022-09-25T21:15:36+02:00 Reverb by @froos in: [#224](https://codeberg.org/uzu/strudel/pulls/224)
-- 2022-09-25T00:27:26+02:00 focus tweak for squeezeJoin - another go at fixing #216 by @yaxu in: [#221](https://codeberg.org/uzu/strudel/pulls/221)
-- 2022-09-24T23:17:21+02:00 support negative speeds by @froos in: [#222](https://codeberg.org/uzu/strudel/pulls/222)
-- 2022-09-24T21:55:15+02:00 Feedback Delay by @froos in: [#213](https://codeberg.org/uzu/strudel/pulls/213)
-- 2022-09-23T12:06:17+02:00 Fix squeeze join by @yaxu in: [#220](https://codeberg.org/uzu/strudel/pulls/220)
-- 2022-09-22T21:23:11+02:00 encapsulate webaudio output by @froos in: [#219](https://codeberg.org/uzu/strudel/pulls/219)
-- 2022-09-22T19:18:18+02:00 samples now have envelopes by @froos in: [#218](https://codeberg.org/uzu/strudel/pulls/218)
-- 2022-09-22T00:03:30+02:00 sampler features + fixes by @froos in: [#217](https://codeberg.org/uzu/strudel/pulls/217)
-- 2022-09-19T23:32:55+02:00 Just another docs PR by @froos in: [#215](https://codeberg.org/uzu/strudel/pulls/215)
-- 2022-09-17T23:47:43+02:00 Even more docs by @froos in: [#212](https://codeberg.org/uzu/strudel/pulls/212)
-- 2022-09-17T15:36:53+02:00 Webaudio guide by @froos in: [#207](https://codeberg.org/uzu/strudel/pulls/207)
-- 2022-09-16T00:21:29+02:00 Coarse crush shape by @froos in: [#205](https://codeberg.org/uzu/strudel/pulls/205)
-- 2022-09-15T21:04:28+02:00 add vowel to .out by @froos in: [#201](https://codeberg.org/uzu/strudel/pulls/201)
-- 2022-09-15T16:55:52+02:00 add rollup-plugin-visualizer to build by @froos in: [#200](https://codeberg.org/uzu/strudel/pulls/200)
-- 2022-09-14T23:46:39+02:00 document random functions by @froos in: [#199](https://codeberg.org/uzu/strudel/pulls/199)
-- 2022-09-09T22:04:40+02:00 Fix numbers in sampler by @froos in: [#196](https://codeberg.org/uzu/strudel/pulls/196)
-
-## august 2022
-
-- 2022-08-14T17:40:41+02:00 change "stride"/"offset" of successive degradeBy/chooseIn by @Ghost in: [#185](https://codeberg.org/uzu/strudel/pulls/185)
-- 2022-08-14T16:04:14+02:00 Soundfont file support by @froos in: [#183](https://codeberg.org/uzu/strudel/pulls/183)
-- 2022-08-14T15:55:50+02:00 fix regression: old way of setting frequencies was broken by @froos in: [#190](https://codeberg.org/uzu/strudel/pulls/190)
-- 2022-08-14T15:45:53+02:00 wait for prebake to finish before evaluating by @froos in: [#189](https://codeberg.org/uzu/strudel/pulls/189)
-- 2022-08-14T11:31:00+02:00 Fix codemirror bug by @froos in: [#186](https://codeberg.org/uzu/strudel/pulls/186)
-- 2022-08-13T16:29:53+02:00 scheduler improvements by @froos in: [#181](https://codeberg.org/uzu/strudel/pulls/181)
-- 2022-08-12T23:10:36+02:00 replace mocha with vitest by @froos in: [#175](https://codeberg.org/uzu/strudel/pulls/175)
-- 2022-08-07T00:59:07+02:00 incorporate elements of randomness to the mini notation by @Ghost in: [#165](https://codeberg.org/uzu/strudel/pulls/165)
-- 2022-08-06T23:32:16+02:00 fix some annoying bugs by @froos in: [#177](https://codeberg.org/uzu/strudel/pulls/177)
-- 2022-08-06T00:27:17+02:00 Replace react-codemirror6 with @uiw/react-codemirror by @froos in: [#173](https://codeberg.org/uzu/strudel/pulls/173)
-- 2022-08-04T22:19:46+02:00 add more shapeshifter flags by @froos in: [#99](https://codeberg.org/uzu/strudel/pulls/99)
-- 2022-08-04T22:06:38+02:00 Amend shapeshifter to allow use of dynamic import by @Ghost in: [#171](https://codeberg.org/uzu/strudel/pulls/171)
-- 2022-08-02T23:43:07+02:00 Talk fixes by @froos in: [#164](https://codeberg.org/uzu/strudel/pulls/164)
-- 2022-08-02T23:04:34+02:00 Pianoroll fixes by @froos in: [#163](https://codeberg.org/uzu/strudel/pulls/163)
-- 2022-08-02T22:57:43+02:00 fix: jsdoc comments by @froos in: [#169](https://codeberg.org/uzu/strudel/pulls/169)
-
-## july 2022
-
-- 2022-07-30T23:51:11+02:00 add chooseInWith/chooseCycles by @yaxu in: [#166](https://codeberg.org/uzu/strudel/pulls/166)
-- 2022-07-28T19:26:42+02:00 update to tutorial documentation by @Ghost in: [#162](https://codeberg.org/uzu/strudel/pulls/162)
-- 2022-07-12T18:58:47+02:00 add webdirt drum samples to prebake for general availability by @Ghost in: [#150](https://codeberg.org/uzu/strudel/pulls/150)
-- 2022-07-12T08:34:17+02:00 Final update to demo.pdf by @yaxu in: [#151](https://codeberg.org/uzu/strudel/pulls/151)
-
-## june 2022
-
-- 2022-06-28T21:54:35+02:00 Sampler optimizations and more by @froos in: [#148](https://codeberg.org/uzu/strudel/pulls/148)
-- 2022-06-26T12:58:53+02:00 can now generate short link for sharing by @froos in: [#146](https://codeberg.org/uzu/strudel/pulls/146)
-- 2022-06-24T22:16:04+02:00 flash effect on ctrl enter by @froos in: [#144](https://codeberg.org/uzu/strudel/pulls/144)
-- 2022-06-22T20:18:17+02:00 Pianoroll Object Support by @froos in: [#142](https://codeberg.org/uzu/strudel/pulls/142)
-- 2022-06-21T22:57:28+02:00 Serial twiddles by @yaxu in: [#141](https://codeberg.org/uzu/strudel/pulls/141)
-- 2022-06-21T22:20:05+02:00 Soundfont Support by @froos in: [#139](https://codeberg.org/uzu/strudel/pulls/139)
-- 2022-06-21T14:11:50+02:00 Fix createParam() by @yaxu in: [#140](https://codeberg.org/uzu/strudel/pulls/140)
-- 2022-06-18T23:24:42+02:00 Webaudio rewrite by @froos in: [#138](https://codeberg.org/uzu/strudel/pulls/138)
-- 2022-06-16T21:58:38+02:00 add onTrigger helper by @froos in: [#136](https://codeberg.org/uzu/strudel/pulls/136)
-- 2022-06-16T20:48:23+02:00 Scheduler improvements by @froos in: [#134](https://codeberg.org/uzu/strudel/pulls/134)
-- 2022-06-16T20:48:07+02:00 remove cycle + delta from onTrigger by @froos in: [#135](https://codeberg.org/uzu/strudel/pulls/135)
-- 2022-06-13T21:28:27+02:00 add createParam + createParams by @froos in: [#110](https://codeberg.org/uzu/strudel/pulls/110)
-- 2022-06-13T21:27:09+02:00 Pianoroll enhancements by @froos in: [#131](https://codeberg.org/uzu/strudel/pulls/131)
-- 2022-06-05T13:06:03+02:00 Fix link to contributing to tutorial docs by @Ghost in: [#129](https://codeberg.org/uzu/strudel/pulls/129)
-- 2022-06-02T00:20:45+02:00 Webdirt by @froos in: [#121](https://codeberg.org/uzu/strudel/pulls/121)
-- 2022-06-01T23:56:04+02:00 fix: #122 ctrl enter would add newline by @froos in: [#124](https://codeberg.org/uzu/strudel/pulls/124)
-- 2022-06-01T18:21:56+02:00 fix: #108 by @froos in: [#123](https://codeberg.org/uzu/strudel/pulls/123)
-
-## may 2022
-
-- 2022-05-29T09:54:59+02:00 In source doc by @froos in: [#117](https://codeberg.org/uzu/strudel/pulls/117)
-- 2022-05-20T11:49:10+02:00 react package + vite build by @froos in: [#116](https://codeberg.org/uzu/strudel/pulls/116)
-- 2022-05-15T22:29:26+02:00 Osc timing improvements by @yaxu in: [#113](https://codeberg.org/uzu/strudel/pulls/113)
-- 2022-05-15T22:28:24+02:00 loopAt by @yaxu in: [#114](https://codeberg.org/uzu/strudel/pulls/114)
-- 2022-05-09T20:09:47+02:00 `.brak()`, `.inside()` and `.outside()` by @yaxu in: [#112](https://codeberg.org/uzu/strudel/pulls/112)
-- 2022-05-06T15:18:41+02:00 In source doc by @yaxu in: [#105](https://codeberg.org/uzu/strudel/pulls/105)
-- 2022-05-04T22:59:31+02:00 Embed style by @froos in: [#109](https://codeberg.org/uzu/strudel/pulls/109)
-- 2022-05-03T01:40:22+02:00 Reset, Restart and other composers by @froos in: [#88](https://codeberg.org/uzu/strudel/pulls/88)
-- 2022-05-02T22:47:21+02:00 /embed package: web component for repl by @froos in: [#106](https://codeberg.org/uzu/strudel/pulls/106)
-- 2022-05-02T22:05:34+02:00 Tune tests by @froos in: [#104](https://codeberg.org/uzu/strudel/pulls/104)
-- 2022-05-01T14:33:11+02:00 Codemirror 6 by @froos in: [#97](https://codeberg.org/uzu/strudel/pulls/97)
-
-## april 2022
-
-- 2022-04-28T23:45:53+02:00 Work on Codemirror 6 highlighting by @Ghost in: [#102](https://codeberg.org/uzu/strudel/pulls/102)
-- 2022-04-28T15:46:06+02:00 Change to Affero GPL by @yaxu in: [#101](https://codeberg.org/uzu/strudel/pulls/101)
-- 2022-04-26T00:13:07+02:00 Paper by @froos in: [#98](https://codeberg.org/uzu/strudel/pulls/98)
-- 2022-04-25T22:10:21+02:00 Fiddles with cat/stack by @yaxu in: [#90](https://codeberg.org/uzu/strudel/pulls/90)
-- 2022-04-22T13:08:11+02:00 Add pattern composers, implements #82 by @yaxu in: [#83](https://codeberg.org/uzu/strudel/pulls/83)
-- 2022-04-21T20:57:45+02:00 Tidy up a couple of old files by @Ghost in: [#84](https://codeberg.org/uzu/strudel/pulls/84)
-- 2022-04-21T15:21:29+02:00 add `striate()` by @yaxu in: [#76](https://codeberg.org/uzu/strudel/pulls/76)
-- 2022-04-20T20:17:27+02:00 Webaudio in REPL by @froos in: [#77](https://codeberg.org/uzu/strudel/pulls/77)
-- 2022-04-19T15:31:34+02:00 Basic webserial support by @yaxu in: [#80](https://codeberg.org/uzu/strudel/pulls/80)
-- 2022-04-17T19:29:49+02:00 Try to fix appLeft / appRight by @yaxu in: [#75](https://codeberg.org/uzu/strudel/pulls/75)
-- 2022-04-16T13:26:57+02:00 More random functions by @yaxu in: [#74](https://codeberg.org/uzu/strudel/pulls/74)
-- 2022-04-16T11:13:26+02:00 Port `perlin` noise, `rangex`, and `palindrome` by @yaxu in: [#73](https://codeberg.org/uzu/strudel/pulls/73)
-- 2022-04-16T10:26:07+02:00 webaudio package by @froos in: [#26](https://codeberg.org/uzu/strudel/pulls/26)
-- 2022-04-15T20:18:06+02:00 More randomness, fix `rand`, and add `brand`, `irand` and `choose` by @yaxu in: [#70](https://codeberg.org/uzu/strudel/pulls/70)
-- 2022-04-15T11:29:51+02:00 First effort at rand() by @yaxu in: [#69](https://codeberg.org/uzu/strudel/pulls/69)
-- 2022-04-14T17:41:18+02:00 use new fixed version of osc-js package by @froos in: [#68](https://codeberg.org/uzu/strudel/pulls/68)
-- 2022-04-14T00:09:18+02:00 Speech output by @froos in: [#67](https://codeberg.org/uzu/strudel/pulls/67)
-- 2022-04-13T23:55:33+02:00 Separate out strudel.mjs, make index.mjs aggregate module by @yaxu in: [#62](https://codeberg.org/uzu/strudel/pulls/62)
-- 2022-04-13T17:26:45+02:00 More functions by @yaxu in: [#61](https://codeberg.org/uzu/strudel/pulls/61)
-- 2022-04-13T10:04:29+02:00 More functions by @yaxu in: [#56](https://codeberg.org/uzu/strudel/pulls/56)
-- 2022-04-12T17:04:04+02:00 OSC and SuperDirt support by @yaxu in: [#27](https://codeberg.org/uzu/strudel/pulls/27)
-- 2022-04-12T13:24:14+02:00 Implement `chop()` by @yaxu in: [#50](https://codeberg.org/uzu/strudel/pulls/50)
-- 2022-04-12T12:37:32+02:00 First run at squeezeBind, ref #32 by @yaxu in: [#48](https://codeberg.org/uzu/strudel/pulls/48)
-- 2022-04-12T00:03:37+02:00 Fix polymeter by @yaxu in: [#44](https://codeberg.org/uzu/strudel/pulls/44)
-- 2022-04-11T22:39:44+02:00 Compose by @froos in: [#40](https://codeberg.org/uzu/strudel/pulls/40)
-- 2022-04-11T21:49:35+02:00 Update tutorial.mdx by @Ghost in: [#38](https://codeberg.org/uzu/strudel/pulls/38)
-- 2022-04-11T08:43:44+02:00 Update tutorial.mdx by @Ghost in: [#37](https://codeberg.org/uzu/strudel/pulls/37)
-
-## march 2022
-
-- 2022-03-28T17:27:22+02:00 Add chunk, chunkBack and iterBack by @yaxu in: [#25](https://codeberg.org/uzu/strudel/pulls/25)
-- 2022-03-28T11:48:22+02:00 packaging by @froos in: [#24](https://codeberg.org/uzu/strudel/pulls/24)
-- 2022-03-22T12:06:48+01:00 Update package.json by @Ghost in: [#23](https://codeberg.org/uzu/strudel/pulls/23)
-- 2022-03-06T20:35:20+01:00 added _asNumber + interprete numbers as midi by @froos in: [#21](https://codeberg.org/uzu/strudel/pulls/21)
-
-## february 2022
-
-- 2022-02-28T00:32:29+01:00 Fix resolveState by @yaxu in: [#22](https://codeberg.org/uzu/strudel/pulls/22)
-- 2022-02-27T20:52:52+01:00 Stateful queries and events (WIP) by @yaxu in: [#14](https://codeberg.org/uzu/strudel/pulls/14)
-- 2022-02-27T18:04:07+01:00 fix: 💄 Enhance visualisation of the Tutorial on mobile by @Ghost in: [#19](https://codeberg.org/uzu/strudel/pulls/19)
-- 2022-02-26T21:16:34+01:00 test: 📦 Add missing dependency and a CI check, to prevent oversights ;p by @Ghost in: [#17](https://codeberg.org/uzu/strudel/pulls/17)
-- 2022-02-26T13:29:19+01:00 higher latencyHint by @froos in: [#16](https://codeberg.org/uzu/strudel/pulls/16)
-- 2022-02-23T22:15:11+01:00 add apply and layer, and missing div/mul methods by @yaxu in: [#15](https://codeberg.org/uzu/strudel/pulls/15)
-- 2022-02-21T01:02:07+01:00 Add continuous signals (sine, cosine, saw, etc) by @yaxu in: [#13](https://codeberg.org/uzu/strudel/pulls/13)
-- 2022-02-19T21:30:04+01:00 Added mask() and struct() by @yaxu in: [#11](https://codeberg.org/uzu/strudel/pulls/11)
-- 2022-02-19T16:07:07+01:00 Failing test for `when` WIP by @yaxu in: [#10](https://codeberg.org/uzu/strudel/pulls/10)
-- 2022-02-10T17:21:31+01:00 Bugfix every, and create more top level functions by @yaxu in: [#9](https://codeberg.org/uzu/strudel/pulls/9)
-- 2022-02-10T00:51:21+01:00 timeCat by @yaxu in: [#8](https://codeberg.org/uzu/strudel/pulls/8)
-- 2022-02-07T23:07:58+01:00 fixed editor crash by @froos in: [#7](https://codeberg.org/uzu/strudel/pulls/7)
-- 2022-02-07T19:08:40+01:00 krill parser + improved repl by @froos in: [#6](https://codeberg.org/uzu/strudel/pulls/6)
-- 2022-02-06T00:59:16+01:00 Patternify all the things by @yaxu in: [#5](https://codeberg.org/uzu/strudel/pulls/5)
-- 2022-02-05T23:31:37+01:00 update readme for local dev by @Ghost in: [#4](https://codeberg.org/uzu/strudel/pulls/4)
-- 2022-02-05T22:36:06+01:00 Fix path by @yaxu in: [#3](https://codeberg.org/uzu/strudel/pulls/3)
-- 2022-02-05T21:52:51+01:00 repl + reify functions by @froos in: [#2](https://codeberg.org/uzu/strudel/pulls/2)
-
-... hello from the bottom of this file! you seem to be interested in the history of strudel, so it's worth noting that the first [commit](https://codeberg.org/uzu/strudel/commit/38b5a0d5cdf28685b2b5e18d460772b70246207b) to the repo was actually on Jan 22, 2022, 09:24 PM GMT+1 by @yaxu which could be seen as strudel's birthday 🎂
+- 2025-11-26T08:23:38+01:00 feat: add prebake script import button for loading .strudel files by @daslyfe in: [#1774](https://codeberg.org/uzu/strudel/pulls/1774)
+- 2025-11-11T09:00:49+01:00 soundAlias example fix by @PepsiiMan in: [#1720](https://codeberg.org/uzu/strudel/pulls/1720)
From e35a2000de9a6f1538a552340f002cca1a654056 Mon Sep 17 00:00:00 2001
From: space-shell
Date: Mon, 19 Jan 2026 10:15:10 +0100
Subject: [PATCH 067/200] reverts changelog changes because human...
---
CHANGELOG.md | 1108 ++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 1081 insertions(+), 27 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2007ee11..911463de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,27 +1,1081 @@
-load page 1/1
--------------
-- 2026-01-18T22:27:46+01:00 fix: move kabelsalat web to superdough by @froos in: [#1932](https://codeberg.org/uzu/strudel/pulls/1932)
-- 2026-01-18T12:36:50+01:00 Add tags to reference by @vvolhejn in: [#1645](https://codeberg.org/uzu/strudel/pulls/1645)
-- 2026-01-18T10:36:02+01:00 fix: performance issues when reference is open by @robojumper in: [#1926](https://codeberg.org/uzu/strudel/pulls/1926)
-- 2026-01-17T21:00:43+01:00 Make kabelsalat stereo out by @glossing in: [#1927](https://codeberg.org/uzu/strudel/pulls/1927)
-- 2026-01-16T10:38:04+01:00 update changelog + fix script to not miss entries by @froos in: [#1916](https://codeberg.org/uzu/strudel/pulls/1916)
-- 2026-01-15T16:59:14+01:00 mondo fix: add registered functions to scope automatically by @daslyfe in: [#1896](https://codeberg.org/uzu/strudel/pulls/1896)
-- 2026-01-15T16:58:14+01:00 Added docs for pattern search by @JohnBjrk in: [#1905](https://codeberg.org/uzu/strudel/pulls/1905)
-- 2026-01-15T16:57:19+01:00 Update kabelsalat dependency by @jeromew in: [#1911](https://codeberg.org/uzu/strudel/pulls/1911)
-- 2026-01-15T00:24:39+01:00 Bug Fix: Set pooled values immediately instead of scheduling by @glossing in: [#1907](https://codeberg.org/uzu/strudel/pulls/1907)
-- 2026-01-14T20:29:16+01:00 Feat: Kabelsalat integration by @glossing in: [#1876](https://codeberg.org/uzu/strudel/pulls/1876)
-- 2026-01-14T19:07:49+01:00 Feat: MIDI Keyboard 🎹🐈 by @glossing in: [#1828](https://codeberg.org/uzu/strudel/pulls/1828)
-- 2026-01-14T18:14:52+01:00 Bug Fix: Fix race condition between worklet termination and port messages by @glossing in: [#1897](https://codeberg.org/uzu/strudel/pulls/1897)
-- 2026-01-13T04:06:52+01:00 Add search/filter in patterns tab by @JohnBjrk in: [#1842](https://codeberg.org/uzu/strudel/pulls/1842)
-- 2026-01-13T00:18:10+01:00 Add shortcut for navigating through labels! by @daslyfe in: [#1807](https://codeberg.org/uzu/strudel/pulls/1807)
-- 2026-01-12T00:17:37+01:00 add warm.strudel.cc to faq by @yaxu in: [#1891](https://codeberg.org/uzu/strudel/pulls/1891)
-- 2026-01-11T19:00:25+01:00 Fix sounds example to work in the REPL by @JesCoding in: [#1851](https://codeberg.org/uzu/strudel/pulls/1851)
-- 2026-01-11T12:07:48+01:00 Feat: Support External AudioContext Injection by @1d10t in: [#1833](https://codeberg.org/uzu/strudel/pulls/1833)
-- 2026-01-10T23:12:48+01:00 Perf: Targeted node pools by @glossing in: [#1810](https://codeberg.org/uzu/strudel/pulls/1810)
-- 2026-01-10T20:52:40+01:00 Allow top level distortions for the purpose of FX by @glossing in: [#1884](https://codeberg.org/uzu/strudel/pulls/1884)
-- 2026-01-07T20:06:18+01:00 Feat: Add ability to turn mini parsing off with mini-off decorator by @glossing in: [#1786](https://codeberg.org/uzu/strudel/pulls/1786)
-- 2026-01-01T19:52:15+01:00 Feat: FX Chains by @glossing in: [#1861](https://codeberg.org/uzu/strudel/pulls/1861)
-- 2025-12-29T20:54:11+01:00 Feature: LFOs and Envelopes by @glossing in: [#1507](https://codeberg.org/uzu/strudel/pulls/1507)
-- 2025-12-11T18:00:49+01:00 Feature: stateful timeline function for jumping between timelines by @yaxu in: [#1669](https://codeberg.org/uzu/strudel/pulls/1669)
-- 2025-11-26T08:23:38+01:00 feat: add prebake script import button for loading .strudel files by @daslyfe in: [#1774](https://codeberg.org/uzu/strudel/pulls/1774)
-- 2025-11-11T09:00:49+01:00 soundAlias example fix by @PepsiiMan in: [#1720](https://codeberg.org/uzu/strudel/pulls/1720)
+# changelog
+
+NOTE: you can generate this with `node warm.js`. it might still not be perfectly sorted...
+
+## january 2026
+
+
+- 2026-01-15T16:59:14+01:00 mondo fix: add registered functions to scope automatically by @daslyfe in: [#1896](https://codeberg.org/uzu/strudel/pulls/1896)
+- 2026-01-15T16:58:14+01:00 Added docs for pattern search by @JohnBjrk in: [#1905](https://codeberg.org/uzu/strudel/pulls/1905)
+- 2026-01-15T16:57:19+01:00 Update kabelsalat dependency by @jeromew in: [#1911](https://codeberg.org/uzu/strudel/pulls/1911)
+- 2026-01-15T00:24:39+01:00 Bug Fix: Set pooled values immediately instead of scheduling by @glossing in: [#1907](https://codeberg.org/uzu/strudel/pulls/1907)
+- 2026-01-14T20:29:16+01:00 Feat: Kabelsalat integration by @glossing in: [#1876](https://codeberg.org/uzu/strudel/pulls/1876)
+- 2026-01-14T19:07:49+01:00 Feat: MIDI Keyboard 🎹🐈 by @glossing in: [#1828](https://codeberg.org/uzu/strudel/pulls/1828)
+- 2026-01-14T18:14:52+01:00 Bug Fix: Fix race condition between worklet termination and port messages by @glossing in: [#1897](https://codeberg.org/uzu/strudel/pulls/1897)
+- 2026-01-13T04:06:52+01:00 Add search/filter in patterns tab by @JohnBjrk in: [#1842](https://codeberg.org/uzu/strudel/pulls/1842)
+- 2026-01-13T00:18:10+01:00 Add shortcut for navigating through labels! by @daslyfe in: [#1807](https://codeberg.org/uzu/strudel/pulls/1807)
+- 2026-01-12T00:17:37+01:00 add warm.strudel.cc to faq by @yaxu in: [#1891](https://codeberg.org/uzu/strudel/pulls/1891)
+- 2026-01-11T19:00:25+01:00 Fix sounds example to work in the REPL by @JesCoding in: [#1851](https://codeberg.org/uzu/strudel/pulls/1851)
+- 2026-01-11T13:52:02+01:00 Improve hint text when sound search has no results by @floy in: [#1883](https://codeberg.org/uzu/strudel/pulls/1883)
+- 2026-01-11T13:19:46+01:00 New page FAQ in "More" by @scrappy_fiddler in: [#1753](https://codeberg.org/uzu/strudel/pulls/1753)
+- 2026-01-11T12:45:07+01:00 fix: remove faulty default readme by @froos in: [#1889](https://codeberg.org/uzu/strudel/pulls/1889)
+- 2026-01-11T12:15:34+01:00 fix/self-hosted-config by @alienmind in: [#1880](https://codeberg.org/uzu/strudel/pulls/1880)
+- 2026-01-11T12:07:48+01:00 Feat: Support External AudioContext Injection by @1d10t in: [#1833](https://codeberg.org/uzu/strudel/pulls/1833)
+- 2026-01-11T11:37:37+01:00 fix: add trem to top level by @froos in: [#1887](https://codeberg.org/uzu/strudel/pulls/1887)
+- 2026-01-11T11:37:18+01:00 fix: export start cycle min 0 by @froos in: [#1888](https://codeberg.org/uzu/strudel/pulls/1888)
+- 2026-01-11T11:05:44+01:00 fix: repl package init audio properly by @froos in: [#1836](https://codeberg.org/uzu/strudel/pulls/1836)
+- 2026-01-11T06:51:24+01:00 Fix: show reload dialog when uploading prebake script by @daslyfe in: [#1886](https://codeberg.org/uzu/strudel/pulls/1886)
+- 2026-01-11T06:01:42+01:00 fixes Serial onTrigger() params #1633 by @gueejla in: [#1885](https://codeberg.org/uzu/strudel/pulls/1885)
+- 2026-01-10T23:12:48+01:00 Perf: Targeted node pools by @glossing in: [#1810](https://codeberg.org/uzu/strudel/pulls/1810)
+- 2026-01-10T20:52:40+01:00 Allow top level distortions for the purpose of FX by @glossing in: [#1884](https://codeberg.org/uzu/strudel/pulls/1884)
+- 2026-01-09T03:43:37+01:00 Bake in scaling by `freq` for FM with a gain node by @glossing in: [#1878](https://codeberg.org/uzu/strudel/pulls/1878)
+- 2026-01-09T02:53:38+01:00 Bug fix: Properly handle subcontrols by @glossing in: [#1877](https://codeberg.org/uzu/strudel/pulls/1877)
+- 2026-01-07T20:06:18+01:00 Feat: Add ability to turn mini parsing off with mini-off decorator by @glossing in: [#1786](https://codeberg.org/uzu/strudel/pulls/1786)
+- 2026-01-07T19:22:24+01:00 Bug Fix: Update loopStart/End to not be offset by @glossing in: [#1826](https://codeberg.org/uzu/strudel/pulls/1826)
+- 2026-01-06T06:35:49+01:00 Update modulator docstrings and allow ids to be patterns by @glossing in: [#1874](https://codeberg.org/uzu/strudel/pulls/1874)
+- 2026-01-04T17:14:11+01:00 Fix doc link in @strudel/osc README.md by @forrcaho in: [#1872](https://codeberg.org/uzu/strudel/pulls/1872)
+- 2026-01-04T02:01:07+01:00 Make stretch modulatable by @glossing in: [#1870](https://codeberg.org/uzu/strudel/pulls/1870)
+- 2026-01-02T18:44:53+01:00 Make pan modulatable by @glossing in: [#1865](https://codeberg.org/uzu/strudel/pulls/1865)
+- 2026-01-01T21:42:23+01:00 Fix transpilation example to have same mini-notation by @JesCoding in: [#1850](https://codeberg.org/uzu/strudel/pulls/1850)
+- 2026-01-01T21:39:05+01:00 fix: missing punctuation by @eddyflux in: [#1860](https://codeberg.org/uzu/strudel/pulls/1860)
+- 2026-01-01T20:20:34+01:00 Fix formatting of docstring by @glossing in: [#1864](https://codeberg.org/uzu/strudel/pulls/1864)
+- 2026-01-01T19:52:15+01:00 Feat: FX Chains by @glossing in: [#1861](https://codeberg.org/uzu/strudel/pulls/1861)
+
+## december 2025
+
+- 2025-12-29T21:59:11+01:00 Bugfix: Fix modulator clamping when min/max not specified by @glossing in: [#1859](https://codeberg.org/uzu/strudel/pulls/1859)
+- 2025-12-29T20:54:11+01:00 Feature: LFOs and Envelopes by @glossing in: [#1507](https://codeberg.org/uzu/strudel/pulls/1507)
+- 2025-12-29T16:07:18+01:00 Fix AudioContext change detection. Use AudioNode.context by @jeromew in: [#1858](https://codeberg.org/uzu/strudel/pulls/1858)
+- 2025-12-28T22:58:38+01:00 Say that @license should use SPDX identifier by @Wuzzy in: [#1817](https://codeberg.org/uzu/strudel/pulls/1817)
+- 2025-12-28T22:56:07+01:00 Expose Vim object in order to create custom keybindings by @JohnBjrk in: [#1816](https://codeberg.org/uzu/strudel/pulls/1816)
+- 2025-12-28T14:20:03+01:00 dough repl fixes by @froos in: [#1855](https://codeberg.org/uzu/strudel/pulls/1855)
+- 2025-12-28T13:40:29+01:00 add basic dough repl by @froos in: [#1749](https://codeberg.org/uzu/strudel/pulls/1749)
+- 2025-12-20T22:27:52+01:00 Document "-" in mini-notation by @Wuzzy in: [#1818](https://codeberg.org/uzu/strudel/pulls/1818)
+- 2025-12-20T22:20:27+01:00 simplify envValAtTime and remove asymmetric behavior (fix #1653) by @pulu in: [#1815](https://codeberg.org/uzu/strudel/pulls/1815)
+- 2025-12-19T08:11:50+01:00 fix: visual block selection mode for vim bindings by @Dsm0 in: [#1839](https://codeberg.org/uzu/strudel/pulls/1839)
+- 2025-12-19T01:03:53+01:00 [perf] Add audiograph `await debugAudiograph()` feature by @jeromew in: [#1763](https://codeberg.org/uzu/strudel/pulls/1763)
+- 2025-12-19T00:45:41+01:00 Feature: non-realtime exporting by @Ghost in: [#1674](https://codeberg.org/uzu/strudel/pulls/1674)
+- 2025-12-14T19:33:37+01:00 Fix: wrong warning in build environments by @jeromew in: [#1835](https://codeberg.org/uzu/strudel/pulls/1835)
+- 2025-12-14T15:09:12+01:00 delta -> per / perx / cyclesPer refinements by @yaxu in: [#1832](https://codeberg.org/uzu/strudel/pulls/1832)
+- 2025-12-14T01:07:36+01:00 Improved randomness by @glossing in: [#1505](https://codeberg.org/uzu/strudel/pulls/1505)
+- 2025-12-12T10:28:27+01:00 Add delta signal for representing the duration of events in patterns that are combined with it by @yaxu in: [#1831](https://codeberg.org/uzu/strudel/pulls/1831)
+- 2025-12-11T18:00:49+01:00 Feature: stateful timeline function for jumping between timelines by @yaxu in: [#1669](https://codeberg.org/uzu/strudel/pulls/1669)
+- 2025-12-11T16:37:58+01:00 Updates relating to LLM, github, etc by @yaxu in: [#1830](https://codeberg.org/uzu/strudel/pulls/1830)
+- 2025-12-10T15:32:44+01:00 fix .as so it doesn't set undefined values by @yaxu in: [#1827](https://codeberg.org/uzu/strudel/pulls/1827)
+- 2025-12-08T19:27:31+01:00 [perf] propagate `onceEnded` and `releaseAudioNode` by @jeromew in: [#1809](https://codeberg.org/uzu/strudel/pulls/1809)
+- 2025-12-07T21:51:40+01:00 Feat: Add channel support to midi in by @glossing in: [#1775](https://codeberg.org/uzu/strudel/pulls/1775)
+- 2025-12-07T21:23:43+01:00 Feat: Transient shaper by @glossing in: [#1777](https://codeberg.org/uzu/strudel/pulls/1777)
+- 2025-12-07T19:15:43+01:00 Add vel as a synonym for velocity & update a few docstrings by @glossing in: [#1781](https://codeberg.org/uzu/strudel/pulls/1781)
+- 2025-12-07T18:53:04+01:00 [perf] fix phaser leak of unused biquads by @jeromew in: [#1800](https://codeberg.org/uzu/strudel/pulls/1800)
+- 2025-12-07T18:42:50+01:00 Bug fix: Remove failing tests due to shabda removal by @glossing in: [#1820](https://codeberg.org/uzu/strudel/pulls/1820)
+- 2025-12-03T18:27:28+01:00 Feature: Eight FMs by @glossing in: [#1628](https://codeberg.org/uzu/strudel/pulls/1628)
+- 2025-12-03T17:35:05+01:00 [perf] release unused AudioBufferSourceNode + releaseAudioNode by @jeromew in: [#1805](https://codeberg.org/uzu/strudel/pulls/1805)
+- **2025-12-01 strudel.cc deployed**
+
+## november 2025
+
+- 2025-11-29T01:00:42+01:00 added export to getSuperdoughAudioController() so that its possible to route superdough audio through other webaudio applications. by @ndr0n in: [#1796](https://codeberg.org/uzu/strudel/pulls/1796)
+- 2025-11-28T23:26:20+01:00 add revv() for reversing whole patterns by @yaxu in: [#1791](https://codeberg.org/uzu/strudel/pulls/1791)
+- 2025-11-28T20:19:16+01:00 [perf] Disconnect lfos for phaser and filters by @glossing in: [#1787](https://codeberg.org/uzu/strudel/pulls/1787)
+- 2025-11-27T23:08:52+01:00 fix: return silence when no pattern is returned by @froos in: [#1795](https://codeberg.org/uzu/strudel/pulls/1795)
+- 2025-11-27T22:47:03+01:00 fix(tool/dbpatch): add missing package name by @peterpf in: [#1765](https://codeberg.org/uzu/strudel/pulls/1765)
+- 2025-11-27T22:38:05+01:00 add CHANGELOG.md + basic script to generate by @froos in: [#1794](https://codeberg.org/uzu/strudel/pulls/1794)
+- 2025-11-27T22:03:53+01:00 [perf] in `noise`, let noiseMix do the disconnect when it exists by @jeromew in: [#1783](https://codeberg.org/uzu/strudel/pulls/1783)
+- 2025-11-27T22:01:01+01:00 Bug Fix: Retries for sounds tab by @glossing in: [#1754](https://codeberg.org/uzu/strudel/pulls/1754)
+- 2025-11-27T20:36:46+01:00 [hydra] return the hydra object when await initHydra(..) is called by @jeromew in: [#1784](https://codeberg.org/uzu/strudel/pulls/1784)
+- 2025-11-27T20:36:14+01:00 Use errorLogger for query and tonal errors by @glossing in: [#1782](https://codeberg.org/uzu/strudel/pulls/1782)
+- 2025-11-27T20:27:10+01:00 [perf] level 5 `connect-leak` on `vowel` by @jeromew in: [#1779](https://codeberg.org/uzu/strudel/pulls/1779)
+- 2025-11-26T08:23:38+01:00 feat: add prebake script import button for loading .strudel files by @daslyfe in: [#1774](https://codeberg.org/uzu/strudel/pulls/1774)
+- 2025-11-26T07:39:28+01:00 Fix Sampler port trampling by @Dayglo in: [#1717](https://codeberg.org/uzu/strudel/pulls/1717)
+- 2025-11-25T20:59:09+01:00 Feat: Hook up octave and fix example by @glossing in: [#1773](https://codeberg.org/uzu/strudel/pulls/1773)
+- 2025-11-25T20:48:09+01:00 [perf] fix `connect-leak` in `tremolo` param by @jeromew in: [#1780](https://codeberg.org/uzu/strudel/pulls/1780)
+- 2025-11-24T17:51:20+01:00 filter modulation improvements! by @daslyfe in: [#1769](https://codeberg.org/uzu/strudel/pulls/1769)
+- 2025-11-24T01:20:53+01:00 Add o as a synonym for orbit by @daslyfe in: [#1766](https://codeberg.org/uzu/strudel/pulls/1766)
+- 2025-11-23T22:23:26+01:00 [perf] fix `connect-leak` in `fm` modulation by @jeromew in: [#1758](https://codeberg.org/uzu/strudel/pulls/1758)
+- 2025-11-23T09:47:24+01:00 Fix interoperability issue between `all` and `await initHydra()` by @jeromew in: [#1663](https://codeberg.org/uzu/strudel/pulls/1663)
+- 2025-11-23T04:23:06+01:00 Bug fix: Swap l/r gains with temp variable by @glossing in: [#1768](https://codeberg.org/uzu/strudel/pulls/1768)
+- 2025-11-23T00:52:28+01:00 FIX: prevent LFO filter modulation from popping from negative values by @daslyfe in: [#1767](https://codeberg.org/uzu/strudel/pulls/1767)
+- 2025-11-23T00:03:32+01:00 prefix "S" for solo by @froos in: [#1481](https://codeberg.org/uzu/strudel/pulls/1481)
+- 2025-11-21T01:56:11+01:00 Bug Fix: Use frac due to negative frequencies from FM by @glossing in: [#1759](https://codeberg.org/uzu/strudel/pulls/1759)
+- 2025-11-20T19:48:16+01:00 [perf] fix `connect-leak` added by #1742 when noise() is not used by @jeromew in: [#1757](https://codeberg.org/uzu/strudel/pulls/1757)
+- 2025-11-20T14:51:00+01:00 [perf] fix `connect-leak` in `delay` effect by @jeromew in: [#1755](https://codeberg.org/uzu/strudel/pulls/1755)
+- 2025-11-20T14:49:41+01:00 [perf] fix `connect leak` when .noise() is in the mix by @jeromew in: [#1742](https://codeberg.org/uzu/strudel/pulls/1742)
+- 2025-11-18T23:52:45+01:00 wchooseCycles has now notes in an example by @scrappy_fiddler in: [#1748](https://codeberg.org/uzu/strudel/pulls/1748)
+- 2025-11-17T05:31:54+01:00 Feature: Partials by @glossing in: [#1659](https://codeberg.org/uzu/strudel/pulls/1659)
+- 2025-11-16T22:06:14+01:00 README: update superdough documentation by @TristanMlct in: [#1741](https://codeberg.org/uzu/strudel/pulls/1741)
+- 2025-11-16T22:00:43+01:00 Feature: Envelope worklet by @glossing in: [#1524](https://codeberg.org/uzu/strudel/pulls/1524)
+- 2025-11-16T21:08:54+01:00 Worklet optimizations by @glossing in: [#1730](https://codeberg.org/uzu/strudel/pulls/1730)
+- 2025-11-15T14:59:30+01:00 [perf] disconnect Offline AudioNode connections in generateReverb by @jeromew in: [#1740](https://codeberg.org/uzu/strudel/pulls/1740)
+- 2025-11-15T14:57:47+01:00 [perf] disconnect `send` effect AudioNode when `room` is used by @jeromew in: [#1736](https://codeberg.org/uzu/strudel/pulls/1736)
+- 2025-11-12T21:22:34+01:00 [supradough] fix: unstable filter by @froos in: [#1593](https://codeberg.org/uzu/strudel/pulls/1593)
+- 2025-11-12T21:12:07+01:00 Fix link syntax in `project-start` by @Kissaki in: [#1724](https://codeberg.org/uzu/strudel/pulls/1724)
+- 2025-11-12T21:06:13+01:00 Fix web README sample code by @Kissaki in: [#1725](https://codeberg.org/uzu/strudel/pulls/1725)
+- 2025-11-12T20:59:39+01:00 Fix typo: 'studel' -> 'strudel' by @drhayes in: [#1726](https://codeberg.org/uzu/strudel/pulls/1726)
+- 2025-11-12T15:23:41+01:00 fix for node 24 support in tests - #1718 by @ausav in: [#1719](https://codeberg.org/uzu/strudel/pulls/1719)
+- 2025-11-11T09:00:49+01:00 soundAlias example fix by @PepsiiMan in: [#1720](https://codeberg.org/uzu/strudel/pulls/1720)
+- 2025-11-10T08:18:36+01:00 fix: Make Supradough package build by @munshkr in: [#1711](https://codeberg.org/uzu/strudel/pulls/1711)
+- 2025-11-10T08:18:00+01:00 added docs for spaces in scale names by @ondras in: [#1715](https://codeberg.org/uzu/strudel/pulls/1715)
+- 2025-11-10T02:44:01+01:00 Feature: LFOs for Filters by @glossing in: [#1636](https://codeberg.org/uzu/strudel/pulls/1636)
+- 2025-11-05T17:07:00+01:00 improvement: sync midi messages to audio context clock by @daslyfe in: [#1708](https://codeberg.org/uzu/strudel/pulls/1708)
+- 2025-11-04T19:36:14+01:00 Add stick button mappings to gamepad implementation and improve docs by @kaosuryoko in: [#1696](https://codeberg.org/uzu/strudel/pulls/1696)
+- 2025-11-04T19:29:35+01:00 Refactor sound stopping and triggering logic in SoundsTab component by @IJOL in: [#1688](https://codeberg.org/uzu/strudel/pulls/1688)
+- 2025-11-04T19:13:21+01:00 Add setting to toggle pattern auto-start on pattern change by @moumar in: [#1690](https://codeberg.org/uzu/strudel/pulls/1690)
+- 2025-11-04T18:58:50+01:00 Voicings JSDoc by @sharkeys_lunchbox in: [#1686](https://codeberg.org/uzu/strudel/pulls/1686)
+- 2025-11-01T07:50:40+01:00 FIX: Loading local samples uses too much memory by @daslyfe in: [#1706](https://codeberg.org/uzu/strudel/pulls/1706)
+
+
+## october 2025
+
+- 2025-10-28T22:58:24+01:00 Bug Fix: Handle scale-for-notes when n also supplied by @glossing in: [#1625](https://codeberg.org/uzu/strudel/pulls/1625)
+- 2025-10-28T22:51:16+01:00 Repurpose vim shortcuts for usability by @dtricks in: [#1624](https://codeberg.org/uzu/strudel/pulls/1624)
+- 2025-10-28T22:35:21+01:00 Add support for euclidian in mondo with `bd&3:8` by @TristanCacqueray in: [#1630](https://codeberg.org/uzu/strudel/pulls/1630)
+- 2025-10-27T10:42:07+01:00 Use bunny cdn for all samples and json files by @yaxu in: [#1701](https://codeberg.org/uzu/strudel/pulls/1701)
+- **2025-10-27 @strudel/core@1.2.5**
+- 2025-10-26T22:52:54+01:00 degithub - switch some samples to bunnycdn by @yaxu in: [#1697](https://codeberg.org/uzu/strudel/pulls/1697)
+- 2025-10-26T17:09:22+01:00 Fix ZZFX example by @moumar in: [#1685](https://codeberg.org/uzu/strudel/pulls/1685)
+- 2025-10-23T15:56:04+02:00 Make osc port and host configurable. Changes dependencies. by @yaxu in: [#1682](https://codeberg.org/uzu/strudel/pulls/1682)
+- 2025-10-23T03:47:31+02:00 Adds back shape to superdough by @glossing in: [#1672](https://codeberg.org/uzu/strudel/pulls/1672)
+- 2025-10-22T22:15:19+02:00 Fix sampler.mjs githubPath by @jeromew in: [#1684](https://codeberg.org/uzu/strudel/pulls/1684)
+- 2025-10-22T16:20:50+02:00 Fix onPaint for widgets by @yaxu in: [#1658](https://codeberg.org/uzu/strudel/pulls/1658)
+- 2025-10-22T15:19:12+02:00 Fix a bug introduced by #4e17cfbdd6 by @milliganf in: [#1679](https://codeberg.org/uzu/strudel/pulls/1679)
+- 2025-10-20T22:37:15+02:00 feature/autocomplete-sound-names by @drdozer in: [#1564](https://codeberg.org/uzu/strudel/pulls/1564)
+- 2025-10-20T21:58:12+02:00 add replicate + use it for ! in mondo by @JoStro in: [#1436](https://codeberg.org/uzu/strudel/pulls/1436)
+- 2025-10-18T04:29:20+02:00 Bug Fix: Wavetable: phase wrapping at 1 and detune by @glossing in: [#1620](https://codeberg.org/uzu/strudel/pulls/1620)
+- 2025-10-16T20:26:28+02:00 github samples: default to "samples" if repository is not specified by @prezmop in: [#1644](https://codeberg.org/uzu/strudel/pulls/1644)
+- 2025-10-16T17:33:39+02:00 Docs: add example of custom chained function by @dariusk in: [#1642](https://codeberg.org/uzu/strudel/pulls/1642)
+- 2025-10-14T12:39:48+02:00 textbox by @yaxu in: [#1650](https://codeberg.org/uzu/strudel/pulls/1650)
+- 2025-10-13T07:26:51+02:00 Feature: Distortion Modes by @glossing in: [#1561](https://codeberg.org/uzu/strudel/pulls/1561)
+- 2025-10-13T07:05:57+02:00 Feature: Wavetable FM by @glossing in: [#1623](https://codeberg.org/uzu/strudel/pulls/1623)
+- 2025-10-12T20:43:42+02:00 fix: repair REPL sample sources and URL concat (#1640) by @erikfox in: [#1646](https://codeberg.org/uzu/strudel/pulls/1646)
+- 2025-10-10T11:25:02+02:00 Add control-metadata by @yaxu in: [#1634](https://codeberg.org/uzu/strudel/pulls/1634)
+- 2025-10-07T22:59:39+02:00 Fix MQTT: change the trigger handler to match new hap by @jurrchen in: [#1629](https://codeberg.org/uzu/strudel/pulls/1629)
+- 2025-10-05T21:42:19+02:00 Fix case sensitivity for synonym search by @vvolhejn in: [#1618](https://codeberg.org/uzu/strudel/pulls/1618)
+- 2025-10-01T08:56:35+02:00 Optimize wavetable synth by @glossing in: [#1613](https://codeberg.org/uzu/strudel/pulls/1613)
+- 2025-10-01T02:02:33+02:00 fix: pattern switching by @daslyfe in: [#1616](https://codeberg.org/uzu/strudel/pulls/1616)
+
+## september 2025
+
+- 2025-09-29T02:10:54+02:00 fix wavetable JSON by @daslyfe in: [#1611](https://codeberg.org/uzu/strudel/pulls/1611)
+- 2025-09-28T21:28:16+02:00 rename wavetable controls to match existing naming conventions by @daslyfe in: [#1610](https://codeberg.org/uzu/strudel/pulls/1610)
+- 2025-09-27T23:28:13+02:00 Feature: Wavetable synth improvements by @glossing in: [#1607](https://codeberg.org/uzu/strudel/pulls/1607)
+- 2025-09-27T21:43:10+02:00 wavetable improvements by @daslyfe in: [#1608](https://codeberg.org/uzu/strudel/pulls/1608)
+- 2025-09-26T08:48:46+02:00 create orbit based DJ filter by @daslyfe in: [#1603](https://codeberg.org/uzu/strudel/pulls/1603)
+- 2025-09-26T08:15:49+02:00 Feature: Wavetable synth by @glossing in: [#1525](https://codeberg.org/uzu/strudel/pulls/1525)
+- 2025-09-23T13:28:20+02:00 fix: time signal by @daslyfe in: [#1583](https://codeberg.org/uzu/strudel/pulls/1583)
+- 2025-09-21T16:16:06+02:00 mondo: fix all + setcpm + setcps by @froos in: [#1600](https://codeberg.org/uzu/strudel/pulls/1600)
+- 2025-09-19T21:59:43+02:00 fix: osc error message by @froos in: [#1597](https://codeberg.org/uzu/strudel/pulls/1597)
+- 2025-09-18T21:09:56+02:00 osc: --debug flag to see incoming messages by @froos in: [#1595](https://codeberg.org/uzu/strudel/pulls/1595)
+- 2025-09-17T14:12:10+02:00 simplify osc usage by @froos in: [#1588](https://codeberg.org/uzu/strudel/pulls/1588)
+- 2025-09-16T16:59:58+02:00 dev: logger errors to console in dev mode by @daslyfe in: [#1585](https://codeberg.org/uzu/strudel/pulls/1585)
+- 2025-09-16T03:19:37+02:00 Bug Fix / Feature: Updates to `duck` to avoid clicks and allow configuration over release/attack by @glossing in: [#1514](https://codeberg.org/uzu/strudel/pulls/1514)
+- 2025-09-15T23:36:31+02:00 Feat: Enhance `scale` function to quantize notes to a named scale by @glossing in: [#1492](https://codeberg.org/uzu/strudel/pulls/1492)
+- 2025-09-15T22:52:14+02:00 add tic80 font by @froos in: [#1579](https://codeberg.org/uzu/strudel/pulls/1579)
+- 2025-09-15T00:00:19+02:00 supradough: fix delay + add some jsdoc types by @froos in: [#1578](https://codeberg.org/uzu/strudel/pulls/1578)
+- 2025-09-14T12:42:58+02:00 added plyWith/plyWithClassic functions by @Dsm0 in: [#1571](https://codeberg.org/uzu/strudel/pulls/1571)
+- 2025-09-14T10:39:12+02:00 feat: sound alias by @Options in: [#1494](https://codeberg.org/uzu/strudel/pulls/1494)
+- 2025-09-14T01:20:15+02:00 Bug Fix: Allow penv values to be falsy by @glossing in: [#1559](https://codeberg.org/uzu/strudel/pulls/1559)
+- 2025-09-14T01:10:48+02:00 Update website/src/pages/workshop/first-sounds.mdx by @fesmith in: [#1566](https://codeberg.org/uzu/strudel/pulls/1566)
+- 2025-09-14T01:07:47+02:00 fix: autocomplete-styles + html rendering by @froos in: [#1570](https://codeberg.org/uzu/strudel/pulls/1570)
+- 2025-09-14T00:49:17+02:00 Feature: Synonyms in autocomplete and reference by @glossing in: [#1535](https://codeberg.org/uzu/strudel/pulls/1535)
+- 2025-09-13T20:01:48+02:00 Docs: add xen package examples by @dudymas in: [#1446](https://codeberg.org/uzu/strudel/pulls/1446)
+- 2025-09-13T19:51:10+02:00 Update website/src/pages/learn/code.mdx by @anecondev in: [#1391](https://codeberg.org/uzu/strudel/pulls/1391)
+- 2025-09-11T22:52:30+02:00 supradough poc by @froos in: [#1362](https://codeberg.org/uzu/strudel/pulls/1362)
+- 2025-09-11T17:29:35+02:00 fix: exclude mondough dependencies by @froos in: [#1563](https://codeberg.org/uzu/strudel/pulls/1563)
+- 2025-09-11T00:27:21+02:00 add basicSetup for keybindings by @Dsm0 in: [#1462](https://codeberg.org/uzu/strudel/pulls/1462)
+- 2025-09-10T17:25:26+02:00 feat: add delete user samples button for convenience by @daslyfe in: [#1556](https://codeberg.org/uzu/strudel/pulls/1556)
+- 2025-09-08T08:54:01+02:00 Fix formatting of REPL footnote by @ddbeck in: [#1547](https://codeberg.org/uzu/strudel/pulls/1547)
+- 2025-09-08T05:38:08+02:00 fix: OSC by @daslyfe in: [#1557](https://codeberg.org/uzu/strudel/pulls/1557)
+- 2025-09-07T22:47:13+02:00 Add examples for ? and | operators to documentation by james-@collapse in: [#1532](https://codeberg.org/uzu/strudel/pulls/1532)
+- 2025-09-07T21:48:19+02:00 Signal flow documentation by @glossing in: [#1504](https://codeberg.org/uzu/strudel/pulls/1504)
+- 2025-09-07T21:27:29+02:00 Allow flattening of dirs with sample server by @glossing in: [#1529](https://codeberg.org/uzu/strudel/pulls/1529)
+- 2025-09-07T21:05:24+02:00 fix: prevent orbit clicking by defining max number of channels in an orbit by @daslyfe in: [#1552](https://codeberg.org/uzu/strudel/pulls/1552)
+- 2025-09-02T08:42:42+02:00 Padding for tooltips, preserving linebreaks in descriptions, not closing autocompletes on click by @glossing in: [#1521](https://codeberg.org/uzu/strudel/pulls/1521)
+- 2025-09-01T17:50:11+02:00 Update "restore defaults" to not delete patterns by @glossing in: [#1519](https://codeberg.org/uzu/strudel/pulls/1519)
+
+## august 2025
+
+- 2025-08-31T19:10:04+02:00 feat: add the ability to control the speed and start time of the reverb IR by @daslyfe in: [#1501](https://codeberg.org/uzu/strudel/pulls/1501)
+- 2025-08-25T02:31:22+02:00 Bug Fix: Duck on Mobile / Safari by @glossing in: [#1503](https://codeberg.org/uzu/strudel/pulls/1503)
+- 2025-08-23T18:39:53+02:00 fix benchmarks by @yaxu in: [#1517](https://codeberg.org/uzu/strudel/pulls/1517)
+- 2025-08-21T20:57:31+02:00 add --json flag to @strudel/sampler to generate a strudel.json by @froos in: [#1513](https://codeberg.org/uzu/strudel/pulls/1513)
+- 2025-08-21T10:04:28+02:00 remove hs2js postinstall by @froos in: [#1510](https://codeberg.org/uzu/strudel/pulls/1510)
+- 2025-08-20T15:15:34+02:00 doc: scrub, duckorbit, duckattack, duckdepth + fix: arp, arpWidth, hush by @fyynn in: [#1502](https://codeberg.org/uzu/strudel/pulls/1502)
+- 2025-08-20T12:31:37+02:00 fix: repl autocomplete not rendering correctly by @robase in: [#1480](https://codeberg.org/uzu/strudel/pulls/1480)
+- 2025-08-20T12:10:02+02:00 BUG FIX: make 'midin' initialization work with multiple controllers by @cbabraham in: [#1469](https://codeberg.org/uzu/strudel/pulls/1469)
+- 2025-08-17T23:18:46+02:00 euclidish by @yaxu in: [#1482](https://codeberg.org/uzu/strudel/pulls/1482)
+- 2025-08-17T06:37:23+02:00 feat: Create a duck (sidechain) orbit effect by @daslyfe in: [#1470](https://codeberg.org/uzu/strudel/pulls/1470)
+- 2025-08-17T04:40:15+02:00 Bug Fix: Restore `log` functionality after onTrigger arg removal by @glossing in: [#1491](https://codeberg.org/uzu/strudel/pulls/1491)
+- 2025-08-17T04:33:39+02:00 Bug Fix: Handle zero appearing first in FM's ADSR by @glossing in: [#1496](https://codeberg.org/uzu/strudel/pulls/1496)
+- 2025-08-17T04:27:19+02:00 Bug Fix: FM (and vibrato) for Supersaws by @glossing in: [#1495](https://codeberg.org/uzu/strudel/pulls/1495)
+- 2025-08-04T07:51:31+02:00 Fix incorrect stack Mini Notation by @samyk in: [#1484](https://codeberg.org/uzu/strudel/pulls/1484)
+- 2025-08-03T18:57:45+02:00 fix: regression caused by incorrectly exported alias for transpose and scaleTranspose by @daslyfe in: [#1489](https://codeberg.org/uzu/strudel/pulls/1489)
+- 2025-08-03T18:34:13+02:00 Add `strans` and `scaleTrans` as synonyms of `scaleTranspose` by @TodePond in: [#1488](https://codeberg.org/uzu/strudel/pulls/1488)
+- 2025-08-02T18:18:28+02:00 Add `trans` alias for `transpose` by @TodePond in: [#1487](https://codeberg.org/uzu/strudel/pulls/1487)
+- 2025-08-02T03:27:26+02:00 feat: Create Accurate 909 bass drum synthesizer by @daslyfe in: [#1478](https://codeberg.org/uzu/strudel/pulls/1478)
+- 2025-08-02T03:23:49+02:00 feat: Add fmwave control and ability to fm with noise by @daslyfe in: [#1472](https://codeberg.org/uzu/strudel/pulls/1472)
+
+## july 2025
+
+- 2025-07-30T00:22:41+02:00 hotfix: uzu kit JSON should be in the repo to avoid offline loading errors by @daslyfe in: [#1485](https://codeberg.org/uzu/strudel/pulls/1485)
+- 2025-07-24T05:28:23+02:00 feat: make uzu-drumkit the default drumkit by @daslyfe in: [#1422](https://codeberg.org/uzu/strudel/pulls/1422)
+- 2025-07-21T07:16:19+02:00 FIX: PWM modulation by @daslyfe in: [#1467](https://codeberg.org/uzu/strudel/pulls/1467)
+- 2025-07-19T17:16:22+02:00 Tremolo modulation by @Ghost in: [#1095](https://codeberg.org/uzu/strudel/pulls/1095)
+- 2025-07-15T09:39:33+02:00 fix: can now use def in mondough by @froos in: [#1461](https://codeberg.org/uzu/strudel/pulls/1461)
+- 2025-07-11T11:10:24+02:00 fixed keybinding presedence issue by @Dsm0 in: [#1456](https://codeberg.org/uzu/strudel/pulls/1456)
+- 2025-07-11T11:09:05+02:00 added multicursor support on ctrl/cmd + click by @Dsm0 in: [#1457](https://codeberg.org/uzu/strudel/pulls/1457)
+- 2025-07-09T09:39:46+02:00 added tab-indent setting by @Dsm0 in: [#1454](https://codeberg.org/uzu/strudel/pulls/1454)
+- 2025-07-08T15:48:41+02:00 ontrigger-refactoring by @froos in: [#1442](https://codeberg.org/uzu/strudel/pulls/1442)
+- 2025-07-07T23:14:24+02:00 fix: remove first gm_synth_bass_1, as it doesn't work in safari by @froos in: [#1452](https://codeberg.org/uzu/strudel/pulls/1452)
+- 2025-07-07T00:06:16+02:00 fix: minor doc changes by @Aurel300 in: [#1435](https://codeberg.org/uzu/strudel/pulls/1435)
+- 2025-07-05T12:53:45+02:00 Fix randrun and deps including shuffle. Fixes #1441 by @yaxu in: [#1447](https://codeberg.org/uzu/strudel/pulls/1447)
+- 2025-07-05T12:15:52+02:00 add setDefault + resetDefaults to superdough by @froos in: [#1433](https://codeberg.org/uzu/strudel/pulls/1433)
+- 2025-07-04T22:36:19+02:00 disable fm for supersaw by @froos in: [#1443](https://codeberg.org/uzu/strudel/pulls/1443)
+
+## june 2025
+
+- 2025-06-30T13:28:10+02:00 corrected minor spelling mistake (#1425) by tj-@mueller in: [#1434](https://codeberg.org/uzu/strudel/pulls/1434)
+- 2025-06-30T05:26:11+02:00 Add browser cache explanation in samples.mdx by @Ghost in: [#1369](https://codeberg.org/uzu/strudel/pulls/1369)
+- 2025-06-30T05:18:42+02:00 add-release-notes by @froos in: [#1432](https://codeberg.org/uzu/strudel/pulls/1432)
+- 2025-06-28T20:30:49+02:00 feat: delaytime in cycles by @Ghost in: [#1341](https://codeberg.org/uzu/strudel/pulls/1341)
+- 2025-06-28T13:44:49+02:00 Case insensitive search in the reference tab by @kdiab in: [#1420](https://codeberg.org/uzu/strudel/pulls/1420)
+- 2025-06-28T00:04:13+02:00 add unjoin, into and chunkinto by @yaxu in: [#1418](https://codeberg.org/uzu/strudel/pulls/1418)
+- 2025-06-26T15:29:46+02:00 mondo notation by @froos in: [#1311](https://codeberg.org/uzu/strudel/pulls/1311)
+- 2025-06-24T06:14:15+02:00 allow calling `all` multiple times by @froos in: [#1417](https://codeberg.org/uzu/strudel/pulls/1417)
+- 2025-06-24T05:53:22+02:00 tonal: allow scales without tonic (default to C) by @Ghost in: [#1092](https://codeberg.org/uzu/strudel/pulls/1092)
+- 2025-06-20T20:00:12+02:00 website intro: fix whitespace and code hosting name by @trofi in: [#1400](https://codeberg.org/uzu/strudel/pulls/1400)
+- 2025-06-19T10:15:07+02:00 fix stepcat #1396 by @yaxu in: [#1398](https://codeberg.org/uzu/strudel/pulls/1398)
+- 2025-06-19T09:52:38+02:00 deprecate cpm + refactor docs to use setcpm by @froos in: [#1397](https://codeberg.org/uzu/strudel/pulls/1397)
+- 2025-06-19T02:49:09+02:00 feat: 3 new codemirror themes by @daslyfe in: [#1394](https://codeberg.org/uzu/strudel/pulls/1394)
+- 2025-06-14T16:23:00+02:00 link back to tech manual in wiki by @yaxu in: [#1382](https://codeberg.org/uzu/strudel/pulls/1382)
+- 2025-06-14T14:55:37+02:00 degithub-links by @froos in: [#1380](https://codeberg.org/uzu/strudel/pulls/1380)
+- 2025-06-13T15:24:42+02:00 fix euclidLegatoRot by @bwagner in: [#1378](https://codeberg.org/uzu/strudel/pulls/1378)
+- 2025-06-13T10:07:51+02:00 remove-ms by @yaxu in: [#1377](https://codeberg.org/uzu/strudel/pulls/1377)
+- 2025-06-12T10:25:48+02:00 Share fat URL by @Ghost in: [#1375](https://codeberg.org/uzu/strudel/pulls/1375)
+- 2025-06-05T20:39:16+02:00 feat: onTriggerTime for interfacing with random APIs and doing illegal things on event time by @Ghost in: [#1364](https://codeberg.org/uzu/strudel/pulls/1364)
+- 2025-06-05T01:08:18+02:00 feat: berlin noise by @Ghost in: [#1363](https://codeberg.org/uzu/strudel/pulls/1363)
+
+## may 2025
+
+- 2025-05-29T14:36:43+02:00 Fix bytebeat sample offset by @Ghost in: [#1359](https://codeberg.org/uzu/strudel/pulls/1359)
+- 2025-05-29T12:14:18+02:00 preserve stepcount in chunks by @yaxu in: [#1358](https://codeberg.org/uzu/strudel/pulls/1358)
+- 2025-05-29T02:34:30+02:00 Byte Beat improvements -> 2 by @Ghost in: [#1357](https://codeberg.org/uzu/strudel/pulls/1357)
+- 2025-05-28T16:40:32+02:00 Bytebeat improvements by @Ghost in: [#1356](https://codeberg.org/uzu/strudel/pulls/1356)
+- 2025-05-27T17:42:39+02:00 feat: Byte Beats! by @Ghost in: [#1354](https://codeberg.org/uzu/strudel/pulls/1354)
+- 2025-05-18T21:43:05+02:00 fix: typo on docs causing problems with autocompletion. by @Ghost in: [#1350](https://codeberg.org/uzu/strudel/pulls/1350)
+- 2025-05-15T01:12:07+02:00 feat: Create pulsewidth (pw) and pulsewidth lfo parameters by @Ghost in: [#1343](https://codeberg.org/uzu/strudel/pulls/1343)
+- 2025-05-14T18:25:17+02:00 feat: Multi Channel Orbits by @Ghost in: [#1344](https://codeberg.org/uzu/strudel/pulls/1344)
+- 2025-05-13T17:45:29+02:00 Fix typo by @Ghost in: [#1346](https://codeberg.org/uzu/strudel/pulls/1346)
+- 2025-05-02T00:01:27+02:00 Fix web + repl package builds by @froos in: [#1339](https://codeberg.org/uzu/strudel/pulls/1339)
+- 2025-05-01T23:50:57+02:00 fix: superdough worklets bundling by @froos in: [#1338](https://codeberg.org/uzu/strudel/pulls/1338)
+
+## april 2025
+
+- 2025-04-27T22:25:54+02:00 FIX: sound import order by @Ghost in: [#1333](https://codeberg.org/uzu/strudel/pulls/1333)
+- 2025-04-27T18:39:49+02:00 update docs to reflect import sounds tab change by @hpunq in: [#1332](https://codeberg.org/uzu/strudel/pulls/1332)
+- 2025-04-27T18:38:26+02:00 feat: Improve gain curve by @Ghost in: [#1318](https://codeberg.org/uzu/strudel/pulls/1318)
+- 2025-04-27T07:54:49+02:00 Add Icon to import sample button by @Ghost in: [#1331](https://codeberg.org/uzu/strudel/pulls/1331)
+- 2025-04-27T07:53:41+02:00 Add new "import-sounds" tab with explanation on folder import by @hpunq in: [#1329](https://codeberg.org/uzu/strudel/pulls/1329)
+- 2025-04-22T00:04:38+02:00 feat: new themes + theme improvements by @Ghost in: [#1326](https://codeberg.org/uzu/strudel/pulls/1326)
+- 2025-04-20T18:24:15+02:00 feat: Create scrub function for scrubbing an audio file by @Ghost in: [#1321](https://codeberg.org/uzu/strudel/pulls/1321)
+- 2025-04-18T07:17:39+02:00 fix: disable astro toolbar by default by @Ghost in: [#1324](https://codeberg.org/uzu/strudel/pulls/1324)
+- 2025-04-18T07:17:38+02:00 fix: udels header by @Ghost in: [#1325](https://codeberg.org/uzu/strudel/pulls/1325)
+- 2025-04-11T09:18:58+02:00 Send delta in OSC message in seconds, to match tidal/superdirt by @yaxu in: [#1323](https://codeberg.org/uzu/strudel/pulls/1323)
+- 2025-04-08T06:08:57+02:00 FIX: Multichannel Audio by @Ghost in: [#1322](https://codeberg.org/uzu/strudel/pulls/1322)
+- 2025-04-08T04:18:03+02:00 feat: add max polyphony feature for superdough by @Ghost in: [#1317](https://codeberg.org/uzu/strudel/pulls/1317)
+- 2025-04-05T21:12:05+02:00 enhancement: make error messages easier to read by @Ghost in: [#1315](https://codeberg.org/uzu/strudel/pulls/1315)
+- 2025-04-04T06:26:57+02:00 fix: replace empty spaces in registered sound keys by @Ghost in: [#1319](https://codeberg.org/uzu/strudel/pulls/1319)
+
+## march 2025
+
+- 2025-03-26T17:01:05+01:00 small feat: Add alias for segment and ribbon by @Ghost in: [#1314](https://codeberg.org/uzu/strudel/pulls/1314)
+- 2025-03-26T14:54:18+01:00 Fix typo pattnr by @Ghost in: [#1316](https://codeberg.org/uzu/strudel/pulls/1316)
+- 2025-03-23T20:05:31+01:00 bugfix: Allow single param to be used in the as function by @Ghost in: [#1312](https://codeberg.org/uzu/strudel/pulls/1312)
+- 2025-03-20T23:35:18+01:00 Add MIDI Program Change, SysEx, NRPN, PitchBend and AfterTouch Output by @nkymut in: [#1244](https://codeberg.org/uzu/strudel/pulls/1244)
+- 2025-03-19T17:32:30+01:00 make soundfont base url configurable by @Ghost in: [#1040](https://codeberg.org/uzu/strudel/pulls/1040)
+- 2025-03-18T20:07:08+01:00 Add num samples from 0 up to 20 by @yaxu in: [#1310](https://codeberg.org/uzu/strudel/pulls/1310)
+- 2025-03-17T10:38:54+01:00 add num samples (edited numbers) by @yaxu in: [#1309](https://codeberg.org/uzu/strudel/pulls/1309)
+- 2025-03-08T22:17:47+01:00 @strudel/sampler improvements by @froos in: [#1288](https://codeberg.org/uzu/strudel/pulls/1288)
+- 2025-03-08T21:56:50+01:00 Add Gamepad module by @nkymut in: [#1223](https://codeberg.org/uzu/strudel/pulls/1223)
+- 2025-03-04T19:17:37+01:00 change behaviour of polymeter, and remove polymeterSteps by @yaxu in: [#1302](https://codeberg.org/uzu/strudel/pulls/1302)
+- 2025-03-04T03:36:57+01:00 feat: Create Pulse Oscillator with variable PWM by @Ghost in: [#1304](https://codeberg.org/uzu/strudel/pulls/1304)
+- 2025-03-02T17:08:31+01:00 feat: Theme improvements by @Ghost in: [#1295](https://codeberg.org/uzu/strudel/pulls/1295)
+- 2025-03-02T11:58:46+01:00 bugfix zoom stepcount by @yaxu in: [#1301](https://codeberg.org/uzu/strudel/pulls/1301)
+
+## february 2025
+
+- 2025-02-28T16:01:20+01:00 Fix test error #1297 by @nkymut in: [#1298](https://codeberg.org/uzu/strudel/pulls/1298)
+- 2025-02-28T02:53:42+01:00 Hotfix: prevent undefined pattern code from crashing strudel on load by @Ghost in: [#1297](https://codeberg.org/uzu/strudel/pulls/1297)
+- 2025-02-27T08:24:58+01:00 Fix misplaced ending sentence by @Ghost in: [#1296](https://codeberg.org/uzu/strudel/pulls/1296)
+- 2025-02-23T10:54:13+01:00 Signpost licenses for source code and samples a bit more, ref #1277 by @yaxu in: [#1289](https://codeberg.org/uzu/strudel/pulls/1289)
+- 2025-02-23T10:52:27+01:00 Allow wchooseCycles probabilities to be patterned by @yaxu in: [#1292](https://codeberg.org/uzu/strudel/pulls/1292)
+- 2025-02-22T02:18:59+01:00 Create Pattern Page Pagination by @Ghost in: [#1287](https://codeberg.org/uzu/strudel/pulls/1287)
+- 2025-02-21T22:38:10+01:00 showcase tweaks by @yaxu in: [#1291](https://codeberg.org/uzu/strudel/pulls/1291)
+- 2025-02-11T16:34:24+01:00 Fixes inverted triangle wave by renaming it to `itri`, making non-inverted `tri` by @yaxu in: [#1283](https://codeberg.org/uzu/strudel/pulls/1283)
+- 2025-02-11T10:02:34+01:00 Fix `squeezejoin` and functions using it, including `bite` by @yaxu in: [#1286](https://codeberg.org/uzu/strudel/pulls/1286)
+- 2025-02-09T16:18:28+01:00 Rename repeat back to extend by @yaxu in: [#1285](https://codeberg.org/uzu/strudel/pulls/1285)
+- 2025-02-07T18:11:52+01:00 mqtt bugfix - connection check by @yaxu in: [#1282](https://codeberg.org/uzu/strudel/pulls/1282)
+- 2025-02-07T17:38:29+01:00 Bugfix: update mqtt connections dictionary by @yaxu in: [#1281](https://codeberg.org/uzu/strudel/pulls/1281)
+- 2025-02-07T11:39:54+01:00 make mqtt topic patternable by @yaxu in: [#1280](https://codeberg.org/uzu/strudel/pulls/1280)
+- 2025-02-07T11:07:32+01:00 midimaps by @froos in: [#1274](https://codeberg.org/uzu/strudel/pulls/1274)
+- 2025-02-06T15:59:03+01:00 MQTT - support adding hap duration and cps metadata to JSON messages by @yaxu in: [#1279](https://codeberg.org/uzu/strudel/pulls/1279)
+- 2025-02-05T16:10:54+01:00 [breaking change] Sample signals from query onset, rather than midpoint by @yaxu in: [#1278](https://codeberg.org/uzu/strudel/pulls/1278)
+- 2025-02-03T00:55:36+01:00 Stepwise documentation tweaks, with mridangam samples by @yaxu in: [#1275](https://codeberg.org/uzu/strudel/pulls/1275)
+- 2025-02-02T21:26:44+01:00 Polish, rename, and document stepwise functions by @yaxu in: [#1262](https://codeberg.org/uzu/strudel/pulls/1262)
+- 2025-02-01T22:57:00+01:00 Fix sf2 timing by @froos in: [#1272](https://codeberg.org/uzu/strudel/pulls/1272)
+- 2025-02-01T22:32:59+01:00 Add bank aliasing and case insensitivity by @TodePond in: [#1245](https://codeberg.org/uzu/strudel/pulls/1245)
+
+## january 2025
+
+- 2025-01-31T09:39:54+01:00 Add Device Motion module by @nkymut in: [#1217](https://codeberg.org/uzu/strudel/pulls/1217)
+- 2025-01-29T15:40:01+01:00 add "as" function + getControlName by @froos in: [#1247](https://codeberg.org/uzu/strudel/pulls/1247)
+- 2025-01-29T15:30:04+01:00 Theme glowup by @froos in: [#1268](https://codeberg.org/uzu/strudel/pulls/1268)
+- 2025-01-29T15:22:18+01:00 patchday by @froos in: [#1264](https://codeberg.org/uzu/strudel/pulls/1264)
+- 2025-01-28T00:00:03+01:00 Revert "Fix sometimes" by @yaxu in: [#1267](https://codeberg.org/uzu/strudel/pulls/1267)
+- 2025-01-24T15:32:48+01:00 add reference package by @froos in: [#1252](https://codeberg.org/uzu/strudel/pulls/1252)
+- 2025-01-24T15:16:55+01:00 support `all(pianoroll)` and `all(pianoroll({labels: true}))` by @yaxu in: [#1234](https://codeberg.org/uzu/strudel/pulls/1234)
+- 2025-01-24T12:13:49+01:00 MQTT - if password isn't provided, prompt for one by @yaxu in: [#1249](https://codeberg.org/uzu/strudel/pulls/1249)
+- 2025-01-21T06:24:03+01:00 fix docs for beat function by @Ghost in: [#1248](https://codeberg.org/uzu/strudel/pulls/1248)
+- 2025-01-18T23:27:51+01:00 understand voicings page by @froos in: [#1230](https://codeberg.org/uzu/strudel/pulls/1230)
+- 2025-01-17T19:27:00+01:00 Add binary and binaryN by @Ghost in: [#1226](https://codeberg.org/uzu/strudel/pulls/1226)
+- 2025-01-16T12:18:33+01:00 Fix sometimes by @yaxu in: [#1243](https://codeberg.org/uzu/strudel/pulls/1243)
+- 2025-01-16T05:55:46+01:00 "beat" function for "step sequencer" style rhythm notation by @Ghost in: [#1237](https://codeberg.org/uzu/strudel/pulls/1237)
+- 2025-01-14T14:39:15+01:00 Add stepBind, and some toplevel aliases for binds and withValue by @yaxu in: [#1241](https://codeberg.org/uzu/strudel/pulls/1241)
+- 2025-01-14T06:11:10+01:00 Add onKey function for custom key commands for patterns by @Ghost in: [#1235](https://codeberg.org/uzu/strudel/pulls/1235)
+- 2025-01-12T11:32:24+01:00 Update documentation for param value modification by @Ghost in: [#1238](https://codeberg.org/uzu/strudel/pulls/1238)
+
+## december 2024
+
+- 2024-12-29T13:16:08+01:00 Documentation for all/each, and bugfix for each by @yaxu in: [#1233](https://codeberg.org/uzu/strudel/pulls/1233)
+- 2024-12-29T11:29:52+01:00 Make `all()` post-stack again, and add `each()` for pre-stack by @yaxu in: [#1229](https://codeberg.org/uzu/strudel/pulls/1229)
+- 2024-12-27T22:16:54+01:00 suggested changes to voicings.mdx by @Ghost in: [#1231](https://codeberg.org/uzu/strudel/pulls/1231)
+- 2024-12-23T11:53:11+01:00 add basic spectrum function by @froos in: [#1213](https://codeberg.org/uzu/strudel/pulls/1213)
+- 2024-12-22T21:04:45+01:00 Fix regression for d1, p1, p(n) by @yaxu in: [#1227](https://codeberg.org/uzu/strudel/pulls/1227)
+
+## november 2024
+
+- 2024-11-30T10:09:36+01:00 Make cps patternable by @eefano in: [#1001](https://codeberg.org/uzu/strudel/pulls/1001)
+- 2024-11-30T10:07:56+01:00 MQTT support by @yaxu in: [#1224](https://codeberg.org/uzu/strudel/pulls/1224)
+- 2024-11-30T09:46:14+01:00 Apply `all` function to individual patterns rather than final stack by @yaxu in: [#1209](https://codeberg.org/uzu/strudel/pulls/1209)
+- 2024-11-20T16:32:51+01:00 REPL: solo and sync configuration by @Ghost in: [#1214](https://codeberg.org/uzu/strudel/pulls/1214)
+
+## october 2024
+
+- 2024-10-30T21:29:43+01:00 Add s_zip for 'cat'-ing patterns together step-by-step, bugfix `steps` by @yaxu in: [#1208](https://codeberg.org/uzu/strudel/pulls/1208)
+- 2024-10-30T21:28:32+01:00 Preserve tactus for 'degrade' and friends, and tidy up 'pick' and friends by @yaxu in: [#1205](https://codeberg.org/uzu/strudel/pulls/1205)
+- 2024-10-30T17:11:53+01:00 chore: Edit run locally instructions in README.md by @Ghost in: [#1206](https://codeberg.org/uzu/strudel/pulls/1206)
+- 2024-10-23T23:08:14+02:00 colorize console + tweak header by @froos in: [#1203](https://codeberg.org/uzu/strudel/pulls/1203)
+- 2024-10-22T22:55:00+02:00 markcss by @froos in: [#1202](https://codeberg.org/uzu/strudel/pulls/1202)
+- 2024-10-21T22:56:37+02:00 add 2 new ui settings by @froos in: [#1200](https://codeberg.org/uzu/strudel/pulls/1200)
+- 2024-10-21T20:22:52+02:00 Make panel hover behavior optional by @Ghost in: [#1199](https://codeberg.org/uzu/strudel/pulls/1199)
+- 2024-10-19T04:28:36+02:00 Menu Panel Improvements! by @Ghost in: [#1193](https://codeberg.org/uzu/strudel/pulls/1193)
+- 2024-10-19T01:10:06+02:00 update lockfile + minor versions by @froos in: [#1198](https://codeberg.org/uzu/strudel/pulls/1198)
+
+## september 2024
+
+- 2024-09-27T00:17:00+02:00 remove redundant example for cat, update snapshot by @Ghost in: [#1189](https://codeberg.org/uzu/strudel/pulls/1189)
+- 2024-09-25T13:18:19+02:00 Adding search bar (soundtab.jsx) by @Ghost in: [#1185](https://codeberg.org/uzu/strudel/pulls/1185)
+- 2024-09-23T17:18:34+02:00 Screenreader improvements by @yaxu in: [#1158](https://codeberg.org/uzu/strudel/pulls/1158)
+- 2024-09-20T22:26:41+02:00 Fix serial timing by @yaxu in: [#1188](https://codeberg.org/uzu/strudel/pulls/1188)
+- 2024-09-20T00:26:30+02:00 Add bite function by @yaxu in: [#1187](https://codeberg.org/uzu/strudel/pulls/1187)
+- 2024-09-14T13:30:53+02:00 better spacing in zen mode by @froos in: [#1147](https://codeberg.org/uzu/strudel/pulls/1147)
+- 2024-09-14T10:50:24+02:00 add filter + filterWhen + within by @froos in: [#1039](https://codeberg.org/uzu/strudel/pulls/1039)
+- 2024-09-14T10:49:21+02:00 refactor sampler by @froos in: [#1101](https://codeberg.org/uzu/strudel/pulls/1101)
+- 2024-09-14T10:43:17+02:00 handle midin device not found error by @froos in: [#1146](https://codeberg.org/uzu/strudel/pulls/1146)
+- 2024-09-13T21:59:23+02:00 Add a search bar to the REPL Reference tab by @Ghost in: [#1165](https://codeberg.org/uzu/strudel/pulls/1165)
+- 2024-09-13T21:58:47+02:00 Correct spelling mistakes by @Ghost in: [#1183](https://codeberg.org/uzu/strudel/pulls/1183)
+- 2024-09-07T23:41:29+02:00 Add seqPLoop from Tidal by @yaxu in: [#1182](https://codeberg.org/uzu/strudel/pulls/1182)
+- 2024-09-05T05:52:50+02:00 make phaser control match superdirt by @Ghost in: [#1180](https://codeberg.org/uzu/strudel/pulls/1180)
+- 2024-09-05T05:33:41+02:00 Revert "Make phaser control consistent with superdirt" by @Ghost in: [#1179](https://codeberg.org/uzu/strudel/pulls/1179)
+- 2024-09-05T05:29:12+02:00 Make phaser control consistent with superdirt by @Ghost in: [#1178](https://codeberg.org/uzu/strudel/pulls/1178)
+- 2024-09-03T04:37:15+02:00 fix sample speed when using splice and fit with superdirt by @Ghost in: [#1172](https://codeberg.org/uzu/strudel/pulls/1172)
+- 2024-09-01T16:02:24+02:00 Create audio target selector for OSC/Superdirt by @Ghost in: [#1160](https://codeberg.org/uzu/strudel/pulls/1160)
+- 2024-09-01T15:03:47+02:00 Fixes fit so it works after a chop or slice by @yaxu in: [#1171](https://codeberg.org/uzu/strudel/pulls/1171)
+
+## august 2024
+
+- 2024-08-30T14:24:08+02:00 polyJoin by @yaxu in: [#1168](https://codeberg.org/uzu/strudel/pulls/1168)
+- 2024-08-23T17:05:10+02:00 Add scramble and shuffle by @yaxu in: [#1167](https://codeberg.org/uzu/strudel/pulls/1167)
+- 2024-08-18T18:22:20+02:00 Improve + simplify neocyclist timing by @Ghost in: [#1164](https://codeberg.org/uzu/strudel/pulls/1164)
+- 2024-08-15T05:18:33+02:00 containerize/seperate out boolean checks for repl types/Repl logic into bespoke components. by @Ghost in: [#1163](https://codeberg.org/uzu/strudel/pulls/1163)
+- 2024-08-12T18:57:21+02:00 [CORS HOTFIX] by @Ghost in: [#1162](https://codeberg.org/uzu/strudel/pulls/1162)
+- 2024-08-09T05:11:28+02:00 Fix OSC clock jitter by @Ghost in: [#1157](https://codeberg.org/uzu/strudel/pulls/1157)
+
+## july 2024
+
+- 2024-07-27T11:02:38+02:00 Fix loopAt tactus by @yaxu in: [#1145](https://codeberg.org/uzu/strudel/pulls/1145)
+- 2024-07-26T04:37:34+02:00 "stretch" function (phase vocoder) by @Ghost in: [#1130](https://codeberg.org/uzu/strudel/pulls/1130)
+- 2024-07-26T04:37:17+02:00 Udels (MultiFrame Strudel) Revisited by @Ghost in: [#1132](https://codeberg.org/uzu/strudel/pulls/1132)
+- 2024-07-24T11:40:28+02:00 Fix tactus marking in mininotation by @yaxu in: [#1144](https://codeberg.org/uzu/strudel/pulls/1144)
+
+## june 2024
+
+- 2024-06-25T18:13:28+02:00 export comment commands by @froos in: [#1136](https://codeberg.org/uzu/strudel/pulls/1136)
+- 2024-06-24T18:19:22+02:00 Chop chop by @yaxu in: [#1078](https://codeberg.org/uzu/strudel/pulls/1078)
+- 2024-06-18T23:58:08+02:00 Fix bug in Fraction.lcm by @yaxu in: [#1133](https://codeberg.org/uzu/strudel/pulls/1133)
+- 2024-06-18T05:32:12+02:00 Fix clock worker dependency path in module builds by @Ghost in: [#1129](https://codeberg.org/uzu/strudel/pulls/1129)
+- 2024-06-04T00:26:48+02:00 Labeled statements doc by @froos in: [#1126](https://codeberg.org/uzu/strudel/pulls/1126)
+- 2024-06-03T22:40:21+02:00 doc: visual functions + refactor onPaint by @froos in: [#1125](https://codeberg.org/uzu/strudel/pulls/1125)
+- 2024-06-02T18:36:29+02:00 Fix indexDB failing with large amount of files by @Ghost in: [#1124](https://codeberg.org/uzu/strudel/pulls/1124)
+
+## may 2024
+
+- 2024-05-31T12:17:34+02:00 Migrate tutorial fanchor by @froos in: [#1122](https://codeberg.org/uzu/strudel/pulls/1122)
+- 2024-05-31T10:46:47+02:00 [BUG FIX] Audio worklets sometimes dont load by @Ghost in: [#1121](https://codeberg.org/uzu/strudel/pulls/1121)
+- 2024-05-31T10:25:24+02:00 change fanchor to 0 by @Ghost in: [#1107](https://codeberg.org/uzu/strudel/pulls/1107)
+- 2024-05-30T14:39:30+02:00 fix: use full repl in web package by @froos in: [#1119](https://codeberg.org/uzu/strudel/pulls/1119)
+- 2024-05-30T09:36:32+02:00 can now access strudelMirror from repl by @froos in: [#1117](https://codeberg.org/uzu/strudel/pulls/1117)
+- 2024-05-29T13:06:34+02:00 Add the mousex and mousey signal by @Ghost in: [#1112](https://codeberg.org/uzu/strudel/pulls/1112)
+- 2024-05-29T13:02:00+02:00 Fixes drawPianoroll import in codemirror example by @Ghost in: [#1116](https://codeberg.org/uzu/strudel/pulls/1116)
+- 2024-05-28T22:43:46+02:00 clarify `off` in pattern-effects.mdx by @Ghost in: [#1074](https://codeberg.org/uzu/strudel/pulls/1074)
+- 2024-05-26T17:33:07+02:00 rollback phaser by @Ghost in: [#1113](https://codeberg.org/uzu/strudel/pulls/1113)
+- 2024-05-26T17:33:06+02:00 Fix audio worklets by @Ghost in: [#1114](https://codeberg.org/uzu/strudel/pulls/1114)
+- 2024-05-26T16:53:56+02:00 Calculate phaser modulation phase based on time by @Ghost in: [#1110](https://codeberg.org/uzu/strudel/pulls/1110)
+- 2024-05-23T15:06:01+02:00 Add analog-style ladder filter by @Ghost in: [#1103](https://codeberg.org/uzu/strudel/pulls/1103)
+- 2024-05-22T12:53:20+02:00 fix sampler on windows by @Ghost in: [#1109](https://codeberg.org/uzu/strudel/pulls/1109)
+- 2024-05-20T23:26:20+02:00 web package fixes by @froos in: [#1044](https://codeberg.org/uzu/strudel/pulls/1044)
+- 2024-05-20T22:41:28+02:00 Fix sampler windows by @froos in: [#1108](https://codeberg.org/uzu/strudel/pulls/1108)
+- 2024-05-19T12:07:26+02:00 hs2js package / tidal parser by @froos in: [#870](https://codeberg.org/uzu/strudel/pulls/870)
+- 2024-05-18T10:49:08+02:00 fix little dub tune example by @Ghost in: [#1104](https://codeberg.org/uzu/strudel/pulls/1104)
+- 2024-05-17T14:43:58+02:00 Samples tab improvements by @Ghost in: [#1102](https://codeberg.org/uzu/strudel/pulls/1102)
+- 2024-05-13T10:10:34+02:00 osc: couple of fixes by @Ghost in: [#1093](https://codeberg.org/uzu/strudel/pulls/1093)
+- 2024-05-13T06:31:20+02:00 Use sessionStorage for viewingPatternData and activePattern by @Ghost in: [#1091](https://codeberg.org/uzu/strudel/pulls/1091)
+- 2024-05-12T18:23:09+02:00 repl: set document.title from @title by @Ghost in: [#1090](https://codeberg.org/uzu/strudel/pulls/1090)
+- 2024-05-07T14:29:22+02:00 fix: missing events due to premature worklet cleanup by @froos in: [#1089](https://codeberg.org/uzu/strudel/pulls/1089)
+- 2024-05-03T11:52:57+02:00 Benchmarks by @yaxu in: [#1079](https://codeberg.org/uzu/strudel/pulls/1079)
+- 2024-05-03T08:46:52+02:00 fix: csound + dough timing by @froos in: [#1086](https://codeberg.org/uzu/strudel/pulls/1086)
+- 2024-05-03T00:37:32+02:00 Improve performance of ! (replicate) by @yaxu in: [#1084](https://codeberg.org/uzu/strudel/pulls/1084)
+- 2024-05-02T23:40:22+02:00 fix: url parsing with extra params by @froos in: [#1083](https://codeberg.org/uzu/strudel/pulls/1083)
+
+## april 2024
+
+- 2024-04-29T12:36:11+02:00 Tactus calculation toggle and breaking change to tactus calculation in fast/slow/hurry by @yaxu in: [#1081](https://codeberg.org/uzu/strudel/pulls/1081)
+- 2024-04-28T20:56:21+02:00 fix docs on alignment.mdx by @Ghost in: [#1076](https://codeberg.org/uzu/strudel/pulls/1076)
+- 2024-04-28T20:49:18+02:00 add signals to recap in first-effects.mdx by @Ghost in: [#1073](https://codeberg.org/uzu/strudel/pulls/1073)
+- 2024-04-28T20:44:57+02:00 fix translation issue in first-effects.mdx by @Ghost in: [#1072](https://codeberg.org/uzu/strudel/pulls/1072)
+- 2024-04-28T20:44:29+02:00 fix failing format test by @Ghost in: [#1077](https://codeberg.org/uzu/strudel/pulls/1077)
+- 2024-04-27T22:50:17+02:00 add nesting to `off` example variation in pattern-effects.mdx by @Ghost in: [#1075](https://codeberg.org/uzu/strudel/pulls/1075)
+- 2024-04-27T22:48:58+02:00 add `<...>` to first-sounds.mdx recap by @Ghost in: [#1070](https://codeberg.org/uzu/strudel/pulls/1070)
+- 2024-04-27T22:48:07+02:00 fix first sounds typo by @Ghost in: [#1069](https://codeberg.org/uzu/strudel/pulls/1069)
+- 2024-04-27T22:47:44+02:00 fix cr typo on first-sounds.mdx by @Ghost in: [#1068](https://codeberg.org/uzu/strudel/pulls/1068)
+- 2024-04-26T15:12:30+02:00 More tactus tidying by @yaxu in: [#1071](https://codeberg.org/uzu/strudel/pulls/1071)
+- 2024-04-23T23:37:21+02:00 Fix stepjoin by @yaxu in: [#1067](https://codeberg.org/uzu/strudel/pulls/1067)
+- 2024-04-23T23:32:00+02:00 clarify license by @yaxu in: [#1064](https://codeberg.org/uzu/strudel/pulls/1064)
+- 2024-04-23T15:14:30+02:00 Tactus tweaks - fixes for maintaining tactus and highlight locations by @yaxu in: [#1065](https://codeberg.org/uzu/strudel/pulls/1065)
+- 2024-04-21T23:57:57+02:00 fix OSC timing for recent scheduler updates by @Ghost in: [#1062](https://codeberg.org/uzu/strudel/pulls/1062)
+- 2024-04-21T22:17:07+02:00 Stepwise functions from Tidal by @yaxu in: [#1060](https://codeberg.org/uzu/strudel/pulls/1060)
+- 2024-04-21T11:03:55+02:00 Fix wchooseCycles not picking the whole pattern by @Ghost in: [#1061](https://codeberg.org/uzu/strudel/pulls/1061)
+- 2024-04-19T00:05:52+02:00 add swing + swingBy by @froos in: [#1038](https://codeberg.org/uzu/strudel/pulls/1038)
+- 2024-04-19T00:05:08+02:00 anonymous patterns + muting by @froos in: [#1059](https://codeberg.org/uzu/strudel/pulls/1059)
+- 2024-04-12T12:34:27+02:00 improve tutorial + custom samples doc by @froos in: [#1053](https://codeberg.org/uzu/strudel/pulls/1053)
+- 2024-04-12T12:31:49+02:00 fix: do not reset cc input values on each eval by @froos in: [#1054](https://codeberg.org/uzu/strudel/pulls/1054)
+- 2024-04-08T17:25:27+02:00 transpose: support all combinations of numbers and strings for notes and intervals by @froos in: [#1048](https://codeberg.org/uzu/strudel/pulls/1048)
+- 2024-04-08T10:46:18+02:00 Wax, wane, taper and taperlist by @yaxu in: [#1042](https://codeberg.org/uzu/strudel/pulls/1042)
+- 2024-04-06T23:44:57+02:00 Midi Time hotfix for scheduler updates by @Ghost in: [#1047](https://codeberg.org/uzu/strudel/pulls/1047)
+- 2024-04-05T12:48:03+02:00 pitchwheel visual by @froos in: [#1041](https://codeberg.org/uzu/strudel/pulls/1041)
+- 2024-04-05T12:47:19+02:00 fix cyclist fizzling out by @froos in: [#1046](https://codeberg.org/uzu/strudel/pulls/1046)
+
+## march 2024
+
+- 2024-03-30T16:05:59+01:00 remove dangerous arithmetic feature by @froos in: [#1030](https://codeberg.org/uzu/strudel/pulls/1030)
+- 2024-03-30T16:05:27+01:00 Fix sampler paths by @froos in: [#1034](https://codeberg.org/uzu/strudel/pulls/1034)
+- 2024-03-30T14:43:08+01:00 local sample server cli by @froos in: [#1033](https://codeberg.org/uzu/strudel/pulls/1033)
+- 2024-03-29T17:14:28+01:00 add font file types to offline cache by @froos in: [#1032](https://codeberg.org/uzu/strudel/pulls/1032)
+- 2024-03-29T17:10:16+01:00 add closeBrackets setting by @froos in: [#1031](https://codeberg.org/uzu/strudel/pulls/1031)
+- 2024-03-29T14:55:06+01:00 Tactus tidy by @yaxu in: [#1027](https://codeberg.org/uzu/strudel/pulls/1027)
+- 2024-03-28T17:06:44+01:00 add setting for sync flag by @froos in: [#1025](https://codeberg.org/uzu/strudel/pulls/1025)
+- 2024-03-28T11:37:57+01:00 better theme integration for visuals + various fixes by @froos in: [#1024](https://codeberg.org/uzu/strudel/pulls/1024)
+- 2024-03-28T11:33:25+01:00 More fonts by @froos in: [#1023](https://codeberg.org/uzu/strudel/pulls/1023)
+- 2024-03-27T13:06:05+01:00 Feature: tactus marking by @yaxu in: [#1021](https://codeberg.org/uzu/strudel/pulls/1021)
+- 2024-03-25T06:02:02+01:00 hotfix for 1017 by @Ghost in: [#1020](https://codeberg.org/uzu/strudel/pulls/1020)
+- 2024-03-24T15:06:33+01:00 Document signals by @Ghost in: [#1015](https://codeberg.org/uzu/strudel/pulls/1015)
+- 2024-03-24T10:16:11+01:00 Repl sync fixes by @Ghost in: [#1014](https://codeberg.org/uzu/strudel/pulls/1014)
+- 2024-03-23T20:21:51+01:00 eliminate chromium clock jitter by @froos in: [#1004](https://codeberg.org/uzu/strudel/pulls/1004)
+- 2024-03-23T15:51:07+01:00 update undocumented script by @froos in: [#1013](https://codeberg.org/uzu/strudel/pulls/1013)
+- 2024-03-23T15:26:52+01:00 fix: await injectPatternMethods by @froos in: [#1012](https://codeberg.org/uzu/strudel/pulls/1012)
+- 2024-03-23T15:18:12+01:00 rename trig -> reset, trigzero -> restart by @froos in: [#1010](https://codeberg.org/uzu/strudel/pulls/1010)
+- 2024-03-23T12:30:03+01:00 Inline punchcard + spiral by @froos in: [#1008](https://codeberg.org/uzu/strudel/pulls/1008)
+- 2024-03-23T12:27:22+01:00 Color in hap value by @froos in: [#1007](https://codeberg.org/uzu/strudel/pulls/1007)
+- 2024-03-22T23:53:51+01:00 using strudel in your project guide + cleanup examples by @froos in: [#1006](https://codeberg.org/uzu/strudel/pulls/1006)
+- 2024-03-22T23:41:50+01:00 accidentals in scale degrees by @eefano in: [#1000](https://codeberg.org/uzu/strudel/pulls/1000)
+- 2024-03-21T22:53:55+01:00 supersaw oscillator by @Ghost in: [#978](https://codeberg.org/uzu/strudel/pulls/978)
+- 2024-03-21T13:00:04+01:00 remove canvas, externalize samples, delete junk by @froos in: [#1003](https://codeberg.org/uzu/strudel/pulls/1003)
+- 2024-03-18T11:37:55+01:00 Fix pure mini highlight by @yaxu in: [#994](https://codeberg.org/uzu/strudel/pulls/994)
+- 2024-03-18T07:12:14+01:00 inline viz / widgets package by @froos in: [#989](https://codeberg.org/uzu/strudel/pulls/989)
+- 2024-03-17T04:07:00+01:00 Labeled statements by @froos in: [#991](https://codeberg.org/uzu/strudel/pulls/991)
+- 2024-03-16T18:24:38+01:00 Beat-oriented functionality by @yaxu in: [#976](https://codeberg.org/uzu/strudel/pulls/976)
+- 2024-03-15T01:47:35+01:00 REPL sync between windows by @Ghost in: [#900](https://codeberg.org/uzu/strudel/pulls/900)
+- 2024-03-14T00:20:07+01:00 Update synths.mdx by @Ghost in: [#984](https://codeberg.org/uzu/strudel/pulls/984)
+- 2024-03-13T00:36:22+01:00 fix: share now shares what's visible instead of active by @froos in: [#985](https://codeberg.org/uzu/strudel/pulls/985)
+- 2024-03-10T01:18:57+01:00 use ireal as default voicing dict by @froos in: [#967](https://codeberg.org/uzu/strudel/pulls/967)
+- 2024-03-10T00:50:23+01:00 little fix for withVal by @eefano in: [#980](https://codeberg.org/uzu/strudel/pulls/980)
+- 2024-03-10T00:46:51+01:00 Velocity in value by @froos in: [#974](https://codeberg.org/uzu/strudel/pulls/974)
+- 2024-03-10T00:42:50+01:00 fix: clear hydra on reset by @froos in: [#983](https://codeberg.org/uzu/strudel/pulls/983)
+- 2024-03-10T00:38:43+01:00 move canvas related helpers from core to new draw package by @froos in: [#971](https://codeberg.org/uzu/strudel/pulls/971)
+- 2024-03-08T05:38:07+01:00 replace shape with distort in learn doc by @Ghost in: [#982](https://codeberg.org/uzu/strudel/pulls/982)
+- 2024-03-06T12:14:49+01:00 alias - for ~ by @yaxu in: [#981](https://codeberg.org/uzu/strudel/pulls/981)
+- 2024-03-04T16:04:23+01:00 Worklet Improvents / fixes by @Ghost in: [#963](https://codeberg.org/uzu/strudel/pulls/963)
+- 2024-03-01T17:30:19+01:00 Nested controls by @froos in: [#973](https://codeberg.org/uzu/strudel/pulls/973)
+
+## february 2024
+
+- 2024-02-29T15:33:12+01:00 feat: can now invert euclid pulses with negative numbers by @froos in: [#959](https://codeberg.org/uzu/strudel/pulls/959)
+- 2024-02-29T15:31:23+01:00 pickOut(), pickRestart(), pickReset() by @eefano in: [#950](https://codeberg.org/uzu/strudel/pulls/950)
+- 2024-02-29T15:12:45+01:00 remove legacy legato + duration implementations by @froos in: [#965](https://codeberg.org/uzu/strudel/pulls/965)
+- 2024-02-29T08:47:53+01:00 fix for transpose(): preserve hap value object structure by @eefano in: [#966](https://codeberg.org/uzu/strudel/pulls/966)
+- 2024-02-29T08:32:00+01:00 add debounce to logger by @froos in: [#968](https://codeberg.org/uzu/strudel/pulls/968)
+- 2024-02-28T18:43:52+01:00 controls refactoring: simplify exports by @froos in: [#962](https://codeberg.org/uzu/strudel/pulls/962)
+- 2024-02-25T19:17:00+01:00 fix: reset global fx on pattern change by @froos in: [#960](https://codeberg.org/uzu/strudel/pulls/960)
+- 2024-02-25T14:15:30+01:00 fix midi issue on firefox and added quote error by @Ghost in: [#936](https://codeberg.org/uzu/strudel/pulls/936)
+- 2024-02-25T13:19:47+01:00 'Enable Bracket Matching' option in Codemirror by @eefano in: [#956](https://codeberg.org/uzu/strudel/pulls/956)
+- 2024-02-23T14:37:46+01:00 fix script importable packages (web + repl) by @froos in: [#957](https://codeberg.org/uzu/strudel/pulls/957)
+- 2024-02-21T16:17:37+01:00 account for cps in midi time duration by @Ghost in: [#954](https://codeberg.org/uzu/strudel/pulls/954)
+- 2024-02-21T10:27:12+01:00 Auto await samples by @froos in: [#955](https://codeberg.org/uzu/strudel/pulls/955)
+- 2024-02-08T13:16:15+01:00 remove cjs builds by @froos in: [#945](https://codeberg.org/uzu/strudel/pulls/945)
+- 2024-02-04T23:15:37+01:00 Minor documentation error: Update first-sounds.mdx by @Ghost in: [#941](https://codeberg.org/uzu/strudel/pulls/941)
+
+
+## january 2024
+
+- 2024-01-24T16:48:57+01:00 fix: pianoroll sorting by @froos in: [#938](https://codeberg.org/uzu/strudel/pulls/938)
+- 2024-01-23T00:04:03+01:00 V1 release notes by @froos in: [#935](https://codeberg.org/uzu/strudel/pulls/935)
+- 2024-01-22T20:20:53+01:00 2 years blog post by @froos in: [#929](https://codeberg.org/uzu/strudel/pulls/929)
+- 2024-01-22T20:02:34+01:00 make 0.5hz cps the default by @yaxu in: [#931](https://codeberg.org/uzu/strudel/pulls/931)
+- 2024-01-22T00:52:01+01:00 Fix pattern tab not showing patterns without created date by @Ghost in: [#934](https://codeberg.org/uzu/strudel/pulls/934)
+- 2024-01-21T20:46:28+01:00 Add useful pattern selection behavior for performing. by @Ghost in: [#897](https://codeberg.org/uzu/strudel/pulls/897)
+- 2024-01-21T01:30:28+01:00 Refactor cps functions by @froos in: [#933](https://codeberg.org/uzu/strudel/pulls/933)
+- 2024-01-20T23:47:31+01:00 Make splice cps-aware by @yaxu in: [#932](https://codeberg.org/uzu/strudel/pulls/932)
+- 2024-01-19T18:50:57+01:00 community bakery by @froos in: [#923](https://codeberg.org/uzu/strudel/pulls/923)
+- 2024-01-19T18:49:54+01:00 add pickF and pickmodF by @Ghost in: [#924](https://codeberg.org/uzu/strudel/pulls/924)
+- 2024-01-19T15:10:48+01:00 Mini-notation additions towards tidal compatibility by @yaxu in: [#926](https://codeberg.org/uzu/strudel/pulls/926)
+- 2024-01-18T23:34:11+01:00 Blog improvements by @froos in: [#919](https://codeberg.org/uzu/strudel/pulls/919)
+- 2024-01-18T18:08:29+01:00 pick, pickmod, inhabit, inhabitmod by @yaxu in: [#921](https://codeberg.org/uzu/strudel/pulls/921)
+- 2024-01-18T18:04:27+01:00 Revert "`pick` now accepts lookup tables, with alternate cycle squeezing behaviour as new `inhabit` function" by @yaxu in: [#920](https://codeberg.org/uzu/strudel/pulls/920)
+- 2024-01-18T17:45:39+01:00 `pick` now accepts lookup tables, with alternate cycle squeezing behaviour as new `inhabit` function by @yaxu in: [#918](https://codeberg.org/uzu/strudel/pulls/918)
+- 2024-01-18T10:30:08+01:00 rename @strudel.cycles/* packages to @strudel/* by @froos in: [#917](https://codeberg.org/uzu/strudel/pulls/917)
+- 2024-01-18T06:54:33+01:00 pitch envelopes by @froos in: [#913](https://codeberg.org/uzu/strudel/pulls/913)
+- 2024-01-18T05:09:38+01:00 Fix: swatch/[name].png.js static path by @Ghost in: [#916](https://codeberg.org/uzu/strudel/pulls/916)
+- 2024-01-16T08:22:28+01:00 Add more vowel qualities for the vowels function by @Ghost in: [#907](https://codeberg.org/uzu/strudel/pulls/907)
+- 2024-01-14T23:56:36+01:00 adds a blog by @froos in: [#911](https://codeberg.org/uzu/strudel/pulls/911)
+- 2024-01-14T23:52:41+01:00 Remove hideHeader for better mobile UI and consistency by @Ghost in: [#894](https://codeberg.org/uzu/strudel/pulls/894)
+- 2024-01-14T22:43:06+01:00 Further Envelope improvements by @Ghost in: [#868](https://codeberg.org/uzu/strudel/pulls/868)
+- 2024-01-14T00:48:04+01:00 public sharing by @froos in: [#910](https://codeberg.org/uzu/strudel/pulls/910)
+- 2024-01-12T18:31:41+01:00 fix some build warnings by @froos in: [#902](https://codeberg.org/uzu/strudel/pulls/902)
+- 2024-01-12T18:01:55+01:00 prevent vite from complaining about additional exports in jsx files by @Ghost in: [#891](https://codeberg.org/uzu/strudel/pulls/891)
+- 2024-01-12T17:05:47+01:00 Update Vite version so hot reload works properly with newest pnpm version by @Ghost in: [#892](https://codeberg.org/uzu/strudel/pulls/892)
+- 2024-01-12T15:09:41+01:00 support , in < > by @froos in: [#886](https://codeberg.org/uzu/strudel/pulls/886)
+- 2024-01-10T13:55:39+01:00 fix: autocomplete / tooltip code example bug by @froos in: [#898](https://codeberg.org/uzu/strudel/pulls/898)
+- 2024-01-06T15:23:55+01:00 fix: invisible selection on vim + emacs mode by @froos in: [#889](https://codeberg.org/uzu/strudel/pulls/889)
+- 2024-01-05T22:29:20+01:00 scales can now be anchored by @froos in: [#888](https://codeberg.org/uzu/strudel/pulls/888)
+- 2024-01-04T11:21:42+01:00 add root mode for voicings by @froos in: [#887](https://codeberg.org/uzu/strudel/pulls/887)
+- 2024-01-01T18:32:17+01:00 Showcase by @froos in: [#885](https://codeberg.org/uzu/strudel/pulls/885)
+- 2024-01-01T14:22:11+01:00 bugfix: suspend and close exisiting audio context when changing interface by @Ghost in: [#882](https://codeberg.org/uzu/strudel/pulls/882)
+- 2024-01-01T14:21:52+01:00 add mastodon link by @froos in: [#884](https://codeberg.org/uzu/strudel/pulls/884)
+
+## december 2023
+
+- 2023-12-31T17:02:57+01:00 fix: make sure n is never undefined before nanFallback by @froos in: [#881](https://codeberg.org/uzu/strudel/pulls/881)
+- 2023-12-31T16:47:22+01:00 Error tolerance by @froos in: [#880](https://codeberg.org/uzu/strudel/pulls/880)
+- 2023-12-31T10:06:24+01:00 bugfix: sound select indexes out of bounds by @Ghost in: [#871](https://codeberg.org/uzu/strudel/pulls/871)
+- 2023-12-31T10:03:01+01:00 Audio device selection by @Ghost in: [#854](https://codeberg.org/uzu/strudel/pulls/854)
+- 2023-12-31T00:59:49+01:00 Dependency update by @froos in: [#879](https://codeberg.org/uzu/strudel/pulls/879)
+- 2023-12-29T16:53:41+01:00 move all examples to separate examples folder by @froos in: [#878](https://codeberg.org/uzu/strudel/pulls/878)
+- 2023-12-29T15:29:17+01:00 final vanillification by @froos in: [#876](https://codeberg.org/uzu/strudel/pulls/876)
+- 2023-12-27T18:38:09+01:00 Bug Fix #119: Clock drift by @Ghost in: [#874](https://codeberg.org/uzu/strudel/pulls/874)
+- 2023-12-27T13:17:03+01:00 main repl vanillification by @froos in: [#873](https://codeberg.org/uzu/strudel/pulls/873)
+- 2023-12-25T17:47:25+01:00 more work on vanilla repl: repl web component + package + MicroRepl by @froos in: [#866](https://codeberg.org/uzu/strudel/pulls/866)
+- 2023-12-15T00:11:35+01:00 Vanilla repl 3 by @froos in: [#865](https://codeberg.org/uzu/strudel/pulls/865)
+- 2023-12-14T21:28:49+01:00 Vanilla repl 2 by @froos in: [#863](https://codeberg.org/uzu/strudel/pulls/863)
+- 2023-12-12T21:49:23+01:00 fix: finally repair envelopes by @froos in: [#861](https://codeberg.org/uzu/strudel/pulls/861)
+- 2023-12-12T21:20:00+01:00 add missing trailing slashes by @froos in: [#860](https://codeberg.org/uzu/strudel/pulls/860)
+- 2023-12-12T19:08:31+01:00 Sound Import from local file system by @Ghost in: [#839](https://codeberg.org/uzu/strudel/pulls/839)
+- 2023-12-11T22:48:35+01:00 Pattern organization by @froos in: [#858](https://codeberg.org/uzu/strudel/pulls/858)
+- 2023-12-09T17:25:11+01:00 Export patterns + ui tweaks by @froos in: [#855](https://codeberg.org/uzu/strudel/pulls/855)
+- 2023-12-08T09:41:10+01:00 patterns tab: import patterns + style by @froos in: [#852](https://codeberg.org/uzu/strudel/pulls/852)
+- 2023-12-07T20:42:00+01:00 Patterns tab + Refactor Panel by @froos in: [#769](https://codeberg.org/uzu/strudel/pulls/769)
+- 2023-12-07T10:25:16+01:00 CHANGES: pnpm 8.1.3 to 8.11.0 by @Ghost in: [#850](https://codeberg.org/uzu/strudel/pulls/850)
+- 2023-12-07T09:35:14+01:00 Fix edge case with rehype-urls and trailing slashes in image file paths by @Ghost in: [#849](https://codeberg.org/uzu/strudel/pulls/849)
+- 2023-12-06T23:02:31+01:00 Fix examples page, piano() and a few workshop imgs by @Ghost in: [#848](https://codeberg.org/uzu/strudel/pulls/848)
+- 2023-12-06T22:30:59+01:00 fix: swatch png src by @froos in: [#846](https://codeberg.org/uzu/strudel/pulls/846)
+- 2023-12-06T22:19:30+01:00 fix: missing hash for links starting with / by @froos in: [#845](https://codeberg.org/uzu/strudel/pulls/845)
+- 2023-12-06T22:05:05+01:00 improve slashing + base href behavior by @froos in: [#842](https://codeberg.org/uzu/strudel/pulls/842)
+- 2023-12-06T21:26:49+01:00 Add in fixes from my fork to slashocalypse branch by @Ghost in: [#843](https://codeberg.org/uzu/strudel/pulls/843)
+- 2023-12-05T18:43:58+01:00 Prevent 404 on Algolia crawls by @Ghost in: [#838](https://codeberg.org/uzu/strudel/pulls/838)
+- 2023-12-05T18:26:47+01:00 Multichannel audio by @Ghost in: [#820](https://codeberg.org/uzu/strudel/pulls/820)
+- 2023-12-05T12:19:58+01:00 ADDS: JetBrains IDE files and directories to .gitignore by @Ghost in: [#840](https://codeberg.org/uzu/strudel/pulls/840)
+- 2023-12-05T12:19:27+01:00 CHANGES: github action pnpm version from 7 to 8.3.1 by @Ghost in: [#835](https://codeberg.org/uzu/strudel/pulls/835)
+- 2023-12-05T12:19:17+01:00 CHANGES: pin pnpm to version 8.3.1 by @Ghost in: [#834](https://codeberg.org/uzu/strudel/pulls/834)
+- 2023-12-05T12:19:06+01:00 CHANGES: github action checkout v2 -> v4 by @Ghost in: [#837](https://codeberg.org/uzu/strudel/pulls/837)
+- 2023-12-05T11:15:14+01:00 FIXES: palindrome abc -> abccba by @Ghost in: [#831](https://codeberg.org/uzu/strudel/pulls/831)
+- 2023-12-02T09:43:50+01:00 Fix a typo by @Ghost in: [#830](https://codeberg.org/uzu/strudel/pulls/830)
+
+## november 2023
+
+- 2023-11-30T10:42:41+01:00 Add and style algolia search by @Ghost in: [#827](https://codeberg.org/uzu/strudel/pulls/827)
+- 2023-11-25T15:50:44+01:00 Hydra fixes and improvements by @Ghost in: [#818](https://codeberg.org/uzu/strudel/pulls/818)
+- 2023-11-24T10:07:17+01:00 add options param to initHydra by @Ghost in: [#808](https://codeberg.org/uzu/strudel/pulls/808)
+- 2023-11-24T09:57:02+01:00 Improve documentation for synonym functions by @Ghost in: [#800](https://codeberg.org/uzu/strudel/pulls/800)
+- 2023-11-24T09:00:54+01:00 New noise type: "crackle" by @Ghost in: [#806](https://codeberg.org/uzu/strudel/pulls/806)
+- 2023-11-18T21:18:16+01:00 Color hsl by @froos in: [#815](https://codeberg.org/uzu/strudel/pulls/815)
+- 2023-11-17T23:18:23+01:00 fix: multiple repls by @froos in: [#813](https://codeberg.org/uzu/strudel/pulls/813)
+- 2023-11-17T20:52:25+01:00 upstream changes by @yaxu in: [#809](https://codeberg.org/uzu/strudel/pulls/809)
+- 2023-11-17T16:04:10+01:00 Add doc for euclidLegatoRot, wordfall and slider by @Ghost in: [#801](https://codeberg.org/uzu/strudel/pulls/801)
+- 2023-11-17T14:42:58+01:00 add option to disable active line highlighting in Code Settings by @Ghost in: [#804](https://codeberg.org/uzu/strudel/pulls/804)
+- 2023-11-17T14:37:52+01:00 tidal style d1 ... d9 functions + more by @froos in: [#805](https://codeberg.org/uzu/strudel/pulls/805)
+- 2023-11-15T20:12:23+01:00 remove unwanted cm6 outline for strudelTheme by @Ghost in: [#802](https://codeberg.org/uzu/strudel/pulls/802)
+- 2023-11-13T23:30:15+01:00 Create phaser effect by @Ghost in: [#798](https://codeberg.org/uzu/strudel/pulls/798)
+- 2023-11-10T12:17:35+01:00 support multiple named serial connections, change default baudrate by @yaxu in: [#551](https://codeberg.org/uzu/strudel/pulls/551)
+- 2023-11-09T09:27:57+01:00 Fix for #1. Enables named instruments for csoundm. by @gogins in: [#662](https://codeberg.org/uzu/strudel/pulls/662)
+- 2023-11-09T09:26:45+01:00 Adding vibrato to Superdough sampler by @Ghost in: [#706](https://codeberg.org/uzu/strudel/pulls/706)
+- 2023-11-09T08:46:03+01:00 Document pianoroll by @Ghost in: [#784](https://codeberg.org/uzu/strudel/pulls/784)
+- 2023-11-09T08:45:29+01:00 Update first-effects.mdx by @Ghost in: [#795](https://codeberg.org/uzu/strudel/pulls/795)
+- 2023-11-09T08:44:43+01:00 Update pattern-effects.mdx by @Ghost in: [#796](https://codeberg.org/uzu/strudel/pulls/796)
+- 2023-11-09T08:43:50+01:00 Update recap.mdx by @Ghost in: [#797](https://codeberg.org/uzu/strudel/pulls/797)
+- 2023-11-07T11:53:49+01:00 Update first-sounds.mdx by @Ghost in: [#794](https://codeberg.org/uzu/strudel/pulls/794)
+- 2023-11-06T23:17:32+01:00 don't use anchor links for reference by @froos in: [#791](https://codeberg.org/uzu/strudel/pulls/791)
+- 2023-11-06T22:40:44+01:00 samples loading shortcuts: by @froos in: [#788](https://codeberg.org/uzu/strudel/pulls/788)
+- 2023-11-05T22:47:48+01:00 Fix scope pos + document by @froos in: [#786](https://codeberg.org/uzu/strudel/pulls/786)
+- 2023-11-05T22:10:25+01:00 Implement optional hover tooltip with function documentation by @Ghost in: [#783](https://codeberg.org/uzu/strudel/pulls/783)
+- 2023-11-05T16:46:06+01:00 Add function params in reference tab by @Ghost in: [#785](https://codeberg.org/uzu/strudel/pulls/785)
+- 2023-11-05T12:42:00+01:00 fix: style issues by @froos in: [#781](https://codeberg.org/uzu/strudel/pulls/781)
+- 2023-11-05T12:21:28+01:00 add xfade by @froos in: [#780](https://codeberg.org/uzu/strudel/pulls/780)
+- 2023-11-02T09:30:26+01:00 Update to Astro 3 by @froos in: [#775](https://codeberg.org/uzu/strudel/pulls/775)
+- 2023-11-02T08:57:14+01:00 add vscode bindings by @Ghost in: [#773](https://codeberg.org/uzu/strudel/pulls/773)
+- 2023-11-02T08:31:30+01:00 fix: share copy to clipboard + alert by @froos in: [#774](https://codeberg.org/uzu/strudel/pulls/774)
+- 2023-11-01T22:22:34+01:00 Fix chunk, add fastChunk and repeatCycles by @yaxu in: [#712](https://codeberg.org/uzu/strudel/pulls/712)
+- 2023-11-01T22:12:49+01:00 Document adsr function by @Ghost in: [#767](https://codeberg.org/uzu/strudel/pulls/767)
+- 2023-11-01T22:11:49+01:00 Add pick and squeeze functions by @Ghost in: [#771](https://codeberg.org/uzu/strudel/pulls/771)
+- 2023-11-01T22:04:00+01:00 Update vite pwa by @froos in: [#772](https://codeberg.org/uzu/strudel/pulls/772)
+
+## october 2023
+
+- 2023-10-28T23:55:07+02:00 replace strudel.tidalcycles.org with strudel.cc by @froos in: [#768](https://codeberg.org/uzu/strudel/pulls/768)
+- 2023-10-28T12:52:37+02:00 Document striate function by @Ghost in: [#766](https://codeberg.org/uzu/strudel/pulls/766)
+- 2023-10-27T23:06:20+02:00 fix zen mode logo overlap by @froos in: [#760](https://codeberg.org/uzu/strudel/pulls/760)
+- 2023-10-27T23:01:18+02:00 fix: scale offset by @froos in: [#764](https://codeberg.org/uzu/strudel/pulls/764)
+- 2023-10-27T21:59:35+02:00 Fix addivite synthesis phases by @froos in: [#762](https://codeberg.org/uzu/strudel/pulls/762)
+- 2023-10-26T16:28:16+02:00 Hydra integration by @froos in: [#759](https://codeberg.org/uzu/strudel/pulls/759)
+- 2023-10-26T14:14:27+02:00 add play function by @froos in: [#758](https://codeberg.org/uzu/strudel/pulls/758)
+- 2023-10-26T13:11:00+02:00 Add shabda shortcut by @Ghost in: [#740](https://codeberg.org/uzu/strudel/pulls/740)
+- 2023-10-26T13:07:23+02:00 mini notation: international alphabets support by @Ghost in: [#751](https://codeberg.org/uzu/strudel/pulls/751)
+- 2023-10-22T23:00:52+02:00 hopefully fix trainling slashes bug by @froos in: [#753](https://codeberg.org/uzu/strudel/pulls/753)
+- 2023-10-21T23:38:50+02:00 Fix krill build command in README by @Ghost in: [#748](https://codeberg.org/uzu/strudel/pulls/748)
+- 2023-10-21T00:20:50+02:00 completely revert config mess by @froos in: [#745](https://codeberg.org/uzu/strudel/pulls/745)
+- 2023-10-20T22:41:39+02:00 fix: try different trailing slash behavior by @froos in: [#744](https://codeberg.org/uzu/strudel/pulls/744)
+- 2023-10-20T22:34:03+02:00 fix: trailing slash confusion by @froos in: [#743](https://codeberg.org/uzu/strudel/pulls/743)
+- 2023-10-20T12:07:04+02:00 [Bug Fix] chooseWith: prevent pattern from stopping audio when selection is >= 1 or < 0 by @Ghost in: [#741](https://codeberg.org/uzu/strudel/pulls/741)
+- 2023-10-20T11:34:26+02:00 Recipes by @froos in: [#742](https://codeberg.org/uzu/strudel/pulls/742)
+- 2023-10-13T12:57:24+02:00 vite-vanilla-repl readme fix by @froos in: [#737](https://codeberg.org/uzu/strudel/pulls/737)
+- 2023-10-10T00:17:59+02:00 Add support for using samples as impulse response buffers for the reverb by @Ghost in: [#717](https://codeberg.org/uzu/strudel/pulls/717)
+- 2023-10-09T21:43:42+02:00 fix: reverb sampleRate by @froos in: [#732](https://codeberg.org/uzu/strudel/pulls/732)
+- 2023-10-09T21:34:33+02:00 fix: reverb roomsize not required by @froos in: [#731](https://codeberg.org/uzu/strudel/pulls/731)
+- 2023-10-08T13:54:52+02:00 Compressor by @froos in: [#729](https://codeberg.org/uzu/strudel/pulls/729)
+- 2023-10-08T13:25:57+02:00 fix: hashes in urls by @froos in: [#728](https://codeberg.org/uzu/strudel/pulls/728)
+- 2023-10-07T15:48:06+02:00 consume n with scale by @froos in: [#727](https://codeberg.org/uzu/strudel/pulls/727)
+- 2023-10-07T00:27:21+02:00 fix: reverb regenerate loophole by @froos in: [#726](https://codeberg.org/uzu/strudel/pulls/726)
+- 2023-10-05T00:04:17+02:00 Better convolution reverb by generating impulse responses by @Ghost in: [#718](https://codeberg.org/uzu/strudel/pulls/718)
+- 2023-10-04T10:31:53+02:00 Slider afterthoughts by @froos in: [#723](https://codeberg.org/uzu/strudel/pulls/723)
+- 2023-10-03T16:39:06+02:00 Add 'white', 'pink' and 'brown' oscillators + refactor synth by @Ghost in: [#713](https://codeberg.org/uzu/strudel/pulls/713)
+- 2023-10-01T14:20:50+02:00 support mininotation '..' range operator, fixes #715 by @yaxu in: [#716](https://codeberg.org/uzu/strudel/pulls/716)
+- 2023-10-01T14:15:09+02:00 widgets by @froos in: [#714](https://codeberg.org/uzu/strudel/pulls/714)
+
+## september 2023
+
+- 2023-09-28T11:03:09+02:00 Midi in by @froos in: [#699](https://codeberg.org/uzu/strudel/pulls/699)
+- 2023-09-27T22:53:49+02:00 add midi clock support by @froos in: [#710](https://codeberg.org/uzu/strudel/pulls/710)
+- 2023-09-25T23:36:10+02:00 Update bournemouth.mdx by @Ghost in: [#708](https://codeberg.org/uzu/strudel/pulls/708)
+- 2023-09-25T23:06:30+02:00 add dough function for raw dsp by @froos in: [#707](https://codeberg.org/uzu/strudel/pulls/707)
+- 2023-09-17T15:53:27+02:00 Update tauri.yml workflow file by @Ghost in: [#705](https://codeberg.org/uzu/strudel/pulls/705)
+- 2023-09-17T11:05:06+02:00 Adding vibrato to base oscillators by @Ghost in: [#693](https://codeberg.org/uzu/strudel/pulls/693)
+- 2023-09-17T08:27:36+02:00 Adding loop points and thus wavetable synthesis by @Ghost in: [#698](https://codeberg.org/uzu/strudel/pulls/698)
+- 2023-09-16T02:03:49+02:00 Adding filter envelopes and filter order selection by @Ghost in: [#692](https://codeberg.org/uzu/strudel/pulls/692)
+- 2023-09-09T11:19:58+02:00 Add logging from tauri by @Ghost in: [#697](https://codeberg.org/uzu/strudel/pulls/697)
+- 2023-09-05T00:33:54+02:00 Direct OSC Support in Tauri by @Ghost in: [#694](https://codeberg.org/uzu/strudel/pulls/694)
+- 2023-09-03T22:19:22+02:00 fix MIDI CC messages by @Ghost in: [#690](https://codeberg.org/uzu/strudel/pulls/690)
+- 2023-09-03T10:22:15+02:00 add sleep timer + improve message iterating by @Ghost in: [#688](https://codeberg.org/uzu/strudel/pulls/688)
+
+## august 2023
+
+- 2023-08-31T13:00:31+02:00 ZZFX Synth support by @Ghost in: [#684](https://codeberg.org/uzu/strudel/pulls/684)
+- 2023-08-31T12:36:37+02:00 Wave Selection and Global Envelope on the FM Synth Modulator by @Ghost in: [#683](https://codeberg.org/uzu/strudel/pulls/683)
+- 2023-08-31T05:52:17+02:00 Create Midi Integration for Tauri Desktop app by @Ghost in: [#685](https://codeberg.org/uzu/strudel/pulls/685)
+- 2023-08-31T05:32:16+02:00 teletext theme + fonts by @froos in: [#681](https://codeberg.org/uzu/strudel/pulls/681)
+- 2023-08-31T05:20:55+02:00 control osc partial count with n by @froos in: [#674](https://codeberg.org/uzu/strudel/pulls/674)
+- 2023-08-27T22:18:06+02:00 add emoji support by @froos in: [#680](https://codeberg.org/uzu/strudel/pulls/680)
+- 2023-08-27T16:12:32+02:00 Pianoroll improvements by @froos in: [#679](https://codeberg.org/uzu/strudel/pulls/679)
+- 2023-08-26T21:22:17+02:00 Scope by @froos in: [#677](https://codeberg.org/uzu/strudel/pulls/677)
+- 2023-08-23T21:50:50+02:00 Midi time fixes by @Ghost in: [#668](https://codeberg.org/uzu/strudel/pulls/668)
+- 2023-08-20T23:19:08+02:00 basic fm by @froos in: [#669](https://codeberg.org/uzu/strudel/pulls/669)
+- 2023-08-18T23:56:20+02:00 togglable panel position by @froos in: [#667](https://codeberg.org/uzu/strudel/pulls/667)
+- 2023-08-18T15:59:20+02:00 fix osc bundle timestamp glitches caused by drifting clock by @Ghost in: [#666](https://codeberg.org/uzu/strudel/pulls/666)
+- 2023-08-17T11:36:07+02:00 superdough: encapsulates web audio output by @froos in: [#664](https://codeberg.org/uzu/strudel/pulls/664)
+- 2023-08-11T00:06:21+02:00 fix: always run previous trigger by @froos in: [#660](https://codeberg.org/uzu/strudel/pulls/660)
+- 2023-08-10T23:54:58+02:00 fix: welcome message for latestCode by @froos in: [#659](https://codeberg.org/uzu/strudel/pulls/659)
+- 2023-08-07T07:45:26+02:00 [Bug Fix] Midi: Don't treat note 0 as false by @Ghost in: [#657](https://codeberg.org/uzu/strudel/pulls/657)
+
+## july 2023
+
+- 2023-07-29T09:06:18+02:00 [Bug Fix] Account for numeral notation when converting to midi by @Ghost in: [#656](https://codeberg.org/uzu/strudel/pulls/656)
+- 2023-07-23T22:18:49+02:00 ireal voicings by @froos in: [#653](https://codeberg.org/uzu/strudel/pulls/653)
+- 2023-07-22T09:30:21+02:00 Understand pitch by @froos in: [#652](https://codeberg.org/uzu/strudel/pulls/652)
+- 2023-07-17T23:42:59+02:00 update vitest by @froos in: [#651](https://codeberg.org/uzu/strudel/pulls/651)
+- 2023-07-17T23:34:33+02:00 stateless voicings + tonleiter lib by @froos in: [#647](https://codeberg.org/uzu/strudel/pulls/647)
+- 2023-07-17T17:57:17+02:00 FIXES: TODO in rotateChroma by @Ghost in: [#650](https://codeberg.org/uzu/strudel/pulls/650)
+- 2023-07-10T19:07:45+02:00 slice: list mode by @froos in: [#645](https://codeberg.org/uzu/strudel/pulls/645)
+- 2023-07-04T23:50:30+02:00 Delete old packages by @froos in: [#639](https://codeberg.org/uzu/strudel/pulls/639)
+- 2023-07-04T23:38:42+02:00 Adaptive Highlighting by @froos in: [#634](https://codeberg.org/uzu/strudel/pulls/634)
+- 2023-07-04T21:55:57+02:00 More work on highlight IDs by @Ghost in: [#636](https://codeberg.org/uzu/strudel/pulls/636)
+- 2023-07-04T18:44:48+02:00 snapshot tests: sort haps by part by @froos in: [#637](https://codeberg.org/uzu/strudel/pulls/637)
+
+## juny 2023
+
+- 2023-06-30T22:47:40+02:00 fix: update canvas size on window resize by @froos in: [#631](https://codeberg.org/uzu/strudel/pulls/631)
+- 2023-06-30T22:45:41+02:00 fix: out of range error by @froos in: [#630](https://codeberg.org/uzu/strudel/pulls/630)
+- 2023-06-30T08:14:40+02:00 fix: midi clock drift by @froos in: [#627](https://codeberg.org/uzu/strudel/pulls/627)
+- 2023-06-29T22:02:28+02:00 desktop: play samples from disk by @froos in: [#621](https://codeberg.org/uzu/strudel/pulls/621)
+- 2023-06-29T21:59:43+02:00 cps dependent functions by @froos in: [#620](https://codeberg.org/uzu/strudel/pulls/620)
+- 2023-06-26T22:45:04+02:00 Fix typo on packages.mdx by @Ghost in: [#520](https://codeberg.org/uzu/strudel/pulls/520)
+- 2023-06-26T22:35:02+02:00 patterning ui settings by @froos in: [#606](https://codeberg.org/uzu/strudel/pulls/606)
+- 2023-06-26T22:32:46+02:00 add spiral viz by @froos in: [#614](https://codeberg.org/uzu/strudel/pulls/614)
+- 2023-06-26T22:17:46+02:00 tauri desktop app by @Ghost in: [#613](https://codeberg.org/uzu/strudel/pulls/613)
+- 2023-06-23T09:59:43+02:00 fix: doc links by @froos in: [#612](https://codeberg.org/uzu/strudel/pulls/612)
+- 2023-06-23T09:55:18+02:00 clip now works like legato in tidal by @froos in: [#598](https://codeberg.org/uzu/strudel/pulls/598)
+- 2023-06-18T10:36:19+02:00 fix: flatten scale lists by @froos in: [#605](https://codeberg.org/uzu/strudel/pulls/605)
+- 2023-06-18T10:36:17+02:00 tonal fixes by @froos in: [#607](https://codeberg.org/uzu/strudel/pulls/607)
+- 2023-06-15T12:51:40+02:00 editor: enable line wrapping by @Ghost in: [#581](https://codeberg.org/uzu/strudel/pulls/581)
+- 2023-06-15T10:22:46+02:00 add ratio function by @froos in: [#602](https://codeberg.org/uzu/strudel/pulls/602)
+- 2023-06-12T23:24:29+02:00 enable auto-completion by @Ghost in: [#588](https://codeberg.org/uzu/strudel/pulls/588)
+- 2023-06-11T22:04:17+02:00 improve cursor by @froos in: [#597](https://codeberg.org/uzu/strudel/pulls/597)
+- 2023-06-11T20:00:45+02:00 Solmization added by @Ghost in: [#570](https://codeberg.org/uzu/strudel/pulls/570)
+- 2023-06-11T19:45:00+02:00 fix: division by zero by @froos in: [#591](https://codeberg.org/uzu/strudel/pulls/591)
+- 2023-06-11T19:44:43+02:00 fix: allow f for flat notes like tidal by @froos in: [#593](https://codeberg.org/uzu/strudel/pulls/593)
+- 2023-06-11T19:44:41+02:00 Fix option dot by @froos in: [#596](https://codeberg.org/uzu/strudel/pulls/596)
+- 2023-06-09T21:02:12+02:00 New Workshop by @froos in: [#587](https://codeberg.org/uzu/strudel/pulls/587)
+- 2023-06-09T17:31:58+02:00 Music metadata by @Ghost in: [#580](https://codeberg.org/uzu/strudel/pulls/580)
+- 2023-06-08T09:33:34+02:00 learn/tonal: fix typo in "scaleTran[s]pose" by @Ghost in: [#585](https://codeberg.org/uzu/strudel/pulls/585)
+- 2023-06-07T20:19:11+02:00 repl: add option to display line numbers by @Ghost in: [#582](https://codeberg.org/uzu/strudel/pulls/582)
+- 2023-06-05T21:35:49+02:00 Vanilla JS Refactoring by @froos in: [#563](https://codeberg.org/uzu/strudel/pulls/563)
+
+## may 2023
+
+- 2023-05-05T08:34:37+02:00 Patchday by @froos in: [#559](https://codeberg.org/uzu/strudel/pulls/559)
+- 2023-05-02T21:48:33+02:00 add basic triads and guidetone voicings by @froos in: [#557](https://codeberg.org/uzu/strudel/pulls/557)
+
+## april 2023
+
+- 2023-04-28T12:46:56+02:00 fix: make soundfonts import dynamic by @froos in: [#556](https://codeberg.org/uzu/strudel/pulls/556)
+- 2023-04-22T15:43:22+02:00 fix: colorable highlighting by @froos in: [#553](https://codeberg.org/uzu/strudel/pulls/553)
+- 2023-04-06T00:11:02+02:00 fix: load soundfonts in prebake by @froos in: [#550](https://codeberg.org/uzu/strudel/pulls/550)
+
+## march 2023
+
+- 2023-03-29T22:23:07+02:00 fix: reset time on stop by @froos in: [#548](https://codeberg.org/uzu/strudel/pulls/548)
+- 2023-03-29T22:16:38+02:00 fix: allow whitespace at the end of a mini pattern by @froos in: [#547](https://codeberg.org/uzu/strudel/pulls/547)
+- 2023-03-24T22:10:56+01:00 add firacode font by @froos in: [#544](https://codeberg.org/uzu/strudel/pulls/544)
+- 2023-03-24T12:54:20+01:00 feat: add loader bar to animate loading state by @froos in: [#542](https://codeberg.org/uzu/strudel/pulls/542)
+- 2023-03-23T22:39:27+01:00 do not reset cps before eval #517 by @froos in: [#539](https://codeberg.org/uzu/strudel/pulls/539)
+- 2023-03-23T22:27:31+01:00 improve initial loading + wait before eval by @froos in: [#538](https://codeberg.org/uzu/strudel/pulls/538)
+- 2023-03-23T21:40:20+01:00 fix period key for dvorak + remove duplicated code by @froos in: [#537](https://codeberg.org/uzu/strudel/pulls/537)
+- 2023-03-23T11:44:56+01:00 Update lerna by @froos in: [#535](https://codeberg.org/uzu/strudel/pulls/535)
+- 2023-03-23T10:21:55+01:00 feat: add freq support to gm soundfonts by @froos in: [#534](https://codeberg.org/uzu/strudel/pulls/534)
+- 2023-03-23T10:18:58+01:00 Maintain random seed state in parser, not globally by @Ghost in: [#531](https://codeberg.org/uzu/strudel/pulls/531)
+- 2023-03-21T22:35:18+01:00 FIXES: alias pm for polymeter by @Ghost in: [#527](https://codeberg.org/uzu/strudel/pulls/527)
+- 2023-03-21T22:29:07+01:00 fix(footer): fix link to tidalcycles by @julienbouquillon in: [#529](https://codeberg.org/uzu/strudel/pulls/529)
+- 2023-03-18T20:25:21+01:00 Update intro.mdx by @Ghost in: [#525](https://codeberg.org/uzu/strudel/pulls/525)
+- 2023-03-18T20:24:38+01:00 Update samples.mdx by @Ghost in: [#524](https://codeberg.org/uzu/strudel/pulls/524)
+- 2023-03-17T09:03:11+01:00 fix: envelopes in chrome by @froos in: [#521](https://codeberg.org/uzu/strudel/pulls/521)
+- 2023-03-16T16:13:30+01:00 registerSound API + improved sounds tab + regroup soundfonts by @froos in: [#516](https://codeberg.org/uzu/strudel/pulls/516)
+- 2023-03-14T21:54:53+01:00 add 2 illegible fonts by @froos in: [#518](https://codeberg.org/uzu/strudel/pulls/518)
+- 2023-03-06T22:55:00+01:00 Update README.md by @Ghost in: [#474](https://codeberg.org/uzu/strudel/pulls/474)
+- 2023-03-05T14:52:30+01:00 add arrange function by @froos in: [#508](https://codeberg.org/uzu/strudel/pulls/508)
+- 2023-03-05T14:29:08+01:00 update react to 18 by @froos in: [#514](https://codeberg.org/uzu/strudel/pulls/514)
+- 2023-03-04T19:06:18+01:00 Support list syntax in mininotation by @yaxu in: [#512](https://codeberg.org/uzu/strudel/pulls/512)
+- 2023-03-03T12:40:23+01:00 can now use : as a replacement for space in scales by @froos in: [#502](https://codeberg.org/uzu/strudel/pulls/502)
+- 2023-03-02T15:44:41+01:00 Reinstate slice and splice by @yaxu in: [#500](https://codeberg.org/uzu/strudel/pulls/500)
+- 2023-03-02T14:49:30+01:00 fix: nano-repl highlighting by @froos in: [#501](https://codeberg.org/uzu/strudel/pulls/501)
+- 2023-03-02T14:17:13+01:00 Add control aliases by @yaxu in: [#497](https://codeberg.org/uzu/strudel/pulls/497)
+- 2023-03-01T09:27:27+01:00 implement cps in scheduler by @froos in: [#493](https://codeberg.org/uzu/strudel/pulls/493)
+
+## february 2023
+
+- 2023-02-28T23:06:29+01:00 react style fixes by @froos in: [#491](https://codeberg.org/uzu/strudel/pulls/491)
+- 2023-02-28T22:57:20+01:00 refactor react package by @froos in: [#490](https://codeberg.org/uzu/strudel/pulls/490)
+- 2023-02-27T23:47:34+01:00 add algolia creds + optimize sidebar for crawling by @froos in: [#488](https://codeberg.org/uzu/strudel/pulls/488)
+- 2023-02-27T19:28:10+01:00 fix app height by @froos in: [#485](https://codeberg.org/uzu/strudel/pulls/485)
+- 2023-02-27T16:04:21+01:00 Revert "Another attempt at composable functions - WIP (#390)" by @froos in: [#484](https://codeberg.org/uzu/strudel/pulls/484)
+- 2023-02-27T15:45:20+01:00 Update mini-notation.mdx by @yaxu in: [#365](https://codeberg.org/uzu/strudel/pulls/365)
+- 2023-02-27T12:52:09+01:00 docs: packages + offline by @froos in: [#482](https://codeberg.org/uzu/strudel/pulls/482)
+- 2023-02-25T14:26:49+01:00 Fix array args by @froos in: [#480](https://codeberg.org/uzu/strudel/pulls/480)
+- 2023-02-25T12:33:22+01:00 midi cc support by @froos in: [#478](https://codeberg.org/uzu/strudel/pulls/478)
+- 2023-02-23T00:11:05+01:00 fix: hash links by @froos in: [#473](https://codeberg.org/uzu/strudel/pulls/473)
+- 2023-02-22T22:54:39+01:00 settings tab with vim / emacs modes + additional themes and fonts by @froos in: [#467](https://codeberg.org/uzu/strudel/pulls/467)
+- 2023-02-22T22:53:22+01:00 Update input-output.mdx by @Ghost in: [#471](https://codeberg.org/uzu/strudel/pulls/471)
+- 2023-02-22T22:52:50+01:00 FIXES: freqs instead of pitches by @Ghost in: [#464](https://codeberg.org/uzu/strudel/pulls/464)
+- 2023-02-22T20:01:00+01:00 fix: osc should not return a promise by @froos in: [#472](https://codeberg.org/uzu/strudel/pulls/472)
+- 2023-02-22T12:51:31+01:00 slice and splice by @yaxu in: [#466](https://codeberg.org/uzu/strudel/pulls/466)
+- 2023-02-18T01:00:19+01:00 weave and weaveWith by @yaxu in: [#465](https://codeberg.org/uzu/strudel/pulls/465)
+- 2023-02-17T00:15:21+01:00 Composable functions by @yaxu in: [#390](https://codeberg.org/uzu/strudel/pulls/390)
+- 2023-02-16T20:38:45+01:00 FIXES: Warning about jsxBracketSameLine deprecation by @Ghost in: [#461](https://codeberg.org/uzu/strudel/pulls/461)
+- 2023-02-14T22:11:02+01:00 Update synths.mdx by @Ghost in: [#438](https://codeberg.org/uzu/strudel/pulls/438)
+- 2023-02-14T19:54:42+01:00 Update mini-notation.mdx by @Ghost in: [#437](https://codeberg.org/uzu/strudel/pulls/437)
+- 2023-02-14T19:54:25+01:00 Update code.mdx by @Ghost in: [#436](https://codeberg.org/uzu/strudel/pulls/436)
+- 2023-02-13T00:43:29+01:00 Fix anchors by @froos in: [#433](https://codeberg.org/uzu/strudel/pulls/433)
+- 2023-02-11T21:02:11+01:00 autocomplete preparations by @froos in: [#427](https://codeberg.org/uzu/strudel/pulls/427)
+- 2023-02-10T23:14:48+01:00 Themes by @froos in: [#431](https://codeberg.org/uzu/strudel/pulls/431)
+- 2023-02-09T19:22:56+01:00 minirepl: add keyboard shortcuts by @froos in: [#429](https://codeberg.org/uzu/strudel/pulls/429)
+- 2023-02-09T08:56:42+01:00 add cdn.freesound to cache list by @froos in: [#425](https://codeberg.org/uzu/strudel/pulls/425)
+- 2023-02-08T20:36:00+01:00 add more offline caching by @froos in: [#421](https://codeberg.org/uzu/strudel/pulls/421)
+- 2023-02-07T22:08:01+01:00 add caching strategy for missing file types + cache all samples loaded from github by @froos in: [#419](https://codeberg.org/uzu/strudel/pulls/419)
+- 2023-02-06T23:29:57+01:00 PWA with offline support by @froos in: [#417](https://codeberg.org/uzu/strudel/pulls/417)
+- 2023-02-05T17:27:32+01:00 improve samples doc by @froos in: [#411](https://codeberg.org/uzu/strudel/pulls/411)
+- 2023-02-05T17:27:10+01:00 google gtfo by @froos in: [#413](https://codeberg.org/uzu/strudel/pulls/413)
+- 2023-02-05T16:27:59+01:00 improve effects doc by @froos in: [#409](https://codeberg.org/uzu/strudel/pulls/409)
+- 2023-02-05T14:56:03+01:00 Update effects.mdx by @Ghost in: [#410](https://codeberg.org/uzu/strudel/pulls/410)
+- 2023-02-03T19:54:47+01:00 add shabda doc by @froos in: [#407](https://codeberg.org/uzu/strudel/pulls/407)
+- 2023-02-02T21:45:56+01:00 fix: share url on subpath by @froos in: [#405](https://codeberg.org/uzu/strudel/pulls/405)
+- 2023-02-02T21:35:45+01:00 update csound + fix sound output by @froos in: [#404](https://codeberg.org/uzu/strudel/pulls/404)
+- 2023-02-02T19:53:36+01:00 pin @csound/browser to 6.18.3 + bump by @froos in: [#403](https://codeberg.org/uzu/strudel/pulls/403)
+- 2023-02-01T22:47:27+01:00 release webaudio by @froos in: [#400](https://codeberg.org/uzu/strudel/pulls/400)
+- 2023-02-01T22:45:16+01:00 can now await initAudio + initAudioOnFirstClick by @froos in: [#399](https://codeberg.org/uzu/strudel/pulls/399)
+- 2023-02-01T22:32:18+01:00 fix: minirepl styles by @froos in: [#398](https://codeberg.org/uzu/strudel/pulls/398)
+- 2023-02-01T22:17:19+01:00 proper builds + use pnpm workspaces by @froos in: [#396](https://codeberg.org/uzu/strudel/pulls/396)
+- 2023-02-01T16:49:55+01:00 add pattern methods hurry, press and pressBy by @yaxu in: [#397](https://codeberg.org/uzu/strudel/pulls/397)
+
+## january 2023
+
+- 2023-01-27T14:40:40+01:00 Add tidal-drum-patterns to examples by @Ghost in: [#379](https://codeberg.org/uzu/strudel/pulls/379)
+- 2023-01-27T14:39:56+01:00 add ribbon + test + docs by @froos in: [#388](https://codeberg.org/uzu/strudel/pulls/388)
+- 2023-01-27T12:10:37+01:00 Notes are not essential :) by @yaxu in: [#393](https://codeberg.org/uzu/strudel/pulls/393)
+- 2023-01-21T17:18:13+01:00 document csound by @froos in: [#391](https://codeberg.org/uzu/strudel/pulls/391)
+- 2023-01-19T12:04:51+01:00 Rename a to angle by @froos in: [#387](https://codeberg.org/uzu/strudel/pulls/387)
+- 2023-01-19T11:49:39+01:00 add run + test + docs by @froos in: [#386](https://codeberg.org/uzu/strudel/pulls/386)
+- 2023-01-19T11:32:56+01:00 docs: use note instead of n to mitigate confusion by @froos in: [#385](https://codeberg.org/uzu/strudel/pulls/385)
+- 2023-01-18T17:10:09+01:00 update my-patterns instructions by @froos in: [#384](https://codeberg.org/uzu/strudel/pulls/384)
+- 2023-01-15T23:19:43+01:00 Draw fixes by @froos in: [#377](https://codeberg.org/uzu/strudel/pulls/377)
+- 2023-01-14T11:07:08+01:00 improve new draw logic by @froos in: [#372](https://codeberg.org/uzu/strudel/pulls/372)
+- 2023-01-12T14:40:59+01:00 document more functions + change arp join by @froos in: [#369](https://codeberg.org/uzu/strudel/pulls/369)
+- 2023-01-10T00:03:47+01:00 add https to url by @Ghost in: [#364](https://codeberg.org/uzu/strudel/pulls/364)
+- 2023-01-09T23:37:34+01:00 doc structuring by @froos in: [#360](https://codeberg.org/uzu/strudel/pulls/360)
+- 2023-01-09T23:23:28+01:00 Support for multiple mininotation operators by @yaxu in: [#350](https://codeberg.org/uzu/strudel/pulls/350)
+- 2023-01-09T00:40:15+01:00 Fix .out(), renaming webaudio's out() to webaudio() by @yaxu in: [#361](https://codeberg.org/uzu/strudel/pulls/361)
+- 2023-01-06T22:02:31+01:00 docs: tidal comparison + add global fx + add missing sampler fx by @froos in: [#356](https://codeberg.org/uzu/strudel/pulls/356)
+- 2023-01-06T12:31:32+01:00 Fix Bjorklund by @yaxu in: [#343](https://codeberg.org/uzu/strudel/pulls/343)
+- 2023-01-04T20:29:35+01:00 Fix prebake base path by @froos in: [#345](https://codeberg.org/uzu/strudel/pulls/345)
+- 2023-01-04T19:55:49+01:00 fixes #346 by @froos in: [#347](https://codeberg.org/uzu/strudel/pulls/347)
+- 2023-01-02T21:28:07+01:00 Patternify euclid, fast, slow and polymeter step parameters in mininotation by @yaxu in: [#341](https://codeberg.org/uzu/strudel/pulls/341)
+- 2023-01-02T00:30:16+01:00 more animate functions + mini repl fix by @froos in: [#340](https://codeberg.org/uzu/strudel/pulls/340)
+- 2023-01-01T12:34:11+01:00 move /my-patterns to /swatch by @yaxu in: [#338](https://codeberg.org/uzu/strudel/pulls/338)
+- 2023-01-01T12:22:55+01:00 animation options by @froos in: [#337](https://codeberg.org/uzu/strudel/pulls/337)
+
+## december 2022
+
+- 2022-12-31T22:42:49+01:00 Tidy parser, implement polymeters by @yaxu in: [#336](https://codeberg.org/uzu/strudel/pulls/336)
+- 2022-12-31T16:51:53+01:00 animate mvp by @froos in: [#335](https://codeberg.org/uzu/strudel/pulls/335)
+- 2022-12-30T20:16:10+01:00 testing + docs docs by @froos in: [#334](https://codeberg.org/uzu/strudel/pulls/334)
+- 2022-12-29T21:11:16+01:00 Embed mode improvements by @froos in: [#333](https://codeberg.org/uzu/strudel/pulls/333)
+- 2022-12-29T14:06:20+01:00 fix: can now multiply floats in mini notation by @froos in: [#332](https://codeberg.org/uzu/strudel/pulls/332)
+- 2022-12-29T13:50:07+01:00 improve displaying 's' in pianoroll by @froos in: [#331](https://codeberg.org/uzu/strudel/pulls/331)
+- 2022-12-28T17:35:35+01:00 my-patterns: fix paths + update readme by @froos in: [#330](https://codeberg.org/uzu/strudel/pulls/330)
+- 2022-12-28T17:11:40+01:00 my-patterns build + deploy by @froos in: [#329](https://codeberg.org/uzu/strudel/pulls/329)
+- 2022-12-28T15:43:31+01:00 add my-patterns by @froos in: [#328](https://codeberg.org/uzu/strudel/pulls/328)
+- 2022-12-28T14:47:14+01:00 add examples route by @froos in: [#327](https://codeberg.org/uzu/strudel/pulls/327)
+- 2022-12-28T14:44:31+01:00 fix: workaround Object.assign globalThis by @froos in: [#326](https://codeberg.org/uzu/strudel/pulls/326)
+- 2022-12-26T23:29:47+01:00 mini repl improvements by @froos in: [#324](https://codeberg.org/uzu/strudel/pulls/324)
+- 2022-12-26T23:00:34+01:00 support notes without octave by @froos in: [#323](https://codeberg.org/uzu/strudel/pulls/323)
+- 2022-12-26T12:37:36+01:00 tutorial updates by @Ghost in: [#320](https://codeberg.org/uzu/strudel/pulls/320)
+- 2022-12-23T18:26:30+01:00 Reference tab sort by @froos in: [#318](https://codeberg.org/uzu/strudel/pulls/318)
+- 2022-12-23T00:49:56+01:00 Astro build by @froos in: [#315](https://codeberg.org/uzu/strudel/pulls/315)
+- 2022-12-19T21:02:37+01:00 object support for .scale by @froos in: [#307](https://codeberg.org/uzu/strudel/pulls/307)
+- 2022-12-19T21:02:22+01:00 Jsdoc component by @froos in: [#312](https://codeberg.org/uzu/strudel/pulls/312)
+- 2022-12-19T20:31:46+01:00 fix: copy share link to clipboard was broken for some browers by @froos in: [#311](https://codeberg.org/uzu/strudel/pulls/311)
+- 2022-12-19T12:11:24+01:00 ICLC2023 paper WIP by @yaxu in: [#306](https://codeberg.org/uzu/strudel/pulls/306)
+- 2022-12-15T21:23:22+01:00 support freq in pianoroll by @froos in: [#308](https://codeberg.org/uzu/strudel/pulls/308)
+- 2022-12-13T22:07:27+01:00 Updated csoundm to use the register facility . by @gogins in: [#303](https://codeberg.org/uzu/strudel/pulls/303)
+- 2022-12-13T21:57:03+01:00 add lint + prettier check before test by @froos in: [#305](https://codeberg.org/uzu/strudel/pulls/305)
+- 2022-12-13T21:21:44+01:00 add freq support to sampler by @froos in: [#301](https://codeberg.org/uzu/strudel/pulls/301)
+- 2022-12-12T00:48:41+01:00 fix whitespace trimming by @froos in: [#300](https://codeberg.org/uzu/strudel/pulls/300)
+- 2022-12-12T00:21:53+01:00 .defragmentHaps() for merging touching haps that share a whole and value by @yaxu in: [#299](https://codeberg.org/uzu/strudel/pulls/299)
+- 2022-12-11T23:17:49+01:00 remove whitespace from highlighted region by @froos in: [#298](https://codeberg.org/uzu/strudel/pulls/298)
+- 2022-12-11T22:03:26+01:00 update vitest by @froos in: [#297](https://codeberg.org/uzu/strudel/pulls/297)
+- 2022-12-11T21:44:16+01:00 can now add bare numbers to numeral object props by @froos in: [#287](https://codeberg.org/uzu/strudel/pulls/287)
+- 2022-12-11T21:27:58+01:00 Move stuff to new register function by @froos in: [#295](https://codeberg.org/uzu/strudel/pulls/295)
+- 2022-12-11T20:07:12+01:00 add prettier task by @froos in: [#296](https://codeberg.org/uzu/strudel/pulls/296)
+- 2022-12-10T15:39:04+01:00 Reorganise pattern.mjs with a 'toplevel first' regime by @yaxu in: [#286](https://codeberg.org/uzu/strudel/pulls/286)
+- 2022-12-10T11:56:16+01:00 Fancy hap show, include part in snapshots by @yaxu in: [#291](https://codeberg.org/uzu/strudel/pulls/291)
+- 2022-12-07T20:07:55+01:00 Switch 'operators' from .whatHow to .what.how by @yaxu in: [#285](https://codeberg.org/uzu/strudel/pulls/285)
+- 2022-12-04T12:51:01+01:00 implement collect + arp function by @froos in: [#281](https://codeberg.org/uzu/strudel/pulls/281)
+- 2022-12-02T13:38:27+01:00 do not recompile orc by @froos in: [#278](https://codeberg.org/uzu/strudel/pulls/278)
+- 2022-12-02T12:49:26+01:00 add basic csound output by @froos in: [#275](https://codeberg.org/uzu/strudel/pulls/275)
+- 2022-12-02T12:18:41+01:00 add licenses / credits to all tunes + remove some by @froos in: [#277](https://codeberg.org/uzu/strudel/pulls/277)
+- 2022-12-02T11:45:02+01:00 Support sending CRC16 bytes with serial messages by @yaxu in: [#276](https://codeberg.org/uzu/strudel/pulls/276)
+
+## november 2022
+
+- 2022-11-29T23:41:39+01:00 release version bumps by @froos in: [#273](https://codeberg.org/uzu/strudel/pulls/273)
+- 2022-11-29T23:34:50+01:00 add eslint by @froos in: [#271](https://codeberg.org/uzu/strudel/pulls/271)
+- 2022-11-29T23:34:26+01:00 tonal update with fixed memory leak by @froos in: [#272](https://codeberg.org/uzu/strudel/pulls/272)
+- 2022-11-22T09:51:26+01:00 Tidying up core by @yaxu in: [#256](https://codeberg.org/uzu/strudel/pulls/256)
+- 2022-11-21T22:15:48+01:00 fix performance bottleneck by @froos in: [#266](https://codeberg.org/uzu/strudel/pulls/266)
+- 2022-11-17T11:08:38+01:00 fix tutorial bugs by @froos in: [#263](https://codeberg.org/uzu/strudel/pulls/263)
+- 2022-11-16T13:15:28+01:00 Binaries by @froos in: [#254](https://codeberg.org/uzu/strudel/pulls/254)
+- 2022-11-13T20:20:32+01:00 Repl refactoring by @froos in: [#255](https://codeberg.org/uzu/strudel/pulls/255)
+- 2022-11-08T23:35:11+01:00 Webaudio build by @froos in: [#250](https://codeberg.org/uzu/strudel/pulls/250)
+- 2022-11-08T22:43:32+01:00 new transpiler based on acorn by @froos in: [#249](https://codeberg.org/uzu/strudel/pulls/249)
+- 2022-11-06T19:03:19+01:00 General purpose scheduler by @froos in: [#248](https://codeberg.org/uzu/strudel/pulls/248)
+- 2022-11-06T11:05:23+01:00 snapshot tests on shared snippets by @froos in: [#243](https://codeberg.org/uzu/strudel/pulls/243)
+- 2022-11-06T11:03:42+01:00 Some tunes by @froos in: [#247](https://codeberg.org/uzu/strudel/pulls/247)
+- 2022-11-06T00:37:11+01:00 patchday by @froos in: [#246](https://codeberg.org/uzu/strudel/pulls/246)
+- 2022-11-04T20:28:23+01:00 Readme + TLC by @froos in: [#244](https://codeberg.org/uzu/strudel/pulls/244)
+- 2022-11-03T15:16:04+01:00 in source example tests by @froos in: [#242](https://codeberg.org/uzu/strudel/pulls/242)
+- 2022-11-02T23:16:43+01:00 feat: support github: links by @froos in: [#240](https://codeberg.org/uzu/strudel/pulls/240)
+- 2022-11-02T21:26:44+01:00 Load samples from url by @froos in: [#239](https://codeberg.org/uzu/strudel/pulls/239)
+- 2022-11-02T11:42:32+01:00 Object arithmetic by @froos in: [#238](https://codeberg.org/uzu/strudel/pulls/238)
+- 2022-11-01T00:36:14+01:00 Tidal drum machines by @froos in: [#237](https://codeberg.org/uzu/strudel/pulls/237)
+- 2022-10-31T21:39:23+01:00 fx on stereo speakers by @froos in: [#236](https://codeberg.org/uzu/strudel/pulls/236)
+
+## october 2022
+
+- 2022-10-30T19:10:33+01:00 add vcsl sample library by @froos in: [#235](https://codeberg.org/uzu/strudel/pulls/235)
+- 2022-10-30T00:23:10+02:00 Fix zero length queries WIP by @yaxu in: [#234](https://codeberg.org/uzu/strudel/pulls/234)
+- 2022-10-29T17:54:05+02:00 Out by default by @froos in: [#232](https://codeberg.org/uzu/strudel/pulls/232)
+- 2022-10-26T23:53:49+02:00 Patternify range by @yaxu in: [#231](https://codeberg.org/uzu/strudel/pulls/231)
+- 2022-10-26T21:42:12+02:00 Just another docs branch by @froos in: [#228](https://codeberg.org/uzu/strudel/pulls/228)
+- 2022-10-26T21:41:12+02:00 Refactor tunes away from tone by @froos in: [#230](https://codeberg.org/uzu/strudel/pulls/230)
+- 2022-10-20T09:26:28+02:00 Core util tests by @Ghost in: [#226](https://codeberg.org/uzu/strudel/pulls/226)
+- 2022-10-06T22:35:45+02:00 fix fastgap for events that go across cycle boundaries by @yaxu in: [#225](https://codeberg.org/uzu/strudel/pulls/225)
+
+## september 2022
+
+- 2022-09-25T21:15:36+02:00 Reverb by @froos in: [#224](https://codeberg.org/uzu/strudel/pulls/224)
+- 2022-09-25T00:27:26+02:00 focus tweak for squeezeJoin - another go at fixing #216 by @yaxu in: [#221](https://codeberg.org/uzu/strudel/pulls/221)
+- 2022-09-24T23:17:21+02:00 support negative speeds by @froos in: [#222](https://codeberg.org/uzu/strudel/pulls/222)
+- 2022-09-24T21:55:15+02:00 Feedback Delay by @froos in: [#213](https://codeberg.org/uzu/strudel/pulls/213)
+- 2022-09-23T12:06:17+02:00 Fix squeeze join by @yaxu in: [#220](https://codeberg.org/uzu/strudel/pulls/220)
+- 2022-09-22T21:23:11+02:00 encapsulate webaudio output by @froos in: [#219](https://codeberg.org/uzu/strudel/pulls/219)
+- 2022-09-22T19:18:18+02:00 samples now have envelopes by @froos in: [#218](https://codeberg.org/uzu/strudel/pulls/218)
+- 2022-09-22T00:03:30+02:00 sampler features + fixes by @froos in: [#217](https://codeberg.org/uzu/strudel/pulls/217)
+- 2022-09-19T23:32:55+02:00 Just another docs PR by @froos in: [#215](https://codeberg.org/uzu/strudel/pulls/215)
+- 2022-09-17T23:47:43+02:00 Even more docs by @froos in: [#212](https://codeberg.org/uzu/strudel/pulls/212)
+- 2022-09-17T15:36:53+02:00 Webaudio guide by @froos in: [#207](https://codeberg.org/uzu/strudel/pulls/207)
+- 2022-09-16T00:21:29+02:00 Coarse crush shape by @froos in: [#205](https://codeberg.org/uzu/strudel/pulls/205)
+- 2022-09-15T21:04:28+02:00 add vowel to .out by @froos in: [#201](https://codeberg.org/uzu/strudel/pulls/201)
+- 2022-09-15T16:55:52+02:00 add rollup-plugin-visualizer to build by @froos in: [#200](https://codeberg.org/uzu/strudel/pulls/200)
+- 2022-09-14T23:46:39+02:00 document random functions by @froos in: [#199](https://codeberg.org/uzu/strudel/pulls/199)
+- 2022-09-09T22:04:40+02:00 Fix numbers in sampler by @froos in: [#196](https://codeberg.org/uzu/strudel/pulls/196)
+
+## august 2022
+
+- 2022-08-14T17:40:41+02:00 change "stride"/"offset" of successive degradeBy/chooseIn by @Ghost in: [#185](https://codeberg.org/uzu/strudel/pulls/185)
+- 2022-08-14T16:04:14+02:00 Soundfont file support by @froos in: [#183](https://codeberg.org/uzu/strudel/pulls/183)
+- 2022-08-14T15:55:50+02:00 fix regression: old way of setting frequencies was broken by @froos in: [#190](https://codeberg.org/uzu/strudel/pulls/190)
+- 2022-08-14T15:45:53+02:00 wait for prebake to finish before evaluating by @froos in: [#189](https://codeberg.org/uzu/strudel/pulls/189)
+- 2022-08-14T11:31:00+02:00 Fix codemirror bug by @froos in: [#186](https://codeberg.org/uzu/strudel/pulls/186)
+- 2022-08-13T16:29:53+02:00 scheduler improvements by @froos in: [#181](https://codeberg.org/uzu/strudel/pulls/181)
+- 2022-08-12T23:10:36+02:00 replace mocha with vitest by @froos in: [#175](https://codeberg.org/uzu/strudel/pulls/175)
+- 2022-08-07T00:59:07+02:00 incorporate elements of randomness to the mini notation by @Ghost in: [#165](https://codeberg.org/uzu/strudel/pulls/165)
+- 2022-08-06T23:32:16+02:00 fix some annoying bugs by @froos in: [#177](https://codeberg.org/uzu/strudel/pulls/177)
+- 2022-08-06T00:27:17+02:00 Replace react-codemirror6 with @uiw/react-codemirror by @froos in: [#173](https://codeberg.org/uzu/strudel/pulls/173)
+- 2022-08-04T22:19:46+02:00 add more shapeshifter flags by @froos in: [#99](https://codeberg.org/uzu/strudel/pulls/99)
+- 2022-08-04T22:06:38+02:00 Amend shapeshifter to allow use of dynamic import by @Ghost in: [#171](https://codeberg.org/uzu/strudel/pulls/171)
+- 2022-08-02T23:43:07+02:00 Talk fixes by @froos in: [#164](https://codeberg.org/uzu/strudel/pulls/164)
+- 2022-08-02T23:04:34+02:00 Pianoroll fixes by @froos in: [#163](https://codeberg.org/uzu/strudel/pulls/163)
+- 2022-08-02T22:57:43+02:00 fix: jsdoc comments by @froos in: [#169](https://codeberg.org/uzu/strudel/pulls/169)
+
+## july 2022
+
+- 2022-07-30T23:51:11+02:00 add chooseInWith/chooseCycles by @yaxu in: [#166](https://codeberg.org/uzu/strudel/pulls/166)
+- 2022-07-28T19:26:42+02:00 update to tutorial documentation by @Ghost in: [#162](https://codeberg.org/uzu/strudel/pulls/162)
+- 2022-07-12T18:58:47+02:00 add webdirt drum samples to prebake for general availability by @Ghost in: [#150](https://codeberg.org/uzu/strudel/pulls/150)
+- 2022-07-12T08:34:17+02:00 Final update to demo.pdf by @yaxu in: [#151](https://codeberg.org/uzu/strudel/pulls/151)
+
+## june 2022
+
+- 2022-06-28T21:54:35+02:00 Sampler optimizations and more by @froos in: [#148](https://codeberg.org/uzu/strudel/pulls/148)
+- 2022-06-26T12:58:53+02:00 can now generate short link for sharing by @froos in: [#146](https://codeberg.org/uzu/strudel/pulls/146)
+- 2022-06-24T22:16:04+02:00 flash effect on ctrl enter by @froos in: [#144](https://codeberg.org/uzu/strudel/pulls/144)
+- 2022-06-22T20:18:17+02:00 Pianoroll Object Support by @froos in: [#142](https://codeberg.org/uzu/strudel/pulls/142)
+- 2022-06-21T22:57:28+02:00 Serial twiddles by @yaxu in: [#141](https://codeberg.org/uzu/strudel/pulls/141)
+- 2022-06-21T22:20:05+02:00 Soundfont Support by @froos in: [#139](https://codeberg.org/uzu/strudel/pulls/139)
+- 2022-06-21T14:11:50+02:00 Fix createParam() by @yaxu in: [#140](https://codeberg.org/uzu/strudel/pulls/140)
+- 2022-06-18T23:24:42+02:00 Webaudio rewrite by @froos in: [#138](https://codeberg.org/uzu/strudel/pulls/138)
+- 2022-06-16T21:58:38+02:00 add onTrigger helper by @froos in: [#136](https://codeberg.org/uzu/strudel/pulls/136)
+- 2022-06-16T20:48:23+02:00 Scheduler improvements by @froos in: [#134](https://codeberg.org/uzu/strudel/pulls/134)
+- 2022-06-16T20:48:07+02:00 remove cycle + delta from onTrigger by @froos in: [#135](https://codeberg.org/uzu/strudel/pulls/135)
+- 2022-06-13T21:28:27+02:00 add createParam + createParams by @froos in: [#110](https://codeberg.org/uzu/strudel/pulls/110)
+- 2022-06-13T21:27:09+02:00 Pianoroll enhancements by @froos in: [#131](https://codeberg.org/uzu/strudel/pulls/131)
+- 2022-06-05T13:06:03+02:00 Fix link to contributing to tutorial docs by @Ghost in: [#129](https://codeberg.org/uzu/strudel/pulls/129)
+- 2022-06-02T00:20:45+02:00 Webdirt by @froos in: [#121](https://codeberg.org/uzu/strudel/pulls/121)
+- 2022-06-01T23:56:04+02:00 fix: #122 ctrl enter would add newline by @froos in: [#124](https://codeberg.org/uzu/strudel/pulls/124)
+- 2022-06-01T18:21:56+02:00 fix: #108 by @froos in: [#123](https://codeberg.org/uzu/strudel/pulls/123)
+
+## may 2022
+
+- 2022-05-29T09:54:59+02:00 In source doc by @froos in: [#117](https://codeberg.org/uzu/strudel/pulls/117)
+- 2022-05-20T11:49:10+02:00 react package + vite build by @froos in: [#116](https://codeberg.org/uzu/strudel/pulls/116)
+- 2022-05-15T22:29:26+02:00 Osc timing improvements by @yaxu in: [#113](https://codeberg.org/uzu/strudel/pulls/113)
+- 2022-05-15T22:28:24+02:00 loopAt by @yaxu in: [#114](https://codeberg.org/uzu/strudel/pulls/114)
+- 2022-05-09T20:09:47+02:00 `.brak()`, `.inside()` and `.outside()` by @yaxu in: [#112](https://codeberg.org/uzu/strudel/pulls/112)
+- 2022-05-06T15:18:41+02:00 In source doc by @yaxu in: [#105](https://codeberg.org/uzu/strudel/pulls/105)
+- 2022-05-04T22:59:31+02:00 Embed style by @froos in: [#109](https://codeberg.org/uzu/strudel/pulls/109)
+- 2022-05-03T01:40:22+02:00 Reset, Restart and other composers by @froos in: [#88](https://codeberg.org/uzu/strudel/pulls/88)
+- 2022-05-02T22:47:21+02:00 /embed package: web component for repl by @froos in: [#106](https://codeberg.org/uzu/strudel/pulls/106)
+- 2022-05-02T22:05:34+02:00 Tune tests by @froos in: [#104](https://codeberg.org/uzu/strudel/pulls/104)
+- 2022-05-01T14:33:11+02:00 Codemirror 6 by @froos in: [#97](https://codeberg.org/uzu/strudel/pulls/97)
+
+## april 2022
+
+- 2022-04-28T23:45:53+02:00 Work on Codemirror 6 highlighting by @Ghost in: [#102](https://codeberg.org/uzu/strudel/pulls/102)
+- 2022-04-28T15:46:06+02:00 Change to Affero GPL by @yaxu in: [#101](https://codeberg.org/uzu/strudel/pulls/101)
+- 2022-04-26T00:13:07+02:00 Paper by @froos in: [#98](https://codeberg.org/uzu/strudel/pulls/98)
+- 2022-04-25T22:10:21+02:00 Fiddles with cat/stack by @yaxu in: [#90](https://codeberg.org/uzu/strudel/pulls/90)
+- 2022-04-22T13:08:11+02:00 Add pattern composers, implements #82 by @yaxu in: [#83](https://codeberg.org/uzu/strudel/pulls/83)
+- 2022-04-21T20:57:45+02:00 Tidy up a couple of old files by @Ghost in: [#84](https://codeberg.org/uzu/strudel/pulls/84)
+- 2022-04-21T15:21:29+02:00 add `striate()` by @yaxu in: [#76](https://codeberg.org/uzu/strudel/pulls/76)
+- 2022-04-20T20:17:27+02:00 Webaudio in REPL by @froos in: [#77](https://codeberg.org/uzu/strudel/pulls/77)
+- 2022-04-19T15:31:34+02:00 Basic webserial support by @yaxu in: [#80](https://codeberg.org/uzu/strudel/pulls/80)
+- 2022-04-17T19:29:49+02:00 Try to fix appLeft / appRight by @yaxu in: [#75](https://codeberg.org/uzu/strudel/pulls/75)
+- 2022-04-16T13:26:57+02:00 More random functions by @yaxu in: [#74](https://codeberg.org/uzu/strudel/pulls/74)
+- 2022-04-16T11:13:26+02:00 Port `perlin` noise, `rangex`, and `palindrome` by @yaxu in: [#73](https://codeberg.org/uzu/strudel/pulls/73)
+- 2022-04-16T10:26:07+02:00 webaudio package by @froos in: [#26](https://codeberg.org/uzu/strudel/pulls/26)
+- 2022-04-15T20:18:06+02:00 More randomness, fix `rand`, and add `brand`, `irand` and `choose` by @yaxu in: [#70](https://codeberg.org/uzu/strudel/pulls/70)
+- 2022-04-15T11:29:51+02:00 First effort at rand() by @yaxu in: [#69](https://codeberg.org/uzu/strudel/pulls/69)
+- 2022-04-14T17:41:18+02:00 use new fixed version of osc-js package by @froos in: [#68](https://codeberg.org/uzu/strudel/pulls/68)
+- 2022-04-14T00:09:18+02:00 Speech output by @froos in: [#67](https://codeberg.org/uzu/strudel/pulls/67)
+- 2022-04-13T23:55:33+02:00 Separate out strudel.mjs, make index.mjs aggregate module by @yaxu in: [#62](https://codeberg.org/uzu/strudel/pulls/62)
+- 2022-04-13T17:26:45+02:00 More functions by @yaxu in: [#61](https://codeberg.org/uzu/strudel/pulls/61)
+- 2022-04-13T10:04:29+02:00 More functions by @yaxu in: [#56](https://codeberg.org/uzu/strudel/pulls/56)
+- 2022-04-12T17:04:04+02:00 OSC and SuperDirt support by @yaxu in: [#27](https://codeberg.org/uzu/strudel/pulls/27)
+- 2022-04-12T13:24:14+02:00 Implement `chop()` by @yaxu in: [#50](https://codeberg.org/uzu/strudel/pulls/50)
+- 2022-04-12T12:37:32+02:00 First run at squeezeBind, ref #32 by @yaxu in: [#48](https://codeberg.org/uzu/strudel/pulls/48)
+- 2022-04-12T00:03:37+02:00 Fix polymeter by @yaxu in: [#44](https://codeberg.org/uzu/strudel/pulls/44)
+- 2022-04-11T22:39:44+02:00 Compose by @froos in: [#40](https://codeberg.org/uzu/strudel/pulls/40)
+- 2022-04-11T21:49:35+02:00 Update tutorial.mdx by @Ghost in: [#38](https://codeberg.org/uzu/strudel/pulls/38)
+- 2022-04-11T08:43:44+02:00 Update tutorial.mdx by @Ghost in: [#37](https://codeberg.org/uzu/strudel/pulls/37)
+
+## march 2022
+
+- 2022-03-28T17:27:22+02:00 Add chunk, chunkBack and iterBack by @yaxu in: [#25](https://codeberg.org/uzu/strudel/pulls/25)
+- 2022-03-28T11:48:22+02:00 packaging by @froos in: [#24](https://codeberg.org/uzu/strudel/pulls/24)
+- 2022-03-22T12:06:48+01:00 Update package.json by @Ghost in: [#23](https://codeberg.org/uzu/strudel/pulls/23)
+- 2022-03-06T20:35:20+01:00 added _asNumber + interprete numbers as midi by @froos in: [#21](https://codeberg.org/uzu/strudel/pulls/21)
+
+## february 2022
+
+- 2022-02-28T00:32:29+01:00 Fix resolveState by @yaxu in: [#22](https://codeberg.org/uzu/strudel/pulls/22)
+- 2022-02-27T20:52:52+01:00 Stateful queries and events (WIP) by @yaxu in: [#14](https://codeberg.org/uzu/strudel/pulls/14)
+- 2022-02-27T18:04:07+01:00 fix: 💄 Enhance visualisation of the Tutorial on mobile by @Ghost in: [#19](https://codeberg.org/uzu/strudel/pulls/19)
+- 2022-02-26T21:16:34+01:00 test: 📦 Add missing dependency and a CI check, to prevent oversights ;p by @Ghost in: [#17](https://codeberg.org/uzu/strudel/pulls/17)
+- 2022-02-26T13:29:19+01:00 higher latencyHint by @froos in: [#16](https://codeberg.org/uzu/strudel/pulls/16)
+- 2022-02-23T22:15:11+01:00 add apply and layer, and missing div/mul methods by @yaxu in: [#15](https://codeberg.org/uzu/strudel/pulls/15)
+- 2022-02-21T01:02:07+01:00 Add continuous signals (sine, cosine, saw, etc) by @yaxu in: [#13](https://codeberg.org/uzu/strudel/pulls/13)
+- 2022-02-19T21:30:04+01:00 Added mask() and struct() by @yaxu in: [#11](https://codeberg.org/uzu/strudel/pulls/11)
+- 2022-02-19T16:07:07+01:00 Failing test for `when` WIP by @yaxu in: [#10](https://codeberg.org/uzu/strudel/pulls/10)
+- 2022-02-10T17:21:31+01:00 Bugfix every, and create more top level functions by @yaxu in: [#9](https://codeberg.org/uzu/strudel/pulls/9)
+- 2022-02-10T00:51:21+01:00 timeCat by @yaxu in: [#8](https://codeberg.org/uzu/strudel/pulls/8)
+- 2022-02-07T23:07:58+01:00 fixed editor crash by @froos in: [#7](https://codeberg.org/uzu/strudel/pulls/7)
+- 2022-02-07T19:08:40+01:00 krill parser + improved repl by @froos in: [#6](https://codeberg.org/uzu/strudel/pulls/6)
+- 2022-02-06T00:59:16+01:00 Patternify all the things by @yaxu in: [#5](https://codeberg.org/uzu/strudel/pulls/5)
+- 2022-02-05T23:31:37+01:00 update readme for local dev by @Ghost in: [#4](https://codeberg.org/uzu/strudel/pulls/4)
+- 2022-02-05T22:36:06+01:00 Fix path by @yaxu in: [#3](https://codeberg.org/uzu/strudel/pulls/3)
+- 2022-02-05T21:52:51+01:00 repl + reify functions by @froos in: [#2](https://codeberg.org/uzu/strudel/pulls/2)
+
+... hello from the bottom of this file! you seem to be interested in the history of strudel, so it's worth noting that the first [commit](https://codeberg.org/uzu/strudel/commit/38b5a0d5cdf28685b2b5e18d460772b70246207b) to the repo was actually on Jan 22, 2022, 09:24 PM GMT+1 by @yaxu which could be seen as strudel's birthday 🎂
From 62bf9abed336c4638bc5a2f760e74ec1b2f942e1 Mon Sep 17 00:00:00 2001
From: Aria
Date: Mon, 19 Jan 2026 09:52:49 -0800
Subject: [PATCH 068/200] Add retrig option for LFOs
---
packages/core/controls.mjs | 4 +++-
packages/superdough/modulators.mjs | 3 ++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index 834d963c..a5a1c0a1 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -3046,6 +3046,7 @@ registerSubControls('lfo', [
['skew', 'sk'],
['curve', 'cu'],
['sync', 's'],
+ ['retrig', 'rt'],
['fxi'],
]);
registerSubControls('env', [
@@ -3132,13 +3133,14 @@ Pattern.prototype.modulate = function (type, config, idPat) {
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
* @param {number | Pattern} [config.rate] Modulation rate. Aliases: r
+ * @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s
* @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
* @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc
* @param {number | Pattern} [config.shape] Shape index. Aliases: sh
* @param {number | Pattern} [config.skew] Skew amount. Aliases: sk
* @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: cu
- * @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s
+ * @param {number | Pattern} [config.retrig] If > 0.5, the LFO will retrigger on each event. Aliases: rt
* @param {number | Pattern} [config.fxi] FX index to target
* @param {string | Pattern} id ID to use for this modulator
* @returns Pattern
diff --git a/packages/superdough/modulators.mjs b/packages/superdough/modulators.mjs
index 05674baa..76fc4c7f 100644
--- a/packages/superdough/modulators.mjs
+++ b/packages/superdough/modulators.mjs
@@ -98,6 +98,7 @@ export const connectLFO = (id, params, nodeTracker) => {
fxi = 'main',
depth = 1,
depthabs,
+ retrig = 0,
...filteredParams
} = params;
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
@@ -109,7 +110,7 @@ export const connectLFO = (id, params, nodeTracker) => {
const modParams = {
...filteredParams,
frequency: sync !== undefined ? sync * cps : rate,
- time: cycle / cps,
+ time: retrig > 0.5 ? 0 : cycle / cps,
depth: depthValue,
min,
max,
From 87e723649ead1a899842c30af71d58dd356c30a5 Mon Sep 17 00:00:00 2001
From: Dsm0
Date: Sun, 18 Jan 2026 23:43:11 -0800
Subject: [PATCH 069/200] fixed highlighting issue
---
packages/codemirror/highlight.mjs | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/packages/codemirror/highlight.mjs b/packages/codemirror/highlight.mjs
index 5cc6d3a1..b37390ef 100644
--- a/packages/codemirror/highlight.mjs
+++ b/packages/codemirror/highlight.mjs
@@ -25,7 +25,6 @@ const miniLocations = StateField.define({
//block-based eval case
if (e.value.range) {
const stateMiniLocations = getMiniLocationsFromDecorations(locations);
- const existingById = new Map(stateMiniLocations.map(({ id, from, to }) => [id, [from, to]]));
const normalized = e.value.locations
.filter(([from]) => from < tr.newDoc.length)
@@ -35,13 +34,12 @@ const miniLocations = StateField.define({
const marks = normalized.map((range) => {
const id = range.join(':');
- const useRange = existingById.get(id) || range;
return Decoration.mark({
id,
// this green is only to verify that the decoration moves when the document is edited
// it will be removed later, so the mark is not visible by default
attributes: { style: `background-color: #00CA2880` },
- }).range(...useRange); // -> Decoration
+ }).range(...range); // -> Decoration
});
const previousMarks = stateMiniLocations
From c96b1c2faf8c31bbe38f31714e2c6432fc9a81e6 Mon Sep 17 00:00:00 2001
From: Dsm0
Date: Mon, 19 Jan 2026 01:15:55 -0800
Subject: [PATCH 070/200] fixed anonymous label casing
---
packages/core/repl.mjs | 21 ++++++++++++---------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs
index 8d023c72..3ff43e2f 100644
--- a/packages/core/repl.mjs
+++ b/packages/core/repl.mjs
@@ -473,15 +473,12 @@ export function repl({
const labels = meta.labels || [];
+ // Check for anonymous labels (labels starting with '$')
+ const hasAnonymousLabel = labels.some((label) => label.name.startsWith('$'));
+
// Store code blocks in dictionary using labels as keys
- if (labels.length > 0) {
- for (let i = 0; i < labels.length; i++) {
- // processing transpiler output instead of code is simply to avoid
- // extra regex in detecting whether or not an inline widget has been commented out
- processLabeledBlock(labels, i, meta.output, options, meta);
- }
- } else if (pattern !== silence) {
- // variable/function declarations that return silence are allowed,
+ if (hasAnonymousLabel) {
+ // variable/function declarations that don't return patterns are allowed,
// but anonymous pattern blocks pose an issue for block-based evaluation
// if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder
@@ -490,11 +487,17 @@ export function repl({
// otherwise we'll have no idea of which pattern is being overridden
// (we probably need to update the docs on this)
-
// we could easily enable it, but it would confuse a lot of people
+
throw new Error(
'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)',
);
+ } else if (labels.length > 0) {
+ for (let i = 0; i < labels.length; i++) {
+ // processing transpiler output instead of code is simply to avoid
+ // extra regex in detecting whether or not an inline widget has been commented out
+ processLabeledBlock(labels, i, meta.output, options, meta);
+ }
} else {
// Declaration block (variable/function that returns silence)
// Store it so its miniLocations are preserved for highlighting patterns stored in variables
From 38b7bcb3d288562424e797f0ccc41111fb5a3773 Mon Sep 17 00:00:00 2001
From: Felix Roos
Date: Mon, 19 Jan 2026 23:00:14 +0100
Subject: [PATCH 071/200] style tweaks
---
.../repl/components/button/action-button.jsx | 9 +--
.../components/incrementor/Incrementor.jsx | 2 +-
.../components/panel/AudioDeviceSelector.jsx | 4 +-
.../panel/AudioEngineTargetSelector.jsx | 6 +-
.../src/repl/components/panel/ExportTab.jsx | 7 +--
website/src/repl/components/panel/Forms.jsx | 2 +-
.../src/repl/components/panel/PatternsTab.jsx | 2 +-
.../src/repl/components/panel/Reference.jsx | 2 +-
.../src/repl/components/panel/SelectInput.jsx | 20 -------
.../src/repl/components/panel/SettingsTab.jsx | 57 +++++++++++++++----
.../src/repl/components/panel/SoundsTab.jsx | 2 +-
.../src/repl/components/textbox/Textbox.jsx | 14 -----
12 files changed, 62 insertions(+), 65 deletions(-)
delete mode 100644 website/src/repl/components/panel/SelectInput.jsx
delete mode 100644 website/src/repl/components/textbox/Textbox.jsx
diff --git a/website/src/repl/components/button/action-button.jsx b/website/src/repl/components/button/action-button.jsx
index de674b69..846d36c7 100644
--- a/website/src/repl/components/button/action-button.jsx
+++ b/website/src/repl/components/button/action-button.jsx
@@ -13,16 +13,13 @@ export function SpecialActionButton(props) {
const { className, ...buttonProps } = props;
return (
-
+
);
}
export function ActionInput({ label, className, ...props }) {
return (
-
+
{label}
@@ -35,7 +32,7 @@ export function SpecialActionInput({ className, ...props }) {
{props.label}}
+ label={{props.label} }
/>
);
}
diff --git a/website/src/repl/components/incrementor/Incrementor.jsx b/website/src/repl/components/incrementor/Incrementor.jsx
index 0645580d..8fdfd492 100644
--- a/website/src/repl/components/incrementor/Incrementor.jsx
+++ b/website/src/repl/components/incrementor/Incrementor.jsx
@@ -1,4 +1,4 @@
-import { Textbox } from '../textbox/Textbox';
+import { Textbox } from '@src/repl/components/panel/SettingsTab';
import cx from '@src/cx.mjs';
function IncButton({ children, className, ...buttonProps }) {
diff --git a/website/src/repl/components/panel/AudioDeviceSelector.jsx b/website/src/repl/components/panel/AudioDeviceSelector.jsx
index 288ffc0a..1e41ed66 100644
--- a/website/src/repl/components/panel/AudioDeviceSelector.jsx
+++ b/website/src/repl/components/panel/AudioDeviceSelector.jsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
-import { SelectInput } from './SelectInput';
+import { SelectInputDuplicate } from '@src/repl/components/panel/SettingsTab';
import { getAudioDevices } from '@strudel/webaudio';
const initdevices = new Map();
@@ -29,7 +29,7 @@ export function AudioDeviceSelector({ audioDeviceName, onChange, isDisabled }) {
options.set(deviceName, deviceName);
});
return (
-
-
+
+
{target === audioEngineTargets.osc && (
diff --git a/website/src/repl/components/panel/ExportTab.jsx b/website/src/repl/components/panel/ExportTab.jsx
index cbb132e4..071d8d69 100644
--- a/website/src/repl/components/panel/ExportTab.jsx
+++ b/website/src/repl/components/panel/ExportTab.jsx
@@ -1,10 +1,7 @@
-import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import cx from '@src/cx.mjs';
-import NumberInput from '@src/repl/components/NumberInput';
-import { useEffect, useState } from 'react';
-import { Textbox } from '../textbox/Textbox';
+import { useState } from 'react';
+import { Textbox } from '@src/repl/components/panel/SettingsTab';
import { getAudioContext } from '@strudel/webaudio';
-import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
function Checkbox({ label, value, onChange, disabled = false }) {
return (
diff --git a/website/src/repl/components/panel/Forms.jsx b/website/src/repl/components/panel/Forms.jsx
index 4cb73a46..79c17e45 100644
--- a/website/src/repl/components/panel/Forms.jsx
+++ b/website/src/repl/components/panel/Forms.jsx
@@ -13,7 +13,7 @@ export function ButtonGroup({ value, onChange, items }) {
// i === 0 && 'rounded-l-md',
// i === arr.length - 1 && 'rounded-r-md',
// value === key ? 'bg-background' : 'bg-lineHighlight',
- value === key ? 'border-foreground' : 'border-transparent',
+ value === key ? 'border-foreground' : 'border-background',
)}
>
{label.toLowerCase()}
diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx
index c72ebaa3..d0e54e34 100644
--- a/website/src/repl/components/panel/PatternsTab.jsx
+++ b/website/src/repl/components/panel/PatternsTab.jsx
@@ -18,7 +18,7 @@ import { Pagination } from '../pagination/Pagination.jsx';
import { useState } from 'react';
import { useDebounce } from '../usedebounce.jsx';
import cx from '@src/cx.mjs';
-import { Textbox } from '../textbox/Textbox.jsx';
+import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx';
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx
index f8132066..40712bfa 100644
--- a/website/src/repl/components/panel/Reference.jsx
+++ b/website/src/repl/components/panel/Reference.jsx
@@ -1,7 +1,7 @@
import { memo, useEffect, useMemo, useState, Fragment } from 'react';
import jsdocJson from '../../../../../doc.json';
-import { Textbox } from '../textbox/Textbox';
+import { Textbox } from '@src/repl/components/panel/SettingsTab';
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
diff --git a/website/src/repl/components/panel/SelectInput.jsx b/website/src/repl/components/panel/SelectInput.jsx
deleted file mode 100644
index b4780967..00000000
--- a/website/src/repl/components/panel/SelectInput.jsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-// value: ?ID, options: Map, onChange: ID => null, onClick: event => void, isDisabled: boolean
-export function SelectInput({ value, options, onChange, onClick, isDisabled }) {
- return (
- onChange(e.target.value)}
- >
- {options.size == 0 && {`${value ?? 'select an option'}`} }
- {Array.from(options.keys()).map((id) => (
-
- {options.get(id)}
-
- ))}
-
- );
-}
diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx
index 7f488c54..3baeec22 100644
--- a/website/src/repl/components/panel/SettingsTab.jsx
+++ b/website/src/repl/components/panel/SettingsTab.jsx
@@ -1,6 +1,5 @@
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
import { themes } from '@strudel/codemirror';
-import { Textbox } from '../textbox/Textbox.jsx';
import { confirmAndReloadPage, isUdels } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx';
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
@@ -10,22 +9,60 @@ import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@
import { SpecialActionButton } from '../button/action-button.jsx';
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
+function cx(...classes) {
+ // : Array
+ return classes.filter(Boolean).join(' ');
+}
+
+const inputClass =
+ 'bg-background text-sm border rounded-0 text-foreground border-foreground placeholder-foreground focus:outline-none focus:ring-0 focus:border-foreground';
+
+export function Textbox({ onChange, className, ...inputProps }) {
+ return (
+ onChange(e.target.value)} {...inputProps} />
+ );
+}
+
function Checkbox({ label, value, onChange, disabled = false }) {
return (
-
+
{' ' + label}
);
}
-function SelectInput({ value, options, onChange }) {
+// value: ?ID, options: Map, onChange: ID => null, onClick: event => void, isDisabled: boolean
+export function SelectInputDuplicate({ value, options, onChange, onClick, isDisabled }) {
return (
onChange(e.target.value)}
>
+ {options.size == 0 && {`${value ?? 'select an option'}`} }
+ {Array.from(options.keys()).map((id) => (
+
+ {options.get(id)}
+
+ ))}
+
+ );
+}
+
+function SelectInput({ value, options, onChange }) {
+ return (
+ onChange(e.target.value)}>
{Object.entries(options).map(([k, label]) => (
{label}
@@ -37,9 +74,9 @@ function SelectInput({ value, options, onChange }) {
function NumberSlider({ value, onChange, step = 1, ...rest }) {
return (
-
+
onChange(Number(e.target.value))}
/>
@@ -60,7 +97,7 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) {
function FormItem({ label, children, sublabel }) {
return (
- {label}
+ {label}
{children}
);
@@ -178,7 +215,7 @@ export function SettingsTab({ started }) {
settingsMap.setKey('theme', theme)} />
-
+
onChange(e.target.value)}
- {...inputProps}
- />
- );
-}
From de83805fd9570b3d0cfdec407220d4dacb53ae81 Mon Sep 17 00:00:00 2001
From: Felix Roos
Date: Tue, 20 Jan 2026 00:06:44 +0100
Subject: [PATCH 072/200] add muted theme color + use it for inputs and tags
---
packages/codemirror/themes/CutiePi.mjs | 3 ++-
packages/codemirror/themes/algoboy.mjs | 1 +
packages/codemirror/themes/androidstudio.mjs | 2 ++
packages/codemirror/themes/archBtw.mjs | 1 +
packages/codemirror/themes/atomone.mjs | 2 ++
packages/codemirror/themes/aura.mjs | 2 ++
packages/codemirror/themes/bbedit.mjs | 2 ++
packages/codemirror/themes/blackscreen.mjs | 3 ++-
packages/codemirror/themes/bluescreen.mjs | 3 ++-
.../codemirror/themes/bluescreenlight.mjs | 1 +
packages/codemirror/themes/darcula.mjs | 2 ++
packages/codemirror/themes/dracula.mjs | 2 ++
packages/codemirror/themes/duotoneDark.mjs | 2 ++
packages/codemirror/themes/eclipse.mjs | 2 ++
packages/codemirror/themes/fruitDaw.mjs | 1 +
packages/codemirror/themes/githubDark.mjs | 2 ++
packages/codemirror/themes/githubLight.mjs | 2 ++
packages/codemirror/themes/green-text.mjs | 1 +
packages/codemirror/themes/gruvboxDark.mjs | 2 ++
packages/codemirror/themes/gruvboxLight.mjs | 2 ++
packages/codemirror/themes/materialDark.mjs | 2 ++
packages/codemirror/themes/materialLight.mjs | 4 +++-
packages/codemirror/themes/monokai.mjs | 2 ++
packages/codemirror/themes/noctisLilac.mjs | 2 ++
packages/codemirror/themes/nord.mjs | 2 ++
packages/codemirror/themes/red-text.mjs | 1 +
packages/codemirror/themes/solarizedDark.mjs | 2 ++
packages/codemirror/themes/solarizedLight.mjs | 2 ++
packages/codemirror/themes/sonic-pink.mjs | 1 +
packages/codemirror/themes/strudel-theme.mjs | 1 +
packages/codemirror/themes/sublime.mjs | 2 ++
packages/codemirror/themes/teletext.mjs | 3 ++-
.../codemirror/themes/tokioNightStorm.mjs | 6 ++++--
packages/codemirror/themes/tokyoNight.mjs | 6 ++++--
packages/codemirror/themes/tokyoNightDay.mjs | 2 ++
packages/codemirror/themes/vscodeDark.mjs | 2 ++
packages/codemirror/themes/vscodeLight.mjs | 2 ++
packages/codemirror/themes/whitescreen.mjs | 3 ++-
packages/codemirror/themes/xcodeLight.mjs | 2 ++
website/src/repl/Repl.css | 12 ------------
website/src/repl/components/panel/Forms.jsx | 6 +++---
.../src/repl/components/panel/Reference.jsx | 10 +++++-----
.../src/repl/components/panel/SettingsTab.jsx | 4 ++--
website/tailwind.config.cjs | 19 ++++++++++---------
44 files changed, 95 insertions(+), 41 deletions(-)
diff --git a/packages/codemirror/themes/CutiePi.mjs b/packages/codemirror/themes/CutiePi.mjs
index 64b38eaa..4d0e768c 100644
--- a/packages/codemirror/themes/CutiePi.mjs
+++ b/packages/codemirror/themes/CutiePi.mjs
@@ -17,10 +17,11 @@ export const settings = {
background: 'white',
lineBackground: 'transparent',
foreground: deepPurple,
+ muted: deepPurple + '50',
caret: '#797977',
selection: yellowPink,
selectionMatch: '#2B323D',
- gutterBackground: grey,
+ gutterBackground: pinkAccent,
gutterForeground: lightGrey,
gutterBorder: 'transparent',
lineHighlight: pinkAccent,
diff --git a/packages/codemirror/themes/algoboy.mjs b/packages/codemirror/themes/algoboy.mjs
index 049ed596..5eb90ff7 100644
--- a/packages/codemirror/themes/algoboy.mjs
+++ b/packages/codemirror/themes/algoboy.mjs
@@ -31,6 +31,7 @@ const palette = palettes['Sour Watermelon B'];
export const settings = {
background: palette[3],
foreground: palette[1],
+ muted: palette[1] + '50',
caret: palette[0],
selection: palette[0],
selectionMatch: palette[1],
diff --git a/packages/codemirror/themes/androidstudio.mjs b/packages/codemirror/themes/androidstudio.mjs
index 77c16c33..35b5b746 100644
--- a/packages/codemirror/themes/androidstudio.mjs
+++ b/packages/codemirror/themes/androidstudio.mjs
@@ -8,6 +8,7 @@ export const settings = {
background: '#282b2e',
lineBackground: '#282b2e99',
foreground: '#a9b7c6',
+ muted: '#a9b7c650',
caret: '#00FF00',
selection: '#343739',
selectionMatch: '#343739',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#282b2e',
foreground: '#a9b7c6',
+ muted: '#a9b7c650',
caret: '#00FF00',
selection: '#4e5254',
selectionMatch: '#4e5254',
diff --git a/packages/codemirror/themes/archBtw.mjs b/packages/codemirror/themes/archBtw.mjs
index 87546569..3bc5684e 100644
--- a/packages/codemirror/themes/archBtw.mjs
+++ b/packages/codemirror/themes/archBtw.mjs
@@ -12,6 +12,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[1],
+ muted: hex[2],
selection: hex[2],
selectionMatch: hex[0],
gutterBackground: hex[0],
diff --git a/packages/codemirror/themes/atomone.mjs b/packages/codemirror/themes/atomone.mjs
index ab9cd360..bc7a3254 100644
--- a/packages/codemirror/themes/atomone.mjs
+++ b/packages/codemirror/themes/atomone.mjs
@@ -11,6 +11,7 @@ export const settings = {
background: '#272C35',
lineBackground: '#272C3599',
foreground: 'hsl(220, 14%, 71%)',
+ muted: 'hsl(220, 14%, 41%)',
caret: '#797977',
selection: '#ffffff30',
selectionMatch: '#2B323D',
@@ -25,6 +26,7 @@ export default createTheme({
settings: {
background: '#272C35',
foreground: '#9d9b97',
+ muted: 'hsl(220, 14%, 41%)',
caret: '#797977',
selection: '#3d4c64',
selectionMatch: '#3d4c64',
diff --git a/packages/codemirror/themes/aura.mjs b/packages/codemirror/themes/aura.mjs
index b42a5839..08bb5ea2 100644
--- a/packages/codemirror/themes/aura.mjs
+++ b/packages/codemirror/themes/aura.mjs
@@ -5,6 +5,7 @@ export const settings = {
background: '#21202e',
lineBackground: '#21202e99',
foreground: '#edecee',
+ muted: '#edecee50',
caret: '#a277ff',
selection: '#3d375e7f',
selectionMatch: '#3d375e7f',
@@ -18,6 +19,7 @@ export default createTheme({
settings: {
background: '#21202e',
foreground: '#edecee',
+ muted: '#edecee50',
caret: '#a277ff',
selection: '#5a51898f',
selectionMatch: '#5a51898f',
diff --git a/packages/codemirror/themes/bbedit.mjs b/packages/codemirror/themes/bbedit.mjs
index 535bf9ea..0555ac1a 100644
--- a/packages/codemirror/themes/bbedit.mjs
+++ b/packages/codemirror/themes/bbedit.mjs
@@ -6,6 +6,7 @@ export const settings = {
background: '#FFFFFF',
lineBackground: '#FFFFFF99',
foreground: '#000000',
+ muted: '#00000050',
caret: '#FBAC52',
selection: '#FFD420',
selectionMatch: '#FFD420',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#FFFFFF',
foreground: '#000000',
+ muted: '#00000050',
caret: '#FBAC52',
selection: '#FFD420',
selectionMatch: '#FFD420',
diff --git a/packages/codemirror/themes/blackscreen.mjs b/packages/codemirror/themes/blackscreen.mjs
index 2c45df11..462dc200 100644
--- a/packages/codemirror/themes/blackscreen.mjs
+++ b/packages/codemirror/themes/blackscreen.mjs
@@ -2,7 +2,8 @@ import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: 'black',
- foreground: 'white', // whats that?
+ foreground: 'white',
+ muted: '#ffffff50',
caret: 'white',
selection: '#ffffff20',
selectionMatch: '#036dd626',
diff --git a/packages/codemirror/themes/bluescreen.mjs b/packages/codemirror/themes/bluescreen.mjs
index 97d165c9..a3b9648d 100644
--- a/packages/codemirror/themes/bluescreen.mjs
+++ b/packages/codemirror/themes/bluescreen.mjs
@@ -3,7 +3,8 @@ import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#051DB5',
lineBackground: '#051DB550',
- foreground: 'white', // whats that?
+ foreground: 'white',
+ muted: '#ffffff50',
caret: 'white',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
diff --git a/packages/codemirror/themes/bluescreenlight.mjs b/packages/codemirror/themes/bluescreenlight.mjs
index 031a3da3..7e10659f 100644
--- a/packages/codemirror/themes/bluescreenlight.mjs
+++ b/packages/codemirror/themes/bluescreenlight.mjs
@@ -11,6 +11,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
+ muted: hex[3],
selection: hex[3],
selectionMatch: hex[0],
gutterBackground: hex[0],
diff --git a/packages/codemirror/themes/darcula.mjs b/packages/codemirror/themes/darcula.mjs
index ead66e66..ee510764 100644
--- a/packages/codemirror/themes/darcula.mjs
+++ b/packages/codemirror/themes/darcula.mjs
@@ -9,6 +9,7 @@ export const settings = {
background: '#242424',
lineBackground: '#24242499',
foreground: '#f8f8f2',
+ muted: '#f8f8f250',
caret: '#FFFFFF',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
@@ -23,6 +24,7 @@ export default createTheme({
settings: {
background: '#242424',
foreground: '#f8f8f2',
+ muted: '#f8f8f250',
caret: '#FFFFFF',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
diff --git a/packages/codemirror/themes/dracula.mjs b/packages/codemirror/themes/dracula.mjs
index bdd9617d..514b6371 100644
--- a/packages/codemirror/themes/dracula.mjs
+++ b/packages/codemirror/themes/dracula.mjs
@@ -11,6 +11,7 @@ export const settings = {
background: '#282a36',
lineBackground: '#282a3699',
foreground: '#f8f8f2',
+ muted: '#f8f8f250',
caret: '#f8f8f0',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
@@ -27,6 +28,7 @@ export default createTheme({
settings: {
background: '#282a36',
foreground: '#f8f8f2',
+ muted: '#f8f8f250',
caret: '#f8f8f0',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
diff --git a/packages/codemirror/themes/duotoneDark.mjs b/packages/codemirror/themes/duotoneDark.mjs
index f54973e8..72e07cd6 100644
--- a/packages/codemirror/themes/duotoneDark.mjs
+++ b/packages/codemirror/themes/duotoneDark.mjs
@@ -10,6 +10,7 @@ export const settings = {
background: '#2a2734',
lineBackground: '#2a273499',
foreground: '#eeebff',
+ muted: '#eeebff50',
caret: '#ffad5c',
selection: 'rgba(255, 255, 255, 0.1)',
gutterBackground: '#2a2734',
@@ -22,6 +23,7 @@ export default createTheme({
settings: {
background: '#2a2734',
foreground: '#6c6783',
+ muted: '#eeebff50',
caret: '#ffad5c',
selection: '#9a86fd',
selectionMatch: '#9a86fd',
diff --git a/packages/codemirror/themes/eclipse.mjs b/packages/codemirror/themes/eclipse.mjs
index 8082b59d..8c3671eb 100644
--- a/packages/codemirror/themes/eclipse.mjs
+++ b/packages/codemirror/themes/eclipse.mjs
@@ -6,6 +6,7 @@ export const settings = {
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#000',
+ muted: '#00000050',
caret: '#FFFFFF',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#fff',
foreground: '#000',
+ muted: '#00000050',
caret: '#FFFFFF',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
diff --git a/packages/codemirror/themes/fruitDaw.mjs b/packages/codemirror/themes/fruitDaw.mjs
index c9e55779..68b7857e 100644
--- a/packages/codemirror/themes/fruitDaw.mjs
+++ b/packages/codemirror/themes/fruitDaw.mjs
@@ -23,6 +23,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[10],
+ muted: hex[7],
selection: hex[8],
selectionMatch: hex[0],
gutterBackground: hex[3],
diff --git a/packages/codemirror/themes/githubDark.mjs b/packages/codemirror/themes/githubDark.mjs
index 477e7d7b..3b11eac3 100644
--- a/packages/codemirror/themes/githubDark.mjs
+++ b/packages/codemirror/themes/githubDark.mjs
@@ -5,6 +5,7 @@ export const settings = {
background: '#0d1117',
lineBackground: '#0d111799',
foreground: '#c9d1d9',
+ muted: '#c9d1d950',
caret: '#c9d1d9',
selection: '#003d73',
selectionMatch: '#003d73',
@@ -16,6 +17,7 @@ export default createTheme({
settings: {
background: '#0d1117',
foreground: '#c9d1d9',
+ muted: '#c9d1d950',
caret: '#c9d1d9',
selection: '#003d73',
selectionMatch: '#003d73',
diff --git a/packages/codemirror/themes/githubLight.mjs b/packages/codemirror/themes/githubLight.mjs
index 2ccec93b..26b1d572 100644
--- a/packages/codemirror/themes/githubLight.mjs
+++ b/packages/codemirror/themes/githubLight.mjs
@@ -6,6 +6,7 @@ export const settings = {
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#24292e',
+ muted: '#24292e50',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
@@ -17,6 +18,7 @@ export default createTheme({
settings: {
background: '#fff',
foreground: '#24292e',
+ muted: '#24292e50',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
diff --git a/packages/codemirror/themes/green-text.mjs b/packages/codemirror/themes/green-text.mjs
index 08c7a4bb..7d3a47fb 100644
--- a/packages/codemirror/themes/green-text.mjs
+++ b/packages/codemirror/themes/green-text.mjs
@@ -13,6 +13,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
+ muted: hex[2] + '50',
selection: hex[4],
selectionMatch: hex[0],
gutterBackground: hex[0],
diff --git a/packages/codemirror/themes/gruvboxDark.mjs b/packages/codemirror/themes/gruvboxDark.mjs
index 67d8bffc..2cc9274e 100644
--- a/packages/codemirror/themes/gruvboxDark.mjs
+++ b/packages/codemirror/themes/gruvboxDark.mjs
@@ -10,6 +10,7 @@ export const settings = {
background: '#282828',
lineBackground: '#28282899',
foreground: '#ebdbb2',
+ muted: '#ebdbb250',
caret: '#ebdbb2',
selection: '#bdae93',
selectionMatch: '#bdae93',
@@ -23,6 +24,7 @@ export default createTheme({
settings: {
background: '#282828',
foreground: '#ebdbb2',
+ muted: '#ebdbb250',
caret: '#ebdbb2',
selection: '#b99d555c',
selectionMatch: '#b99d555c',
diff --git a/packages/codemirror/themes/gruvboxLight.mjs b/packages/codemirror/themes/gruvboxLight.mjs
index 5d4b4ec0..8e529b21 100644
--- a/packages/codemirror/themes/gruvboxLight.mjs
+++ b/packages/codemirror/themes/gruvboxLight.mjs
@@ -11,6 +11,7 @@ export const settings = {
background: '#fbf1c7',
lineBackground: '#fbf1c799',
foreground: '#3c3836',
+ muted: '#3c383650',
caret: '#af3a03',
selection: '#ebdbb2',
selectionMatch: '#bdae93',
@@ -25,6 +26,7 @@ export default createTheme({
settings: {
background: '#fbf1c7',
foreground: '#3c3836',
+ muted: '#3c383650',
caret: '#af3a03',
selection: '#bdae9391',
selectionMatch: '#bdae9391',
diff --git a/packages/codemirror/themes/materialDark.mjs b/packages/codemirror/themes/materialDark.mjs
index f1341288..0c4ef054 100644
--- a/packages/codemirror/themes/materialDark.mjs
+++ b/packages/codemirror/themes/materialDark.mjs
@@ -5,6 +5,7 @@ export const settings = {
background: '#212121',
lineBackground: '#21212199',
foreground: '#bdbdbd',
+ muted: '#bdbdbd50',
caret: '#a0a4ae',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#212121',
foreground: '#bdbdbd',
+ muted: '#bdbdbd50',
caret: '#a0a4ae',
selection: '#d7d4f063',
selectionMatch: '#d7d4f063',
diff --git a/packages/codemirror/themes/materialLight.mjs b/packages/codemirror/themes/materialLight.mjs
index d2598c64..798842bf 100644
--- a/packages/codemirror/themes/materialLight.mjs
+++ b/packages/codemirror/themes/materialLight.mjs
@@ -5,7 +5,8 @@ export const settings = {
light: true,
background: '#FAFAFA',
lineBackground: '#FAFAFA99',
- foreground: '#90A4AE',
+ foreground: '#6182B8',
+ muted: '#6182B850',
caret: '#272727',
selection: '#80CBC440',
selectionMatch: '#FAFAFA',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#FAFAFA',
foreground: '#90A4AE',
+ muted: '#6182B850',
caret: '#272727',
selection: '#80CBC440',
selectionMatch: '#80CBC440',
diff --git a/packages/codemirror/themes/monokai.mjs b/packages/codemirror/themes/monokai.mjs
index 294b946b..926e3821 100644
--- a/packages/codemirror/themes/monokai.mjs
+++ b/packages/codemirror/themes/monokai.mjs
@@ -5,6 +5,7 @@ export const settings = {
background: '#272822',
lineBackground: '#27282299',
foreground: '#FFFFFF',
+ muted: '#FFFFFF50',
caret: '#FFFFFF',
selection: '#49483E',
selectionMatch: '#49483E',
@@ -18,6 +19,7 @@ export default createTheme({
settings: {
background: '#272822',
foreground: '#FFFFFF',
+ muted: '#FFFFFF50',
caret: '#FFFFFF',
selection: '#49483E',
selectionMatch: '#49483E',
diff --git a/packages/codemirror/themes/noctisLilac.mjs b/packages/codemirror/themes/noctisLilac.mjs
index b11c6b40..39804339 100644
--- a/packages/codemirror/themes/noctisLilac.mjs
+++ b/packages/codemirror/themes/noctisLilac.mjs
@@ -6,6 +6,7 @@ export const settings = {
background: '#f2f1f8',
lineBackground: '#f2f1f899',
foreground: '#0c006b',
+ muted: '#0c006b50',
caret: '#5c49e9',
selection: '#d5d1f2',
selectionMatch: '#d5d1f2',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#f2f1f8',
foreground: '#0c006b',
+ muted: '#0c006b50',
caret: '#5c49e9',
selection: '#d5d1f2',
selectionMatch: '#d5d1f2',
diff --git a/packages/codemirror/themes/nord.mjs b/packages/codemirror/themes/nord.mjs
index ce96bca5..efa4b6bf 100644
--- a/packages/codemirror/themes/nord.mjs
+++ b/packages/codemirror/themes/nord.mjs
@@ -5,6 +5,7 @@ export const settings = {
background: '#2e3440',
lineBackground: '#2e344099',
foreground: '#FFFFFF',
+ muted: '#FFFFFF50',
caret: '#FFFFFF',
selection: '#3b4252',
selectionMatch: '#e5e9f0',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#2e3440',
foreground: '#FFFFFF',
+ muted: '#FFFFFF50',
caret: '#FFFFFF',
selection: '#00000073',
selectionMatch: '#00000073',
diff --git a/packages/codemirror/themes/red-text.mjs b/packages/codemirror/themes/red-text.mjs
index 44dda5dc..5ba9d215 100644
--- a/packages/codemirror/themes/red-text.mjs
+++ b/packages/codemirror/themes/red-text.mjs
@@ -13,6 +13,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
+ muted: hex[2] + '50',
selection: hex[4],
selectionMatch: hex[0],
gutterBackground: hex[0],
diff --git a/packages/codemirror/themes/solarizedDark.mjs b/packages/codemirror/themes/solarizedDark.mjs
index c6b645f7..6848b537 100644
--- a/packages/codemirror/themes/solarizedDark.mjs
+++ b/packages/codemirror/themes/solarizedDark.mjs
@@ -5,6 +5,7 @@ export const settings = {
background: '#002b36',
lineBackground: '#002b3699',
foreground: '#93a1a1',
+ muted: '#93a1a150',
caret: '#839496',
selection: '#173541',
selectionMatch: '#aafe661a',
@@ -45,6 +46,7 @@ export default createTheme({
settings: {
background: c.background,
foreground: c.foreground,
+ muted: c.muted,
caret: c.cursor,
selection: c.selection,
selectionMatch: c.selection,
diff --git a/packages/codemirror/themes/solarizedLight.mjs b/packages/codemirror/themes/solarizedLight.mjs
index b6828ff6..0a0c61a1 100644
--- a/packages/codemirror/themes/solarizedLight.mjs
+++ b/packages/codemirror/themes/solarizedLight.mjs
@@ -8,6 +8,7 @@ export const settings = {
background: '#fdf6e3',
lineBackground: '#fdf6e399',
foreground: '#657b83',
+ muted: '#657b8350',
caret: '#586e75',
selection: '#dfd9c8',
selectionMatch: '#dfd9c8',
@@ -19,6 +20,7 @@ export const settings = {
const c = {
background: '#FDF6E3',
foreground: '#657B83',
+ muted: '#657b8350',
selection: '#EEE8D5',
selectionMatch: '#EEE8D5',
cursor: '#657B83',
diff --git a/packages/codemirror/themes/sonic-pink.mjs b/packages/codemirror/themes/sonic-pink.mjs
index 5a19555a..19c5e5e8 100644
--- a/packages/codemirror/themes/sonic-pink.mjs
+++ b/packages/codemirror/themes/sonic-pink.mjs
@@ -13,6 +13,7 @@ export const settings = {
background: '#000000',
lineBackground: 'transparent',
foreground: hex[4],
+ muted: hex[6],
selection: hex[6],
gutterBackground: hex[0],
gutterForeground: hex[5],
diff --git a/packages/codemirror/themes/strudel-theme.mjs b/packages/codemirror/themes/strudel-theme.mjs
index 7c4a3a91..658a682f 100644
--- a/packages/codemirror/themes/strudel-theme.mjs
+++ b/packages/codemirror/themes/strudel-theme.mjs
@@ -5,6 +5,7 @@ export const settings = {
background: '#222',
lineBackground: '#22222299',
foreground: '#fff',
+ muted: '#ffffff50',
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
diff --git a/packages/codemirror/themes/sublime.mjs b/packages/codemirror/themes/sublime.mjs
index 068b9bac..111f1e50 100644
--- a/packages/codemirror/themes/sublime.mjs
+++ b/packages/codemirror/themes/sublime.mjs
@@ -5,6 +5,7 @@ export const settings = {
background: '#303841',
lineBackground: '#30384199',
foreground: '#FFFFFF',
+ muted: '#FFFFFF50',
caret: '#FBAC52',
selection: '#4C5964',
selectionMatch: '#3A546E',
@@ -18,6 +19,7 @@ export default createTheme({
settings: {
background: '#303841',
foreground: '#FFFFFF',
+ muted: '#FFFFFF50',
caret: '#FBAC52',
selection: '#4C5964',
selectionMatch: '#3A546E',
diff --git a/packages/codemirror/themes/teletext.mjs b/packages/codemirror/themes/teletext.mjs
index 7857cb13..280eda97 100644
--- a/packages/codemirror/themes/teletext.mjs
+++ b/packages/codemirror/themes/teletext.mjs
@@ -9,7 +9,8 @@ let colorD = '#f8fc55';
export const settings = {
background: '#000000',
- foreground: colorA, // whats that?
+ foreground: colorA,
+ muted: colorA + '50',
caret: colorC,
selection: colorD,
selectionMatch: colorA,
diff --git a/packages/codemirror/themes/tokioNightStorm.mjs b/packages/codemirror/themes/tokioNightStorm.mjs
index be973d52..ab940da0 100644
--- a/packages/codemirror/themes/tokioNightStorm.mjs
+++ b/packages/codemirror/themes/tokioNightStorm.mjs
@@ -4,7 +4,8 @@ import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#24283b',
lineBackground: '#24283b99',
- foreground: '#7982a9',
+ foreground: '#f9f2f9',
+ muted: '#f9f2f950',
caret: '#c0caf5',
selection: '#6f7bb630',
selectionMatch: '#1f2335',
@@ -18,7 +19,8 @@ export default createTheme({
theme: 'dark',
settings: {
background: '#24283b',
- foreground: '#7982a9',
+ foreground: '#f9f2f9',
+ muted: '#f9f2f950',
caret: '#c0caf5',
selection: '#6f7bb630',
selectionMatch: '#343b5f',
diff --git a/packages/codemirror/themes/tokyoNight.mjs b/packages/codemirror/themes/tokyoNight.mjs
index c325b4ad..29a65667 100644
--- a/packages/codemirror/themes/tokyoNight.mjs
+++ b/packages/codemirror/themes/tokyoNight.mjs
@@ -4,7 +4,8 @@ import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#1a1b26',
lineBackground: '#1a1b2699',
- foreground: '#787c99',
+ foreground: '#f8fcf9',
+ muted: '#f8fcf950',
caret: '#c0caf5',
selection: '#515c7e40',
selectionMatch: '#16161e',
@@ -18,7 +19,8 @@ export default createTheme({
theme: 'dark',
settings: {
background: '#1a1b26',
- foreground: '#787c99',
+ foreground: '#f8fcf9',
+ muted: '#f8fcf950',
caret: '#c0caf5',
selection: '#515c7e40',
selectionMatch: '#16161e',
diff --git a/packages/codemirror/themes/tokyoNightDay.mjs b/packages/codemirror/themes/tokyoNightDay.mjs
index 1cd58a41..a8f5f75b 100644
--- a/packages/codemirror/themes/tokyoNightDay.mjs
+++ b/packages/codemirror/themes/tokyoNightDay.mjs
@@ -6,6 +6,7 @@ export const settings = {
background: '#e1e2e7',
lineBackground: '#e1e2e799',
foreground: '#3760bf',
+ muted: '#3760bf50',
caret: '#3760bf',
selection: '#99a7df',
selectionMatch: '#99a7df',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#e1e2e7',
foreground: '#3760bf',
+ muted: '#3760bf50',
caret: '#3760bf',
selection: '#99a7df',
selectionMatch: '#99a7df',
diff --git a/packages/codemirror/themes/vscodeDark.mjs b/packages/codemirror/themes/vscodeDark.mjs
index 703790fd..9ccc4d07 100644
--- a/packages/codemirror/themes/vscodeDark.mjs
+++ b/packages/codemirror/themes/vscodeDark.mjs
@@ -5,6 +5,7 @@ export const settings = {
background: '#1e1e1e',
lineBackground: '#1e1e1e99',
foreground: '#fff',
+ muted: '#ffffff50',
caret: '#c6c6c6',
selection: '#6199ff2f',
selectionMatch: '#72a1ff59',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#1e1e1e',
foreground: '#fff',
+ muted: '#ffffff50',
caret: '#c6c6c6',
selection: '#6199ff2f',
selectionMatch: '#72a1ff59',
diff --git a/packages/codemirror/themes/vscodeLight.mjs b/packages/codemirror/themes/vscodeLight.mjs
index 9a734511..71817e8a 100644
--- a/packages/codemirror/themes/vscodeLight.mjs
+++ b/packages/codemirror/themes/vscodeLight.mjs
@@ -5,6 +5,7 @@ export const settings = {
background: '#ffffff',
lineBackground: '#ffffff50',
foreground: '#383a42',
+ muted: '#383a4250',
caret: '#000',
selection: '#add6ff',
selectionMatch: '#a8ac94',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#ffffff',
foreground: '#383a42',
+ muted: '#383a4250',
caret: '#000',
selection: '#add6ff',
selectionMatch: '#a8ac94',
diff --git a/packages/codemirror/themes/whitescreen.mjs b/packages/codemirror/themes/whitescreen.mjs
index a2937c38..fa034fb8 100644
--- a/packages/codemirror/themes/whitescreen.mjs
+++ b/packages/codemirror/themes/whitescreen.mjs
@@ -2,7 +2,8 @@ import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: 'white',
- foreground: 'black', // whats that?
+ foreground: 'black',
+ muted: '#00000050',
caret: 'black',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#ffffff26',
diff --git a/packages/codemirror/themes/xcodeLight.mjs b/packages/codemirror/themes/xcodeLight.mjs
index 8977c133..b2c85798 100644
--- a/packages/codemirror/themes/xcodeLight.mjs
+++ b/packages/codemirror/themes/xcodeLight.mjs
@@ -6,6 +6,7 @@ export const settings = {
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#3D3D3D',
+ muted: '#3D3D3D50',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
@@ -18,6 +19,7 @@ export default createTheme({
settings: {
background: '#fff',
foreground: '#3D3D3D',
+ muted: '#3D3D3D50',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css
index b5cd34d8..a00600ae 100644
--- a/website/src/repl/Repl.css
+++ b/website/src/repl/Repl.css
@@ -1,15 +1,3 @@
-:root {
- --background: #222;
- --lineBackground: #22222299;
- --foreground: #fff;
- --caret: #ffcc00;
- --selection: rgba(128, 203, 196, 0.5);
- --selectionMatch: #036dd626;
- --lineHighlight: #00000050;
- --gutterBackground: transparent;
- --gutterForeground: #8a919966;
-}
-
.darken::before {
content: ' ';
position: fixed;
diff --git a/website/src/repl/components/panel/Forms.jsx b/website/src/repl/components/panel/Forms.jsx
index 79c17e45..1837bfcf 100644
--- a/website/src/repl/components/panel/Forms.jsx
+++ b/website/src/repl/components/panel/Forms.jsx
@@ -2,18 +2,18 @@ import cx from '@src/cx.mjs';
export function ButtonGroup({ value, onChange, items }) {
return (
-
+
{Object.entries(items).map(([key, label], i, arr) => (
onChange(key)}
className={cx(
- 'px-2 border-b h-8 whitespace-nowrap',
+ 'px-2 border-b-2 h-8 whitespace-nowrap bg-background',
// i === 0 && 'rounded-l-md',
// i === arr.length - 1 && 'rounded-r-md',
// value === key ? 'bg-background' : 'bg-lineHighlight',
- value === key ? 'border-foreground' : 'border-background',
+ value === key ? 'border-foreground' : 'border-transparent',
)}
>
{label.toLowerCase()}
diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx
index 40712bfa..9c98cbad 100644
--- a/website/src/repl/components/panel/Reference.jsx
+++ b/website/src/repl/components/panel/Reference.jsx
@@ -128,14 +128,14 @@ export const Reference = memo(function Reference() {
{selectedTag && (
{selectedTag}
)}
-
+
{searchVisibleFunctions.map((entry, i) => (
toggleTag(t)}
>
@@ -191,7 +191,7 @@ export const Reference = memo(function Reference() {
{entry.name}
{entry.tags && (
-
+
{entry.tags.join(', ')}
)}
diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx
index 3baeec22..d7f005ef 100644
--- a/website/src/repl/components/panel/SettingsTab.jsx
+++ b/website/src/repl/components/panel/SettingsTab.jsx
@@ -15,7 +15,7 @@ function cx(...classes) {
}
const inputClass =
- 'bg-background text-sm border rounded-0 text-foreground border-foreground placeholder-foreground focus:outline-none focus:ring-0 focus:border-foreground';
+ 'bg-background text-sm border rounded-0 text-foreground border-muted placeholder-foreground focus:outline-none focus:ring-0 focus:border-foreground';
export function Textbox({ onChange, className, ...inputProps }) {
return (
@@ -28,7 +28,7 @@ function Checkbox({ label, value, onChange, disabled = false }) {
Date: Tue, 20 Jan 2026 00:59:43 +0100
Subject: [PATCH 073/200] fix: adapt color-theme attribute on root based on
theme
---
packages/codemirror/themes.mjs | 1 +
packages/codemirror/themes/CutiePi.mjs | 1 +
website/src/components/HeadCommon.astro | 1 +
website/src/components/HeadCommonNext.astro | 1 +
4 files changed, 4 insertions(+)
diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs
index 1a523b39..dd561d30 100644
--- a/packages/codemirror/themes.mjs
+++ b/packages/codemirror/themes.mjs
@@ -198,6 +198,7 @@ export function activateTheme(name) {
const themeSettings = settings[name] || settings.strudelTheme;
// set css variables
themeStyle.innerHTML = `:root {
+ color-scheme: ${themeSettings.light ? 'light' : 'dark'};
${Object.entries(themeSettings)
// important to override fallback
.map(([key, value]) => `--${key}: ${value} !important;`)
diff --git a/packages/codemirror/themes/CutiePi.mjs b/packages/codemirror/themes/CutiePi.mjs
index 4d0e768c..f535a2da 100644
--- a/packages/codemirror/themes/CutiePi.mjs
+++ b/packages/codemirror/themes/CutiePi.mjs
@@ -25,6 +25,7 @@ export const settings = {
gutterForeground: lightGrey,
gutterBorder: 'transparent',
lineHighlight: pinkAccent,
+ light: true,
};
export default createTheme({
diff --git a/website/src/components/HeadCommon.astro b/website/src/components/HeadCommon.astro
index fe8d34bb..550222e4 100644
--- a/website/src/components/HeadCommon.astro
+++ b/website/src/components/HeadCommon.astro
@@ -33,6 +33,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL