Compare commits

..

8 Commits

Author SHA1 Message Date
Jade (Rose) Rowland 90c05ec38a fix edge cases 2026-02-08 14:12:21 -05:00
Jade (Rose) Rowland 7e0eed87cb normalize shortcuts 2026-01-27 17:21:36 -05:00
Jade (Rose) Rowland 1a1197f67a Merge branch 'main' into soloshortcut 2026-01-22 21:11:31 -05:00
Jade (Rose) Rowland 3c5afb32f3 scroll to center 2026-01-22 02:04:27 -05:00
Jade (Rose) Rowland 76cbd23859 Merge branch 'soloshortcut' of ssh://codeberg.org/uzu/strudel into soloshortcut 2026-01-22 00:25:41 -05:00
Jade (Rose) Rowland b436ae789c rm dead code 2026-01-22 00:25:28 -05:00
Switch Angel AKA Jade Rose 275731afc7 Merge branch 'main' into soloshortcut 2026-01-22 06:22:17 +01:00
Jade (Rose) Rowland 5cb2214d88 working 2026-01-22 00:20:44 -05:00
12 changed files with 530 additions and 436 deletions
+75 -3
View File
@@ -24,7 +24,12 @@ import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
import { activateTheme, initTheme, theme } from './themes.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
import { updateWidgets, widgetPlugin } from './widget.mjs';
import { jumpToCharacter } from './labelJump.mjs';
import {
deleteAllInlineBeforeCharacter,
InsertCharBeforeChar,
jumpToCharacter,
jumpToNextCharacter,
} from './labelJump.mjs';
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
@@ -74,6 +79,10 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
decode: JSON.parse,
});
const ANON_LABEL = '$';
const SOLO_LABEL = 'S';
const MUTE_LABEL = '_';
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) {
const settings = codemirrorSettings.get();
@@ -122,12 +131,75 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
},
{
key: 'Alt-w',
run: (view) => jumpToCharacter(view, '$', 1),
run: (view) => jumpToNextCharacter(view, ANON_LABEL, 1),
},
{
key: 'Alt-q',
run: (view) => jumpToCharacter(view, '$', -1),
run: (view) => {
return jumpToNextCharacter(view, ANON_LABEL, -1);
},
},
// clear all muted
{
key: `Alt-Ctrl-0`,
run: (view) => {
return deleteAllInlineBeforeCharacter(view, MUTE_LABEL + ANON_LABEL);
},
},
// clear all solod
{
key: `Alt-Shift-0`,
run: (view) => {
return deleteAllInlineBeforeCharacter(view, SOLO_LABEL + ANON_LABEL);
},
},
// clear all solo and mute
{
key: `Ctrl-Shift-0`,
run: (view) => {
return deleteAllInlineBeforeCharacter(view, ANON_LABEL);
},
},
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Alt-${num}`,
run: (view) => {
return jumpToCharacter(view, ANON_LABEL, i);
},
};
}),
// handle solo toggles 1-9
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Alt-Shift-${num}`,
run: (view) => {
return InsertCharBeforeChar(view, ANON_LABEL, SOLO_LABEL, i);
},
};
}),
// handle mute toggles 1-9
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Alt-Ctrl-${num}`,
run: (view) => {
return InsertCharBeforeChar(view, ANON_LABEL, MUTE_LABEL, i);
},
};
}),
// Handle clearing mutes and solos 1-9
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Ctrl-Shift-${num}`,
run: (view) => {
return InsertCharBeforeChar(view, ANON_LABEL,'', i);
},
};
}),
/* {
key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()),
+133 -11
View File
@@ -1,18 +1,54 @@
import { EditorSelection } from '@codemirror/state';
import { SearchCursor } from '@codemirror/search';
import { EditorView } from '@codemirror/view';
import { syntaxTree } from '@codemirror/language';
export function jumpToCharacter(view, character, direction = 1) {
/**
* gets all of the positions of a character in a document, excluding commented out lines
* @param { EditorState} state
* @param {String} character
* @returns {number[]}
*/
function getCharacterPositions(state, character) {
const cursor = new SearchCursor(state.doc, character);
const characterPositions = [];
while (!cursor.next().done) {
const linestartpos = state.doc.lineAt(cursor.value.to).from
if (!isLineCommentedOut(state, linestartpos)) {
characterPositions.push(cursor.value.to);
}
}
return characterPositions;
}
function isLineCommentedOut(state, pos) {
const line = state.doc.lineAt(pos);
// remove white space
pos = line.from + line.text.search(/\S/)
const tree = syntaxTree(state);
const node = tree.resolveInner(pos, 1)
return node.name.includes("Comment")
}
/**
* jump to the next character in a document
* @param {EditorView} view
* @param {String} character
* @param {number} direction 0 or 1
* @returns {boolean}
*/
export function jumpToNextCharacter(view, character, direction = 1) {
const { state, dispatch } = view;
const pos = state.selection.main.head;
const cursor = new SearchCursor(state.doc, character);
let characterPositions = [];
let jumpPos;
while (!cursor.next().done) {
characterPositions.push(cursor.value.to);
}
const characterPositions = getCharacterPositions(state, character);
if (!characterPositions.length) {
return false;
return true;
}
if (direction > 0) {
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
@@ -21,11 +57,97 @@ export function jumpToCharacter(view, character, direction = 1) {
}
if (jumpPos == null) {
return false;
return true;
}
const selection = EditorSelection.cursor(jumpPos - 1);
dispatch({
selection: EditorSelection.cursor(jumpPos - 1),
scrollIntoView: true,
selection,
effects: EditorView.scrollIntoView(
selection.head,
{ y: "start" }
)
});
return true;
}
/**
*
* @param {EditorView} view
* @param {String} character
* @param {number} index the instance of the character
* @returns
*/
export function jumpToCharacter(view, character, index) {
const { state, dispatch } = view;
const characterPositions = getCharacterPositions(state, character);
const pos = characterPositions.at(index) ?? characterPositions.at(-1);
if (pos == null) {
return true;
}
const selection = EditorSelection.cursor(pos - 1);
dispatch({
selection,
effects: EditorView.scrollIntoView(
selection.head,
{ y: "start" }
)
});
return true;
}
/**
*
* @param {EditorView} view
* @param {String} character
* @returns {true}
*/
export function deleteAllInlineBeforeCharacter(view, character) {
const { state, dispatch } = view;
const characterPositions = getCharacterPositions(state, character);
const changes = [];
characterPositions.forEach((pos) => {
const line = state.doc.lineAt(pos);
if (state.doc.sliceString(line.from, line.from + 2) === COMMENT_STRING) {
return;
}
changes.push({
from: line.from,
to: pos - 1,
insert: '',
});
});
dispatch({ changes });
return true;
}
/**
*
* @param {EditorView} view
* @param {String} character
* @param {String} character2
* @param {number} index
* @returns
*/
export function InsertCharBeforeChar(view, character, character2, index) {
const { state, dispatch } = view;
const changes = [];
const characterPositions = getCharacterPositions(state, character);
const labelpos = characterPositions.at(index) ?? characterPositions.at(-1);
const line = state.doc.lineAt(labelpos);
//delete preceeding characters
changes.push({
from: line.from,
to: labelpos - 1,
insert: '',
});
changes.push({
insert: character2,
from: line.from,
});
dispatch({ changes });
return true;
}
+217 -219
View File
File diff suppressed because it is too large Load Diff
+59 -55
View File
@@ -818,6 +818,7 @@ export class Pattern {
* @name layer
* @tags combiners
* @memberof Pattern
* @synonyms apply
* @returns Pattern
* @example
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8"
@@ -1264,7 +1265,6 @@ const ALIGNMENT_KEYS = ALIGNMENTS.map((how) => how.toLowerCase());
* 'add.mix', 'set.squeeze', etc.
*
* @param {string} method Default join method to use. Options: 'in', 'out', 'mix', 'squeeze', 'squeezeout', 'reset', 'restart', 'poly'
* @tags combiners
* @example
* setDefaultJoin('mix') // also try 'in', 'out', 'squeeze', etc.
* s("saw").vel("1 0.5").note("F A C E").delay("0 0.2 0.3")
@@ -1564,10 +1564,10 @@ export function arrange(...sections) {
* @tags combiners
* @return {Pattern}
* @example
seqPLoop(
[0, 2, "bd(3,8)"],
[1, 3, "cp(3,8)"]
).sound()
seqPLoop([0, 2, "bd(3,8)"],
[1, 3, "cp(3,8)"]
)
.sound()
*/
export function seqPLoop(...parts) {
let total = Fraction(0);
@@ -1610,7 +1610,7 @@ export function sequence(...pats) {
/** Like **cat**, but the items are crammed into one cycle.
* @tags combiners
* @synonyms fastcat
* @synonyms seq, fastcat
* @example
* seq("e5", "b4", ["d5", "c5"]).note()
* // "e5 b4 [d5 c5]".note()
@@ -2137,12 +2137,13 @@ export const { firstOf, every } = register(['firstOf', 'every'], function (n, fu
});
/**
* Applies the given function to the pattern. Like layer, but with a single function:
* @tags combiners
* Like layer, but with a single function:
* @tags temporal
* @name apply
* @example
* "<c3 eb3 g3>".scale('C minor').apply(scaleTranspose("0,2,4")).note()
*/
// TODO: remove or dedupe with layer?
export const apply = register('apply', function (func, pat) {
return func(pat);
});
@@ -2499,7 +2500,7 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func,
/**
* The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.
* @tags temporal, superdough
* @tags temporal
* @example
* s("bd lt [~ ht] mt cp ~ bd hh").jux(rev)
* @example
@@ -2513,7 +2514,7 @@ export const jux = register('jux', function (func, pat) {
/**
* Superimpose and offset multiple times, applying the given function each time.
* @tags temporal, functional
* @tags temporal
* @name echoWith
* @synonyms echowith, stutWith, stutwith
* @param {number} times how many times to repeat
@@ -2687,7 +2688,7 @@ export const { repeatCycles } = register(
/**
* Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle).
* @tags temporal, functional
* @tags temporal
* @name chunk
* @synonyms slowChunk, slowchunk
* @memberof Pattern
@@ -2851,7 +2852,7 @@ Pattern.prototype.tag = function (tag) {
/**
* Filters haps using the given function
* @name filter
* @tags temporal, functional
* @tags temporal
* @param {Function} test function to test Hap
* @example
* s("hh!7 oh").filter(hap => hap.value.s === 'hh')
@@ -2861,7 +2862,7 @@ export const filter = register('filter', (test, pat) => pat.withHaps((haps) => h
/**
* Filters haps by their begin time
* @name filterWhen
* @tags temporal, functional
* @tags temporal
* @param {Function} test function to test Hap.whole.begin
* @example
* oneCycle: s("bd*4").filterWhen((t) => t < 1)
@@ -2871,7 +2872,7 @@ export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) =
/**
* Use within to apply a function to only a part of a pattern.
* @name within
* @tags temporal, functional
* @tags temporal
* @param {number} start start within cycle (0 - 1)
* @param {number} end end within cycle (0 - 1). Must be > start
* @param {Function} func function to be applied to the sub-pattern
@@ -2946,7 +2947,7 @@ export function _match(span, hap_p) {
* *Experimental*
*
* Speeds a pattern up or down, to fit to the given number of steps per cycle.
* @tags stepwise
* @tags temporal
* @example
* sound("bd sd cp").pace(4)
* // The same as sound("{bd sd cp}%4") or sound("<bd sd cp>*4")
@@ -2988,7 +2989,7 @@ export function _polymeterListSteps(steps, ...args) {
* *Experimental*
*
* Aligns the steps of the patterns, creating polymeters. The patterns are repeated until they all fit the cycle. For example, in the below the first pattern is repeated twice, and the second is repeated three times, to fit the lowest common multiple of six steps.
* @tags stepwise
* @tags temporal
* @synonyms pm
* @example
* // The same as note("{c eb g, c2 g2}%6")
@@ -3021,7 +3022,7 @@ export function polymeter(...args) {
* The steps can either be inferred from the pattern, or provided as a [length, pattern] pair.
* Has the alias `timecat`.
* @name stepcat
* @tags stepwise
* @tags combiners
* @synonyms timeCat, timecat
* @return {Pattern}
* @example
@@ -3079,7 +3080,7 @@ export function stepcat(...timepats) {
* Concatenates patterns stepwise, according to an inferred 'steps per cycle'.
* Similar to `stepcat`, but if an argument is a list, the whole pattern will alternate between the elements in the list.
*
* @tags stepwise
* @tags combiners
* @return {Pattern}
* @example
* stepalt(["bd cp", "mt"], "bd").sound()
@@ -3106,7 +3107,7 @@ export function stepalt(...groups) {
*
* Takes the given number of steps from a pattern (dropping the rest).
* A positive number will take steps from the start of a pattern, and a negative number from the end.
* @tags stepwise
* @tags temporal
* @return {Pattern}
* @example
* "bd cp ht mt".take("2").sound()
@@ -3151,7 +3152,7 @@ export const take = stepRegister('take', function (i, pat) {
*
* Drops the given number of steps from a pattern.
* A positive number will drop steps from the start of a pattern, and a negative number from the end.
* @tags stepwise
* @tags temporal
* @return {Pattern}
* @example
* "tha dhi thom nam".drop("1").sound().bank("mridangam")
@@ -3180,7 +3181,7 @@ export const drop = stepRegister('drop', function (i, pat) {
* `extend` is similar to `fast` in that it increases its density, but it also increases the step count
* accordingly. So `stepcat("a b".extend(2), "c d")` would be the same as `"a b a b c d"`, whereas
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
* @tags stepwise
* @tags temporal
* @example
* stepcat(
* sound("bd bd - cp").extend(2),
@@ -3199,7 +3200,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 stepwise
* @tags temporal
* @example
* stepcat(
* sound("bd bd - cp").replicate(2),
@@ -3214,7 +3215,7 @@ export const replicate = stepRegister('replicate', function (factor, pat) {
* *Experimental*
*
* Expands the step size of the pattern by the given factor.
* @tags stepwise
* @tags temporal
* @example
* sound("tha dhi thom nam").bank("mridangam").expand("3 2 1 1 2 3").pace(8)
*/
@@ -3226,7 +3227,7 @@ export const expand = stepRegister('expand', function (factor, pat) {
* *Experimental*
*
* Contracts the step size of the pattern by the given factor. See also `expand`.
* @tags stepwise
* @tags temporal
* @example
* sound("tha dhi thom nam").bank("mridangam").contract("3 2 1 1 2 3").pace(8)
*/
@@ -3281,7 +3282,7 @@ export const shrinklist = (amount, pat) => pat.shrinklist(amount);
* Progressively shrinks the pattern by 'n' steps until there's nothing left, or if a second value is given (using mininotation list syntax with `:`),
* that number of times.
* A positive number will progressively drop steps from the start of a pattern, and a negative number from the end.
* @tags stepwise
* @tags temporal
* @return {Pattern}
* @example
* "tha dhi thom nam".shrink("1").sound()
@@ -3321,7 +3322,7 @@ export const shrink = register(
* Progressively grows the pattern by 'n' steps until the full pattern is played, or if a second value is given (using mininotation list syntax with `:`),
* that number of times.
* A positive number will progressively grow steps from the start of a pattern, and a negative number from the end.
* @tags stepwise
* @tags temporal
* @return {Pattern}
* @example
* "tha dhi thom nam".grow("1").sound()
@@ -3362,7 +3363,7 @@ export const grow = register(
* on successive repetitions. The patterns are added together stepwise, with all repetitions taking place over a single cycle. Using `pace` to set the
* number of steps per cycle is therefore usually recommended.
*
* @tags stepwise
* @tags combiners
* @return {Pattern}
* @example
* "[c g]".tour("e f", "e f g", "g f e c").note()
@@ -3389,7 +3390,7 @@ Pattern.prototype.tour = function (...many) {
* 'zips' together the steps of the provided patterns. This can create a long repetition, taking place over a single, dense cycle.
* Using `pace` to set the number of steps per cycle is therefore usually recommended.
*
* @tags stepwise
* @tags combiners
* @returns {Pattern}
* @example
* zip("e f", "e f g", "g [f e] a f4 c").note()
@@ -3441,7 +3442,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 samples
* @tags temporal
* @memberof Pattern
* @returns Pattern
* @example
@@ -3472,7 +3473,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 samples
* @tags temporal
* @memberof Pattern
* @returns Pattern
* @example
@@ -3491,13 +3492,14 @@ export const striate = register('striate', function (n, pat) {
/**
* Makes the sample fit the given number of cycles by changing the speed.
* @name loopAt
* @tags samples, pitch
* @tags temporal
* @memberof Pattern
* @returns Pattern
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes").loopAt(2)
*/
// TODO - global cps clock
const _loopAt = function (factor, pat, cps = 0.5) {
return pat
.speed((1 / factor) * cps)
@@ -3505,16 +3507,11 @@ const _loopAt = function (factor, pat, cps = 0.5) {
.slow(factor);
};
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
const steps = pat._steps ? pat._steps.div(factor) : undefined;
return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state), steps);
});
/**
* 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 samples
* @tags temporal
* @memberof Pattern
* @returns Pattern
* @example
@@ -3568,7 +3565,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 samples, pitch
* @tags temporal
* @example
* samples('github:tidalcycles/dirt-samples')
* s("breaks165")
@@ -3597,11 +3594,16 @@ export const splice = register(
false, // turns off auto-patternification
);
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
const steps = pat._steps ? pat._steps.div(factor) : undefined;
return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state), steps);
});
/**
* Makes the sample fit its event duration. Good for rhythmical loops like drum breaks.
* Similar to `loopAt`.
* @name fit
* @tags samples, pitch
* @tags temporal
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes/2").fit()
@@ -3623,16 +3625,18 @@ export const fit = register('fit', (pat) =>
/**
* Makes the sample fit the given number of cycles and cps value, by
* changing the speed. deprecated: use loopAt or fit instead, together with setCps / setCpm.
* changing the speed. Please note that at some point cps will be
* given by a global clock and this function will be
* deprecated/removed.
* @name loopAtCps
* @tags samples, pitch
* @tags temporal
* @memberof Pattern
* @deprecated
* @returns Pattern
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes").loopAtCps(4,1.5).cps(1.5)
*/
// TODO - global cps clock
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
return _loopAt(factor, pat, cps);
});
@@ -3654,7 +3658,7 @@ let fadeGain = (p) => (p < 0.5 ? 1 : 1 - (p - 0.5) / 0.5);
* - 1 = (no left, full right)
*
* @name xfade
* @tags amplitude
* @tags combiners
* @example
* xfade(s("bd*2"), "<0 .25 .5 .75 1>", s("hh*8"))
*/
@@ -3780,7 +3784,7 @@ const _distortWithAlg = function (name) {
* Soft-clipping distortion
*
* @name soft
* @tags distortion, superdough
* @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3791,7 +3795,7 @@ export const soft = _distortWithAlg('soft');
* Hard-clipping distortion
*
* @name hard
* @tags distortion, superdough
* @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3802,7 +3806,7 @@ export const hard = _distortWithAlg('hard');
* Cubic polynomial distortion
*
* @name cubic
* @tags distortion, superdough
* @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3813,7 +3817,7 @@ export const cubic = _distortWithAlg('cubic');
* Diode-emulating distortion
*
* @name diode
* @tags distortion, superdough
* @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3824,7 +3828,7 @@ export const diode = _distortWithAlg('diode');
* Asymmetrical diode distortion
*
* @name asym
* @tags distortion, superdough
* @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3835,7 +3839,7 @@ export const asym = _distortWithAlg('asym');
* Wavefolding distortion
*
* @name fold
* @tags distortion, superdough
* @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3846,7 +3850,7 @@ export const fold = _distortWithAlg('fold');
* Wavefolding distortion composed with sinusoid
*
* @name sinefold
* @tags distortion, superdough
* @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3857,7 +3861,7 @@ export const sinefold = _distortWithAlg('sinefold');
* Distortion via Chebyshev polynomials
*
* @name chebyshev
* @tags distortion, superdough
* @tags fx
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
@@ -3891,7 +3895,7 @@ const _ensureListPattern = (list) => {
* Can also be used to create a new synth via `s('user').partials(...)`
*
* @name partials
* @tags superdough
* @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")
@@ -3913,7 +3917,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 superdough
* @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
@@ -3935,7 +3939,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 superdough
* @tags fx, superdough
* @memberof Pattern
* @returns Pattern
* @example
@@ -3986,7 +3990,7 @@ const _asArrayPattern = (pats) => {
* by wrapping them inside a function in K (see example).
*
* @name K
* @tags generators, superdough
* @tags generators, fx, superdough
* @param {KabelsalatExpression | Function} expr Kabelsalat graph definition
* @memberof Pattern
* @returns Pattern
+15 -17
View File
@@ -78,7 +78,7 @@ export const pickmod = register('pickmod', function (lookup, pat) {
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
* @example
* note("<c2 d2>(3,8)").s("square")
* .pickF("<0 2> 1", [jux(rev), fast(2), x=>x.lpf(800)])
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
*/
export const pickF = register('pickF', function (lookup, funcs, pat) {
return pat.apply(pick(lookup, funcs));
@@ -167,22 +167,20 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) {
});
/** Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @tags combiners
* @synonyms pickSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* let a = s("bd(3,8)")
* let b = s("cp sd")
* "<a b [a,b]>".inhabit({ a, b })
* @example
* s("a@2 [a b] a"
* .inhabit({a: "bd(3,8)", b: "sd sd"}))
* .slow(4)
*/
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @tags combiners
* @synonyms pickSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
b: s("cp sd")
})
* @example
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
*/
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, false).squeezeJoin();
});
+4 -4
View File
@@ -138,7 +138,7 @@ function githubPath(base, subpath = '') {
/**
* configures the default midimap, which is used when no "midimap" port is set
* @tags external_io, midi
* @tags external_io
* @example
* defaultmidimap({ lpf: 74 })
* $: note("c a f e").midi();
@@ -152,7 +152,7 @@ let loadCache = {};
/**
* Adds midimaps to the registry. Inside each midimap, control names (e.g. lpf) are mapped to cc numbers.
* @tags external_io, midi
* @tags external_io
* @example
* midimaps({ mymap: { lpf: 74 } })
* $: note("c a f e")
@@ -529,7 +529,7 @@ async function _initialize(input) {
* The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel
*
* @name midin
* @tags external_io, midi
* @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)
@@ -577,7 +577,7 @@ export async function midin(input) {
* note durations
*
* @name midikeys
* @tags external_io, midi
* @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,
+2 -2
View File
@@ -40,7 +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 superdough
* @tags fx, superdough
* @param {number} Max polyphony. Defaults to 128
* @example
* setMaxPolyphony(4)
@@ -74,7 +74,7 @@ export function applyGainCurve(val) {
* quadratic, exponential, etc. rather than linear
*
* @name setGainCurve
* @tags amplitude, superdough
* @tags fx, superdough
* @param {Function} function to apply to all gain values
* @example
* setGainCurve((x) => x * x) // quadratic gain
+1 -1
View File
@@ -186,7 +186,7 @@ export function registerWaveTable(key, tables, params) {
* Loads a collection of wavetables to use with `s`
*
* @name tables
* @tags wavetable
* @tags fx
*/
export const tables = async (url, frameLen, json, options = {}) => {
if (json !== undefined) return _processTables(json, url, frameLen);
+3 -3
View File
@@ -100,7 +100,7 @@ function scaleOffset(scale, offset, note) {
* - 5P = perfect fifth
* - 5d = diminished fifth
*
* @tags tonal
* @tags music_theory
* @param {string | number} amount Either number of semitones or interval string.
* @returns Pattern
* @memberof Pattern
@@ -155,7 +155,7 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
*
* @memberof Pattern
* @name scaleTranspose
* @tags tonal
* @tags music_theory
* @param {offset} offset number of steps inside the scale
* @returns Pattern
* @synonyms scaleTrans, strans
@@ -246,7 +246,7 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
* The root note defaults to octave 3, if no octave number is given.
*
* @name scale
* @tags tonal
* @tags music_theory
* @param {string} scale Name of scale
* @returns Pattern
* @example
+4 -4
View File
@@ -90,7 +90,7 @@ export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistr
* Adds a new custom voicing dictionary.
*
* @name addVoicings
* @tags tonal
* @tags music_theory
* @memberof Pattern
* @param {string} name identifier for the voicing dictionary
* @param {Object} dictionary maps chord symbol to possible voicings
@@ -134,7 +134,7 @@ const getVoicing = (chord, dictionaryName, lastVoicing) => {
* Uses [chord-voicings package](https://github.com/felixroos/chord-voicings#chord-voicings).
*
* @name voicings
* @tags tonal
* @tags music_theory
* @memberof Pattern
* @param {string} dictionary which voicing dictionary to use.
* @returns Pattern
@@ -159,7 +159,7 @@ export const voicings = register('voicings', function (dictionary, pat) {
* Maps the chords of the incoming pattern to root notes in the given octave.
*
* @name rootNotes
* @tags tonal
* @tags music_theory
* @memberof Pattern
* @param {octave} octave octave to use
* @returns Pattern
@@ -192,7 +192,7 @@ export const rootNotes = register('rootNotes', function (octave, pat) {
* If you pass a pattern of strings to voicing, they will be interpreted as chords.
*
* @name voicing
* @tags tonal
* @tags music_theory
* @returns Pattern
* @example
* n("0 1 2 3").chord("<C Am F G>").voicing()
-100
View File
@@ -1037,43 +1037,6 @@ exports[`runs examples > example "anchor" example index 0 1`] = `
]
`;
exports[`runs examples > example "anchor" example index 1 1`] = `
[
"[ 0/1 → 1/8 | anchor:c4 note:60 ]",
"[ 1/8 → 1/4 | anchor:c4 note:62 ]",
"[ 1/4 → 3/8 | anchor:c4 note:64 ]",
"[ 3/8 → 1/2 | anchor:c4 note:65 ]",
"[ 1/2 → 5/8 | anchor:c4 note:67 ]",
"[ 5/8 → 3/4 | anchor:c4 note:69 ]",
"[ 3/4 → 7/8 | anchor:c4 note:71 ]",
"[ 7/8 → 1/1 | anchor:c4 note:72 ]",
"[ 1/1 → 9/8 | anchor:g4 note:67 ]",
"[ 9/8 → 5/4 | anchor:g4 note:68 ]",
"[ 5/4 → 11/8 | anchor:g4 note:70 ]",
"[ 11/8 → 3/2 | anchor:g4 note:72 ]",
"[ 3/2 → 13/8 | anchor:g4 note:73 ]",
"[ 13/8 → 7/4 | anchor:g4 note:75 ]",
"[ 7/4 → 15/8 | anchor:g4 note:77 ]",
"[ 15/8 → 2/1 | anchor:g4 note:79 ]",
"[ 2/1 → 17/8 | anchor:c5 note:72 ]",
"[ 17/8 → 9/4 | anchor:c5 note:74 ]",
"[ 9/4 → 19/8 | anchor:c5 note:76 ]",
"[ 19/8 → 5/2 | anchor:c5 note:77 ]",
"[ 5/2 → 21/8 | anchor:c5 note:79 ]",
"[ 21/8 → 11/4 | anchor:c5 note:81 ]",
"[ 11/4 → 23/8 | anchor:c5 note:83 ]",
"[ 23/8 → 3/1 | anchor:c5 note:84 ]",
"[ 3/1 → 25/8 | anchor:g5 note:79 ]",
"[ 25/8 → 13/4 | anchor:g5 note:80 ]",
"[ 13/4 → 27/8 | anchor:g5 note:82 ]",
"[ 27/8 → 7/2 | anchor:g5 note:84 ]",
"[ 7/2 → 29/8 | anchor:g5 note:85 ]",
"[ 29/8 → 15/4 | anchor:g5 note:87 ]",
"[ 15/4 → 31/8 | anchor:g5 note:89 ]",
"[ 31/8 → 4/1 | anchor:g5 note:91 ]",
]
`;
exports[`runs examples > example "apply" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:C3 ]",
@@ -4230,27 +4193,6 @@ exports[`runs examples > example "extend" example index 0 1`] = `
]
`;
exports[`runs examples > example "fadeTime" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:oh end:0.1 fadeTime:0 ]",
"[ 1/4 → 1/2 | s:oh end:0.1 fadeTime:0 ]",
"[ 1/2 → 3/4 | s:oh end:0.1 fadeTime:0 ]",
"[ 3/4 → 1/1 | s:oh end:0.1 fadeTime:0 ]",
"[ 1/1 → 5/4 | s:oh end:0.1 fadeTime:0.2 ]",
"[ 5/4 → 3/2 | s:oh end:0.1 fadeTime:0.2 ]",
"[ 3/2 → 7/4 | s:oh end:0.1 fadeTime:0.2 ]",
"[ 7/4 → 2/1 | s:oh end:0.1 fadeTime:0.2 ]",
"[ 2/1 → 9/4 | s:oh end:0.1 fadeTime:0.4 ]",
"[ 9/4 → 5/2 | s:oh end:0.1 fadeTime:0.4 ]",
"[ 5/2 → 11/4 | s:oh end:0.1 fadeTime:0.4 ]",
"[ 11/4 → 3/1 | s:oh end:0.1 fadeTime:0.4 ]",
"[ 3/1 → 13/4 | s:oh end:0.1 fadeTime:0.8 ]",
"[ 13/4 → 7/2 | s:oh end:0.1 fadeTime:0.8 ]",
"[ 7/2 → 15/4 | s:oh end:0.1 fadeTime:0.8 ]",
"[ 15/4 → 4/1 | s:oh end:0.1 fadeTime:0.8 ]",
]
`;
exports[`runs examples > example "fanchor" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:f s:sawtooth cutoff:1000 lpenv:8 fanchor:0 ]",
@@ -8157,48 +8099,6 @@ exports[`runs examples > example "panchor" example index 0 1`] = `
]
`;
exports[`runs examples > example "panspan" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd pan:0.5 panspan:0 ]",
"[ 1/4 → 1/2 | s:hh pan:0.5 panspan:0 ]",
"[ 1/2 → 3/4 | s:bd pan:0.5 panspan:0 ]",
"[ 3/4 → 1/1 | s:hh pan:0.5 panspan:0 ]",
"[ 1/1 → 5/4 | s:bd pan:1 panspan:0.5 ]",
"[ 5/4 → 3/2 | s:hh pan:1 panspan:0.5 ]",
"[ 3/2 → 7/4 | s:bd pan:1 panspan:0.5 ]",
"[ 7/4 → 2/1 | s:hh pan:1 panspan:0.5 ]",
"[ 2/1 → 9/4 | s:bd pan:0.5 panspan:1 ]",
"[ 9/4 → 5/2 | s:hh pan:0.5 panspan:1 ]",
"[ 5/2 → 11/4 | s:bd pan:0.5 panspan:1 ]",
"[ 11/4 → 3/1 | s:hh pan:0.5 panspan:1 ]",
"[ 3/1 → 13/4 | s:bd pan:0 panspan:0 ]",
"[ 13/4 → 7/2 | s:hh pan:0 panspan:0 ]",
"[ 7/2 → 15/4 | s:bd pan:0 panspan:0 ]",
"[ 15/4 → 4/1 | s:hh pan:0 panspan:0 ]",
]
`;
exports[`runs examples > example "pansplay" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd pan:0.5 pansplay:0 ]",
"[ 1/4 → 1/2 | s:hh pan:0.5 pansplay:0 ]",
"[ 1/2 → 3/4 | s:bd pan:0.5 pansplay:0 ]",
"[ 3/4 → 1/1 | s:hh pan:0.5 pansplay:0 ]",
"[ 1/1 → 5/4 | s:bd pan:1 pansplay:0.5 ]",
"[ 5/4 → 3/2 | s:hh pan:1 pansplay:0.5 ]",
"[ 3/2 → 7/4 | s:bd pan:1 pansplay:0.5 ]",
"[ 7/4 → 2/1 | s:hh pan:1 pansplay:0.5 ]",
"[ 2/1 → 9/4 | s:bd pan:0.5 pansplay:1 ]",
"[ 9/4 → 5/2 | s:hh pan:0.5 pansplay:1 ]",
"[ 5/2 → 11/4 | s:bd pan:0.5 pansplay:1 ]",
"[ 11/4 → 3/1 | s:hh pan:0.5 pansplay:1 ]",
"[ 3/1 → 13/4 | s:bd pan:0 pansplay:0 ]",
"[ 13/4 → 7/2 | s:hh pan:0 pansplay:0 ]",
"[ 7/2 → 15/4 | s:bd pan:0 pansplay:0 ]",
"[ 15/4 → 4/1 | s:hh pan:0 pansplay:0 ]",
]
`;
exports[`runs examples > example "partials" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:user note:A3 partials:[1 0 1 0 0 1] ]",
+17 -17
View File
@@ -4,11 +4,7 @@ import jsdocJson from '../../../../../doc.json';
import { Textbox } from '@src/repl/components/panel/SettingsTab';
import { settingsMap, useSettings } from '@src/settings.mjs';
const isValid = ({ name, description, tags = [] }) => {
const isSupradoughOnly = tags.includes('supradough') && !tags.includes('superdough');
const isSuperdirtOnly = tags.includes('superdirt') && !tags.includes('superdough');
return name && !name.startsWith('_') && !!description && !isSupradoughOnly && !isSuperdirtOnly;
};
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
const availableFunctions = (() => {
const seen = new Set(); // avoid repetition
@@ -18,32 +14,36 @@ const availableFunctions = (() => {
if (seen.has(doc.name)) continue;
// 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 superdough` here.
// @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 || [];
let names = [doc.name];
seen.add(doc.name);
for (const s of synonyms) {
if (!s || seen.has(s)) continue;
names.push(s);
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(', '),
});
}
doc.allNames = names.join(' ');
doc.synonyms = names.slice(1);
functions.push(doc);
}
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
})();
const tagCounts = {};
const ignoredTags = ['supradough', 'superdirt'];
// const tagOptions = { all: `all (${availableFunctions.length})` };
const tagOptions = { all: `all` };
for (const doc of availableFunctions) {
(doc.tags || ['untagged']).forEach((t) => {
if (typeof t === 'string' && t && !ignoredTags.includes(t)) {
if (typeof t === 'string' && t) {
tagCounts[t] = (tagCounts[t] || 0) + 1;
//tagOptions[t] = `${t} (${tagCounts[t]})`;
tagOptions[t] = t;
@@ -82,7 +82,7 @@ export const Reference = memo(function Reference() {
}
const lowerCaseSearch = search.toLowerCase();
return (
(entry.allNames || entry.name).toLowerCase().includes(lowerCaseSearch) ||
entry.name.toLowerCase().includes(lowerCaseSearch) ||
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
);
});
@@ -151,7 +151,7 @@ export const Reference = memo(function Reference() {
<Fragment key={`entry-${entry.name}`}>
<a
className={
'whitespace-nowrap cursor-pointer hover:opacity-50 text-ellipsis block' +
'cursor-pointer hover:opacity-50 text-ellipsis block' +
(entry.name === selectedFunction ? 'bg-lineHighlight font-bold' : '')
}
onClick={() => {
@@ -162,7 +162,7 @@ export const Reference = memo(function Reference() {
}
}}
>
{entry.name} {entry.synonyms && <small className="opacity-50">{entry.synonyms?.join(', ')}</small>}
{entry.name}
</a>{' '}
</Fragment>
))}
@@ -204,7 +204,7 @@ export const Reference = memo(function Reference() {
</h3>
{entry.tags && (
<span className="ml-2 text-xs text-foreground border border-muted px-1 py-0.5">
{entry.tags.filter((t) => !ignoredTags.includes(t)).join(', ')}
{entry.tags.join(', ')}
</span>
)}
</div>