Merge branch 'main' into glossing/ducking-click

This commit is contained in:
Aria 2025-09-14 18:28:00 -05:00
commit ab74ccfe7c
14 changed files with 400 additions and 62 deletions

View file

@ -1,6 +1,6 @@
name: Strudel tests name: Strudel tests
on: [push] on: [push, pull_request]
jobs: jobs:
build: build:
@ -19,7 +19,7 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: "pnpm"
- run: pnpm install - run: pnpm install
- run: pnpm run format-check - run: pnpm run format-check
- run: pnpm run lint - run: pnpm run lint

View file

@ -54,11 +54,12 @@ const buildExamples = (examples) =>
` `
: ''; : '';
export const Autocomplete = ({ doc, label }) => export const Autocomplete = (doc) =>
h` h`
<div class="autocomplete-info-container"> <div class="autocomplete-info-container">
<div class="autocomplete-info-tooltip"> <div class="autocomplete-info-tooltip">
<h3 class="autocomplete-info-function-name">${label || getDocLabel(doc)}</h3> <h3 class="autocomplete-info-function-name">${getDocLabel(doc)}</h3>
${doc.synonyms_text ? `<div class="autocomplete-info-function-synonyms">Synonyms: ${doc.synonyms_text}</div>` : ''}
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''} ${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
${buildParamsList(doc.params)} ${buildParamsList(doc.params)}
${buildExamples(doc.examples)} ${buildExamples(doc.examples)}
@ -74,15 +75,43 @@ const isValidDoc = (doc) => {
const hasExcludedTags = (doc) => const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
const jsdocCompletions = jsdoc.docs export const getSynonymDoc = (doc, synonym) => {
.filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc)) const synonyms = doc.synonyms || [];
// https://codemirror.net/docs/ref/#autocomplete.Completion const docLabel = getDocLabel(doc);
.map((doc) => ({ // Swap `doc.name` in for `s` in the list of synonyms
label: getDocLabel(doc), const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label. return {
info: () => Autocomplete({ doc }), ...doc,
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type 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) => { export const strudelAutocomplete = (context) => {
const word = context.matchBefore(/\w*/); const word = context.matchBefore(/\w*/);

View file

@ -1,6 +1,7 @@
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
export let html = (string) => { 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) => { let parseChunk = (chunk) => {
if (Array.isArray(chunk)) return chunk.flat().join(''); if (Array.isArray(chunk)) return chunk.flat().join('');

View file

@ -1,6 +1,6 @@
import { hoverTooltip } from '@codemirror/view'; import { hoverTooltip } from '@codemirror/view';
import jsdoc from '../../doc.json'; import jsdoc from '../../doc.json';
import { Autocomplete } from './autocomplete.mjs'; import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
const getDocLabel = (doc) => doc.name || doc.longname; 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]; let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
if (!entry) { if (!entry) {
// Try for synonyms // Try for synonyms
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0]; const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
if (!entry) { if (!doc) {
return null; return null;
} }
entry = getSynonymDoc(doc, word);
} }
return { return {
@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip(
create(view) { create(view) {
let dom = document.createElement('div'); let dom = document.createElement('div');
dom.className = 'strudel-tooltip'; dom.className = 'strudel-tooltip';
const ac = Autocomplete({ doc: entry, label: word }); const ac = Autocomplete(entry);
dom.appendChild(ac); dom.appendChild(ac);
return { dom }; return { dom };
}, },

View file

@ -91,6 +91,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
* Define a custom webaudio node to use as a sound source. * Define a custom webaudio node to use as a sound source.
* *
* @name source * @name source
* @synonyms src
* @param {function} getSource * @param {function} getSource
* @synonyms src * @synonyms src
* *
@ -536,6 +537,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
* Shape of amplitude modulation * Shape of amplitude modulation
* *
* @name tremoloshape * @name tremoloshape
* @synonyms tremshape
* @param {number | Pattern} shape tri | square | sine | saw | ramp * @param {number | Pattern} shape tri | square | sine | saw | ramp
* @example * @example
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth") * note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").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>") * 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. * 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")` * Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")`
* *
* @name duckorbit * @name duckorbit
* @synonyms duck
* @param {number | Pattern} orbit target orbit * @param {number | Pattern} orbit target orbit
* @example * @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2) * $: 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 { duckattack } = registerControl('duckattack', 'duckatt');
export const { drive } = registerControl('drive');
/** /**
* Create byte beats with custom expressions * 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 * The amount the signal is affected by the phaser effect. Defaults to 0.75
* *
* @name phaserdepth * @name phaserdepth
* @synonyms phd * @synonyms phd, phasdp
* @param {number | Pattern} depth number between 0 and 1 * @param {number | Pattern} depth number between 0 and 1
* @example * @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5) * 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. * Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
* *
* @name fadeTime * @name fadeTime
* @synonyms fadeOutTime
* @param {number | Pattern} time between 0 and 1 * @param {number | Pattern} time between 0 and 1
* @example * @example
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc() * s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()

View file

@ -1246,7 +1246,8 @@ export const silence = gap(1);
/* Like silence, but with a 'steps' (relative duration) of 0 */ /* Like silence, but with a 'steps' (relative duration) of 0 */
export const nothing = gap(0); export const nothing = gap(0);
/** A discrete value that repeats once per cycle. /**
* A discrete value that repeats once per cycle.
* *
* @returns {Pattern} * @returns {Pattern}
* @example * @example
@ -1299,7 +1300,8 @@ export function sequenceP(pats) {
return result; 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} * @return {Pattern}
* @synonyms polyrhythm, pr * @synonyms polyrhythm, pr
@ -1382,11 +1384,11 @@ export function stackBy(by, ...pats) {
.setSteps(steps); .setSteps(steps);
} }
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle: /**
* * Concatenation: combines a list of patterns, switching between them successively, one per cycle.
* synonyms: `cat`
* *
* @return {Pattern} * @return {Pattern}
* @synonyms cat
* @example * @example
* slowcat("e5", "b4", ["d5", "c5"]) * 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))); 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. * 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 * @name iter

View file

@ -174,7 +174,7 @@ let curves = ['linear', 'exponential'];
export function getPitchEnvelope(param, value, t, holdEnd) { export function getPitchEnvelope(param, value, t, holdEnd) {
// envelope is active when any of these values is set // envelope is active when any of these values is set
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv; const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
if (!hasEnvelope) { if (hasEnvelope === undefined) {
return; return;
} }
const penv = nanFallback(value.penv, 1, true); const penv = nanFallback(value.penv, 1, true);

View file

@ -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) { export function getSound(s) {
if (typeof s !== 'string') { if (typeof s !== 'string') {
console.warn(`getSound: expected string got "${s}". fall back to triangle`); console.warn(`getSound: expected string got "${s}". fall back to triangle`);

View file

@ -1,4 +1,6 @@
// this is dough, the superdough without dependencies // 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 SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000;
const PI_DIV_SR = Math.PI / SAMPLE_RATE; const PI_DIV_SR = Math.PI / SAMPLE_RATE;
const ISR = 1 / SAMPLE_RATE; const ISR = 1 / SAMPLE_RATE;
@ -641,8 +643,8 @@ const note2midi = (note, defaultOctave = 3) => {
} }
const chroma = chromas[pc.toLowerCase()]; const chroma = chromas[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0; const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
oct = Number(oct || defaultOctave); const octave = Number(oct || defaultOctave);
return (oct + 1) * 12 + chroma + offset; return (octave + 1) * 12 + chroma + offset;
}; };
const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440; const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440;
const note2freq = (note) => { const note2freq = (note) => {
@ -654,9 +656,152 @@ const note2freq = (note) => {
}; };
export class DoughVoice { export class DoughVoice {
/** @type {number} */
id = 0;
/** @type {number[]} */
out = [0, 0]; 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) { 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; let $ = this;
Object.assign($, value); Object.assign($, value);
$.s = $.s ?? getDefaultValue('s'); $.s = $.s ?? getDefaultValue('s');
@ -773,7 +918,7 @@ export class DoughVoice {
let freq = this.freq * this.speed; let freq = this.freq * this.speed;
// frequency modulation // frequency modulation
if (this._fm) { if (this._fm && this.fmh !== undefined && this.fmi !== undefined) {
let fmi = this.fmi; let fmi = this.fmi;
if (this._fmenv) { if (this._fmenv) {
const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease); const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease);
@ -785,37 +930,31 @@ export class DoughVoice {
} }
// vibrato // vibrato
if (this._vib) { if (this._vib && this.vibmod !== undefined) {
freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12); freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12);
} }
// pitch envelope // 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); const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease);
freq = freq + env * this.penv; freq = freq + env * this.penv;
} }
// filters // filters
let lpf = this.cutoff; let lpf = this.cutoff;
if (this._lpf) { if (lpf !== undefined && this._lpenv) {
if (this._lpenv) { const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease);
const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease); lpf = this.lpenv * env * lpf + lpf;
lpf = this.lpenv * env * lpf + lpf;
}
} }
let hpf = this.hcutoff; let hpf = this.hcutoff;
if (this._hpf) { if (hpf !== undefined && this._hpenv && this.hpenv !== undefined) {
if (this._hpenv) { const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease);
const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease); hpf = 2 ** this.hpenv * env * hpf + hpf;
hpf = 2 ** this.hpenv * env * hpf + hpf;
}
} }
let bpf = this.bandf; let bpf = this.bandf;
if (this._bpf) { if (bpf !== undefined && this._bpenv && this.bpenv !== undefined) {
if (this._bpenv) { const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease);
const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease); bpf = 2 ** this.bpenv * env * bpf + bpf;
bpf = 2 ** this.bpenv * env * bpf + bpf;
}
} }
// gain envelope // gain envelope
const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); 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.sampleRate = sampleRate;
this.t = Math.floor(currentTime * sampleRate); // samples this.t = Math.floor(currentTime * sampleRate); // samples
// console.log('init dough', this.sampleRate, this.t); // console.log('init dough', this.sampleRate, this.t);
this._delayL = new PitchDelay(); this._delayL = new Delay();
this._delayR = new PitchDelay(); this._delayR = new Delay();
} }
loadSample(name, channels, sampleRate) { loadSample(name, channels, sampleRate) {
BufferPlayer.samples.set(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? // todo: how to change delaytime / delayfeedback from a voice?
const delayL = this._delayL.update(this.delaysend[0], 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.delayspeed); const delayR = this._delayR.update(this.delaysend[1], this.delaytime);
this.delaysend[0] = delayL * this.delayfeedback; this.delaysend[0] = delayL * this.delayfeedback;
this.delaysend[1] = delayR * this.delayfeedback; this.delaysend[1] = delayR * this.delayfeedback;
this.out[0] += delayL; this.out[0] += delayL;

View file

@ -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`] = ` exports[`runs examples > example "polymeter" example index 0 1`] = `
[ [
"[ 0/1 → 1/6 | note:c ]", "[ 0/1 → 1/6 | note:c ]",

View file

@ -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/). 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:
<MiniRepl
client:idle
tune={`soundAlias("RolandTR808_bd", "kick")
s("kick")`}
/>
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. 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`. 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. 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.

View file

@ -262,14 +262,14 @@ It is quite common that there are many ways to express the same idea.
**selecting sample numbers separately** **selecting sample numbers separately**
Instead of using ":", we can also use the `n` function to select sample numbers: Instead of selecting sample numbers one by one:
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
This is shorter and more readable than:
<MiniRepl client:visible tune={`sound("jazz:0 jazz:1 [jazz:4 jazz:2] jazz:3*2")`} punchcard /> <MiniRepl client:visible tune={`sound("jazz:0 jazz:1 [jazz:4 jazz:2] jazz:3*2")`} punchcard />
We can also use the `n` function to make it shorter and more readable:
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
## Recap ## Recap
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal. Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.

View file

@ -80,6 +80,8 @@
min-width: 300px !important; min-width: 300px !important;
max-height: 400px !important; max-height: 400px !important;
background-color: var(--lineHighlight) !important; background-color: var(--lineHighlight) !important;
overflow: auto;
background: var(--background) !important;
} }
/* Main tooltip container */ /* Main tooltip container */
@ -91,7 +93,7 @@
font-size: var(--font-size, 13px); font-size: var(--font-size, 13px);
line-height: 1.4; line-height: 1.4;
max-width: 600px; max-width: 600px;
max-height: 400px; height: 100%;
min-width: 400px; min-width: 400px;
white-space: normal !important; white-space: normal !important;
overflow-y: auto !important; overflow-y: auto !important;
@ -112,6 +114,13 @@
margin: 0 0 8px 0; 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 { .autocomplete-info-function-description {
margin: 0 0 12px 0; margin: 0 0 12px 0;
color: var(--foreground); color: var(--foreground);

View file

@ -2,9 +2,32 @@ import { useMemo, useState } from 'react';
import jsdocJson from '../../../../../doc.json'; import jsdocJson from '../../../../../doc.json';
import { Textbox } from '../textbox/Textbox'; import { Textbox } from '../textbox/Textbox';
const availableFunctions = jsdocJson.docs
.filter(({ name, description }) => name && !name.startsWith('_') && !!description) const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
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) => { const getInnerText = (html) => {
var div = document.createElement('div'); var div = document.createElement('div');