mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 13:48:34 +00:00
Merge branch 'main' into add-program-change
This commit is contained in:
commit
09dd374722
@ -31,6 +31,10 @@ This project is organized into many [packages](./packages), which are also avail
|
|||||||
|
|
||||||
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start).
|
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start).
|
||||||
|
|
||||||
|
You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE.md). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details.
|
||||||
|
|
||||||
|
Licensing info for the default sound banks can be found over on the [dough-samples](https://github.com/felixroos/dough-samples/blob/main/README.md) repository.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
|
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
|
||||||
|
|||||||
@ -59,6 +59,7 @@
|
|||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@eslint/js": "^9.19.0",
|
"@eslint/js": "^9.19.0",
|
||||||
"@tauri-apps/cli": "^2.2.7",
|
"@tauri-apps/cli": "^2.2.7",
|
||||||
|
"@vitest/coverage-v8": "3.0.4",
|
||||||
"@vitest/ui": "^3.0.4",
|
"@vitest/ui": "^3.0.4",
|
||||||
"acorn": "^8.14.0",
|
"acorn": "^8.14.0",
|
||||||
"dependency-tree": "^11.0.1",
|
"dependency-tree": "^11.0.1",
|
||||||
|
|||||||
@ -67,6 +67,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
const initialSettings = Object.keys(compartments).map((key) =>
|
const initialSettings = Object.keys(compartments).map((key) =>
|
||||||
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
||||||
);
|
);
|
||||||
|
|
||||||
initTheme(settings.theme);
|
initTheme(settings.theme);
|
||||||
let state = EditorState.create({
|
let state = EditorState.create({
|
||||||
doc: initialCode,
|
doc: initialCode,
|
||||||
|
|||||||
3
packages/codemirror/themes.mjs
vendored
3
packages/codemirror/themes.mjs
vendored
@ -4,6 +4,7 @@ import blackscreen, { settings as blackscreenSettings } from './themes/blackscre
|
|||||||
import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen.mjs';
|
import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen.mjs';
|
||||||
import teletext, { settings as teletextSettings } from './themes/teletext.mjs';
|
import teletext, { settings as teletextSettings } from './themes/teletext.mjs';
|
||||||
import algoboy, { settings as algoboySettings } from './themes/algoboy.mjs';
|
import algoboy, { settings as algoboySettings } from './themes/algoboy.mjs';
|
||||||
|
import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs';
|
||||||
import terminal, { settings as terminalSettings } from './themes/terminal.mjs';
|
import terminal, { settings as terminalSettings } from './themes/terminal.mjs';
|
||||||
import abcdef, { settings as abcdefSettings } from './themes/abcdef.mjs';
|
import abcdef, { settings as abcdefSettings } from './themes/abcdef.mjs';
|
||||||
import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
|
import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
|
||||||
@ -55,6 +56,7 @@ export const themes = {
|
|||||||
androidstudio,
|
androidstudio,
|
||||||
duotoneDark,
|
duotoneDark,
|
||||||
githubDark,
|
githubDark,
|
||||||
|
CutiePi,
|
||||||
gruvboxDark,
|
gruvboxDark,
|
||||||
materialDark,
|
materialDark,
|
||||||
nord,
|
nord,
|
||||||
@ -98,6 +100,7 @@ export const settings = {
|
|||||||
duotoneLight: duotoneLightSettings,
|
duotoneLight: duotoneLightSettings,
|
||||||
duotoneDark: duotoneDarkSettings,
|
duotoneDark: duotoneDarkSettings,
|
||||||
eclipse: eclipseSettings,
|
eclipse: eclipseSettings,
|
||||||
|
CutiePi: CutiePiSettings,
|
||||||
githubLight: githubLightSettings,
|
githubLight: githubLightSettings,
|
||||||
githubDark: githubDarkSettings,
|
githubDark: githubDarkSettings,
|
||||||
gruvboxDark: gruvboxDarkSettings,
|
gruvboxDark: gruvboxDarkSettings,
|
||||||
|
|||||||
45
packages/codemirror/themes/CutiePi.mjs
vendored
Normal file
45
packages/codemirror/themes/CutiePi.mjs
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* @name Cutie Pi
|
||||||
|
* by Switch Angel
|
||||||
|
*/
|
||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
import { createTheme } from './theme-helper.mjs';
|
||||||
|
const deepPurple = '#5c019a';
|
||||||
|
const yellowPink = '#fbeffc';
|
||||||
|
const grey = '#272C35';
|
||||||
|
const pinkAccent = '#fee1ff';
|
||||||
|
const lightGrey = '#465063';
|
||||||
|
const bratGreen = '#9acd3f';
|
||||||
|
const lighterGrey = '#97a1b7';
|
||||||
|
const pink = '#f6a6fd';
|
||||||
|
|
||||||
|
export const settings = {
|
||||||
|
background: 'white',
|
||||||
|
lineBackground: 'transparent',
|
||||||
|
foreground: deepPurple,
|
||||||
|
caret: '#797977',
|
||||||
|
selection: yellowPink,
|
||||||
|
selectionMatch: '#2B323D',
|
||||||
|
gutterBackground: grey,
|
||||||
|
gutterForeground: lightGrey,
|
||||||
|
gutterBorder: 'transparent',
|
||||||
|
lineHighlight: pinkAccent,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createTheme({
|
||||||
|
theme: 'light',
|
||||||
|
settings,
|
||||||
|
styles: [
|
||||||
|
{
|
||||||
|
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||||
|
color: deepPurple,
|
||||||
|
},
|
||||||
|
{ tag: [t.tagName, t.heading], color: settings.foreground },
|
||||||
|
{ tag: t.comment, color: lighterGrey },
|
||||||
|
{ tag: [t.variableName, t.propertyName, t.labelName], color: pink },
|
||||||
|
{ tag: [t.attributeName, t.number], color: '#d19a66' },
|
||||||
|
{ tag: t.className, color: grey },
|
||||||
|
{ tag: t.keyword, color: deepPurple },
|
||||||
|
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: bratGreen },
|
||||||
|
],
|
||||||
|
});
|
||||||
@ -388,7 +388,7 @@ export class Pattern {
|
|||||||
|
|
||||||
polyJoin = function () {
|
polyJoin = function () {
|
||||||
const pp = this;
|
const pp = this;
|
||||||
return pp.fmap((p) => p.repeat(pp._steps.div(p._steps))).outerJoin();
|
return pp.fmap((p) => p.extend(pp._steps.div(p._steps))).outerJoin();
|
||||||
};
|
};
|
||||||
|
|
||||||
polyBind(func) {
|
polyBind(func) {
|
||||||
@ -1244,6 +1244,16 @@ export function reify(thing) {
|
|||||||
return pure(thing);
|
return pure(thing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Takes a list of patterns, and returns a pattern of lists.
|
||||||
|
*/
|
||||||
|
export function sequenceP(pats) {
|
||||||
|
let result = pure([]);
|
||||||
|
for (const pat of pats) {
|
||||||
|
result = result.bind((list) => pat.fmap((v) => list.concat([v])));
|
||||||
|
}
|
||||||
|
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}
|
||||||
@ -1318,7 +1328,7 @@ export function stackBy(by, ...pats) {
|
|||||||
left: stackLeft,
|
left: stackLeft,
|
||||||
right: stackRight,
|
right: stackRight,
|
||||||
expand: stack,
|
expand: stack,
|
||||||
repeat: (...args) => polymeterSteps(steps, ...args),
|
repeat: (...args) => polymeter(...args).steps(steps),
|
||||||
};
|
};
|
||||||
return by
|
return by
|
||||||
.inhabit(lookup)
|
.inhabit(lookup)
|
||||||
@ -1820,7 +1830,10 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f
|
|||||||
export const focus = register('focus', function (b, e, pat) {
|
export const focus = register('focus', function (b, e, pat) {
|
||||||
b = Fraction(b);
|
b = Fraction(b);
|
||||||
e = Fraction(e);
|
e = Fraction(e);
|
||||||
return pat._fast(Fraction(1).div(e.sub(b))).late(b.cyclePos());
|
return pat
|
||||||
|
._early(b.sam())
|
||||||
|
._fast(Fraction(1).div(e.sub(b)))
|
||||||
|
._late(b);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], function (span, pat) {
|
export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], function (span, pat) {
|
||||||
@ -2030,7 +2043,7 @@ export const zoom = register('zoom', function (s, e, pat) {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
const d = e.sub(s);
|
const d = e.sub(s);
|
||||||
const steps = __steps ? pat._steps.mulmaybe(d) : undefined;
|
const steps = __steps ? pat._steps?.mulmaybe(d) : undefined;
|
||||||
return pat
|
return pat
|
||||||
.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
|
.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
|
||||||
.withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
|
.withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
|
||||||
@ -2635,34 +2648,10 @@ export function _polymeterListSteps(steps, ...args) {
|
|||||||
/**
|
/**
|
||||||
* *Experimental*
|
* *Experimental*
|
||||||
*
|
*
|
||||||
* Aligns the steps of the patterns, to match the given number of steps per cycle, creating polymeters.
|
* 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.
|
||||||
*
|
|
||||||
* @name polymeterSteps
|
|
||||||
* @param {number} steps how many items are placed in one cycle
|
|
||||||
* @param {any[]} patterns one or more patterns
|
|
||||||
* @example
|
|
||||||
* // the same as "{c d, e f g}%4"
|
|
||||||
* polymeterSteps(4, "c d", "e f g").note()
|
|
||||||
*/
|
|
||||||
export function polymeterSteps(steps, ...args) {
|
|
||||||
if (args.length == 0) {
|
|
||||||
return silence;
|
|
||||||
}
|
|
||||||
if (Array.isArray(args[0])) {
|
|
||||||
// Support old behaviour
|
|
||||||
return _polymeterListSteps(steps, ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
return polymeter(...args).pace(steps);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* *Experimental*
|
|
||||||
*
|
|
||||||
* Aligns the steps of the patterns, to match the steps per cycle of the first pattern, creating polymeters. See `polymeterSteps` to set the target steps explicitly.
|
|
||||||
* @synonyms pm
|
* @synonyms pm
|
||||||
* @example
|
* @example
|
||||||
* // The same as note("{c eb g, c2 g2}")
|
* // The same as note("{c eb g, c2 g2}%6")
|
||||||
* polymeter("c eb g", "c2 g2").note()
|
* polymeter("c eb g", "c2 g2").note()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@ -2678,13 +2667,12 @@ export function polymeter(...args) {
|
|||||||
if (args.length == 0) {
|
if (args.length == 0) {
|
||||||
return silence;
|
return silence;
|
||||||
}
|
}
|
||||||
const steps = args[0]._steps;
|
const steps = lcm(...args.map((x) => x._steps));
|
||||||
if (steps.eq(Fraction(0))) {
|
if (steps.eq(Fraction(0))) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
const [head, ...tail] = args;
|
|
||||||
|
|
||||||
const result = stack(head, ...tail.map((pat) => pat._slow(pat._steps.div(steps))));
|
const result = stack(...args.map((x) => x.pace(steps)));
|
||||||
result._steps = steps;
|
result._steps = steps;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -2845,16 +2833,16 @@ export const drop = stepRegister('drop', function (i, pat) {
|
|||||||
/**
|
/**
|
||||||
* *Experimental*
|
* *Experimental*
|
||||||
*
|
*
|
||||||
* `repeat` is similar to `fast` in that it 'speeds up' the pattern, but it also increases the step count
|
* `extend` is similar to `fast` in that it increases its density, but it also increases the step count
|
||||||
* accordingly. So `stepcat("a b".repeat(2), "c d")` would be the same as `"a b a b c d"`, whereas
|
* 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"`.
|
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
|
||||||
* @example
|
* @example
|
||||||
* stepcat(
|
* stepcat(
|
||||||
* sound("bd bd - cp").repeat(2),
|
* sound("bd bd - cp").extend(2),
|
||||||
* sound("bd - sd -")
|
* sound("bd - sd -")
|
||||||
* ).pace(8)
|
* ).pace(8)
|
||||||
*/
|
*/
|
||||||
export const repeat = stepRegister('repeat', function (factor, pat) {
|
export const extend = stepRegister('extend', function (factor, pat) {
|
||||||
return pat.fast(factor).expand(factor);
|
return pat.fast(factor).expand(factor);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -3052,8 +3040,6 @@ export const timeCat = stepcat;
|
|||||||
// Deprecated stepwise aliases
|
// Deprecated stepwise aliases
|
||||||
export const s_cat = stepcat;
|
export const s_cat = stepcat;
|
||||||
export const s_alt = stepalt;
|
export const s_alt = stepalt;
|
||||||
export const s_polymeterSteps = polymeterSteps;
|
|
||||||
Pattern.prototype.s_polymeterSteps = Pattern.prototype.polymeterSteps;
|
|
||||||
export const s_polymeter = polymeter;
|
export const s_polymeter = polymeter;
|
||||||
Pattern.prototype.s_polymeter = Pattern.prototype.polymeter;
|
Pattern.prototype.s_polymeter = Pattern.prototype.polymeter;
|
||||||
export const s_taper = shrink;
|
export const s_taper = shrink;
|
||||||
@ -3066,8 +3052,8 @@ export const s_sub = drop;
|
|||||||
Pattern.prototype.s_sub = Pattern.prototype.drop;
|
Pattern.prototype.s_sub = Pattern.prototype.drop;
|
||||||
export const s_expand = expand;
|
export const s_expand = expand;
|
||||||
Pattern.prototype.s_expand = Pattern.prototype.expand;
|
Pattern.prototype.s_expand = Pattern.prototype.expand;
|
||||||
export const s_extend = repeat;
|
export const s_extend = extend;
|
||||||
Pattern.prototype.s_extend = Pattern.prototype.repeat;
|
Pattern.prototype.s_extend = Pattern.prototype.extend;
|
||||||
export const s_contract = contract;
|
export const s_contract = contract;
|
||||||
Pattern.prototype.s_contract = Pattern.prototype.contract;
|
Pattern.prototype.s_contract = Pattern.prototype.contract;
|
||||||
export const s_tour = tour;
|
export const s_tour = tour;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Hap } from './hap.mjs';
|
import { Hap } from './hap.mjs';
|
||||||
import { Pattern, fastcat, pure, register, reify, silence, stack } from './pattern.mjs';
|
import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
|
|
||||||
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
|
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
|
||||||
@ -20,9 +20,6 @@ export const signal = (func) => {
|
|||||||
return new Pattern(query);
|
return new Pattern(query);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isaw = signal((t) => 1 - (t % 1));
|
|
||||||
export const isaw2 = isaw.toBipolar();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sawtooth signal between 0 and 1.
|
* A sawtooth signal between 0 and 1.
|
||||||
*
|
*
|
||||||
@ -36,8 +33,40 @@ export const isaw2 = isaw.toBipolar();
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const saw = signal((t) => t % 1);
|
export const saw = signal((t) => t % 1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sawtooth signal between -1 and 1 (like `saw`, but bipolar).
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
*/
|
||||||
export const saw2 = saw.toBipolar();
|
export const saw2 = saw.toBipolar();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sawtooth signal between 1 and 0 (like `saw`, but flipped).
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
* @example
|
||||||
|
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
|
||||||
|
* .clip(isaw.slow(2))
|
||||||
|
* @example
|
||||||
|
* n(isaw.range(0,8).segment(8))
|
||||||
|
* .scale('C major')
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const isaw = signal((t) => 1 - (t % 1));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sawtooth signal between 1 and -1 (like `saw2`, but flipped).
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
*/
|
||||||
|
export const isaw2 = isaw.toBipolar();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sine signal between -1 and 1 (like `sine`, but bipolar).
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
*/
|
||||||
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,6 +90,12 @@ export const sine = sine2.fromBipolar();
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const cosine = sine._early(Fraction(1).div(4));
|
export const cosine = sine._early(Fraction(1).div(4));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cosine signal between -1 and 1 (like `cosine`, but bipolar).
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
*/
|
||||||
export const cosine2 = sine2._early(Fraction(1).div(4));
|
export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,6 +107,12 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
export const square = signal((t) => Math.floor((t * 2) % 2));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A square signal between -1 and 1 (like `square`, but bipolar).
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
*/
|
||||||
export const square2 = square.toBipolar();
|
export const square2 = square.toBipolar();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -82,9 +123,37 @@ export const square2 = square.toBipolar();
|
|||||||
* n(tri.segment(8).range(0,7)).scale("C:minor")
|
* n(tri.segment(8).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const tri = fastcat(isaw, saw);
|
export const tri = fastcat(saw, isaw);
|
||||||
export const tri2 = fastcat(isaw2, saw2);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A triangle signal between -1 and 1 (like `tri`, but bipolar).
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
*/
|
||||||
|
export const tri2 = fastcat(saw2, isaw2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An inverted triangle signal between 1 and 0 (like `tri`, but flipped).
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
* @example
|
||||||
|
* n(itri.segment(8).range(0,7)).scale("C:minor")
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const itri = fastcat(isaw, saw);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An inverted triangle signal between -1 and 1 (like `itri`, but bipolar).
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
*/
|
||||||
|
export const itri2 = fastcat(isaw2, saw2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A signal representing the cycle time.
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
*/
|
||||||
export const time = signal(id);
|
export const time = signal(id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -364,19 +433,30 @@ export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
|
|||||||
export const randcat = chooseCycles;
|
export const randcat = chooseCycles;
|
||||||
|
|
||||||
const _wchooseWith = function (pat, ...pairs) {
|
const _wchooseWith = function (pat, ...pairs) {
|
||||||
|
// A list of patterns of values
|
||||||
const values = pairs.map((pair) => reify(pair[0]));
|
const values = pairs.map((pair) => reify(pair[0]));
|
||||||
|
|
||||||
|
// A list of weight patterns
|
||||||
const weights = [];
|
const weights = [];
|
||||||
let accum = 0;
|
|
||||||
|
let total = pure(0);
|
||||||
for (const pair of pairs) {
|
for (const pair of pairs) {
|
||||||
accum += pair[1];
|
// 'add' accepts either values or patterns of values here, so no need
|
||||||
weights.push(accum);
|
// to explicitly reify
|
||||||
|
total = total.add(pair[1]);
|
||||||
|
// accumulate our list of weight patterns
|
||||||
|
weights.push(total);
|
||||||
}
|
}
|
||||||
const total = accum;
|
// a pattern of lists of weights
|
||||||
|
const weightspat = sequenceP(weights);
|
||||||
|
|
||||||
|
// Takes a number from 0-1, returns a pattern of patterns of values
|
||||||
const match = function (r) {
|
const match = function (r) {
|
||||||
const find = r * total;
|
const findpat = total.mul(r);
|
||||||
return values[weights.findIndex((x) => x > find, weights)];
|
return weightspat.fmap((weights) => (find) => values[weights.findIndex((x) => x > find, weights)]).appLeft(findpat);
|
||||||
};
|
};
|
||||||
return pat.fmap(match);
|
// This returns a pattern of patterns.. The innerJoin is in wchooseCycles
|
||||||
|
return pat.bind(match);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
|
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
|
||||||
@ -398,6 +478,9 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
|||||||
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
|
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
|
||||||
* @example
|
* @example
|
||||||
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
|
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
|
||||||
|
* @example
|
||||||
|
* // The probability can itself be a pattern
|
||||||
|
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
|
||||||
*/
|
*/
|
||||||
export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pairs).innerJoin();
|
export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pairs).innerJoin();
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,6 @@ import {
|
|||||||
sequence,
|
sequence,
|
||||||
palindrome,
|
palindrome,
|
||||||
polymeter,
|
polymeter,
|
||||||
polymeterSteps,
|
|
||||||
polyrhythm,
|
polyrhythm,
|
||||||
silence,
|
silence,
|
||||||
fast,
|
fast,
|
||||||
@ -611,17 +610,10 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
describe('polymeter()', () => {
|
describe('polymeter()', () => {
|
||||||
it('Can layer up cycles, stepwise, with lists', () => {
|
it('Can layer up cycles, stepwise, with lists', () => {
|
||||||
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
|
|
||||||
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
|
expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
|
||||||
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
|
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('Can layer up cycles, stepwise, with weighted patterns', () => {
|
|
||||||
sameFirst(polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('firstOf()', () => {
|
describe('firstOf()', () => {
|
||||||
@ -1265,4 +1257,14 @@ describe('Pattern', () => {
|
|||||||
expect(s('bev').chop(8).loopAt(2)._steps).toStrictEqual(Fraction(4));
|
expect(s('bev').chop(8).loopAt(2)._steps).toStrictEqual(Fraction(4));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('bite', () => {
|
||||||
|
it('works with uneven patterns', () => {
|
||||||
|
sameFirst(
|
||||||
|
fastcat(slowcat('a', 'b', 'c', 'd', 'e'), slowcat(1, 2, 3, 4, 5))
|
||||||
|
.bite(2, stepcat(pure(0), pure(1).expand(2)))
|
||||||
|
.fast(5),
|
||||||
|
stepcat(slowcat('a', 'b', 'c', 'd', 'e'), slowcat(1, 2, 3, 4, 5).expand(2)).fast(5),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
93
packages/gamepad/README.md
Normal file
93
packages/gamepad/README.md
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# @strudel/gamepad
|
||||||
|
|
||||||
|
This package adds gamepad input functionality to strudel Patterns.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i @strudel/gamepad --save
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { gamepad } from '@strudel/gamepad';
|
||||||
|
|
||||||
|
// Initialize gamepad (optional index parameter, defaults to 0)
|
||||||
|
const pad = gamepad(0);
|
||||||
|
|
||||||
|
// Use gamepad inputs in patterns
|
||||||
|
const pattern = sequence([
|
||||||
|
// Button inputs
|
||||||
|
pad.a, // A button value (0-1)
|
||||||
|
pad.tglA, // A button toggle (0 or 1)
|
||||||
|
|
||||||
|
// Analog stick inputs
|
||||||
|
pad.x1, // Left stick X (0-1)
|
||||||
|
pad.x1_2, // Left stick X (-1 to 1)
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Controls
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
- Face Buttons
|
||||||
|
- `a`, `b`, `x`, `y` (or uppercase `A`, `B`, `X`, `Y`)
|
||||||
|
- Toggle versions: `tglA`, `tglB`, `tglX`, `tglY`
|
||||||
|
- Shoulder Buttons
|
||||||
|
- `lb`, `rb`, `lt`, `rt` (or uppercase `LB`, `RB`, `LT`, `RT`)
|
||||||
|
- Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT`
|
||||||
|
- D-Pad
|
||||||
|
- `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase)
|
||||||
|
- Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight`(or `tglU`, `tglD`, `tglL`, `tglR`)
|
||||||
|
|
||||||
|
### Analog Sticks
|
||||||
|
- Left Stick
|
||||||
|
- `x1`, `y1` (0 to 1 range)
|
||||||
|
- `x1_2`, `y1_2` (-1 to 1 range)
|
||||||
|
- Right Stick
|
||||||
|
- `x2`, `y2` (0 to 1 range)
|
||||||
|
- `x2_2`, `y2_2` (-1 to 1 range)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Use button values to control amplitude
|
||||||
|
$: sequence([
|
||||||
|
s("bd").gain(pad.X), // X button controls gain
|
||||||
|
s("[hh oh]").gain(pad.tglY), // Y button toggles gain
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Use analog stick for continuous control
|
||||||
|
$: note("c4*4".add(pad.y1_2.range(-24,24))) // Left stick Y controls pitch shift
|
||||||
|
.pan(pad.x1_2); // Left stick X controls panning
|
||||||
|
|
||||||
|
// Use toggle buttons to switch patterns on/off
|
||||||
|
|
||||||
|
// Define button sequences
|
||||||
|
const HADOKEN = [
|
||||||
|
'd', // Down
|
||||||
|
'r', // Right
|
||||||
|
'a', // A
|
||||||
|
];
|
||||||
|
|
||||||
|
const KONAMI = 'uuddlrlrba' //Konami Code ↑↑↓↓←→←→BA
|
||||||
|
|
||||||
|
// Add these lines to enable buttons(but why?)
|
||||||
|
$:pad.D.segment(16).gain(0)
|
||||||
|
$:pad.R.segment(16).gain(0)
|
||||||
|
$:pad.A.segment(16).gain(0)
|
||||||
|
|
||||||
|
// Check button sequence (returns 1 when detected, 0 when not within last 1 second)
|
||||||
|
$: sound("hadoken").gain(pad.checkSequence(HADOKEN))
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multiple Gamepads
|
||||||
|
|
||||||
|
You can connect multiple gamepads by specifying the gamepad index:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const pad1 = gamepad(0); // First gamepad
|
||||||
|
const pad2 = gamepad(1); // Second gamepad
|
||||||
|
```
|
||||||
117
packages/gamepad/docs/gamepad.mdx
Normal file
117
packages/gamepad/docs/gamepad.mdx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { MiniRepl } from '../../../website/src/docs/MiniRepl';
|
||||||
|
|
||||||
|
# Gamepad
|
||||||
|
|
||||||
|
The Gamepad module allows you to integrate gamepad input functionality into your musical patterns. This can be particularly useful for live performances or interactive installations where you want to manipulate sounds using a game controller.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Initialize a gamepad by calling the gamepad() function with an optional index parameter.
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`// Initialize gamepad (optional index parameter, defaults to 0)
|
||||||
|
const gp = gamepad(0)
|
||||||
|
note("c a f e").mask(gp.a)`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Available Controls
|
||||||
|
|
||||||
|
The gamepad module provides access to buttons and analog sticks as normalized signals (0-1) that can modulate your patterns.
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
| Type | Controls |
|
||||||
|
| ---------------- | ---------------------------------------------------------------------------------------------- |
|
||||||
|
| Face Buttons | `a`, `b`, `x`, `y` (or uppercase `A`, `B`, `X`, `Y`) |
|
||||||
|
| | Toggle versions: `tglA`, `tglB`, `tglX`, `tglY` |
|
||||||
|
| Shoulder Buttons | `lb`, `rb`, `lt`, `rt` (or uppercase `LB`, `RB`, `LT`, `RT`) |
|
||||||
|
| | Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT` |
|
||||||
|
| D-Pad | `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) |
|
||||||
|
| | Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight` (or `tglU`, `tglD`, `tglL`, `tglR`) |
|
||||||
|
|
||||||
|
### Analog Sticks
|
||||||
|
|
||||||
|
| Stick | Controls |
|
||||||
|
| ----------- | ------------------------------ |
|
||||||
|
| Left Stick | `x1`, `y1` (0 to 1 range) |
|
||||||
|
| | `x1_2`, `y1_2` (-1 to 1 range) |
|
||||||
|
| Right Stick | `x2`, `y2` (0 to 1 range) |
|
||||||
|
| | `x2_2`, `y2_2` (-1 to 1 range) |
|
||||||
|
|
||||||
|
### Button Sequence
|
||||||
|
|
||||||
|
| Stick | Controls |
|
||||||
|
| --------------- | --------------------------------------- |
|
||||||
|
| Button Sequence | `btnSequence()`, `btnSeq()`, `btnseq()` |
|
||||||
|
|
||||||
|
## Using Gamepad Inputs
|
||||||
|
|
||||||
|
Once initialized, you can use various gamepad inputs in your patterns. Here are some examples:
|
||||||
|
|
||||||
|
### Button Inputs
|
||||||
|
|
||||||
|
You can use button inputs to control different aspects of your music, such as gain or triggering events.
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`const gp = gamepad(0)
|
||||||
|
// Use button values to control amplitude
|
||||||
|
$: stack(
|
||||||
|
s("[[hh hh] oh hh oh]/2").mask(gp.tglX).bank("RolandTR909"), // X btn for HH
|
||||||
|
s("cr*1").mask(gp.Y).bank("RolandTR909"), // LB btn for CR
|
||||||
|
s("bd").mask(gp.tglA).bank("RolandTR909"), // A btn for BD
|
||||||
|
s("[ht - - mt - - lt - ]/2").mask(gp.tglB).bank("RolandTR909"), // B btn for Toms
|
||||||
|
s("sd*4").mask(gp.RB).bank("RolandTR909"), // RB btn for SD
|
||||||
|
).cpm(120)
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### Analog Stick Inputs
|
||||||
|
|
||||||
|
Analog sticks can be used for continuous control, such as pitch shifting or panning.
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`const gp = gamepad(0)
|
||||||
|
// Use analog stick for continuous control
|
||||||
|
$: note("c4 d3 a3 e3").sound("sawtooth")
|
||||||
|
.lpf(gp.x1.range(100,4000))
|
||||||
|
.lpq(gp.y1.range(5,30))
|
||||||
|
.decay(gp.y2.range(0.1,2))
|
||||||
|
.lpenv(gp.x2.range(-5,5))
|
||||||
|
.cpm(120)
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### Button Sequences
|
||||||
|
|
||||||
|
You can define button sequences to trigger specific actions, like playing a sound when a sequence is detected.
|
||||||
|
|
||||||
|
<MiniRepl client:idle tune={`const gp = gamepad(0)
|
||||||
|
// Define button sequences
|
||||||
|
const HADOUKEN = [
|
||||||
|
'd', // Down
|
||||||
|
'r', // Right
|
||||||
|
'a', // A
|
||||||
|
]
|
||||||
|
const KONAMI = 'uuddlrlrba' //Konami Code ↑↑↓↓←→←→BA
|
||||||
|
|
||||||
|
// Check butto-n sequence (returns 1 while detected, 0 when not within last 1 second)
|
||||||
|
$: s("free_hadouken -").slow(2)
|
||||||
|
.mask(gp.btnSequence(HADOUKEN)).room(1).cpm(120)
|
||||||
|
|
||||||
|
// hadouken.wav by Syna-Max
|
||||||
|
//https://freesound.org/people/Syna-Max/sounds/67674/
|
||||||
|
samples({free_hadouken: 'https://cdn.freesound.org/previews/67/67674_111920-lq.mp3'})
|
||||||
|
`} />
|
||||||
|
|
||||||
|
## Multiple Gamepads
|
||||||
|
|
||||||
|
Strudel supports multiple gamepads. You can specify the gamepad index to connect to different devices.
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`const pad1 = gamepad(0); // First gamepad
|
||||||
|
const pad2 = gamepad(1); // Second gamepad`}
|
||||||
|
/>
|
||||||
246
packages/gamepad/gamepad.mjs
Normal file
246
packages/gamepad/gamepad.mjs
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
// @strudel/gamepad/index.mjs
|
||||||
|
|
||||||
|
import { signal } from '@strudel/core';
|
||||||
|
|
||||||
|
// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)
|
||||||
|
export const buttonMap = {
|
||||||
|
a: 0,
|
||||||
|
b: 1,
|
||||||
|
x: 2,
|
||||||
|
y: 3,
|
||||||
|
lb: 4,
|
||||||
|
rb: 5,
|
||||||
|
lt: 6,
|
||||||
|
rt: 7,
|
||||||
|
back: 8,
|
||||||
|
start: 9,
|
||||||
|
u: 12,
|
||||||
|
up: 12,
|
||||||
|
d: 13,
|
||||||
|
down: 13,
|
||||||
|
l: 14,
|
||||||
|
left: 14,
|
||||||
|
r: 15,
|
||||||
|
right: 15,
|
||||||
|
};
|
||||||
|
|
||||||
|
class ButtonSequenceDetector {
|
||||||
|
constructor(timeWindow = 1000) {
|
||||||
|
this.sequence = [];
|
||||||
|
this.timeWindow = timeWindow;
|
||||||
|
this.lastInputTime = 0;
|
||||||
|
this.buttonStates = Array(16).fill(0); // Track previous state of each button
|
||||||
|
// Button mapping for character inputs
|
||||||
|
}
|
||||||
|
|
||||||
|
addInput(buttonIndex, buttonValue) {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
|
||||||
|
// Only add input on button press (rising edge)
|
||||||
|
if (buttonValue === 1 && this.buttonStates[buttonIndex] === 0) {
|
||||||
|
// Clear sequence if too much time has passed
|
||||||
|
if (currentTime - this.lastInputTime > this.timeWindow) {
|
||||||
|
this.sequence = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the button name instead of index
|
||||||
|
const buttonName = Object.keys(buttonMap).find((key) => buttonMap[key] === buttonIndex) || buttonIndex.toString();
|
||||||
|
|
||||||
|
this.sequence.push({
|
||||||
|
input: buttonName,
|
||||||
|
timestamp: currentTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.lastInputTime = currentTime;
|
||||||
|
|
||||||
|
//console.log(this.sequence);
|
||||||
|
// Keep only inputs within the time window
|
||||||
|
this.sequence = this.sequence.filter((entry) => currentTime - entry.timestamp <= this.timeWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update button state
|
||||||
|
this.buttonStates[buttonIndex] = buttonValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkSequence(targetSequence) {
|
||||||
|
if (!Array.isArray(targetSequence) && typeof targetSequence !== 'string') {
|
||||||
|
console.error('ButtonSequenceDetector: targetSequence must be an array or string');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sequence.length < targetSequence.length) return 0;
|
||||||
|
|
||||||
|
// Convert string input to array if needed
|
||||||
|
const sequence =
|
||||||
|
typeof targetSequence === 'string'
|
||||||
|
? targetSequence.toLowerCase().split('')
|
||||||
|
: targetSequence.map((s) => s.toString().toLowerCase());
|
||||||
|
|
||||||
|
//console.log(this.sequence);
|
||||||
|
|
||||||
|
// Get the last n inputs where n is the target sequence length
|
||||||
|
const lastInputs = this.sequence.slice(-targetSequence.length).map((entry) => entry.input);
|
||||||
|
|
||||||
|
// Compare sequences
|
||||||
|
return lastInputs.every((input, index) => {
|
||||||
|
const target = sequence[index];
|
||||||
|
// Check if either the input matches directly or they refer to the same button in the map
|
||||||
|
return (
|
||||||
|
input === target ||
|
||||||
|
buttonMap[input] === buttonMap[target] ||
|
||||||
|
// Also check if the numerical index matches
|
||||||
|
buttonMap[input] === parseInt(target)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GamepadHandler {
|
||||||
|
constructor(index = 0) {
|
||||||
|
// Add index parameter
|
||||||
|
this._gamepads = {};
|
||||||
|
this._activeGamepad = index; // Use provided index
|
||||||
|
this._axes = [0, 0, 0, 0];
|
||||||
|
this._buttons = Array(16).fill(0);
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
window.addEventListener('gamepadconnected', (e) => {
|
||||||
|
this._gamepads[e.gamepad.index] = e.gamepad;
|
||||||
|
if (!this._activeGamepad) {
|
||||||
|
this._activeGamepad = e.gamepad.index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('gamepaddisconnected', (e) => {
|
||||||
|
delete this._gamepads[e.gamepad.index];
|
||||||
|
if (this._activeGamepad === e.gamepad.index) {
|
||||||
|
this._activeGamepad = Object.keys(this._gamepads)[0] || null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
poll() {
|
||||||
|
if (this._activeGamepad !== null) {
|
||||||
|
const gamepad = navigator.getGamepads()[this._activeGamepad];
|
||||||
|
if (gamepad) {
|
||||||
|
// Update axes (normalized to 0-1 range)
|
||||||
|
this._axes = gamepad.axes.map((axis) => (axis + 1) / 2);
|
||||||
|
// Update buttons
|
||||||
|
this._buttons = gamepad.buttons.map((button) => button.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAxes() {
|
||||||
|
return this._axes;
|
||||||
|
}
|
||||||
|
getButtons() {
|
||||||
|
return this._buttons;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module-level state store for toggle states
|
||||||
|
const gamepadStates = new Map();
|
||||||
|
|
||||||
|
export const gamepad = (index = 0) => {
|
||||||
|
const handler = new GamepadHandler(index);
|
||||||
|
const sequenceDetector = new ButtonSequenceDetector(2000);
|
||||||
|
|
||||||
|
// Base signal that polls gamepad state and handles sequence detection
|
||||||
|
const baseSignal = signal((t) => {
|
||||||
|
handler.poll();
|
||||||
|
const axes = handler.getAxes();
|
||||||
|
const buttons = handler.getButtons();
|
||||||
|
|
||||||
|
// Add all button inputs to sequence detector
|
||||||
|
buttons.forEach((value, i) => {
|
||||||
|
sequenceDetector.addInput(i, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { axes, buttons, t };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create axes patterns
|
||||||
|
const axes = {
|
||||||
|
x1: baseSignal.fmap((state) => state.axes[0]),
|
||||||
|
y1: baseSignal.fmap((state) => state.axes[1]),
|
||||||
|
x2: baseSignal.fmap((state) => state.axes[2]),
|
||||||
|
y2: baseSignal.fmap((state) => state.axes[3]),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add bipolar versions
|
||||||
|
axes.x1_2 = axes.x1.toBipolar();
|
||||||
|
axes.y1_2 = axes.y1.toBipolar();
|
||||||
|
axes.x2_2 = axes.x2.toBipolar();
|
||||||
|
axes.y2_2 = axes.y2.toBipolar();
|
||||||
|
|
||||||
|
// Create button patterns
|
||||||
|
const buttons = Array(16)
|
||||||
|
.fill(null)
|
||||||
|
.map((_, i) => {
|
||||||
|
// Create unique key for this gamepad+button combination
|
||||||
|
const stateKey = `gamepad${index}_btn${i}`;
|
||||||
|
|
||||||
|
// Initialize toggle state if it doesn't exist
|
||||||
|
if (!gamepadStates.has(stateKey)) {
|
||||||
|
gamepadStates.set(stateKey, {
|
||||||
|
lastButtonState: 0,
|
||||||
|
toggleState: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct button value pattern (no longer needs to call addInput)
|
||||||
|
const btn = baseSignal.fmap((state) => state.buttons[i]);
|
||||||
|
|
||||||
|
// Button toggle pattern with persistent state
|
||||||
|
const toggle = baseSignal.fmap((state) => {
|
||||||
|
const currentState = state.buttons[i];
|
||||||
|
const buttonState = gamepadStates.get(stateKey);
|
||||||
|
|
||||||
|
if (currentState === 1 && buttonState.lastButtonState === 0) {
|
||||||
|
// Toggle the state on rising edge
|
||||||
|
buttonState.toggleState = buttonState.toggleState === 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonState.lastButtonState = currentState;
|
||||||
|
return buttonState.toggleState;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { value: btn, toggle };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create sequence checker pattern
|
||||||
|
const btnSequence = (sequence) => {
|
||||||
|
return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence));
|
||||||
|
};
|
||||||
|
const checkSequence = btnSequence;
|
||||||
|
const btnSeq = btnSequence;
|
||||||
|
const btnseq = btnSeq;
|
||||||
|
|
||||||
|
// Return an object with all controls
|
||||||
|
return {
|
||||||
|
...axes,
|
||||||
|
buttons,
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(buttonMap).flatMap(([key, index]) => [
|
||||||
|
[key.toLowerCase(), buttons[index].value],
|
||||||
|
[key.toUpperCase(), buttons[index].value],
|
||||||
|
[`tgl${key.toLowerCase()}`, buttons[index].toggle],
|
||||||
|
[`tgl${key.toUpperCase()}`, buttons[index].toggle],
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
checkSequence,
|
||||||
|
btnSequence,
|
||||||
|
btnSeq,
|
||||||
|
btnseq,
|
||||||
|
raw: baseSignal,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optional: Export for debugging or state management
|
||||||
|
export const getGamepadStates = () => Object.fromEntries(gamepadStates);
|
||||||
|
export const clearGamepadStates = () => gamepadStates.clear();
|
||||||
3
packages/gamepad/index.mjs
Normal file
3
packages/gamepad/index.mjs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import './gamepad.mjs';
|
||||||
|
|
||||||
|
export * from './gamepad.mjs';
|
||||||
38
packages/gamepad/package.json
Normal file
38
packages/gamepad/package.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "@strudel/gamepad",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"description": "Gamepad Inputs for strudel",
|
||||||
|
"main": "index.mjs",
|
||||||
|
"type": "module",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.mjs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"titdalcycles",
|
||||||
|
"strudel",
|
||||||
|
"pattern",
|
||||||
|
"livecoding",
|
||||||
|
"algorave"
|
||||||
|
],
|
||||||
|
"author": "Yuta Nakayama <nkymut@gmail.com>",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"@strudel/core": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.0.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
19
packages/gamepad/vite.config.js
Normal file
19
packages/gamepad/vite.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { dependencies } from './package.json';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'index.mjs'),
|
||||||
|
formats: ['es'],
|
||||||
|
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [...Object.keys(dependencies)],
|
||||||
|
},
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -133,6 +133,65 @@ export function registerSynthSounds() {
|
|||||||
{ prebake: true, type: 'synth' },
|
{ prebake: true, type: 'synth' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
registerSound(
|
||||||
|
'pulse',
|
||||||
|
(begin, value, onended) => {
|
||||||
|
const ac = getAudioContext();
|
||||||
|
let { duration, n: pulsewidth = 0.5 } = value;
|
||||||
|
const frequency = getFrequencyFromValue(value);
|
||||||
|
|
||||||
|
const [attack, decay, sustain, release] = getADSRValues(
|
||||||
|
[value.attack, value.decay, value.sustain, value.release],
|
||||||
|
'linear',
|
||||||
|
[0.001, 0.05, 0.6, 0.01],
|
||||||
|
);
|
||||||
|
|
||||||
|
const holdend = begin + duration;
|
||||||
|
const end = holdend + release + 0.01;
|
||||||
|
|
||||||
|
let o = getWorklet(
|
||||||
|
ac,
|
||||||
|
'pulse-oscillator',
|
||||||
|
{
|
||||||
|
frequency,
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
pulsewidth,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputChannelCount: [2],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||||
|
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||||
|
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
||||||
|
let envGain = gainNode(1);
|
||||||
|
envGain = o.connect(envGain);
|
||||||
|
|
||||||
|
webAudioTimeout(
|
||||||
|
ac,
|
||||||
|
() => {
|
||||||
|
o.disconnect();
|
||||||
|
envGain.disconnect();
|
||||||
|
onended();
|
||||||
|
fm?.stop();
|
||||||
|
vibratoOscillator?.stop();
|
||||||
|
},
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
);
|
||||||
|
|
||||||
|
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||||
|
|
||||||
|
return {
|
||||||
|
node: envGain,
|
||||||
|
stop: (time) => {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ prebake: true, type: 'synth' },
|
||||||
|
);
|
||||||
|
|
||||||
[...noises].forEach((s) => {
|
[...noises].forEach((s) => {
|
||||||
registerSound(
|
registerSound(
|
||||||
s,
|
s,
|
||||||
|
|||||||
@ -75,7 +75,12 @@ const waveshapes = {
|
|||||||
return v - polyBlep(phase, dt);
|
return v - polyBlep(phase, dt);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
function getParamValue(block, param) {
|
||||||
|
if (param.length > 1) {
|
||||||
|
return param[block];
|
||||||
|
}
|
||||||
|
return param[0];
|
||||||
|
}
|
||||||
const waveShapeNames = Object.keys(waveshapes);
|
const waveShapeNames = Object.keys(waveshapes);
|
||||||
class LFOProcessor extends AudioWorkletProcessor {
|
class LFOProcessor extends AudioWorkletProcessor {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
@ -362,6 +367,11 @@ function getUnisonDetune(unison, detune, voiceIndex) {
|
|||||||
}
|
}
|
||||||
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
|
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySemitoneDetuneToFrequency(frequency, detune) {
|
||||||
|
return frequency * Math.pow(2, detune / 12);
|
||||||
|
}
|
||||||
|
|
||||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -438,7 +448,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
const isOdd = (n & 1) == 1;
|
const isOdd = (n & 1) == 1;
|
||||||
|
|
||||||
//applies unison "spread" detune in semitones
|
//applies unison "spread" detune in semitones
|
||||||
const freq = frequency * Math.pow(2, getUnisonDetune(voices, freqspread, n) / 12);
|
const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n));
|
||||||
let gainL = gain1;
|
let gainL = gain1;
|
||||||
let gainR = gain2;
|
let gainR = gain2;
|
||||||
// invert right and left gain
|
// invert right and left gain
|
||||||
@ -648,3 +658,103 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
|
registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
|
||||||
|
|
||||||
|
// Adapted from https://www.musicdsp.org/en/latest/Effects/221-band-limited-pwm-generator.html
|
||||||
|
class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.pi = _PI;
|
||||||
|
this.phi = -this.pi; // phase
|
||||||
|
this.Y0 = 0; // feedback memories
|
||||||
|
this.Y1 = 0;
|
||||||
|
this.PW = this.pi; // pulse width
|
||||||
|
this.B = 2.3; // feedback coefficient
|
||||||
|
this.dphif = 0; // filtered phase increment
|
||||||
|
this.envf = 0; // filtered envelope
|
||||||
|
}
|
||||||
|
|
||||||
|
static get parameterDescriptors() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'begin',
|
||||||
|
defaultValue: 0,
|
||||||
|
max: Number.POSITIVE_INFINITY,
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'end',
|
||||||
|
defaultValue: 0,
|
||||||
|
max: Number.POSITIVE_INFINITY,
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'frequency',
|
||||||
|
defaultValue: 440,
|
||||||
|
min: Number.EPSILON,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'detune',
|
||||||
|
defaultValue: 0,
|
||||||
|
min: Number.NEGATIVE_INFINITY,
|
||||||
|
max: Number.POSITIVE_INFINITY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pulsewidth',
|
||||||
|
defaultValue: 1,
|
||||||
|
min: 0,
|
||||||
|
max: Number.POSITIVE_INFINITY,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs, outputs, params) {
|
||||||
|
if (currentTime <= params.begin[0]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (currentTime >= params.end[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const output = outputs[0];
|
||||||
|
let env = 1,
|
||||||
|
dphi;
|
||||||
|
|
||||||
|
for (let i = 0; i < (output[0].length ?? 0); i++) {
|
||||||
|
const pw = (1 - clamp(getParamValue(i, params.pulsewidth), -0.99, 0.99)) * this.pi;
|
||||||
|
const detune = getParamValue(i, params.detune);
|
||||||
|
const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100);
|
||||||
|
|
||||||
|
dphi = freq * (this.pi / (sampleRate * 0.5)); // phase increment
|
||||||
|
this.dphif += 0.1 * (dphi - this.dphif);
|
||||||
|
|
||||||
|
env *= 0.9998; // exponential decay envelope
|
||||||
|
this.envf += 0.1 * (env - this.envf);
|
||||||
|
|
||||||
|
// Feedback coefficient control
|
||||||
|
this.B = 2.3 * (1 - 0.0001 * freq); // feedback limitation
|
||||||
|
if (this.B < 0) this.B = 0;
|
||||||
|
|
||||||
|
// Waveform generation (half-Tomisawa oscillators)
|
||||||
|
this.phi += this.dphif; // phase increment
|
||||||
|
if (this.phi >= this.pi) this.phi -= 2 * this.pi; // phase wrapping
|
||||||
|
|
||||||
|
// First half-Tomisawa generator
|
||||||
|
let out0 = Math.cos(this.phi + this.B * this.Y0); // self-phase modulation
|
||||||
|
this.Y0 = 0.5 * (out0 + this.Y0); // anti-hunting filter
|
||||||
|
|
||||||
|
// Second half-Tomisawa generator (with phase offset for pulse width)
|
||||||
|
let out1 = Math.cos(this.phi + this.B * this.Y1 + pw);
|
||||||
|
this.Y1 = 0.5 * (out1 + this.Y1); // anti-hunting filter
|
||||||
|
|
||||||
|
for (let o = 0; o < output.length; o++) {
|
||||||
|
// Combination of both oscillators with envelope applied
|
||||||
|
output[o][i] = 0.15 * (out0 - out1) * this.envf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // keep the audio processing going
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
"module": "tidal.mjs",
|
"module": "tidal.mjs",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/felixroos/hs2js.git"
|
"url": "git+https://github.com/tidalcycles/strudel/tree/main/packages/tidal"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"haskell",
|
"haskell",
|
||||||
|
|||||||
108
pnpm-lock.yaml
generated
108
pnpm-lock.yaml
generated
@ -39,6 +39,9 @@ importers:
|
|||||||
'@tauri-apps/cli':
|
'@tauri-apps/cli':
|
||||||
specifier: ^2.2.7
|
specifier: ^2.2.7
|
||||||
version: 2.2.7
|
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))
|
||||||
'@vitest/ui':
|
'@vitest/ui':
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(vitest@3.0.4)
|
version: 3.0.4(vitest@3.0.4)
|
||||||
@ -267,6 +270,16 @@ importers:
|
|||||||
|
|
||||||
packages/embed: {}
|
packages/embed: {}
|
||||||
|
|
||||||
|
packages/gamepad:
|
||||||
|
dependencies:
|
||||||
|
'@strudel/core':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core
|
||||||
|
devDependencies:
|
||||||
|
vite:
|
||||||
|
specifier: ^6.0.11
|
||||||
|
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||||
|
|
||||||
packages/hs2js:
|
packages/hs2js:
|
||||||
dependencies:
|
dependencies:
|
||||||
web-tree-sitter:
|
web-tree-sitter:
|
||||||
@ -640,6 +653,9 @@ importers:
|
|||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/draw
|
version: link:../packages/draw
|
||||||
|
'@strudel/gamepad':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../packages/gamepad
|
||||||
'@strudel/hydra':
|
'@strudel/hydra':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/hydra
|
version: link:../packages/hydra
|
||||||
@ -1412,6 +1428,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==}
|
resolution: {integrity: sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@bcoe/v8-coverage@1.0.2':
|
||||||
|
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@codemirror/autocomplete@6.18.4':
|
'@codemirror/autocomplete@6.18.4':
|
||||||
resolution: {integrity: sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==}
|
resolution: {integrity: sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==}
|
||||||
|
|
||||||
@ -1834,6 +1854,10 @@ packages:
|
|||||||
'@isaacs/string-locale-compare@1.1.0':
|
'@isaacs/string-locale-compare@1.1.0':
|
||||||
resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==}
|
resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==}
|
||||||
|
|
||||||
|
'@istanbuljs/schema@0.1.3':
|
||||||
|
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
'@jest/schemas@29.6.3':
|
'@jest/schemas@29.6.3':
|
||||||
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
|
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
|
||||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||||
@ -2818,6 +2842,15 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0
|
vite: ^4.2.0 || ^5.0.0 || ^6.0.0
|
||||||
|
|
||||||
|
'@vitest/coverage-v8@3.0.4':
|
||||||
|
resolution: {integrity: sha512-f0twgRCHgbs24Dp8cLWagzcObXMcuKtAwgxjJV/nnysPAJJk1JiKu/W0gIehZLmkljhJXU/E0/dmuQzsA/4jhA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vitest/browser': 3.0.4
|
||||||
|
vitest: 3.0.4
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@vitest/browser':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vitest/expect@3.0.4':
|
'@vitest/expect@3.0.4':
|
||||||
resolution: {integrity: sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==}
|
resolution: {integrity: sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==}
|
||||||
|
|
||||||
@ -4487,6 +4520,9 @@ packages:
|
|||||||
hs2js@0.1.0:
|
hs2js@0.1.0:
|
||||||
resolution: {integrity: sha512-THlUIMX8tZf6gtbz5RUZ8xQUyKJEItsx7bxEBcouFIEWjeo90376WMocj3JEz6qTv5nM+tjo3vNvLf89XruMvg==}
|
resolution: {integrity: sha512-THlUIMX8tZf6gtbz5RUZ8xQUyKJEItsx7bxEBcouFIEWjeo90376WMocj3JEz6qTv5nM+tjo3vNvLf89XruMvg==}
|
||||||
|
|
||||||
|
html-escaper@2.0.2:
|
||||||
|
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||||
|
|
||||||
html-escaper@3.0.3:
|
html-escaper@3.0.3:
|
||||||
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
|
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
|
||||||
|
|
||||||
@ -4837,6 +4873,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
|
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
istanbul-lib-coverage@3.2.2:
|
||||||
|
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
istanbul-lib-report@3.0.1:
|
||||||
|
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
istanbul-lib-source-maps@5.0.6:
|
||||||
|
resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
istanbul-reports@3.1.7:
|
||||||
|
resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
jackspeak@3.4.3:
|
jackspeak@3.4.3:
|
||||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
@ -6887,6 +6939,10 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
test-exclude@7.0.1:
|
||||||
|
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
text-encoding-shim@1.0.5:
|
text-encoding-shim@1.0.5:
|
||||||
resolution: {integrity: sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==}
|
resolution: {integrity: sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==}
|
||||||
|
|
||||||
@ -7505,6 +7561,7 @@ packages:
|
|||||||
|
|
||||||
workbox-google-analytics@7.0.0:
|
workbox-google-analytics@7.0.0:
|
||||||
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
||||||
|
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
|
||||||
|
|
||||||
workbox-navigation-preload@7.0.0:
|
workbox-navigation-preload@7.0.0:
|
||||||
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
|
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
|
||||||
@ -8568,6 +8625,8 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.25.9
|
'@babel/helper-string-parser': 7.25.9
|
||||||
'@babel/helper-validator-identifier': 7.25.9
|
'@babel/helper-validator-identifier': 7.25.9
|
||||||
|
|
||||||
|
'@bcoe/v8-coverage@1.0.2': {}
|
||||||
|
|
||||||
'@codemirror/autocomplete@6.18.4':
|
'@codemirror/autocomplete@6.18.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/language': 6.10.8
|
'@codemirror/language': 6.10.8
|
||||||
@ -8934,6 +8993,8 @@ snapshots:
|
|||||||
|
|
||||||
'@isaacs/string-locale-compare@1.1.0': {}
|
'@isaacs/string-locale-compare@1.1.0': {}
|
||||||
|
|
||||||
|
'@istanbuljs/schema@0.1.3': {}
|
||||||
|
|
||||||
'@jest/schemas@29.6.3':
|
'@jest/schemas@29.6.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sinclair/typebox': 0.27.8
|
'@sinclair/typebox': 0.27.8
|
||||||
@ -10187,6 +10248,24 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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))':
|
||||||
|
dependencies:
|
||||||
|
'@ampproject/remapping': 2.3.0
|
||||||
|
'@bcoe/v8-coverage': 1.0.2
|
||||||
|
debug: 4.4.0
|
||||||
|
istanbul-lib-coverage: 3.2.2
|
||||||
|
istanbul-lib-report: 3.0.1
|
||||||
|
istanbul-lib-source-maps: 5.0.6
|
||||||
|
istanbul-reports: 3.1.7
|
||||||
|
magic-string: 0.30.17
|
||||||
|
magicast: 0.3.5
|
||||||
|
std-env: 3.8.0
|
||||||
|
test-exclude: 7.0.1
|
||||||
|
tinyrainbow: 2.0.0
|
||||||
|
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)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@vitest/expect@3.0.4':
|
'@vitest/expect@3.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 3.0.4
|
'@vitest/spy': 3.0.4
|
||||||
@ -12202,6 +12281,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
web-tree-sitter: 0.20.8
|
web-tree-sitter: 0.20.8
|
||||||
|
|
||||||
|
html-escaper@2.0.2: {}
|
||||||
|
|
||||||
html-escaper@3.0.3: {}
|
html-escaper@3.0.3: {}
|
||||||
|
|
||||||
html-void-elements@3.0.0: {}
|
html-void-elements@3.0.0: {}
|
||||||
@ -12540,6 +12621,27 @@ snapshots:
|
|||||||
|
|
||||||
isobject@3.0.1: {}
|
isobject@3.0.1: {}
|
||||||
|
|
||||||
|
istanbul-lib-coverage@3.2.2: {}
|
||||||
|
|
||||||
|
istanbul-lib-report@3.0.1:
|
||||||
|
dependencies:
|
||||||
|
istanbul-lib-coverage: 3.2.2
|
||||||
|
make-dir: 4.0.0
|
||||||
|
supports-color: 7.2.0
|
||||||
|
|
||||||
|
istanbul-lib-source-maps@5.0.6:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/trace-mapping': 0.3.25
|
||||||
|
debug: 4.4.0
|
||||||
|
istanbul-lib-coverage: 3.2.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
istanbul-reports@3.1.7:
|
||||||
|
dependencies:
|
||||||
|
html-escaper: 2.0.2
|
||||||
|
istanbul-lib-report: 3.0.1
|
||||||
|
|
||||||
jackspeak@3.4.3:
|
jackspeak@3.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/cliui': 8.0.2
|
'@isaacs/cliui': 8.0.2
|
||||||
@ -15262,6 +15364,12 @@ snapshots:
|
|||||||
commander: 2.20.3
|
commander: 2.20.3
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
|
|
||||||
|
test-exclude@7.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@istanbuljs/schema': 0.1.3
|
||||||
|
glob: 10.4.5
|
||||||
|
minimatch: 9.0.5
|
||||||
|
|
||||||
text-encoding-shim@1.0.5: {}
|
text-encoding-shim@1.0.5: {}
|
||||||
|
|
||||||
text-extensions@1.9.0: {}
|
text-extensions@1.9.0: {}
|
||||||
|
|||||||
@ -3019,6 +3019,33 @@ exports[`runs examples > example "expand" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "extend" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/8 | s:bd ]",
|
||||||
|
"[ 1/8 → 1/4 | s:bd ]",
|
||||||
|
"[ 3/8 → 1/2 | s:cp ]",
|
||||||
|
"[ 1/2 → 5/8 | s:bd ]",
|
||||||
|
"[ 5/8 → 3/4 | s:bd ]",
|
||||||
|
"[ 7/8 → 1/1 | s:cp ]",
|
||||||
|
"[ 1/1 → 9/8 | s:bd ]",
|
||||||
|
"[ 5/4 → 11/8 | s:sd ]",
|
||||||
|
"[ 3/2 → 13/8 | s:bd ]",
|
||||||
|
"[ 13/8 → 7/4 | s:bd ]",
|
||||||
|
"[ 15/8 → 2/1 | s:cp ]",
|
||||||
|
"[ 2/1 → 17/8 | s:bd ]",
|
||||||
|
"[ 17/8 → 9/4 | s:bd ]",
|
||||||
|
"[ 19/8 → 5/2 | s:cp ]",
|
||||||
|
"[ 5/2 → 21/8 | s:bd ]",
|
||||||
|
"[ 11/4 → 23/8 | s:sd ]",
|
||||||
|
"[ 3/1 → 25/8 | s:bd ]",
|
||||||
|
"[ 25/8 → 13/4 | s:bd ]",
|
||||||
|
"[ 27/8 → 7/2 | s:cp ]",
|
||||||
|
"[ 7/2 → 29/8 | s:bd ]",
|
||||||
|
"[ 29/8 → 15/4 | s:bd ]",
|
||||||
|
"[ 31/8 → 4/1 | s:cp ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "fanchor" example index 0 1`] = `
|
exports[`runs examples > example "fanchor" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:f s:sawtooth cutoff:1000 lpenv:8 fanchor:0 ]",
|
"[ 0/1 → 1/8 | note:f s:sawtooth cutoff:1000 lpenv:8 fanchor:0 ]",
|
||||||
@ -4234,6 +4261,96 @@ exports[`runs examples > example "iresponse" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "isaw" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/8 | note:c3 clip:1 ]",
|
||||||
|
"[ 1/8 → 1/4 | note:eb3 clip:0.9375 ]",
|
||||||
|
"[ 1/8 → 1/4 | note:g3 clip:0.9375 ]",
|
||||||
|
"[ 1/4 → 3/8 | note:g2 clip:0.875 ]",
|
||||||
|
"[ 3/8 → 1/2 | note:g3 clip:0.8125 ]",
|
||||||
|
"[ 3/8 → 1/2 | note:bb3 clip:0.8125 ]",
|
||||||
|
"[ 1/2 → 5/8 | note:c3 clip:0.75 ]",
|
||||||
|
"[ 5/8 → 3/4 | note:eb3 clip:0.6875 ]",
|
||||||
|
"[ 5/8 → 3/4 | note:g3 clip:0.6875 ]",
|
||||||
|
"[ 3/4 → 7/8 | note:g2 clip:0.625 ]",
|
||||||
|
"[ 7/8 → 1/1 | note:g3 clip:0.5625 ]",
|
||||||
|
"[ 7/8 → 1/1 | note:bb3 clip:0.5625 ]",
|
||||||
|
"[ 1/1 → 9/8 | note:c3 clip:0.5 ]",
|
||||||
|
"[ 9/8 → 5/4 | note:eb3 clip:0.4375 ]",
|
||||||
|
"[ 9/8 → 5/4 | note:g3 clip:0.4375 ]",
|
||||||
|
"[ 5/4 → 11/8 | note:g2 clip:0.375 ]",
|
||||||
|
"[ 11/8 → 3/2 | note:g3 clip:0.3125 ]",
|
||||||
|
"[ 11/8 → 3/2 | note:bb3 clip:0.3125 ]",
|
||||||
|
"[ 3/2 → 13/8 | note:c3 clip:0.25 ]",
|
||||||
|
"[ 13/8 → 7/4 | note:eb3 clip:0.1875 ]",
|
||||||
|
"[ 13/8 → 7/4 | note:g3 clip:0.1875 ]",
|
||||||
|
"[ 7/4 → 15/8 | note:g2 clip:0.125 ]",
|
||||||
|
"[ 15/8 → 2/1 | note:g3 clip:0.0625 ]",
|
||||||
|
"[ 15/8 → 2/1 | note:bb3 clip:0.0625 ]",
|
||||||
|
"[ 2/1 → 17/8 | note:c3 clip:1 ]",
|
||||||
|
"[ 17/8 → 9/4 | note:eb3 clip:0.9375 ]",
|
||||||
|
"[ 17/8 → 9/4 | note:g3 clip:0.9375 ]",
|
||||||
|
"[ 9/4 → 19/8 | note:g2 clip:0.875 ]",
|
||||||
|
"[ 19/8 → 5/2 | note:g3 clip:0.8125 ]",
|
||||||
|
"[ 19/8 → 5/2 | note:bb3 clip:0.8125 ]",
|
||||||
|
"[ 5/2 → 21/8 | note:c3 clip:0.75 ]",
|
||||||
|
"[ 21/8 → 11/4 | note:eb3 clip:0.6875 ]",
|
||||||
|
"[ 21/8 → 11/4 | note:g3 clip:0.6875 ]",
|
||||||
|
"[ 11/4 → 23/8 | note:g2 clip:0.625 ]",
|
||||||
|
"[ 23/8 → 3/1 | note:g3 clip:0.5625 ]",
|
||||||
|
"[ 23/8 → 3/1 | note:bb3 clip:0.5625 ]",
|
||||||
|
"[ 3/1 → 25/8 | note:c3 clip:0.5 ]",
|
||||||
|
"[ 25/8 → 13/4 | note:eb3 clip:0.4375 ]",
|
||||||
|
"[ 25/8 → 13/4 | note:g3 clip:0.4375 ]",
|
||||||
|
"[ 13/4 → 27/8 | note:g2 clip:0.375 ]",
|
||||||
|
"[ 27/8 → 7/2 | note:g3 clip:0.3125 ]",
|
||||||
|
"[ 27/8 → 7/2 | note:bb3 clip:0.3125 ]",
|
||||||
|
"[ 7/2 → 29/8 | note:c3 clip:0.25 ]",
|
||||||
|
"[ 29/8 → 15/4 | note:eb3 clip:0.1875 ]",
|
||||||
|
"[ 29/8 → 15/4 | note:g3 clip:0.1875 ]",
|
||||||
|
"[ 15/4 → 31/8 | note:g2 clip:0.125 ]",
|
||||||
|
"[ 31/8 → 4/1 | note:g3 clip:0.0625 ]",
|
||||||
|
"[ 31/8 → 4/1 | note:bb3 clip:0.0625 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "isaw" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/8 | note:D4 ]",
|
||||||
|
"[ 1/8 → 1/4 | note:C4 ]",
|
||||||
|
"[ 1/4 → 3/8 | note:B3 ]",
|
||||||
|
"[ 3/8 → 1/2 | note:A3 ]",
|
||||||
|
"[ 1/2 → 5/8 | note:G3 ]",
|
||||||
|
"[ 5/8 → 3/4 | note:F3 ]",
|
||||||
|
"[ 3/4 → 7/8 | note:E3 ]",
|
||||||
|
"[ 7/8 → 1/1 | note:D3 ]",
|
||||||
|
"[ 1/1 → 9/8 | note:D4 ]",
|
||||||
|
"[ 9/8 → 5/4 | note:C4 ]",
|
||||||
|
"[ 5/4 → 11/8 | note:B3 ]",
|
||||||
|
"[ 11/8 → 3/2 | note:A3 ]",
|
||||||
|
"[ 3/2 → 13/8 | note:G3 ]",
|
||||||
|
"[ 13/8 → 7/4 | note:F3 ]",
|
||||||
|
"[ 7/4 → 15/8 | note:E3 ]",
|
||||||
|
"[ 15/8 → 2/1 | note:D3 ]",
|
||||||
|
"[ 2/1 → 17/8 | note:D4 ]",
|
||||||
|
"[ 17/8 → 9/4 | note:C4 ]",
|
||||||
|
"[ 9/4 → 19/8 | note:B3 ]",
|
||||||
|
"[ 19/8 → 5/2 | note:A3 ]",
|
||||||
|
"[ 5/2 → 21/8 | note:G3 ]",
|
||||||
|
"[ 21/8 → 11/4 | note:F3 ]",
|
||||||
|
"[ 11/4 → 23/8 | note:E3 ]",
|
||||||
|
"[ 23/8 → 3/1 | note:D3 ]",
|
||||||
|
"[ 3/1 → 25/8 | note:D4 ]",
|
||||||
|
"[ 25/8 → 13/4 | note:C4 ]",
|
||||||
|
"[ 13/4 → 27/8 | note:B3 ]",
|
||||||
|
"[ 27/8 → 7/2 | note:A3 ]",
|
||||||
|
"[ 7/2 → 29/8 | note:G3 ]",
|
||||||
|
"[ 29/8 → 15/4 | note:F3 ]",
|
||||||
|
"[ 15/4 → 31/8 | note:E3 ]",
|
||||||
|
"[ 31/8 → 4/1 | note:D3 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "iter" example index 0 1`] = `
|
exports[`runs examples > example "iter" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:A3 ]",
|
"[ 0/1 → 1/4 | note:A3 ]",
|
||||||
@ -4276,6 +4393,43 @@ exports[`runs examples > example "iterBack" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "itri" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/8 | note:C4 ]",
|
||||||
|
"[ 1/8 → 1/4 | note:Bb3 ]",
|
||||||
|
"[ 1/4 → 3/8 | note:G3 ]",
|
||||||
|
"[ 3/8 → 1/2 | note:Eb3 ]",
|
||||||
|
"[ 1/2 → 5/8 | note:C3 ]",
|
||||||
|
"[ 5/8 → 3/4 | note:Eb3 ]",
|
||||||
|
"[ 3/4 → 7/8 | note:G3 ]",
|
||||||
|
"[ 7/8 → 1/1 | note:Bb3 ]",
|
||||||
|
"[ 1/1 → 9/8 | note:C4 ]",
|
||||||
|
"[ 9/8 → 5/4 | note:Bb3 ]",
|
||||||
|
"[ 5/4 → 11/8 | note:G3 ]",
|
||||||
|
"[ 11/8 → 3/2 | note:Eb3 ]",
|
||||||
|
"[ 3/2 → 13/8 | note:C3 ]",
|
||||||
|
"[ 13/8 → 7/4 | note:Eb3 ]",
|
||||||
|
"[ 7/4 → 15/8 | note:G3 ]",
|
||||||
|
"[ 15/8 → 2/1 | note:Bb3 ]",
|
||||||
|
"[ 2/1 → 17/8 | note:C4 ]",
|
||||||
|
"[ 17/8 → 9/4 | note:Bb3 ]",
|
||||||
|
"[ 9/4 → 19/8 | note:G3 ]",
|
||||||
|
"[ 19/8 → 5/2 | note:Eb3 ]",
|
||||||
|
"[ 5/2 → 21/8 | note:C3 ]",
|
||||||
|
"[ 21/8 → 11/4 | note:Eb3 ]",
|
||||||
|
"[ 11/4 → 23/8 | note:G3 ]",
|
||||||
|
"[ 23/8 → 3/1 | note:Bb3 ]",
|
||||||
|
"[ 3/1 → 25/8 | note:C4 ]",
|
||||||
|
"[ 25/8 → 13/4 | note:Bb3 ]",
|
||||||
|
"[ 13/4 → 27/8 | note:G3 ]",
|
||||||
|
"[ 27/8 → 7/2 | note:Eb3 ]",
|
||||||
|
"[ 7/2 → 29/8 | note:C3 ]",
|
||||||
|
"[ 29/8 → 15/4 | note:Eb3 ]",
|
||||||
|
"[ 15/4 → 31/8 | note:G3 ]",
|
||||||
|
"[ 31/8 → 4/1 | note:Bb3 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "jux" example index 0 1`] = `
|
exports[`runs examples > example "jux" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:bd pan:0 ]",
|
"[ 0/1 → 1/8 | s:bd pan:0 ]",
|
||||||
@ -6298,67 +6452,54 @@ exports[`runs examples > example "ply" example index 0 1`] = `
|
|||||||
|
|
||||||
exports[`runs examples > example "polymeter" example index 0 1`] = `
|
exports[`runs examples > example "polymeter" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/3 | note:c ]",
|
"[ 0/1 → 1/6 | note:c ]",
|
||||||
"[ 0/1 → 1/3 | note:c2 ]",
|
"[ 0/1 → 1/6 | note:c2 ]",
|
||||||
"[ 1/3 → 2/3 | note:eb ]",
|
"[ 1/6 → 1/3 | note:eb ]",
|
||||||
"[ 1/3 → 2/3 | note:g2 ]",
|
"[ 1/6 → 1/3 | note:g2 ]",
|
||||||
"[ 2/3 → 1/1 | note:g ]",
|
"[ 1/3 → 1/2 | note:g ]",
|
||||||
"[ 2/3 → 1/1 | note:c2 ]",
|
"[ 1/3 → 1/2 | note:c2 ]",
|
||||||
"[ 1/1 → 4/3 | note:c ]",
|
"[ 1/2 → 2/3 | note:c ]",
|
||||||
"[ 1/1 → 4/3 | note:g2 ]",
|
"[ 1/2 → 2/3 | note:g2 ]",
|
||||||
"[ 4/3 → 5/3 | note:eb ]",
|
"[ 2/3 → 5/6 | note:eb ]",
|
||||||
"[ 4/3 → 5/3 | note:c2 ]",
|
"[ 2/3 → 5/6 | note:c2 ]",
|
||||||
"[ 5/3 → 2/1 | note:g ]",
|
"[ 5/6 → 1/1 | note:g ]",
|
||||||
"[ 5/3 → 2/1 | note:g2 ]",
|
"[ 5/6 → 1/1 | note:g2 ]",
|
||||||
"[ 2/1 → 7/3 | note:c ]",
|
"[ 1/1 → 7/6 | note:c ]",
|
||||||
"[ 2/1 → 7/3 | note:c2 ]",
|
"[ 1/1 → 7/6 | note:c2 ]",
|
||||||
"[ 7/3 → 8/3 | note:eb ]",
|
"[ 7/6 → 4/3 | note:eb ]",
|
||||||
"[ 7/3 → 8/3 | note:g2 ]",
|
"[ 7/6 → 4/3 | note:g2 ]",
|
||||||
"[ 8/3 → 3/1 | note:g ]",
|
"[ 4/3 → 3/2 | note:g ]",
|
||||||
"[ 8/3 → 3/1 | note:c2 ]",
|
"[ 4/3 → 3/2 | note:c2 ]",
|
||||||
"[ 3/1 → 10/3 | note:c ]",
|
"[ 3/2 → 5/3 | note:c ]",
|
||||||
"[ 3/1 → 10/3 | note:g2 ]",
|
"[ 3/2 → 5/3 | note:g2 ]",
|
||||||
"[ 10/3 → 11/3 | note:eb ]",
|
"[ 5/3 → 11/6 | note:eb ]",
|
||||||
"[ 10/3 → 11/3 | note:c2 ]",
|
"[ 5/3 → 11/6 | note:c2 ]",
|
||||||
"[ 11/3 → 4/1 | note:g ]",
|
"[ 11/6 → 2/1 | note:g ]",
|
||||||
"[ 11/3 → 4/1 | note:g2 ]",
|
"[ 11/6 → 2/1 | note:g2 ]",
|
||||||
]
|
"[ 2/1 → 13/6 | note:c ]",
|
||||||
`;
|
"[ 2/1 → 13/6 | note:c2 ]",
|
||||||
|
"[ 13/6 → 7/3 | note:eb ]",
|
||||||
exports[`runs examples > example "polymeterSteps" example index 0 1`] = `
|
"[ 13/6 → 7/3 | note:g2 ]",
|
||||||
[
|
"[ 7/3 → 5/2 | note:g ]",
|
||||||
"[ 0/1 → 1/4 | note:c ]",
|
"[ 7/3 → 5/2 | note:c2 ]",
|
||||||
"[ 0/1 → 1/4 | note:e ]",
|
"[ 5/2 → 8/3 | note:c ]",
|
||||||
"[ 1/4 → 1/2 | note:d ]",
|
"[ 5/2 → 8/3 | note:g2 ]",
|
||||||
"[ 1/4 → 1/2 | note:f ]",
|
"[ 8/3 → 17/6 | note:eb ]",
|
||||||
"[ 1/2 → 3/4 | note:c ]",
|
"[ 8/3 → 17/6 | note:c2 ]",
|
||||||
"[ 1/2 → 3/4 | note:g ]",
|
"[ 17/6 → 3/1 | note:g ]",
|
||||||
"[ 3/4 → 1/1 | note:d ]",
|
"[ 17/6 → 3/1 | note:g2 ]",
|
||||||
"[ 3/4 → 1/1 | note:e ]",
|
"[ 3/1 → 19/6 | note:c ]",
|
||||||
"[ 1/1 → 5/4 | note:c ]",
|
"[ 3/1 → 19/6 | note:c2 ]",
|
||||||
"[ 1/1 → 5/4 | note:f ]",
|
"[ 19/6 → 10/3 | note:eb ]",
|
||||||
"[ 5/4 → 3/2 | note:d ]",
|
"[ 19/6 → 10/3 | note:g2 ]",
|
||||||
"[ 5/4 → 3/2 | note:g ]",
|
"[ 10/3 → 7/2 | note:g ]",
|
||||||
"[ 3/2 → 7/4 | note:c ]",
|
"[ 10/3 → 7/2 | note:c2 ]",
|
||||||
"[ 3/2 → 7/4 | note:e ]",
|
"[ 7/2 → 11/3 | note:c ]",
|
||||||
"[ 7/4 → 2/1 | note:d ]",
|
"[ 7/2 → 11/3 | note:g2 ]",
|
||||||
"[ 7/4 → 2/1 | note:f ]",
|
"[ 11/3 → 23/6 | note:eb ]",
|
||||||
"[ 2/1 → 9/4 | note:c ]",
|
"[ 11/3 → 23/6 | note:c2 ]",
|
||||||
"[ 2/1 → 9/4 | note:g ]",
|
"[ 23/6 → 4/1 | note:g ]",
|
||||||
"[ 9/4 → 5/2 | note:d ]",
|
"[ 23/6 → 4/1 | note:g2 ]",
|
||||||
"[ 9/4 → 5/2 | note:e ]",
|
|
||||||
"[ 5/2 → 11/4 | note:c ]",
|
|
||||||
"[ 5/2 → 11/4 | note:f ]",
|
|
||||||
"[ 11/4 → 3/1 | note:d ]",
|
|
||||||
"[ 11/4 → 3/1 | note:g ]",
|
|
||||||
"[ 3/1 → 13/4 | note:c ]",
|
|
||||||
"[ 3/1 → 13/4 | note:e ]",
|
|
||||||
"[ 13/4 → 7/2 | note:d ]",
|
|
||||||
"[ 13/4 → 7/2 | note:f ]",
|
|
||||||
"[ 7/2 → 15/4 | note:c ]",
|
|
||||||
"[ 7/2 → 15/4 | note:g ]",
|
|
||||||
"[ 15/4 → 4/1 | note:d ]",
|
|
||||||
"[ 15/4 → 4/1 | note:e ]",
|
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -6773,33 +6914,6 @@ exports[`runs examples > example "release" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "repeat" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/8 | s:bd ]",
|
|
||||||
"[ 1/8 → 1/4 | s:bd ]",
|
|
||||||
"[ 3/8 → 1/2 | s:cp ]",
|
|
||||||
"[ 1/2 → 5/8 | s:bd ]",
|
|
||||||
"[ 5/8 → 3/4 | s:bd ]",
|
|
||||||
"[ 7/8 → 1/1 | s:cp ]",
|
|
||||||
"[ 1/1 → 9/8 | s:bd ]",
|
|
||||||
"[ 5/4 → 11/8 | s:sd ]",
|
|
||||||
"[ 3/2 → 13/8 | s:bd ]",
|
|
||||||
"[ 13/8 → 7/4 | s:bd ]",
|
|
||||||
"[ 15/8 → 2/1 | s:cp ]",
|
|
||||||
"[ 2/1 → 17/8 | s:bd ]",
|
|
||||||
"[ 17/8 → 9/4 | s:bd ]",
|
|
||||||
"[ 19/8 → 5/2 | s:cp ]",
|
|
||||||
"[ 5/2 → 21/8 | s:bd ]",
|
|
||||||
"[ 11/4 → 23/8 | s:sd ]",
|
|
||||||
"[ 3/1 → 25/8 | s:bd ]",
|
|
||||||
"[ 25/8 → 13/4 | s:bd ]",
|
|
||||||
"[ 27/8 → 7/2 | s:cp ]",
|
|
||||||
"[ 7/2 → 29/8 | s:bd ]",
|
|
||||||
"[ 29/8 → 15/4 | s:bd ]",
|
|
||||||
"[ 31/8 → 4/1 | s:cp ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "repeatCycles" example index 0 1`] = `
|
exports[`runs examples > example "repeatCycles" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:34 s:gm_acoustic_guitar_nylon ]",
|
"[ 0/1 → 1/4 | note:34 s:gm_acoustic_guitar_nylon ]",
|
||||||
@ -9309,38 +9423,38 @@ exports[`runs examples > example "transpose" example index 1 1`] = `
|
|||||||
|
|
||||||
exports[`runs examples > example "tri" example index 0 1`] = `
|
exports[`runs examples > example "tri" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:C4 ]",
|
"[ 0/1 → 1/8 | note:C3 ]",
|
||||||
"[ 1/8 → 1/4 | note:Bb3 ]",
|
"[ 1/8 → 1/4 | note:Eb3 ]",
|
||||||
"[ 1/4 → 3/8 | note:G3 ]",
|
"[ 1/4 → 3/8 | note:G3 ]",
|
||||||
"[ 3/8 → 1/2 | note:Eb3 ]",
|
"[ 3/8 → 1/2 | note:Bb3 ]",
|
||||||
"[ 1/2 → 5/8 | note:C3 ]",
|
"[ 1/2 → 5/8 | note:C4 ]",
|
||||||
"[ 5/8 → 3/4 | note:Eb3 ]",
|
"[ 5/8 → 3/4 | note:Bb3 ]",
|
||||||
"[ 3/4 → 7/8 | note:G3 ]",
|
"[ 3/4 → 7/8 | note:G3 ]",
|
||||||
"[ 7/8 → 1/1 | note:Bb3 ]",
|
"[ 7/8 → 1/1 | note:Eb3 ]",
|
||||||
"[ 1/1 → 9/8 | note:C4 ]",
|
"[ 1/1 → 9/8 | note:C3 ]",
|
||||||
"[ 9/8 → 5/4 | note:Bb3 ]",
|
"[ 9/8 → 5/4 | note:Eb3 ]",
|
||||||
"[ 5/4 → 11/8 | note:G3 ]",
|
"[ 5/4 → 11/8 | note:G3 ]",
|
||||||
"[ 11/8 → 3/2 | note:Eb3 ]",
|
"[ 11/8 → 3/2 | note:Bb3 ]",
|
||||||
"[ 3/2 → 13/8 | note:C3 ]",
|
"[ 3/2 → 13/8 | note:C4 ]",
|
||||||
"[ 13/8 → 7/4 | note:Eb3 ]",
|
"[ 13/8 → 7/4 | note:Bb3 ]",
|
||||||
"[ 7/4 → 15/8 | note:G3 ]",
|
"[ 7/4 → 15/8 | note:G3 ]",
|
||||||
"[ 15/8 → 2/1 | note:Bb3 ]",
|
"[ 15/8 → 2/1 | note:Eb3 ]",
|
||||||
"[ 2/1 → 17/8 | note:C4 ]",
|
"[ 2/1 → 17/8 | note:C3 ]",
|
||||||
"[ 17/8 → 9/4 | note:Bb3 ]",
|
"[ 17/8 → 9/4 | note:Eb3 ]",
|
||||||
"[ 9/4 → 19/8 | note:G3 ]",
|
"[ 9/4 → 19/8 | note:G3 ]",
|
||||||
"[ 19/8 → 5/2 | note:Eb3 ]",
|
"[ 19/8 → 5/2 | note:Bb3 ]",
|
||||||
"[ 5/2 → 21/8 | note:C3 ]",
|
"[ 5/2 → 21/8 | note:C4 ]",
|
||||||
"[ 21/8 → 11/4 | note:Eb3 ]",
|
"[ 21/8 → 11/4 | note:Bb3 ]",
|
||||||
"[ 11/4 → 23/8 | note:G3 ]",
|
"[ 11/4 → 23/8 | note:G3 ]",
|
||||||
"[ 23/8 → 3/1 | note:Bb3 ]",
|
"[ 23/8 → 3/1 | note:Eb3 ]",
|
||||||
"[ 3/1 → 25/8 | note:C4 ]",
|
"[ 3/1 → 25/8 | note:C3 ]",
|
||||||
"[ 25/8 → 13/4 | note:Bb3 ]",
|
"[ 25/8 → 13/4 | note:Eb3 ]",
|
||||||
"[ 13/4 → 27/8 | note:G3 ]",
|
"[ 13/4 → 27/8 | note:G3 ]",
|
||||||
"[ 27/8 → 7/2 | note:Eb3 ]",
|
"[ 27/8 → 7/2 | note:Bb3 ]",
|
||||||
"[ 7/2 → 29/8 | note:C3 ]",
|
"[ 7/2 → 29/8 | note:C4 ]",
|
||||||
"[ 29/8 → 15/4 | note:Eb3 ]",
|
"[ 29/8 → 15/4 | note:Bb3 ]",
|
||||||
"[ 15/4 → 31/8 | note:G3 ]",
|
"[ 15/4 → 31/8 | note:G3 ]",
|
||||||
"[ 31/8 → 4/1 | note:Bb3 ]",
|
"[ 31/8 → 4/1 | note:Eb3 ]",
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -9870,6 +9984,59 @@ exports[`runs examples > example "wchooseCycles" example index 1 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "wchooseCycles" example index 2 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/32 | s:bd ]",
|
||||||
|
"[ 3/32 → 1/8 | s:bd ]",
|
||||||
|
"[ 3/16 → 7/32 | s:bd ]",
|
||||||
|
"[ 1/4 → 1/3 | s:hh ]",
|
||||||
|
"[ 1/3 → 5/12 | s:hh ]",
|
||||||
|
"[ 5/12 → 1/2 | s:hh ]",
|
||||||
|
"[ 1/2 → 7/12 | s:hh ]",
|
||||||
|
"[ 7/12 → 2/3 | s:hh ]",
|
||||||
|
"[ 2/3 → 3/4 | s:hh ]",
|
||||||
|
"[ 3/4 → 5/6 | s:hh ]",
|
||||||
|
"[ 5/6 → 11/12 | s:hh ]",
|
||||||
|
"[ 11/12 → 1/1 | s:hh ]",
|
||||||
|
"[ 1/1 → 33/32 | s:bd ]",
|
||||||
|
"[ 35/32 → 9/8 | s:bd ]",
|
||||||
|
"[ 19/16 → 39/32 | s:bd ]",
|
||||||
|
"[ 5/4 → 4/3 | s:hh ]",
|
||||||
|
"[ 4/3 → 17/12 | s:hh ]",
|
||||||
|
"[ 17/12 → 3/2 | s:hh ]",
|
||||||
|
"[ 3/2 → 49/32 | s:bd ]",
|
||||||
|
"[ 51/32 → 13/8 | s:bd ]",
|
||||||
|
"[ 27/16 → 55/32 | s:bd ]",
|
||||||
|
"[ 7/4 → 11/6 | s:hh ]",
|
||||||
|
"[ 11/6 → 23/12 | s:hh ]",
|
||||||
|
"[ 23/12 → 2/1 | s:hh ]",
|
||||||
|
"[ 2/1 → 25/12 | s:hh ]",
|
||||||
|
"[ 25/12 → 13/6 | s:hh ]",
|
||||||
|
"[ 13/6 → 9/4 | s:hh ]",
|
||||||
|
"[ 9/4 → 7/3 | s:hh ]",
|
||||||
|
"[ 7/3 → 29/12 | s:hh ]",
|
||||||
|
"[ 29/12 → 5/2 | s:hh ]",
|
||||||
|
"[ 5/2 → 81/32 | s:bd ]",
|
||||||
|
"[ 83/32 → 21/8 | s:bd ]",
|
||||||
|
"[ 43/16 → 87/32 | s:bd ]",
|
||||||
|
"[ 11/4 → 17/6 | s:hh ]",
|
||||||
|
"[ 17/6 → 35/12 | s:hh ]",
|
||||||
|
"[ 35/12 → 3/1 | s:hh ]",
|
||||||
|
"[ 3/1 → 97/32 | s:bd ]",
|
||||||
|
"[ 99/32 → 25/8 | s:bd ]",
|
||||||
|
"[ 51/16 → 103/32 | s:bd ]",
|
||||||
|
"[ 13/4 → 10/3 | s:hh ]",
|
||||||
|
"[ 10/3 → 41/12 | s:hh ]",
|
||||||
|
"[ 41/12 → 7/2 | s:hh ]",
|
||||||
|
"[ 7/2 → 43/12 | s:hh ]",
|
||||||
|
"[ 43/12 → 11/3 | s:hh ]",
|
||||||
|
"[ 11/3 → 15/4 | s:hh ]",
|
||||||
|
"[ 15/4 → 23/6 | s:hh ]",
|
||||||
|
"[ 23/6 → 47/12 | s:hh ]",
|
||||||
|
"[ 47/12 → 4/1 | s:hh ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "when" example index 0 1`] = `
|
exports[`runs examples > example "when" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/3 | note:c3 ]",
|
"[ 0/1 → 1/3 | note:c3 ]",
|
||||||
|
|||||||
@ -21,6 +21,9 @@ import '@strudel/xen/xen.mjs';
|
|||||||
// import '@strudel/webaudio/webaudio.mjs';
|
// import '@strudel/webaudio/webaudio.mjs';
|
||||||
// import '@strudel/serial/serial.mjs';
|
// import '@strudel/serial/serial.mjs';
|
||||||
import '../website/src/repl/piano';
|
import '../website/src/repl/piano';
|
||||||
|
//import * as motionHelpers from '../packages/motion/index.mjs';
|
||||||
|
//import * as geolocationHelpers from '../packages/geolocation/index.mjs';
|
||||||
|
import * as gamepadHelpers from '../packages/gamepad/index.mjs';
|
||||||
|
|
||||||
class MockedNode {
|
class MockedNode {
|
||||||
chain() {
|
chain() {
|
||||||
@ -137,7 +140,7 @@ evalScope(
|
|||||||
uiHelpersMocked,
|
uiHelpersMocked,
|
||||||
webaudio,
|
webaudio,
|
||||||
tonalHelpers,
|
tonalHelpers,
|
||||||
|
gamepadHelpers,
|
||||||
/*
|
/*
|
||||||
toneHelpers,
|
toneHelpers,
|
||||||
voicingHelpers,
|
voicingHelpers,
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
"@strudel/csound": "workspace:*",
|
"@strudel/csound": "workspace:*",
|
||||||
"@strudel/desktopbridge": "workspace:*",
|
"@strudel/desktopbridge": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
|
"@strudel/gamepad": "workspace:*",
|
||||||
"@strudel/hydra": "workspace:*",
|
"@strudel/hydra": "workspace:*",
|
||||||
"@strudel/midi": "workspace:*",
|
"@strudel/midi": "workspace:*",
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
|
|||||||
BIN
website/public/fonts/CutiePi/Cute_Aurora_demo.ttf
Normal file
BIN
website/public/fonts/CutiePi/Cute_Aurora_demo.ttf
Normal file
Binary file not shown.
7
website/public/fonts/CutiePi/LICENSE.txt
Normal file
7
website/public/fonts/CutiePi/LICENSE.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
100% free for personal and commercial use.
|
||||||
|
However it's limited on basic latin only,
|
||||||
|
contact riedjal@gmail.com for full glyph (based on ANSI encoding)
|
||||||
|
and OTF features (alternates).
|
||||||
|
|
||||||
|
src: https://www.dafont.com/cute-aurora.font?text=%24%3A+s%28%22bd%285%2C8%29%22%29.superimpose%28x+%3D%3E+x.note%28%22c2%22%29.midi%28device%29%29
|
||||||
@ -36,8 +36,6 @@ export function Showcase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _videos = [
|
let _videos = [
|
||||||
{ title: 'Coding Music With Strudel Workhop by Dan Gorelick and Viola He', id: 'oqyAJ4WeKoU' },
|
|
||||||
{ title: 'Hexe - playing w strudel live coding music', id: '03m3F5xVOMg' },
|
|
||||||
{ title: 'DJ_Dave - Array [Lil Data Edit]', id: 'KUujFuTcuKc' },
|
{ title: 'DJ_Dave - Array [Lil Data Edit]', id: 'KUujFuTcuKc' },
|
||||||
{ title: 'DJ_Dave - Bitrot [v10101a Edit]', id: 'z_cJMdBp67Q' },
|
{ title: 'DJ_Dave - Bitrot [v10101a Edit]', id: 'z_cJMdBp67Q' },
|
||||||
{ title: 'you will not steve reich your way out of it', id: 'xpILnXcWyuo' },
|
{ title: 'you will not steve reich your way out of it', id: 'xpILnXcWyuo' },
|
||||||
@ -58,7 +56,6 @@ let _videos = [
|
|||||||
},
|
},
|
||||||
{ title: 'letSeaTstrudeL @ solstice stream 2023', id: 'fTiX6dVtdWQ' },
|
{ title: 'letSeaTstrudeL @ solstice stream 2023', id: 'fTiX6dVtdWQ' },
|
||||||
{ title: 'totalgee (Glen F) @ solstice stream 2023', id: 'IvI6uaE3nLU' },
|
{ title: 'totalgee (Glen F) @ solstice stream 2023', id: 'IvI6uaE3nLU' },
|
||||||
{ title: 'Dan Gorelick @ solstice stream 2023', id: 'qMJEljJyPi0' },
|
|
||||||
//
|
//
|
||||||
/* { // not sure if this is copyrighted ...
|
/* { // not sure if this is copyrighted ...
|
||||||
title: 'Creative Coding @ Chalmers University of Technology, video by svt.se',
|
title: 'Creative Coding @ Chalmers University of Technology, video by svt.se',
|
||||||
@ -126,6 +123,11 @@ let _videos = [
|
|||||||
'A first foray into combining (an early version) strudel and hydra, using flok for collaborative coding.',
|
'A first foray into combining (an early version) strudel and hydra, using flok for collaborative coding.',
|
||||||
},
|
},
|
||||||
{ title: 'froos @ Algorave 10th Birthday stream', id: 'IcMSocdKwvw' },
|
{ title: 'froos @ Algorave 10th Birthday stream', id: 'IcMSocdKwvw' },
|
||||||
|
{ title: 'todepasta 1.5', id: 'gCwaVu1Mijg' },
|
||||||
|
{ title: 'Djenerative Music by Bogdan Vera @ TOPLAP solstice Dec 2024', id: 'LtMX4Lr1nzY' },
|
||||||
|
{ title: 'La musique by BuboBubo @ TOPLAP solstice Dec 2024', id: 'Oz00Y_f80wU' },
|
||||||
|
{ title: 'Livecode and vocal breaks by Switch Angel @ TOPLAP solstice Dec 2024', id: '2kzjOIsL6CM' },
|
||||||
|
{ title: 'Eddyflux algorave set @ rudolf5', id: 'MXz8131Ut0A' },
|
||||||
];
|
];
|
||||||
|
|
||||||
_shuffled = shuffleArray(_videos);
|
_shuffled = shuffleArray(_videos);
|
||||||
|
|||||||
@ -9,11 +9,11 @@ import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
export default function UdelsEditor(Props) {
|
export default function UdelsEditor(Props) {
|
||||||
const { context } = Props;
|
const { context, ...editorProps } = Props;
|
||||||
const { containerRef, editorRef, error, init, pending, started, handleTogglePlay } = context;
|
const { containerRef, editorRef, error, init, pending, started, handleTogglePlay } = context;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'h-full flex w-full flex-col relative'}>
|
<div className={'h-full flex w-full flex-col relative'} {...editorProps}>
|
||||||
<Loader active={pending} />
|
<Loader active={pending} />
|
||||||
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
||||||
<div className="grow flex relative overflow-hidden">
|
<div className="grow flex relative overflow-hidden">
|
||||||
|
|||||||
@ -4,7 +4,7 @@ export default function UdelsHeader(Props) {
|
|||||||
const { numWindows, setNumWindows } = Props;
|
const { numWindows, setNumWindows } = Props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header id="header" className="flex text-white z-[100] text-lg select-none bg-neutral-900">
|
<header id="header" className="flex text-white z-[100] text-lg select-none bg-neutral-800">
|
||||||
<div className="px-4 items-center gap-2 flex space-x-2 md:pt-0 select-none">
|
<div className="px-4 items-center gap-2 flex space-x-2 md:pt-0 select-none">
|
||||||
<h1 onClick={() => {}} className={'text-l cursor-pointer flex gap-4'}>
|
<h1 onClick={() => {}} className={'text-l cursor-pointer flex gap-4'}>
|
||||||
<div className={'mt-[1px] cursor-pointer'}>🌀</div>
|
<div className={'mt-[1px] cursor-pointer'}>🌀</div>
|
||||||
|
|||||||
@ -84,6 +84,7 @@ export const SIDEBAR: Sidebar = {
|
|||||||
{ text: 'Music metadata', link: 'learn/metadata' },
|
{ text: 'Music metadata', link: 'learn/metadata' },
|
||||||
{ text: 'CSound', link: 'learn/csound' },
|
{ text: 'CSound', link: 'learn/csound' },
|
||||||
{ text: 'Hydra', link: 'learn/hydra' },
|
{ text: 'Hydra', link: 'learn/hydra' },
|
||||||
|
{ text: 'Input Devices', link: 'learn/input-devices' },
|
||||||
{ text: 'Device Motion', link: 'learn/devicemotion' },
|
{ text: 'Device Motion', link: 'learn/devicemotion' },
|
||||||
],
|
],
|
||||||
'Pattern Functions': [
|
'Pattern Functions': [
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
const ALLOW_MANY = ['by', 'url', 'genre', 'license'];
|
const ALLOW_MANY = ['by', 'url', 'genre', 'license'];
|
||||||
|
|
||||||
export function getMetadata(raw_code) {
|
export function getMetadata(raw_code) {
|
||||||
|
if (raw_code == null) {
|
||||||
|
console.error('could not extract metadata from missing pattern code');
|
||||||
|
raw_code = '';
|
||||||
|
}
|
||||||
const comment_regexp = /\/\*([\s\S]*?)\*\/|\/\/(.*)$/gm;
|
const comment_regexp = /\/\*([\s\S]*?)\*\/|\/\/(.*)$/gm;
|
||||||
const comments = [...raw_code.matchAll(comment_regexp)].map((c) => (c[1] || c[2] || '').trim());
|
const comments = [...raw_code.matchAll(comment_regexp)].map((c) => (c[1] || c[2] || '').trim());
|
||||||
const tags = {};
|
const tags = {};
|
||||||
|
|||||||
@ -5,6 +5,6 @@ layout: ../../layouts/MainLayout.astro
|
|||||||
|
|
||||||
import { MiniRepl } from '../../docs/MiniRepl';
|
import { MiniRepl } from '../../docs/MiniRepl';
|
||||||
import { JsDoc } from '../../docs/JsDoc';
|
import { JsDoc } from '../../docs/JsDoc';
|
||||||
import DeviceMotion from '../../../../packages/motion/docs/devicemotion.mdx';
|
import DeviceMotion from '@strudel/motion/docs/devicemotion.mdx';
|
||||||
|
|
||||||
<DeviceMotion />
|
<DeviceMotion />
|
||||||
|
|||||||
@ -293,8 +293,6 @@ global effects use the same chain for all events of the same orbit:
|
|||||||
|
|
||||||
<JsDoc client:idle name="iresponse" h={0} />
|
<JsDoc client:idle name="iresponse" h={0} />
|
||||||
|
|
||||||
Next, we'll look at strudel's support for [Csound](/learn/csound).
|
|
||||||
|
|
||||||
## Phaser
|
## Phaser
|
||||||
|
|
||||||
### phaser
|
### phaser
|
||||||
@ -312,3 +310,5 @@ Next, we'll look at strudel's support for [Csound](/learn/csound).
|
|||||||
### phasersweep
|
### phasersweep
|
||||||
|
|
||||||
<JsDoc client:idle name="phasersweep" h={0} />
|
<JsDoc client:idle name="phasersweep" h={0} />
|
||||||
|
|
||||||
|
Next, we'll look at input / output via [MIDI, OSC and other methods](/learn/input-output).
|
||||||
|
|||||||
15
website/src/pages/learn/input-devices.mdx
Normal file
15
website/src/pages/learn/input-devices.mdx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: Input Devices
|
||||||
|
layout: ../../layouts/MainLayout.astro
|
||||||
|
---
|
||||||
|
|
||||||
|
import { MiniRepl } from '../../docs/MiniRepl';
|
||||||
|
import { JsDoc } from '../../docs/JsDoc';
|
||||||
|
|
||||||
|
import Gamepad from '@strudel/gamepad/docs/gamepad.mdx';
|
||||||
|
|
||||||
|
# Input Devices
|
||||||
|
|
||||||
|
Strudel supports various input devices like Gamepads and MIDI controllers to manipulate patterns in real-time.
|
||||||
|
|
||||||
|
<Gamepad />
|
||||||
@ -100,9 +100,9 @@ Earlier versions of many of these functions had `s_` prefixes, and the `pace` fu
|
|||||||
|
|
||||||
<JsDoc client:idle name="contract" h={0} />
|
<JsDoc client:idle name="contract" h={0} />
|
||||||
|
|
||||||
### repeat
|
### extend
|
||||||
|
|
||||||
<JsDoc client:idle name="repeat" h={0} />
|
<JsDoc client:idle name="extend" h={0} />
|
||||||
|
|
||||||
### take
|
### take
|
||||||
|
|
||||||
@ -116,10 +116,6 @@ Earlier versions of many of these functions had `s_` prefixes, and the `pace` fu
|
|||||||
|
|
||||||
<JsDoc client:idle name="polymeter" h={0} />
|
<JsDoc client:idle name="polymeter" h={0} />
|
||||||
|
|
||||||
### polymeterSteps
|
|
||||||
|
|
||||||
<JsDoc client:idle name="polymeterSteps" h={0} />
|
|
||||||
|
|
||||||
### shrink
|
### shrink
|
||||||
|
|
||||||
<JsDoc client:idle name="shrink" h={0} />
|
<JsDoc client:idle name="shrink" h={0} />
|
||||||
|
|||||||
@ -9,10 +9,12 @@ import UdelsEditor from '@components/Udels/UdelsEditor';
|
|||||||
import ReplEditor from './components/ReplEditor';
|
import ReplEditor from './components/ReplEditor';
|
||||||
import EmbeddedReplEditor from './components/EmbeddedReplEditor';
|
import EmbeddedReplEditor from './components/EmbeddedReplEditor';
|
||||||
import { useReplContext } from './useReplContext';
|
import { useReplContext } from './useReplContext';
|
||||||
|
import { useSettings } from '@src/settings.mjs';
|
||||||
|
|
||||||
export function Repl({ embedded = false }) {
|
export function Repl({ embedded = false }) {
|
||||||
const isEmbedded = embedded || isIframe();
|
const isEmbedded = embedded || isIframe();
|
||||||
const Editor = isUdels() ? UdelsEditor : isEmbedded ? EmbeddedReplEditor : ReplEditor;
|
const Editor = isUdels() ? UdelsEditor : isEmbedded ? EmbeddedReplEditor : ReplEditor;
|
||||||
const context = useReplContext();
|
const context = useReplContext();
|
||||||
return <Editor context={context} />;
|
const { fontFamily } = useSettings();
|
||||||
|
return <Editor context={context} style={{ fontFamily }} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import { Header } from './Header';
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
export default function EmbeddedReplEditor(Props) {
|
export default function EmbeddedReplEditor(Props) {
|
||||||
const { context } = Props;
|
const { context, ...editorProps } = Props;
|
||||||
const { pending, started, handleTogglePlay, containerRef, editorRef, error, init } = context;
|
const { pending, started, handleTogglePlay, containerRef, editorRef, error, init } = context;
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col relative">
|
<div className="h-full flex flex-col relative" {...editorProps}>
|
||||||
<Loader active={pending} />
|
<Loader active={pending} />
|
||||||
<Header context={context} embedded={true} />
|
<Header context={context} embedded={true} />
|
||||||
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export function Header({ context, embedded = false }) {
|
|||||||
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } =
|
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } =
|
||||||
context;
|
context;
|
||||||
const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location);
|
const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location);
|
||||||
const { isZen, isButtonRowHidden, isCSSAnimationDisabled } = useSettings();
|
const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
@ -22,6 +22,7 @@ export function Header({ context, embedded = false }) {
|
|||||||
isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full py-1 justify-between',
|
isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full py-1 justify-between',
|
||||||
isEmbedded ? 'flex' : 'md:flex',
|
isEmbedded ? 'flex' : 'md:flex',
|
||||||
)}
|
)}
|
||||||
|
style={{ fontFamily }}
|
||||||
>
|
>
|
||||||
<div className="px-4 flex space-x-2 md:pt-0 select-none">
|
<div className="px-4 flex space-x-2 md:pt-0 select-none">
|
||||||
<h1
|
<h1
|
||||||
@ -46,7 +47,7 @@ export function Header({ context, embedded = false }) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="block rotate-90">꩜</span>
|
<span className="block text-foreground rotate-90">꩜</span>
|
||||||
</div>
|
</div>
|
||||||
{!isZen && (
|
{!isZen && (
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
|
|||||||
@ -10,13 +10,13 @@ import { useSettings } from '@src/settings.mjs';
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
export default function ReplEditor(Props) {
|
export default function ReplEditor(Props) {
|
||||||
const { context } = Props;
|
const { context, ...editorProps } = Props;
|
||||||
const { containerRef, editorRef, error, init, pending } = context;
|
const { containerRef, editorRef, error, init, pending } = context;
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const { panelPosition, isZen } = settings;
|
const { panelPosition, isZen } = settings;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col relative">
|
<div className="h-full flex flex-col relative" {...editorProps}>
|
||||||
<Loader active={pending} />
|
<Loader active={pending} />
|
||||||
<Header context={context} />
|
<Header context={context} />
|
||||||
<div className="grow flex relative overflow-hidden">
|
<div className="grow flex relative overflow-hidden">
|
||||||
|
|||||||
67
website/src/repl/components/incrementor/Incrementor.jsx
Normal file
67
website/src/repl/components/incrementor/Incrementor.jsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { Textbox } from '../textbox/Textbox';
|
||||||
|
import cx from '@src/cx.mjs';
|
||||||
|
|
||||||
|
function IncButton({ children, className, ...buttonProps }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
tabIndex={-1}
|
||||||
|
className={cx(
|
||||||
|
'border border-transparent p-1 text-center hover:text-background text-sm transition-all hover:bg-foreground active:bg-lineBackground disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
type="button"
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function Incrementor({
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
min = -Infinity,
|
||||||
|
max = Infinity,
|
||||||
|
className,
|
||||||
|
incrementLabel = 'next page',
|
||||||
|
decrementLabel = 'prev page',
|
||||||
|
...incrementorProps
|
||||||
|
}) {
|
||||||
|
value = parseInt(value);
|
||||||
|
value = isNaN(value) ? '' : value;
|
||||||
|
return (
|
||||||
|
<div className={cx('w-fit bg-background relative flex items-center"> rounded-md', className)}>
|
||||||
|
<Textbox
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
onChange={(v) => {
|
||||||
|
if (v.length && v < min) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChange(v);
|
||||||
|
}}
|
||||||
|
type="number"
|
||||||
|
placeholder=""
|
||||||
|
value={value}
|
||||||
|
className="w-32 mb-0 mt-0 border-none rounded-r-none bg-transparent appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
|
{...incrementorProps}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-1 ">
|
||||||
|
<IncButton disabled={value <= min} onClick={() => onChange(value - 1)} aria-label={decrementLabel}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" className="w-4 h-4">
|
||||||
|
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
|
||||||
|
</svg>
|
||||||
|
</IncButton>
|
||||||
|
<IncButton
|
||||||
|
className="rounded-r-md"
|
||||||
|
disabled={value >= max}
|
||||||
|
onClick={() => onChange(value + 1)}
|
||||||
|
aria-label={incrementLabel}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" className="w-4 h-4">
|
||||||
|
<path d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" />
|
||||||
|
</svg>
|
||||||
|
</IncButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
website/src/repl/components/pagination/Pagination.jsx
Normal file
5
website/src/repl/components/pagination/Pagination.jsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Incrementor } from '../incrementor/Incrementor';
|
||||||
|
|
||||||
|
export function Pagination({ currPage, onPageChange, className, ...incrementorProps }) {
|
||||||
|
return <Incrementor min={1} value={currPage} onChange={onPageChange} className={className} {...incrementorProps} />;
|
||||||
|
}
|
||||||
@ -1,53 +1,32 @@
|
|||||||
import { logger } from '@strudel/core';
|
|
||||||
import useEvent from '@src/useEvent.mjs';
|
|
||||||
import cx from '@src/cx.mjs';
|
import cx from '@src/cx.mjs';
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { useCallback, useState } from 'react';
|
|
||||||
import { useSettings } from '../../../settings.mjs';
|
import { useSettings } from '../../../settings.mjs';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { $strudel_log_history } from '../useLogger';
|
||||||
|
|
||||||
export function ConsoleTab() {
|
export function ConsoleTab() {
|
||||||
const [log, setLog] = useState([]);
|
const log = useStore($strudel_log_history);
|
||||||
const { fontFamily, fontSize } = useSettings();
|
const { fontFamily } = useSettings();
|
||||||
useLogger(
|
|
||||||
useCallback((e) => {
|
|
||||||
const { message, type, data } = e.detail;
|
|
||||||
setLog((l) => {
|
|
||||||
const lastLog = l.length ? l[l.length - 1] : undefined;
|
|
||||||
const id = nanoid(12);
|
|
||||||
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
|
|
||||||
if (type === 'loaded-sample') {
|
|
||||||
// const loadIndex = l.length - 1;
|
|
||||||
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
|
|
||||||
l[loadIndex] = { message, type, id, data };
|
|
||||||
} else if (lastLog && lastLog.message === message) {
|
|
||||||
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
|
|
||||||
} else {
|
|
||||||
l = l.concat([{ message, type, id, data }]);
|
|
||||||
}
|
|
||||||
return l.slice(-20);
|
|
||||||
});
|
|
||||||
}, []),
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div id="console-tab" className="break-all w-full first-line:text-sm p-2 h-full" style={{ fontFamily }}>
|
||||||
id="console-tab"
|
<div className="bg-background h-full w-full overflow-auto space-y-1 p-2 rounded-md">
|
||||||
className="break-all px-4 dark:text-white text-stone-900 text-sm py-2 space-y-1"
|
{log.map((l, i) => {
|
||||||
style={{ fontFamily, fontSize }}
|
const message = linkify(l.message);
|
||||||
>
|
const color = l.data?.hap?.value?.color;
|
||||||
{log.map((l, i) => {
|
return (
|
||||||
const message = linkify(l.message);
|
<div
|
||||||
const color = l.data?.hap?.value?.color;
|
key={l.id}
|
||||||
return (
|
className={cx(
|
||||||
<div
|
l.type === 'error' ? 'text-background bg-foreground' : 'text-foreground',
|
||||||
key={l.id}
|
l.type === 'highlight' && 'underline',
|
||||||
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}
|
)}
|
||||||
style={color ? { color } : {}}
|
style={color ? { color } : {}}
|
||||||
>
|
>
|
||||||
<span dangerouslySetInnerHTML={{ __html: message }} />
|
<span dangerouslySetInnerHTML={{ __html: message }} />
|
||||||
{l.count ? ` (${l.count})` : ''}
|
{l.count ? ` (${l.count})` : ''}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -72,7 +51,3 @@ function linkify(inputText) {
|
|||||||
|
|
||||||
return replacedText;
|
return replacedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useLogger(onTrigger) {
|
|
||||||
useEvent(logger.key, onTrigger);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { FilesTab } from './FilesTab';
|
|||||||
import { Reference } from './Reference';
|
import { Reference } from './Reference';
|
||||||
import { SettingsTab } from './SettingsTab';
|
import { SettingsTab } from './SettingsTab';
|
||||||
import { SoundsTab } from './SoundsTab';
|
import { SoundsTab } from './SoundsTab';
|
||||||
|
import { useLogger } from '../useLogger';
|
||||||
import { WelcomeTab } from './WelcomeTab';
|
import { WelcomeTab } from './WelcomeTab';
|
||||||
import { PatternsTab } from './PatternsTab';
|
import { PatternsTab } from './PatternsTab';
|
||||||
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
|
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
|
||||||
@ -115,6 +116,7 @@ function PanelNav({ children, className, settings, ...props }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PanelContent({ context, tab }) {
|
function PanelContent({ context, tab }) {
|
||||||
|
useLogger();
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case tabNames.patterns:
|
case tabNames.patterns:
|
||||||
return <PatternsTab context={context} />;
|
return <PatternsTab context={context} />;
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
exportPatterns,
|
exportPatterns,
|
||||||
importPatterns,
|
importPatterns,
|
||||||
|
loadAndSetFeaturedPatterns,
|
||||||
|
loadAndSetPublicPatterns,
|
||||||
patternFilterName,
|
patternFilterName,
|
||||||
useActivePattern,
|
useActivePattern,
|
||||||
useViewingPatternData,
|
useViewingPatternData,
|
||||||
@ -12,10 +14,10 @@ import { useExamplePatterns } from '../../useExamplePatterns.jsx';
|
|||||||
import { parseJSON, isUdels } from '../../util.mjs';
|
import { parseJSON, isUdels } from '../../util.mjs';
|
||||||
import { ButtonGroup } from './Forms.jsx';
|
import { ButtonGroup } from './Forms.jsx';
|
||||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
import { settingsMap, useSettings } from '../../../settings.mjs';
|
||||||
|
import { Pagination } from '../pagination/Pagination.jsx';
|
||||||
function classNames(...classes) {
|
import { useState } from 'react';
|
||||||
return classes.filter(Boolean).join(' ');
|
import { useDebounce } from '../usedebounce.jsx';
|
||||||
}
|
import cx from '@src/cx.mjs';
|
||||||
|
|
||||||
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||||
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
|
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
|
||||||
@ -25,21 +27,19 @@ export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
|||||||
const date = new Date(pattern.created_at);
|
const date = new Date(pattern.created_at);
|
||||||
if (!isNaN(date)) {
|
if (!isNaN(date)) {
|
||||||
title = date.toLocaleDateString();
|
title = date.toLocaleDateString();
|
||||||
|
} else {
|
||||||
|
title = 'unnamed';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (title == null) {
|
|
||||||
title = pattern.hash;
|
const author = Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous';
|
||||||
}
|
return <>{`${pattern.id}: ${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
|
||||||
if (title == null) {
|
|
||||||
title = 'unnamed';
|
|
||||||
}
|
|
||||||
return <>{`${pattern.id}: ${title} by ${Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'}`}</>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
|
function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className={classNames(
|
className={cx(
|
||||||
'mr-4 hover:opacity-50 cursor-pointer block',
|
'mr-4 hover:opacity-50 cursor-pointer block',
|
||||||
showOutline && 'outline outline-1',
|
showOutline && 'outline outline-1',
|
||||||
showHiglight && 'bg-selection',
|
showHiglight && 'bg-selection',
|
||||||
@ -56,7 +56,7 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
|
|||||||
const viewingPatternData = parseJSON(viewingPatternStore);
|
const viewingPatternData = parseJSON(viewingPatternStore);
|
||||||
const viewingPatternID = viewingPatternData.id;
|
const viewingPatternID = viewingPatternData.id;
|
||||||
return (
|
return (
|
||||||
<div className="font-mono text-sm">
|
<div className="">
|
||||||
{Object.values(patterns)
|
{Object.values(patterns)
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((pattern) => {
|
.map((pattern) => {
|
||||||
@ -84,82 +84,72 @@ function ActionButton({ children, onClick, label, labelIsHidden }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PatternsTab({ context }) {
|
const updateCodeWindow = (context, patternData, reset = false) => {
|
||||||
|
context.handleUpdate(patternData, reset);
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoResetPatternOnChange = !isUdels();
|
||||||
|
|
||||||
|
function UserPatterns({ context }) {
|
||||||
const activePattern = useActivePattern();
|
const activePattern = useActivePattern();
|
||||||
const viewingPatternStore = useViewingPatternData();
|
const viewingPatternStore = useViewingPatternData();
|
||||||
const viewingPatternData = parseJSON(viewingPatternStore);
|
const viewingPatternData = parseJSON(viewingPatternStore);
|
||||||
|
|
||||||
const { userPatterns, patternFilter } = useSettings();
|
const { userPatterns, patternFilter } = useSettings();
|
||||||
|
|
||||||
const examplePatterns = useExamplePatterns();
|
|
||||||
const collections = examplePatterns.collections;
|
|
||||||
|
|
||||||
const updateCodeWindow = (patternData, reset = false) => {
|
|
||||||
context.handleUpdate(patternData, reset);
|
|
||||||
};
|
|
||||||
const viewingPatternID = viewingPatternData?.id;
|
const viewingPatternID = viewingPatternData?.id;
|
||||||
|
|
||||||
const autoResetPatternOnChange = !isUdels();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 flex flex-col overflow-hidden max-h-full h-full">
|
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
|
||||||
<ButtonGroup
|
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
|
||||||
value={patternFilter}
|
<ActionButton
|
||||||
onChange={(value) => settingsMap.setKey('patternFilter', value)}
|
label="new"
|
||||||
items={patternFilterName}
|
onClick={() => {
|
||||||
></ButtonGroup>
|
const { data } = userPattern.createAndAddToDB();
|
||||||
{patternFilter === patternFilterName.user && (
|
updateCodeWindow(context, data);
|
||||||
<div>
|
}}
|
||||||
<div className="pr-4 space-x-4 border-b border-foreground flex max-w-full overflow-x-auto">
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
label="new"
|
label="duplicate"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const { data } = userPattern.createAndAddToDB();
|
const { data } = userPattern.duplicate(viewingPatternData);
|
||||||
updateCodeWindow(data);
|
updateCodeWindow(context, data);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
label="duplicate"
|
label="delete"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const { data } = userPattern.duplicate(viewingPatternData);
|
const { data } = userPattern.delete(viewingPatternID);
|
||||||
updateCodeWindow(data);
|
updateCodeWindow(context, { ...data, collection: userPattern.collection });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<label className="hover:opacity-50 cursor-pointer">
|
||||||
label="delete"
|
<input
|
||||||
onClick={() => {
|
style={{ display: 'none' }}
|
||||||
const { data } = userPattern.delete(viewingPatternID);
|
type="file"
|
||||||
updateCodeWindow({ ...data, collection: userPattern.collection });
|
multiple
|
||||||
}}
|
accept="text/plain,application/json"
|
||||||
/>
|
onChange={(e) => importPatterns(e.target.files)}
|
||||||
<label className="hover:opacity-50 cursor-pointer">
|
/>
|
||||||
<input
|
import
|
||||||
style={{ display: 'none' }}
|
</label>
|
||||||
type="file"
|
<ActionButton label="export" onClick={exportPatterns} />
|
||||||
multiple
|
|
||||||
accept="text/plain,application/json"
|
|
||||||
onChange={(e) => importPatterns(e.target.files)}
|
|
||||||
/>
|
|
||||||
import
|
|
||||||
</label>
|
|
||||||
<ActionButton label="export" onClick={exportPatterns} />
|
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
label="delete-all"
|
label="delete-all"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const { data } = userPattern.clearAll();
|
const { data } = userPattern.clearAll();
|
||||||
updateCodeWindow(data);
|
updateCodeWindow(context, data);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<section className="flex overflow-y-auto max-h-full flex-grow flex-col">
|
<div className="overflow-auto h-full bg-background p-2 rounded-md">
|
||||||
{patternFilter === patternFilterName.user && (
|
{patternFilter === patternFilterName.user && (
|
||||||
<PatternButtons
|
<PatternButtons
|
||||||
onClick={(id) =>
|
onClick={(id) =>
|
||||||
updateCodeWindow({ ...userPatterns[id], collection: userPattern.collection }, autoResetPatternOnChange)
|
updateCodeWindow(
|
||||||
|
context,
|
||||||
|
{ ...userPatterns[id], collection: userPattern.collection },
|
||||||
|
autoResetPatternOnChange,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
patterns={userPatterns}
|
patterns={userPatterns}
|
||||||
started={context.started}
|
started={context.started}
|
||||||
@ -167,24 +157,111 @@ export function PatternsTab({ context }) {
|
|||||||
viewingPatternID={viewingPatternID}
|
viewingPatternID={viewingPatternID}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{patternFilter !== patternFilterName.user &&
|
</div>
|
||||||
Array.from(collections.keys()).map((collection) => {
|
</div>
|
||||||
const patterns = collections.get(collection);
|
);
|
||||||
return (
|
}
|
||||||
<section key={collection} className="py-2">
|
|
||||||
<h2 className="text-xl mb-2">{collection}</h2>
|
function PatternPageWithPagination({ patterns, patternOnClick, context, paginationOnChange, initialPage }) {
|
||||||
<div className="font-mono text-sm">
|
const [page, setPage] = useState(initialPage);
|
||||||
<PatternButtons
|
const debouncedPageChange = useDebounce(() => {
|
||||||
onClick={(id) => updateCodeWindow({ ...patterns[id], collection }, autoResetPatternOnChange)}
|
paginationOnChange(page);
|
||||||
started={context.started}
|
});
|
||||||
patterns={patterns}
|
|
||||||
activePattern={activePattern}
|
const onPageChange = (pageNum) => {
|
||||||
/>
|
setPage(pageNum);
|
||||||
</div>
|
debouncedPageChange();
|
||||||
</section>
|
};
|
||||||
);
|
|
||||||
})}
|
const activePattern = useActivePattern();
|
||||||
</section>
|
return (
|
||||||
|
<div className="flex flex-grow flex-col h-full overflow-hidden justify-between">
|
||||||
|
<div className="overflow-auto flex flex-col flex-grow bg-background p-2 rounded-md ">
|
||||||
|
<PatternButtons
|
||||||
|
onClick={(id) => patternOnClick(id)}
|
||||||
|
started={context.started}
|
||||||
|
patterns={patterns}
|
||||||
|
activePattern={activePattern}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 py-2">
|
||||||
|
<label htmlFor="pattern pagination">Page</label>
|
||||||
|
<Pagination id="pattern pagination" currPage={page} onPageChange={onPageChange} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let featuredPageNum = 1;
|
||||||
|
function FeaturedPatterns({ context }) {
|
||||||
|
const examplePatterns = useExamplePatterns();
|
||||||
|
const collections = examplePatterns.collections;
|
||||||
|
const patterns = collections.get(patternFilterName.featured);
|
||||||
|
return (
|
||||||
|
<PatternPageWithPagination
|
||||||
|
patterns={patterns}
|
||||||
|
context={context}
|
||||||
|
initialPage={featuredPageNum}
|
||||||
|
patternOnClick={(id) => {
|
||||||
|
updateCodeWindow(
|
||||||
|
context,
|
||||||
|
{ ...patterns[id], collection: patternFilterName.featured },
|
||||||
|
autoResetPatternOnChange,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
paginationOnChange={async (pageNum) => {
|
||||||
|
await loadAndSetFeaturedPatterns(pageNum - 1);
|
||||||
|
featuredPageNum = pageNum;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let latestPageNum = 1;
|
||||||
|
function LatestPatterns({ context }) {
|
||||||
|
const examplePatterns = useExamplePatterns();
|
||||||
|
const collections = examplePatterns.collections;
|
||||||
|
const patterns = collections.get(patternFilterName.public);
|
||||||
|
return (
|
||||||
|
<PatternPageWithPagination
|
||||||
|
patterns={patterns}
|
||||||
|
context={context}
|
||||||
|
initialPage={latestPageNum}
|
||||||
|
patternOnClick={(id) => {
|
||||||
|
updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, autoResetPatternOnChange);
|
||||||
|
}}
|
||||||
|
paginationOnChange={async (pageNum) => {
|
||||||
|
await loadAndSetPublicPatterns(pageNum - 1);
|
||||||
|
latestPageNum = pageNum;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PublicPatterns({ context }) {
|
||||||
|
const { patternFilter } = useSettings();
|
||||||
|
if (patternFilter === patternFilterName.featured) {
|
||||||
|
return <FeaturedPatterns context={context} />;
|
||||||
|
}
|
||||||
|
return <LatestPatterns context={context} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PatternsTab({ context }) {
|
||||||
|
const { patternFilter } = useSettings();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
|
||||||
|
<ButtonGroup
|
||||||
|
value={patternFilter}
|
||||||
|
onChange={(value) => settingsMap.setKey('patternFilter', value)}
|
||||||
|
items={patternFilterName}
|
||||||
|
></ButtonGroup>
|
||||||
|
|
||||||
|
{patternFilter === patternFilterName.user ? (
|
||||||
|
<UserPatterns context={context} />
|
||||||
|
) : (
|
||||||
|
<PublicPatterns context={context} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import jsdocJson from '../../../../../doc.json';
|
import jsdocJson from '../../../../../doc.json';
|
||||||
|
import { Textbox } from '../textbox/Textbox';
|
||||||
const availableFunctions = jsdocJson.docs
|
const availableFunctions = jsdocJson.docs
|
||||||
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
|
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
|
||||||
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||||
@ -25,21 +26,16 @@ export function Reference() {
|
|||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full p-2 text-foreground overflow-hidden">
|
<div className="flex h-full w-full p-2 overflow-hidden">
|
||||||
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
|
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
|
||||||
<div class="w-full flex">
|
<div class="w-full flex">
|
||||||
<input
|
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
|
||||||
className="w-full p-1 bg-background rounded-md border-none"
|
|
||||||
placeholder="Search"
|
|
||||||
value={search}
|
|
||||||
onInput={(event) => setSearch(event.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
|
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
|
||||||
{visibleFunctions.map((entry, i) => (
|
{visibleFunctions.map((entry, i) => (
|
||||||
<a
|
<a
|
||||||
key={i}
|
key={i}
|
||||||
className="cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
|
className="cursor-pointer text-foreground flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const el = document.getElementById(`doc-${i}`);
|
const el = document.getElementById(`doc-${i}`);
|
||||||
const container = document.getElementById('reference-container');
|
const container = document.getElementById('reference-container');
|
||||||
@ -79,7 +75,9 @@ export function Reference() {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{entry.examples?.map((example, j) => (
|
{entry.examples?.map((example, j) => (
|
||||||
<pre key={j}>{example}</pre>
|
<pre className="bg-background" key={j}>
|
||||||
|
{example}
|
||||||
|
</pre>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -66,6 +66,7 @@ const themeOptions = Object.fromEntries(Object.keys(themes).map((k) => [k, k]));
|
|||||||
const fontFamilyOptions = {
|
const fontFamilyOptions = {
|
||||||
monospace: 'monospace',
|
monospace: 'monospace',
|
||||||
Courier: 'Courier',
|
Courier: 'Courier',
|
||||||
|
CutiePi: 'CutiePi',
|
||||||
JetBrains: 'JetBrains',
|
JetBrains: 'JetBrains',
|
||||||
Hack: 'Hack',
|
Hack: 'Hack',
|
||||||
FiraCode: 'FiraCode',
|
FiraCode: 'FiraCode',
|
||||||
@ -108,7 +109,7 @@ export function SettingsTab({ started }) {
|
|||||||
const shouldAlwaysSync = isUdels();
|
const shouldAlwaysSync = isUdels();
|
||||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||||
return (
|
return (
|
||||||
<div className="text-foreground p-4 space-y-4 w-full">
|
<div className="text-foreground p-4 space-y-4 w-full" style={{ fontFamily }}>
|
||||||
{canChangeAudioDevice && (
|
{canChangeAudioDevice && (
|
||||||
<FormItem label="Audio Output Device">
|
<FormItem label="Audio Output Device">
|
||||||
<AudioDeviceSelector
|
<AudioDeviceSelector
|
||||||
@ -141,7 +142,7 @@ export function SettingsTab({ started }) {
|
|||||||
<FormItem label="Theme">
|
<FormItem label="Theme">
|
||||||
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
|
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 font-sans">
|
||||||
<FormItem label="Font Family">
|
<FormItem label="Font Family">
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={fontFamilyOptions}
|
options={fontFamilyOptions}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useMemo, useRef, useState } from 'react';
|
|||||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
import { settingsMap, useSettings } from '../../../settings.mjs';
|
||||||
import { ButtonGroup } from './Forms.jsx';
|
import { ButtonGroup } from './Forms.jsx';
|
||||||
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
||||||
|
import { Textbox } from '../textbox/Textbox.jsx';
|
||||||
|
|
||||||
const getSamples = (samples) =>
|
const getSamples = (samples) =>
|
||||||
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
|
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
|
||||||
@ -52,13 +53,8 @@ export function SoundsTab() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
|
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
|
||||||
<input
|
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
|
||||||
className="w-full p-1 bg-background rounded-md my-2"
|
|
||||||
placeholder="Search"
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="pb-2 flex shrink-0 flex-wrap">
|
<div className="pb-2 flex shrink-0 flex-wrap">
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
@ -74,7 +70,7 @@ export function SoundsTab() {
|
|||||||
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
|
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal pb-2">
|
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
|
||||||
{soundEntries.map(([name, { data, onTrigger }]) => {
|
{soundEntries.map(([name, { data, onTrigger }]) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import cx from '@src/cx.mjs';
|
import { useSettings } from '@src/settings.mjs';
|
||||||
|
|
||||||
const { BASE_URL } = import.meta.env;
|
const { BASE_URL } = import.meta.env;
|
||||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||||
|
|
||||||
export function WelcomeTab({ context }) {
|
export function WelcomeTab({ context }) {
|
||||||
|
const { fontFamily } = useSettings();
|
||||||
return (
|
return (
|
||||||
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
|
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 " style={{ fontFamily }}>
|
||||||
<h3>꩜ welcome</h3>
|
<h3>꩜ welcome</h3>
|
||||||
<p>
|
<p>
|
||||||
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music
|
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music
|
||||||
@ -43,7 +44,8 @@ export function WelcomeTab({ context }) {
|
|||||||
<a href="https://github.com/tidalcycles/strudel" target="_blank">
|
<a href="https://github.com/tidalcycles/strudel" target="_blank">
|
||||||
github
|
github
|
||||||
</a>
|
</a>
|
||||||
. Please consider to{' '}
|
. You can also find <a href="https://github.com/felixroos/dough-samples/blob/main/README.md">licensing info</a>{' '}
|
||||||
|
for the default sound banks there. Please consider to{' '}
|
||||||
<a href="https://opencollective.com/tidalcycles" target="_blank">
|
<a href="https://opencollective.com/tidalcycles" target="_blank">
|
||||||
support this project
|
support this project
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
|
|||||||
11
website/src/repl/components/textbox/Textbox.jsx
Normal file
11
website/src/repl/components/textbox/Textbox.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import cx from '@src/cx.mjs';
|
||||||
|
|
||||||
|
export function Textbox({ onChange, className, ...inputProps }) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className={cx('p-1 bg-background rounded-md my-2 border-foreground', className)}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
website/src/repl/components/useLogger.jsx
Normal file
33
website/src/repl/components/useLogger.jsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import useEvent from '@src/useEvent.mjs';
|
||||||
|
import { logger } from '@strudel/core';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { atom } from 'nanostores';
|
||||||
|
|
||||||
|
export const $strudel_log_history = atom([]);
|
||||||
|
|
||||||
|
function useLoggerEvent(onTrigger) {
|
||||||
|
useEvent(logger.key, onTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpdatedLog(log, event) {
|
||||||
|
const { message, type, data } = event.detail;
|
||||||
|
const lastLog = log.length ? log[log.length - 1] : undefined;
|
||||||
|
const id = nanoid(12);
|
||||||
|
if (type === 'loaded-sample') {
|
||||||
|
const loadIndex = log.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
|
||||||
|
log[loadIndex] = { message, type, id, data };
|
||||||
|
} else if (lastLog && lastLog.message === message) {
|
||||||
|
log = log.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
|
||||||
|
} else {
|
||||||
|
log = log.concat([{ message, type, id, data }]);
|
||||||
|
}
|
||||||
|
return log.slice(-20);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLogger() {
|
||||||
|
useLoggerEvent((event) => {
|
||||||
|
const log = $strudel_log_history.get();
|
||||||
|
const newLog = getUpdatedLog(log, event);
|
||||||
|
$strudel_log_history.set(newLog);
|
||||||
|
});
|
||||||
|
}
|
||||||
30
website/src/repl/components/usedebounce.jsx
Normal file
30
website/src/repl/components/usedebounce.jsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
function debounce(fn, wait) {
|
||||||
|
let timer;
|
||||||
|
return function (...args) {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
timer = setTimeout(() => fn(...args), wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDebounce(callback) {
|
||||||
|
const ref = useRef;
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = callback;
|
||||||
|
}, [callback]);
|
||||||
|
|
||||||
|
const debouncedCallback = useMemo(() => {
|
||||||
|
const func = () => {
|
||||||
|
ref.current?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
return debounce(func, 1000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return debouncedCallback;
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { $featuredPatterns, $publicPatterns, collectionName } from '../user_pattern_utils.mjs';
|
import { $featuredPatterns, $publicPatterns, patternFilterName } from '../user_pattern_utils.mjs';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import * as tunes from '../repl/tunes.mjs';
|
import * as tunes from '../repl/tunes.mjs';
|
||||||
@ -12,9 +12,9 @@ export const useExamplePatterns = () => {
|
|||||||
const publicPatterns = useStore($publicPatterns);
|
const publicPatterns = useStore($publicPatterns);
|
||||||
const collections = useMemo(() => {
|
const collections = useMemo(() => {
|
||||||
const pats = new Map();
|
const pats = new Map();
|
||||||
pats.set(collectionName.featured, featuredPatterns);
|
pats.set(patternFilterName.featured, featuredPatterns);
|
||||||
pats.set(collectionName.public, publicPatterns);
|
pats.set(patternFilterName.public, publicPatterns);
|
||||||
// pats.set(collectionName.stock, stockPatterns);
|
// pats.set(patternFilterName.stock, stockPatterns);
|
||||||
return pats;
|
return pats;
|
||||||
}, [featuredPatterns, publicPatterns]);
|
}, [featuredPatterns, publicPatterns]);
|
||||||
|
|
||||||
|
|||||||
@ -81,6 +81,7 @@ export function loadModules() {
|
|||||||
import('@strudel/soundfonts'),
|
import('@strudel/soundfonts'),
|
||||||
import('@strudel/csound'),
|
import('@strudel/csound'),
|
||||||
import('@strudel/tidal'),
|
import('@strudel/tidal'),
|
||||||
|
import('@strudel/gamepad'),
|
||||||
import('@strudel/motion'),
|
import('@strudel/motion'),
|
||||||
import('@strudel/mqtt'),
|
import('@strudel/mqtt'),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'PressStart';
|
font-family: 'PressStart';
|
||||||
src: url('/fonts/PressStart2P/PressStart2P-Regular.ttf');
|
src: url('/fonts/PressStart2P/PressStart2P-Regular.ttf');
|
||||||
|
size-adjust: 65%;
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'BigBlueTerminal';
|
font-family: 'BigBlueTerminal';
|
||||||
@ -14,6 +15,11 @@
|
|||||||
font-family: 'galactico';
|
font-family: 'galactico';
|
||||||
src: url('/fonts/galactico/Galactico-Basic.otf');
|
src: url('/fonts/galactico/Galactico-Basic.otf');
|
||||||
}
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'CutiePi';
|
||||||
|
src: url('/fonts/CutiePi/Cute_Aurora_demo.ttf');
|
||||||
|
size-adjust: 120%;
|
||||||
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'JetBrains';
|
font-family: 'JetBrains';
|
||||||
src: url('/fonts/JetBrains/JetBrainsMono.woff2');
|
src: url('/fonts/JetBrains/JetBrainsMono.woff2');
|
||||||
@ -21,6 +27,7 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Monocraft';
|
font-family: 'Monocraft';
|
||||||
src: url('/fonts/Monocraft/Monocraft.ttf');
|
src: url('/fonts/Monocraft/Monocraft.ttf');
|
||||||
|
size-adjust: 90%;
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Hack';
|
font-family: 'Hack';
|
||||||
@ -41,10 +48,12 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'teletext';
|
font-family: 'teletext';
|
||||||
src: url('/fonts/teletext/EuropeanTeletext.ttf');
|
src: url('/fonts/teletext/EuropeanTeletext.ttf');
|
||||||
|
size-adjust: 90%;
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'mode7';
|
font-family: 'mode7';
|
||||||
src: url('/fonts/mode7/MODE7GX3.TTF');
|
src: url('/fonts/mode7/MODE7GX3.TTF');
|
||||||
|
size-adjust: 82%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose > h1:not(:first-child) {
|
.prose > h1:not(:first-child) {
|
||||||
|
|||||||
@ -8,16 +8,12 @@ import { confirmDialog, parseJSON, supabase } from './repl/util.mjs';
|
|||||||
export let $publicPatterns = atom([]);
|
export let $publicPatterns = atom([]);
|
||||||
export let $featuredPatterns = atom([]);
|
export let $featuredPatterns = atom([]);
|
||||||
|
|
||||||
export const collectionName = {
|
const patternQueryLimit = 20;
|
||||||
user: 'user',
|
|
||||||
public: 'Last Creations',
|
|
||||||
stock: 'Stock Examples',
|
|
||||||
featured: 'Featured',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const patternFilterName = {
|
export const patternFilterName = {
|
||||||
community: 'community',
|
public: 'latest',
|
||||||
|
featured: 'featured',
|
||||||
user: 'user',
|
user: 'user',
|
||||||
|
// stock: 'stock examples',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sessionAtom = (name, initial = undefined) => {
|
const sessionAtom = (name, initial = undefined) => {
|
||||||
@ -36,7 +32,7 @@ const sessionAtom = (name, initial = undefined) => {
|
|||||||
export let $viewingPatternData = sessionAtom('viewingPatternData', {
|
export let $viewingPatternData = sessionAtom('viewingPatternData', {
|
||||||
id: '',
|
id: '',
|
||||||
code: '',
|
code: '',
|
||||||
collection: collectionName.user,
|
collection: patternFilterName.user,
|
||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -51,25 +47,50 @@ export const setViewingPatternData = (data) => {
|
|||||||
$viewingPatternData.set(JSON.stringify(data));
|
$viewingPatternData.set(JSON.stringify(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
export function loadPublicPatterns() {
|
function parsePageNum(page) {
|
||||||
return supabase.from('code_v1').select().eq('public', true).limit(20).order('id', { ascending: false });
|
return isNaN(page) ? 0 : page;
|
||||||
|
}
|
||||||
|
export function loadPublicPatterns(page) {
|
||||||
|
page = parsePageNum(page);
|
||||||
|
const offset = page * patternQueryLimit;
|
||||||
|
return supabase
|
||||||
|
.from('code_v1')
|
||||||
|
.select()
|
||||||
|
.eq('public', true)
|
||||||
|
.range(offset, offset + patternQueryLimit)
|
||||||
|
.order('id', { ascending: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadFeaturedPatterns() {
|
export function loadFeaturedPatterns(page = 0) {
|
||||||
return supabase.from('code_v1').select().eq('featured', true).limit(20).order('id', { ascending: false });
|
page = parsePageNum(page);
|
||||||
|
const offset = page * patternQueryLimit;
|
||||||
|
return supabase
|
||||||
|
.from('code_v1')
|
||||||
|
.select()
|
||||||
|
.eq('featured', true)
|
||||||
|
.range(offset, offset + patternQueryLimit)
|
||||||
|
.order('id', { ascending: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadAndSetPublicPatterns(page) {
|
||||||
|
const p = await loadPublicPatterns(page);
|
||||||
|
const data = p?.data;
|
||||||
|
const pats = {};
|
||||||
|
data?.forEach((data, key) => (pats[data.id ?? key] = data));
|
||||||
|
$publicPatterns.set(pats);
|
||||||
|
}
|
||||||
|
export async function loadAndSetFeaturedPatterns(page) {
|
||||||
|
const p = await loadFeaturedPatterns(page);
|
||||||
|
const data = p?.data;
|
||||||
|
const pats = {};
|
||||||
|
data?.forEach((data, key) => (pats[data.id ?? key] = data));
|
||||||
|
$featuredPatterns.set(pats);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadDBPatterns() {
|
export async function loadDBPatterns() {
|
||||||
try {
|
try {
|
||||||
const { data: publicPatterns } = await loadPublicPatterns();
|
await loadAndSetPublicPatterns();
|
||||||
const { data: featuredPatterns } = await loadFeaturedPatterns();
|
await loadAndSetFeaturedPatterns();
|
||||||
const featured = {};
|
|
||||||
const pub = {};
|
|
||||||
|
|
||||||
publicPatterns?.forEach((data, key) => (pub[data.id ?? key] = data));
|
|
||||||
featuredPatterns?.forEach((data, key) => (featured[data.id ?? key] = data));
|
|
||||||
$publicPatterns.set(pub);
|
|
||||||
$featuredPatterns.set(featured);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('error loading patterns', err);
|
console.error('error loading patterns', err);
|
||||||
}
|
}
|
||||||
@ -90,9 +111,9 @@ export function useActivePattern() {
|
|||||||
|
|
||||||
export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
|
export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
|
||||||
|
|
||||||
const defaultCode = '';
|
export const defaultCode = '';
|
||||||
export const userPattern = {
|
export const userPattern = {
|
||||||
collection: collectionName.user,
|
collection: patternFilterName.user,
|
||||||
getAll() {
|
getAll() {
|
||||||
const patterns = parseJSON(settingsMap.get().userPatterns);
|
const patterns = parseJSON(settingsMap.get().userPatterns);
|
||||||
return patterns ?? {};
|
return patterns ?? {};
|
||||||
|
|||||||
@ -46,6 +46,29 @@ module.exports = {
|
|||||||
'code::after': {
|
'code::after': {
|
||||||
content: 'none',
|
content: 'none',
|
||||||
},
|
},
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
a: {
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
},
|
||||||
|
h1: {
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
},
|
||||||
|
pre: {
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
background: 'var(--background)',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user