-
${label || getDocLabel(doc)}
+
${getDocLabel(doc)}
+ ${doc.synonyms_text ? `
Synonyms: ${doc.synonyms_text}
` : ''}
${doc.description ? `
${doc.description}
` : ''}
${buildParamsList(doc.params)}
${buildExamples(doc.examples)}
@@ -74,15 +75,43 @@ const isValidDoc = (doc) => {
const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
-const jsdocCompletions = jsdoc.docs
- .filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc))
- // https://codemirror.net/docs/ref/#autocomplete.Completion
- .map((doc) => ({
- label: getDocLabel(doc),
- // detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
- info: () => Autocomplete({ doc }),
- type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
- }));
+export const getSynonymDoc = (doc, synonym) => {
+ const synonyms = doc.synonyms || [];
+ const docLabel = getDocLabel(doc);
+ // Swap `doc.name` in for `s` in the list of synonyms
+ const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
+ return {
+ ...doc,
+ name: synonym,
+ longname: synonym,
+ synonyms: synonymsWithDoc,
+ synonyms_text: synonymsWithDoc.join(', '),
+ };
+};
+
+const jsdocCompletions = (() => {
+ const seen = new Set(); // avoid repetition
+ const completions = [];
+ for (const doc of jsdoc.docs) {
+ if (!isValidDoc(doc) || hasExcludedTags(doc)) continue;
+ const docLabel = getDocLabel(doc);
+ // Remove duplicates
+ const synonyms = doc.synonyms || [];
+ let labels = [docLabel, ...synonyms];
+ for (const label of labels) {
+ // https://codemirror.net/docs/ref/#autocomplete.Completion
+ if (label && !seen.has(label)) {
+ seen.add(label);
+ completions.push({
+ label,
+ info: () => Autocomplete(getSynonymDoc(doc, label)),
+ type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
+ });
+ }
+ }
+ }
+ return completions;
+})();
export const strudelAutocomplete = (context) => {
const word = context.matchBefore(/\w*/);
diff --git a/packages/codemirror/html.mjs b/packages/codemirror/html.mjs
index 527275ef6..f240059d3 100644
--- a/packages/codemirror/html.mjs
+++ b/packages/codemirror/html.mjs
@@ -1,6 +1,7 @@
-const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
export let html = (string) => {
- return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
+ const template = document.createElement('template');
+ template.innerHTML = string.trim();
+ return template.content.childNodes;
};
let parseChunk = (chunk) => {
if (Array.isArray(chunk)) return chunk.flat().join('');
diff --git a/packages/codemirror/tooltip.mjs b/packages/codemirror/tooltip.mjs
index f67e6d14a..d1d0479b2 100644
--- a/packages/codemirror/tooltip.mjs
+++ b/packages/codemirror/tooltip.mjs
@@ -1,6 +1,6 @@
import { hoverTooltip } from '@codemirror/view';
import jsdoc from '../../doc.json';
-import { Autocomplete } from './autocomplete.mjs';
+import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
const getDocLabel = (doc) => doc.name || doc.longname;
@@ -52,10 +52,11 @@ export const strudelTooltip = hoverTooltip(
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
if (!entry) {
// Try for synonyms
- entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
- if (!entry) {
+ const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
+ if (!doc) {
return null;
}
+ entry = getSynonymDoc(doc, word);
}
return {
@@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip(
create(view) {
let dom = document.createElement('div');
dom.className = 'strudel-tooltip';
- const ac = Autocomplete({ doc: entry, label: word });
+ const ac = Autocomplete(entry);
dom.appendChild(ac);
return { dom };
},
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index f6c1aa92f..9fcfbafe1 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -91,6 +91,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
* Define a custom webaudio node to use as a sound source.
*
* @name source
+ * @synonyms src
* @param {function} getSource
* @synonyms src
*
@@ -536,6 +537,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
* Shape of amplitude modulation
*
* @name tremoloshape
+ * @synonyms tremshape
* @param {number | Pattern} shape tri | square | sine | saw | ramp
* @example
* note("{f g c d}%16").tremsync(4).tremoloshape("
").s("sawtooth")
@@ -551,6 +553,7 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
*
*/
+export const { drive } = registerControl('drive');
/**
* Modulate the amplitude of an orbit to create a "sidechain" like effect.
@@ -558,6 +561,7 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
* Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")`
*
* @name duckorbit
+ * @synonyms duck
* @param {number | Pattern} orbit target orbit
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
@@ -637,8 +641,6 @@ export const { duckonset } = registerControl('duckonset', 'duckons');
*/
export const { duckattack } = registerControl('duckattack', 'duckatt');
-export const { drive } = registerControl('drive');
-
/**
* Create byte beats with custom expressions
*
@@ -761,7 +763,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
* The amount the signal is affected by the phaser effect. Defaults to 0.75
*
* @name phaserdepth
- * @synonyms phd
+ * @synonyms phd, phasdp
* @param {number | Pattern} depth number between 0 and 1
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
@@ -1256,6 +1258,7 @@ export const { dry } = registerControl('dry');
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
*
* @name fadeTime
+ * @synonyms fadeOutTime
* @param {number | Pattern} time between 0 and 1
* @example
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs
index addc301d5..262b92bb3 100644
--- a/packages/core/pattern.mjs
+++ b/packages/core/pattern.mjs
@@ -1246,7 +1246,8 @@ export const silence = gap(1);
/* Like silence, but with a 'steps' (relative duration) of 0 */
export const nothing = gap(0);
-/** A discrete value that repeats once per cycle.
+/**
+ * A discrete value that repeats once per cycle.
*
* @returns {Pattern}
* @example
@@ -1299,7 +1300,8 @@ export function sequenceP(pats) {
return result;
}
-/** The given items are played at the same time at the same length.
+/**
+ * The given items are played at the same time at the same length.
*
* @return {Pattern}
* @synonyms polyrhythm, pr
@@ -1382,11 +1384,11 @@ export function stackBy(by, ...pats) {
.setSteps(steps);
}
-/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
- *
- * synonyms: `cat`
+/**
+ * Concatenation: combines a list of patterns, switching between them successively, one per cycle.
*
* @return {Pattern}
+ * @synonyms cat
* @example
* slowcat("e5", "b4", ["d5", "c5"])
*
@@ -2397,6 +2399,57 @@ export const stut = register('stut', function (times, feedback, time, pat) {
return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
});
+export const applyN = register('applyN', function (n, func, p) {
+ let result = p;
+ for (let i = 0; i < n; i++) {
+ result = func(result);
+ }
+ return result;
+});
+
+/**
+ * The plyWith function repeats each event the given number of times, applying the given function to each event.\n
+ * @name plyWith
+ * @synonyms plywith
+ * @param {number} factor how many times to repeat
+ * @param {function} func function to apply, given the pattern
+ * @example
+ * "<0 [2 4]>"
+ * .plyWith(4, (p) => p.add(2))
+ * .scale("C:minor").note()
+ */
+export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) {
+ const result = pat
+ .fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor))
+ .squeezeJoin();
+ if (__steps) {
+ result._steps = Fraction(factor).mulmaybe(pat._steps);
+ }
+ return result;
+});
+
+/**
+ * The plyForEach function repeats each event the given number of times, applying the given function to each event.
+ * This version of ply uses the iteration index as an argument to the function, similar to echoWith.
+ * @name plyForEach
+ * @synonyms plyforeach
+ * @param {number} factor how many times to repeat
+ * @param {function} func function to apply, given the pattern and the iteration index
+ * @example
+ * "<0 [2 4]>"
+ * .plyForEach(4, (p,n) => p.add(n*2))
+ * .scale("C:minor").note()
+ */
+export const plyForEach = register(['plyForEach', 'plyforeach'], function (factor, func, pat) {
+ const result = pat
+ .fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor))
+ .squeezeJoin();
+ if (__steps) {
+ result._steps = Fraction(factor).mulmaybe(pat._steps);
+ }
+ return result;
+});
+
/**
* Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played.
* @name iter
diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs
index 81bec9399..69e7e8560 100644
--- a/packages/superdough/helpers.mjs
+++ b/packages/superdough/helpers.mjs
@@ -174,7 +174,7 @@ let curves = ['linear', 'exponential'];
export function getPitchEnvelope(param, value, t, holdEnd) {
// envelope is active when any of these values is set
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
- if (!hasEnvelope) {
+ if (hasEnvelope === undefined) {
return;
}
const penv = nanFallback(value.penv, 1, true);
diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs
index cae9ffd41..f1f308a7d 100644
--- a/packages/superdough/superdough.mjs
+++ b/packages/superdough/superdough.mjs
@@ -113,6 +113,19 @@ export async function aliasBank(...args) {
}
}
+/**
+ * Register an alias for a sound.
+ * @param {string} original - The original sound name
+ * @param {string} alias - The alias to use for the sound
+ */
+export function soundAlias(original, alias) {
+ if (getSound(original) == null) {
+ logger('soundAlias: original sound not found');
+ return;
+ }
+ soundMap.setKey(alias, getSound(original));
+}
+
export function getSound(s) {
if (typeof s !== 'string') {
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs
index 711922b63..1cb9241ad 100644
--- a/packages/supradough/dough.mjs
+++ b/packages/supradough/dough.mjs
@@ -1,4 +1,6 @@
// this is dough, the superdough without dependencies
+// @ts-check
+// @ts-ignore ignore next line because sampleRate is unknown
const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000;
const PI_DIV_SR = Math.PI / SAMPLE_RATE;
const ISR = 1 / SAMPLE_RATE;
@@ -641,8 +643,8 @@ const note2midi = (note, defaultOctave = 3) => {
}
const chroma = chromas[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
- oct = Number(oct || defaultOctave);
- return (oct + 1) * 12 + chroma + offset;
+ const octave = Number(oct || defaultOctave);
+ return (octave + 1) * 12 + chroma + offset;
};
const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440;
const note2freq = (note) => {
@@ -654,9 +656,152 @@ const note2freq = (note) => {
};
export class DoughVoice {
+ /** @type {number} */
+ id = 0;
+ /** @type {number[]} */
out = [0, 0];
+
+ /** @type {number | undefined} */
+ attack;
+ /** @type {number | undefined} */
+ decay;
+ /** @type {number | undefined} */
+ sustain;
+ /** @type {number} */
+ release;
+ /** @type {number} */
+ _begin;
+ /** @type {number} */
+ _duration;
+
+ /** @type {any} */
+ _sound;
+ /** @type {number} */
+ _channels = 1;
+ /** @type {BufferPlayer[] | undefined} */
+ _buffers;
+ /** @type {string | undefined} */
+ unit;
+
+ /** @type {ADSR | undefined} */
+ _penv;
+ /** @type {number | undefined} */
+ penv;
+ /** @type {number | undefined} */
+ pattack;
+ /** @type {number | undefined} */
+ pdecay;
+ /** @type {number | undefined} */
+ psustain;
+ /** @type {number | undefined} */
+ prelease;
+
+ /** @type {number | undefined} */
+ vib;
+
+ _vib;
+ /** @type {number | undefined} */
+ vibmod;
+
+ /** @type {SineOsc | undefined} */
+ _fm;
+ /** @type {number | undefined} */
+ fmh;
+ /** @type {number | undefined} */
+ fmi;
+
+ /** @type {ADSR | undefined} */
+ _fmenv;
+ /** @type {number | undefined} */
+ fmattack;
+ /** @type {number | undefined} */
+ fmdecay;
+ /** @type {number | undefined} */
+ fmsustain;
+ /** @type {number | undefined} */
+ fmrelease;
+
+ /** @type {ADSR | undefined} */
+ _lpenv;
+ lpenv;
+ /** @type {number | undefined} */
+ lpattack;
+ /** @type {number | undefined} */
+ lpdecay;
+ /** @type {number | undefined} */
+ lpsustain;
+ /** @type {number | undefined} */
+ lprelease;
+
+ /** @type {ADSR | undefined} */
+ _hpenv;
+ /** @type {number | undefined} */
+ hpenv;
+ /** @type {number | undefined} */
+ hpattack;
+ /** @type {number | undefined} */
+ hpdecay;
+ /** @type {number | undefined} */
+ hpsustain;
+ /** @type {number | undefined} */
+ hprelease;
+
+ /** @type {ADSR | undefined} */
+ _bpenv;
+ /** @type {number | undefined} */
+ bpenv;
+ /** @type {number | undefined} */
+ bpattack;
+ /** @type {number | undefined} */
+ bpdecay;
+ /** @type {number | undefined} */
+ bpsustain;
+ /** @type {number | undefined} */
+ bprelease;
+
+ /** @type {number | undefined} */
+ cutoff;
+ /** @type {number | undefined} */
+ hcutoff;
+ /** @type {number | undefined} */
+ bandf;
+ /** @type {number | undefined} */
+ coarse;
+ /** @type {number | undefined} */
+ crush;
+ /** @type {number | undefined} */
+ distort;
+
+ /** @type {number} */
+ freq;
+ /** @type {string | undefined} */
+ note;
+
+ /** @type {TwoPoleFilter[] | null | undefined} */
+ _lpf;
+ /** @type {TwoPoleFilter[] | null | undefined} */
+ _hpf;
+ /** @type {TwoPoleFilter[] | null | undefined} */
+ _bpf;
+ /** @type {Chorus[] | null | undefined} */
+ _chorus;
+ /** @type {Coarse[] | null | undefined} */
+ _coarse;
+ /** @type {Crush[] | null | undefined} */
+ _crush;
+ /** @type {Distort[] | null | undefined} */
+ _distort;
+
+ /**
+ * @param {DoughVoice} value
+ */
constructor(value) {
- value.freq ??= note2freq(value.note);
+ // mandatory controls
+ this.freq ??= note2freq(value.note);
+ this._begin = value._begin;
+ this._duration = value._duration;
+ this.release = value.release ?? 0;
+ // the rest.. we use $ for readability
let $ = this;
Object.assign($, value);
$.s = $.s ?? getDefaultValue('s');
@@ -773,7 +918,7 @@ export class DoughVoice {
let freq = this.freq * this.speed;
// frequency modulation
- if (this._fm) {
+ if (this._fm && this.fmh !== undefined && this.fmi !== undefined) {
let fmi = this.fmi;
if (this._fmenv) {
const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease);
@@ -785,37 +930,31 @@ export class DoughVoice {
}
// vibrato
- if (this._vib) {
+ if (this._vib && this.vibmod !== undefined) {
freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12);
}
// pitch envelope
- if (this._penv) {
+ if (this._penv && this.penv !== undefined) {
const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease);
freq = freq + env * this.penv;
}
// filters
let lpf = this.cutoff;
- if (this._lpf) {
- if (this._lpenv) {
- const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease);
- lpf = this.lpenv * env * lpf + lpf;
- }
+ if (lpf !== undefined && this._lpenv) {
+ const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease);
+ lpf = this.lpenv * env * lpf + lpf;
}
let hpf = this.hcutoff;
- if (this._hpf) {
- if (this._hpenv) {
- const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease);
- hpf = 2 ** this.hpenv * env * hpf + hpf;
- }
+ if (hpf !== undefined && this._hpenv && this.hpenv !== undefined) {
+ const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease);
+ hpf = 2 ** this.hpenv * env * hpf + hpf;
}
let bpf = this.bandf;
- if (this._bpf) {
- if (this._bpenv) {
- const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease);
- bpf = 2 ** this.bpenv * env * bpf + bpf;
- }
+ if (bpf !== undefined && this._bpenv && this.bpenv !== undefined) {
+ const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease);
+ bpf = 2 ** this.bpenv * env * bpf + bpf;
}
// gain envelope
const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release);
@@ -891,8 +1030,8 @@ export class Dough {
this.sampleRate = sampleRate;
this.t = Math.floor(currentTime * sampleRate); // samples
// console.log('init dough', this.sampleRate, this.t);
- this._delayL = new PitchDelay();
- this._delayR = new PitchDelay();
+ this._delayL = new Delay();
+ this._delayR = new Delay();
}
loadSample(name, channels, sampleRate) {
BufferPlayer.samples.set(name, { channels, sampleRate });
@@ -965,8 +1104,9 @@ export class Dough {
}
}
// todo: how to change delaytime / delayfeedback from a voice?
- const delayL = this._delayL.update(this.delaysend[0], this.delaytime, this.delayspeed);
- const delayR = this._delayR.update(this.delaysend[1], this.delaytime, this.delayspeed);
+ const delayL = this._delayL.update(this.delaysend[0], this.delaytime);
+ const delayR = this._delayR.update(this.delaysend[1], this.delaytime);
+
this.delaysend[0] = delayL * this.delayfeedback;
this.delaysend[1] = delayR * this.delayfeedback;
this.out[0] += delayL;
diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap
index e38e22fd4..78fd6821a 100644
--- a/test/__snapshots__/examples.test.mjs.snap
+++ b/test/__snapshots__/examples.test.mjs.snap
@@ -7471,6 +7471,64 @@ exports[`runs examples > example "ply" example index 0 1`] = `
]
`;
+exports[`runs examples > example "plyForEach" example index 0 1`] = `
+[
+ "[ 0/1 → 1/4 | note:C3 ]",
+ "[ 1/4 → 1/2 | note:Eb3 ]",
+ "[ 1/2 → 3/4 | note:G3 ]",
+ "[ 3/4 → 1/1 | note:Bb3 ]",
+ "[ 1/1 → 9/8 | note:Eb3 ]",
+ "[ 9/8 → 5/4 | note:G3 ]",
+ "[ 5/4 → 11/8 | note:Bb3 ]",
+ "[ 11/8 → 3/2 | note:D4 ]",
+ "[ 3/2 → 13/8 | note:G3 ]",
+ "[ 13/8 → 7/4 | note:Bb3 ]",
+ "[ 7/4 → 15/8 | note:D4 ]",
+ "[ 15/8 → 2/1 | note:F4 ]",
+ "[ 2/1 → 9/4 | note:C3 ]",
+ "[ 9/4 → 5/2 | note:Eb3 ]",
+ "[ 5/2 → 11/4 | note:G3 ]",
+ "[ 11/4 → 3/1 | note:Bb3 ]",
+ "[ 3/1 → 25/8 | note:Eb3 ]",
+ "[ 25/8 → 13/4 | note:G3 ]",
+ "[ 13/4 → 27/8 | note:Bb3 ]",
+ "[ 27/8 → 7/2 | note:D4 ]",
+ "[ 7/2 → 29/8 | note:G3 ]",
+ "[ 29/8 → 15/4 | note:Bb3 ]",
+ "[ 15/4 → 31/8 | note:D4 ]",
+ "[ 31/8 → 4/1 | note:F4 ]",
+]
+`;
+
+exports[`runs examples > example "plyWith" example index 0 1`] = `
+[
+ "[ 0/1 → 1/4 | note:C3 ]",
+ "[ 1/4 → 1/2 | note:Eb3 ]",
+ "[ 1/2 → 3/4 | note:G3 ]",
+ "[ 3/4 → 1/1 | note:Bb3 ]",
+ "[ 1/1 → 9/8 | note:Eb3 ]",
+ "[ 9/8 → 5/4 | note:G3 ]",
+ "[ 5/4 → 11/8 | note:Bb3 ]",
+ "[ 11/8 → 3/2 | note:D4 ]",
+ "[ 3/2 → 13/8 | note:G3 ]",
+ "[ 13/8 → 7/4 | note:Bb3 ]",
+ "[ 7/4 → 15/8 | note:D4 ]",
+ "[ 15/8 → 2/1 | note:F4 ]",
+ "[ 2/1 → 9/4 | note:C3 ]",
+ "[ 9/4 → 5/2 | note:Eb3 ]",
+ "[ 5/2 → 11/4 | note:G3 ]",
+ "[ 11/4 → 3/1 | note:Bb3 ]",
+ "[ 3/1 → 25/8 | note:Eb3 ]",
+ "[ 25/8 → 13/4 | note:G3 ]",
+ "[ 13/4 → 27/8 | note:Bb3 ]",
+ "[ 27/8 → 7/2 | note:D4 ]",
+ "[ 7/2 → 29/8 | note:G3 ]",
+ "[ 29/8 → 15/4 | note:Bb3 ]",
+ "[ 15/4 → 31/8 | note:D4 ]",
+ "[ 31/8 → 4/1 | note:F4 ]",
+]
+`;
+
exports[`runs examples > example "polymeter" example index 0 1`] = `
[
"[ 0/1 → 1/6 | note:c ]",
diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx
index a87b8f7dd..eb79cccf7 100644
--- a/website/src/pages/learn/samples.mdx
+++ b/website/src/pages/learn/samples.mdx
@@ -59,6 +59,14 @@ Furthermore, strudel also loads instrument samples from [VCSL](https://github.co
To see which sample names are available, open the `sounds` tab in the [REPL](https://strudel.cc/).
+You can also create custom aliases for existing sounds using the `soundAlias` function:
+
+
+
Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples themselves are not loaded until they are actually played.
This behaviour of loading things only when they are needed is also called `lazy loading`.
While it saves resources, it can also lead to sounds not being audible the first time they are triggered, because the sound is still loading.
diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx
index 74daf4bab..1d659972b 100644
--- a/website/src/pages/workshop/first-sounds.mdx
+++ b/website/src/pages/workshop/first-sounds.mdx
@@ -262,14 +262,14 @@ It is quite common that there are many ways to express the same idea.
**selecting sample numbers separately**
-Instead of using ":", we can also use the `n` function to select sample numbers:
-
-
-
-This is shorter and more readable than:
+Instead of selecting sample numbers one by one:
+We can also use the `n` function to make it shorter and more readable:
+
+
+
## Recap
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css
index 498679090..b5cd34d81 100644
--- a/website/src/repl/Repl.css
+++ b/website/src/repl/Repl.css
@@ -80,6 +80,8 @@
min-width: 300px !important;
max-height: 400px !important;
background-color: var(--lineHighlight) !important;
+ overflow: auto;
+ background: var(--background) !important;
}
/* Main tooltip container */
@@ -91,7 +93,7 @@
font-size: var(--font-size, 13px);
line-height: 1.4;
max-width: 600px;
- max-height: 400px;
+ height: 100%;
min-width: 400px;
white-space: normal !important;
overflow-y: auto !important;
@@ -112,6 +114,13 @@
margin: 0 0 8px 0;
}
+.autocomplete-info-function-synonyms {
+ margin: 0 0 12px 0;
+ color: var(--foreground);
+ line-height: 1.5;
+ opacity: 0.8;
+}
+
.autocomplete-info-function-description {
margin: 0 0 12px 0;
color: var(--foreground);
diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx
index 1b617341b..6007667fa 100644
--- a/website/src/repl/components/panel/Reference.jsx
+++ b/website/src/repl/components/panel/Reference.jsx
@@ -2,9 +2,32 @@ import { useMemo, useState } from 'react';
import jsdocJson from '../../../../../doc.json';
import { Textbox } from '../textbox/Textbox';
-const availableFunctions = jsdocJson.docs
- .filter(({ name, description }) => name && !name.startsWith('_') && !!description)
- .sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
+
+const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
+
+const availableFunctions = (() => {
+ const seen = new Set(); // avoid repetition
+ const functions = [];
+ for (const doc of jsdocJson.docs) {
+ if (!isValid(doc)) continue;
+ functions.push(doc);
+ const synonyms = doc.synonyms || [];
+ for (const s of synonyms) {
+ if (!s || seen.has(s)) continue;
+ seen.add(s);
+ // Swap `doc.name` in for `s` in the list of synonyms
+ const 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(', '),
+ });
+ }
+ }
+ return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
+})();
const getInnerText = (html) => {
var div = document.createElement('div');