mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-24 05:57:17 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06463da7fb | |||
| 2a0e66a862 | |||
| 9c37725436 | |||
| 06f3696ab0 | |||
| a4c4c6025c |
@@ -31,10 +31,6 @@ 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).
|
||||
|
||||
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
|
||||
|
||||
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
|
||||
|
||||
@@ -59,7 +59,6 @@
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@tauri-apps/cli": "^2.2.7",
|
||||
"@vitest/coverage-v8": "3.0.4",
|
||||
"@vitest/ui": "^3.0.4",
|
||||
"acorn": "^8.14.0",
|
||||
"dependency-tree": "^11.0.1",
|
||||
|
||||
@@ -67,7 +67,6 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
const initialSettings = Object.keys(compartments).map((key) =>
|
||||
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
||||
);
|
||||
|
||||
initTheme(settings.theme);
|
||||
let state = EditorState.create({
|
||||
doc: initialCode,
|
||||
|
||||
Vendored
-3
@@ -4,7 +4,6 @@ import blackscreen, { settings as blackscreenSettings } from './themes/blackscre
|
||||
import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen.mjs';
|
||||
import teletext, { settings as teletextSettings } from './themes/teletext.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 abcdef, { settings as abcdefSettings } from './themes/abcdef.mjs';
|
||||
import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
|
||||
@@ -56,7 +55,6 @@ export const themes = {
|
||||
androidstudio,
|
||||
duotoneDark,
|
||||
githubDark,
|
||||
CutiePi,
|
||||
gruvboxDark,
|
||||
materialDark,
|
||||
nord,
|
||||
@@ -100,7 +98,6 @@ export const settings = {
|
||||
duotoneLight: duotoneLightSettings,
|
||||
duotoneDark: duotoneDarkSettings,
|
||||
eclipse: eclipseSettings,
|
||||
CutiePi: CutiePiSettings,
|
||||
githubLight: githubLightSettings,
|
||||
githubDark: githubDarkSettings,
|
||||
gruvboxDark: gruvboxDarkSettings,
|
||||
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* @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 },
|
||||
],
|
||||
});
|
||||
@@ -140,3 +140,8 @@ registerWidget('_spectrum', (id, options = {}, pat) => {
|
||||
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||
return pat.spectrum({ ...options, ctx, id });
|
||||
});
|
||||
|
||||
registerWidget('_cyclecounter', (id, options = {}, pat) => {
|
||||
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||
return pat.cyclecounter({ ...options, ctx, id });
|
||||
});
|
||||
|
||||
@@ -23,6 +23,8 @@ export class Cyclist {
|
||||
this.beforeStart = beforeStart;
|
||||
this.cps = 0.5;
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
this.loopStart = 0;
|
||||
this.loopLength = 0;
|
||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||
this.lastBegin = 0; // query begin of last tick
|
||||
this.lastEnd = 0; // query end of last tick
|
||||
@@ -48,6 +50,10 @@ export class Cyclist {
|
||||
this.lastBegin = begin;
|
||||
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
|
||||
this.lastEnd = end;
|
||||
if (this.loopLength > 0 && this.lastEnd >= this.loopStart + this.loopLength) {
|
||||
this.lastEnd = this.loopStart + (this.lastEnd % 1);
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
}
|
||||
this.lastTick = phase;
|
||||
|
||||
if (phase < t) {
|
||||
@@ -116,7 +122,7 @@ export class Cyclist {
|
||||
stop() {
|
||||
logger('[cyclist] stop');
|
||||
this.clock.stop();
|
||||
this.lastEnd = 0;
|
||||
this.lastEnd = this.loopStart;
|
||||
this.setStarted(false);
|
||||
}
|
||||
async setPattern(pat, autostart = false) {
|
||||
@@ -132,6 +138,19 @@ export class Cyclist {
|
||||
this.cps = cps;
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
}
|
||||
setLoop(start = 0, length = 0) {
|
||||
if (start >= 0 && length >= 0) {
|
||||
start = Math.floor(start);
|
||||
length = Math.floor(length);
|
||||
|
||||
if (start != this.loopStart || length != this.loopLength) {
|
||||
this.loopStart = Math.floor(start);
|
||||
this.loopLength = Math.floor(length);
|
||||
this.lastEnd = this.loopStart + (this.lastEnd % 1);
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
log(begin, end, haps) {
|
||||
const onsets = haps.filter((h) => h.hasOnset());
|
||||
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
|
||||
|
||||
+41
-27
@@ -388,7 +388,7 @@ export class Pattern {
|
||||
|
||||
polyJoin = function () {
|
||||
const pp = this;
|
||||
return pp.fmap((p) => p.extend(pp._steps.div(p._steps))).outerJoin();
|
||||
return pp.fmap((p) => p.repeat(pp._steps.div(p._steps))).outerJoin();
|
||||
};
|
||||
|
||||
polyBind(func) {
|
||||
@@ -1244,16 +1244,6 @@ export function reify(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.
|
||||
*
|
||||
* @return {Pattern}
|
||||
@@ -1328,7 +1318,7 @@ export function stackBy(by, ...pats) {
|
||||
left: stackLeft,
|
||||
right: stackRight,
|
||||
expand: stack,
|
||||
repeat: (...args) => polymeter(...args).steps(steps),
|
||||
repeat: (...args) => polymeterSteps(steps, ...args),
|
||||
};
|
||||
return by
|
||||
.inhabit(lookup)
|
||||
@@ -1830,10 +1820,7 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f
|
||||
export const focus = register('focus', function (b, e, pat) {
|
||||
b = Fraction(b);
|
||||
e = Fraction(e);
|
||||
return pat
|
||||
._early(b.sam())
|
||||
._fast(Fraction(1).div(e.sub(b)))
|
||||
._late(b);
|
||||
return pat._fast(Fraction(1).div(e.sub(b))).late(b.cyclePos());
|
||||
});
|
||||
|
||||
export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], function (span, pat) {
|
||||
@@ -2043,7 +2030,7 @@ export const zoom = register('zoom', function (s, e, pat) {
|
||||
return nothing;
|
||||
}
|
||||
const d = e.sub(s);
|
||||
const steps = __steps ? pat._steps?.mulmaybe(d) : undefined;
|
||||
const steps = __steps ? pat._steps.mulmaybe(d) : undefined;
|
||||
return pat
|
||||
.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
|
||||
.withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
|
||||
@@ -2648,10 +2635,34 @@ export function _polymeterListSteps(steps, ...args) {
|
||||
/**
|
||||
* *Experimental*
|
||||
*
|
||||
* Aligns the steps of the patterns, creating polymeters. The patterns are repeated until they all fit the cycle. For example, in the below the first pattern is repeated twice, and the second is repeated three times, to fit the lowest common multiple of six steps.
|
||||
* Aligns the steps of the patterns, to match the given number of steps per cycle, creating polymeters.
|
||||
*
|
||||
* @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
|
||||
* @example
|
||||
* // The same as note("{c eb g, c2 g2}%6")
|
||||
* // The same as note("{c eb g, c2 g2}")
|
||||
* polymeter("c eb g", "c2 g2").note()
|
||||
*
|
||||
*/
|
||||
@@ -2667,12 +2678,13 @@ export function polymeter(...args) {
|
||||
if (args.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
const steps = lcm(...args.map((x) => x._steps));
|
||||
const steps = args[0]._steps;
|
||||
if (steps.eq(Fraction(0))) {
|
||||
return nothing;
|
||||
}
|
||||
const [head, ...tail] = args;
|
||||
|
||||
const result = stack(...args.map((x) => x.pace(steps)));
|
||||
const result = stack(head, ...tail.map((pat) => pat._slow(pat._steps.div(steps))));
|
||||
result._steps = steps;
|
||||
return result;
|
||||
}
|
||||
@@ -2833,16 +2845,16 @@ export const drop = stepRegister('drop', function (i, pat) {
|
||||
/**
|
||||
* *Experimental*
|
||||
*
|
||||
* `extend` is similar to `fast` in that it increases its density, but it also increases the step count
|
||||
* accordingly. So `stepcat("a b".extend(2), "c d")` would be the same as `"a b a b c d"`, whereas
|
||||
* `repeat` is similar to `fast` in that it 'speeds up' the pattern, 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
|
||||
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
|
||||
* @example
|
||||
* stepcat(
|
||||
* sound("bd bd - cp").extend(2),
|
||||
* sound("bd bd - cp").repeat(2),
|
||||
* sound("bd - sd -")
|
||||
* ).pace(8)
|
||||
*/
|
||||
export const extend = stepRegister('extend', function (factor, pat) {
|
||||
export const repeat = stepRegister('repeat', function (factor, pat) {
|
||||
return pat.fast(factor).expand(factor);
|
||||
});
|
||||
|
||||
@@ -3040,6 +3052,8 @@ export const timeCat = stepcat;
|
||||
// Deprecated stepwise aliases
|
||||
export const s_cat = stepcat;
|
||||
export const s_alt = stepalt;
|
||||
export const s_polymeterSteps = polymeterSteps;
|
||||
Pattern.prototype.s_polymeterSteps = Pattern.prototype.polymeterSteps;
|
||||
export const s_polymeter = polymeter;
|
||||
Pattern.prototype.s_polymeter = Pattern.prototype.polymeter;
|
||||
export const s_taper = shrink;
|
||||
@@ -3052,8 +3066,8 @@ export const s_sub = drop;
|
||||
Pattern.prototype.s_sub = Pattern.prototype.drop;
|
||||
export const s_expand = expand;
|
||||
Pattern.prototype.s_expand = Pattern.prototype.expand;
|
||||
export const s_extend = extend;
|
||||
Pattern.prototype.s_extend = Pattern.prototype.extend;
|
||||
export const s_extend = repeat;
|
||||
Pattern.prototype.s_extend = Pattern.prototype.repeat;
|
||||
export const s_contract = contract;
|
||||
Pattern.prototype.s_contract = Pattern.prototype.contract;
|
||||
export const s_tour = tour;
|
||||
|
||||
@@ -86,6 +86,7 @@ export function repl({
|
||||
const toggle = () => scheduler.toggle();
|
||||
const setCps = (cps) => scheduler.setCps(cps);
|
||||
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
||||
const setLoop = (start, length) => scheduler.setLoop(start, length);
|
||||
|
||||
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
|
||||
|
||||
@@ -167,6 +168,8 @@ export function repl({
|
||||
setcps: setCps,
|
||||
setCpm,
|
||||
setcpm: setCpm,
|
||||
setLoop,
|
||||
setloop: setLoop,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+13
-96
@@ -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 { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs';
|
||||
import { Pattern, fastcat, pure, register, reify, silence, stack } from './pattern.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
|
||||
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
|
||||
@@ -20,6 +20,9 @@ export const signal = (func) => {
|
||||
return new Pattern(query);
|
||||
};
|
||||
|
||||
export const isaw = signal((t) => 1 - (t % 1));
|
||||
export const isaw2 = isaw.toBipolar();
|
||||
|
||||
/**
|
||||
* A sawtooth signal between 0 and 1.
|
||||
*
|
||||
@@ -33,40 +36,8 @@ export const signal = (func) => {
|
||||
*
|
||||
*/
|
||||
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();
|
||||
|
||||
/**
|
||||
* 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));
|
||||
|
||||
/**
|
||||
@@ -90,12 +61,6 @@ export const sine = sine2.fromBipolar();
|
||||
*
|
||||
*/
|
||||
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));
|
||||
|
||||
/**
|
||||
@@ -107,12 +72,6 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||
*
|
||||
*/
|
||||
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();
|
||||
|
||||
/**
|
||||
@@ -123,37 +82,9 @@ export const square2 = square.toBipolar();
|
||||
* n(tri.segment(8).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
export const tri = fastcat(saw, isaw);
|
||||
export const tri = fastcat(isaw, saw);
|
||||
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);
|
||||
|
||||
/**
|
||||
@@ -433,30 +364,19 @@ export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
|
||||
export const randcat = chooseCycles;
|
||||
|
||||
const _wchooseWith = function (pat, ...pairs) {
|
||||
// A list of patterns of values
|
||||
const values = pairs.map((pair) => reify(pair[0]));
|
||||
|
||||
// A list of weight patterns
|
||||
const weights = [];
|
||||
|
||||
let total = pure(0);
|
||||
let accum = 0;
|
||||
for (const pair of pairs) {
|
||||
// 'add' accepts either values or patterns of values here, so no need
|
||||
// to explicitly reify
|
||||
total = total.add(pair[1]);
|
||||
// accumulate our list of weight patterns
|
||||
weights.push(total);
|
||||
accum += pair[1];
|
||||
weights.push(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 total = accum;
|
||||
const match = function (r) {
|
||||
const findpat = total.mul(r);
|
||||
return weightspat.fmap((weights) => (find) => values[weights.findIndex((x) => x > find, weights)]).appLeft(findpat);
|
||||
const find = r * total;
|
||||
return values[weights.findIndex((x) => x > find, weights)];
|
||||
};
|
||||
// This returns a pattern of patterns.. The innerJoin is in wchooseCycles
|
||||
return pat.bind(match);
|
||||
return pat.fmap(match);
|
||||
};
|
||||
|
||||
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
|
||||
@@ -478,9 +398,6 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
||||
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
|
||||
* @example
|
||||
* 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();
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
sequence,
|
||||
palindrome,
|
||||
polymeter,
|
||||
polymeterSteps,
|
||||
polyrhythm,
|
||||
silence,
|
||||
fast,
|
||||
@@ -610,10 +611,17 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('polymeter()', () => {
|
||||
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(
|
||||
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()', () => {
|
||||
@@ -1257,14 +1265,4 @@ describe('Pattern', () => {
|
||||
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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Pattern } from '@strudel/core';
|
||||
|
||||
Pattern.prototype.cyclecounter = function (options = {}) {
|
||||
return this.onPaint((ctx, time, haps, drawTime) => {
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
ctx.font = options.font || '2em monospace';
|
||||
ctx.fillStyle = options.color || 'red';
|
||||
ctx.textBaseline = 'bottom';
|
||||
ctx.textAlign = 'right';
|
||||
|
||||
const div = options.div || 1;
|
||||
|
||||
ctx.fillText('Cycle' + (div > 1 ? '/' + div : '') + ' ' + ((time / div) >> 0), ctx.canvas.width, ctx.canvas.height);
|
||||
});
|
||||
};
|
||||
@@ -4,3 +4,4 @@ export * from './draw.mjs';
|
||||
export * from './pianoroll.mjs';
|
||||
export * from './spiral.mjs';
|
||||
export * from './pitchwheel.mjs';
|
||||
export * from './cyclecounter.mjs';
|
||||
|
||||
@@ -38,10 +38,15 @@ Pattern.prototype.mqtt = function (
|
||||
add_meta = true,
|
||||
) {
|
||||
const key = host + '-' + client;
|
||||
let connected = false;
|
||||
let password_entered = false;
|
||||
|
||||
if (!client) {
|
||||
client = 'strudel-' + String(Math.floor(Math.random() * 1000000));
|
||||
}
|
||||
function onConnect() {
|
||||
console.log('Connected to mqtt broker');
|
||||
connected = true;
|
||||
if (password_entered) {
|
||||
document.cookie = 'mqtt_pass=' + password;
|
||||
}
|
||||
@@ -51,9 +56,6 @@ Pattern.prototype.mqtt = function (
|
||||
if (connections[key]) {
|
||||
cx = connections[key];
|
||||
} else {
|
||||
if (!client) {
|
||||
client = 'strudel-' + String(Math.floor(Math.random() * 1000000));
|
||||
}
|
||||
cx = new Paho.Client(host, client);
|
||||
connections[key] = cx;
|
||||
cx.onConnectionLost = onConnectionLost;
|
||||
@@ -84,7 +86,7 @@ Pattern.prototype.mqtt = function (
|
||||
return this.withHap((hap) => {
|
||||
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
let msg_topic = topic;
|
||||
if (!cx || !cx.isConnected()) {
|
||||
if (!connected) {
|
||||
return;
|
||||
}
|
||||
let message = '';
|
||||
|
||||
@@ -133,65 +133,6 @@ export function registerSynthSounds() {
|
||||
{ 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) => {
|
||||
registerSound(
|
||||
s,
|
||||
|
||||
@@ -75,12 +75,7 @@ const waveshapes = {
|
||||
return v - polyBlep(phase, dt);
|
||||
},
|
||||
};
|
||||
function getParamValue(block, param) {
|
||||
if (param.length > 1) {
|
||||
return param[block];
|
||||
}
|
||||
return param[0];
|
||||
}
|
||||
|
||||
const waveShapeNames = Object.keys(waveshapes);
|
||||
class LFOProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
@@ -367,11 +362,6 @@ function getUnisonDetune(unison, detune, voiceIndex) {
|
||||
}
|
||||
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 {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -448,7 +438,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
const isOdd = (n & 1) == 1;
|
||||
|
||||
//applies unison "spread" detune in semitones
|
||||
const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n));
|
||||
const freq = frequency * Math.pow(2, getUnisonDetune(voices, freqspread, n) / 12);
|
||||
let gainL = gain1;
|
||||
let gainR = gain2;
|
||||
// invert right and left gain
|
||||
@@ -658,103 +648,3 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
||||
}
|
||||
|
||||
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",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel/tree/main/packages/tidal"
|
||||
"url": "git+https://github.com/felixroos/hs2js.git"
|
||||
},
|
||||
"keywords": [
|
||||
"haskell",
|
||||
|
||||
Generated
-95
@@ -39,9 +39,6 @@ importers:
|
||||
'@tauri-apps/cli':
|
||||
specifier: ^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':
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(vitest@3.0.4)
|
||||
@@ -1415,10 +1412,6 @@ packages:
|
||||
resolution: {integrity: sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==}
|
||||
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':
|
||||
resolution: {integrity: sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==}
|
||||
|
||||
@@ -1841,10 +1834,6 @@ packages:
|
||||
'@isaacs/string-locale-compare@1.1.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@@ -2829,15 +2818,6 @@ packages:
|
||||
peerDependencies:
|
||||
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':
|
||||
resolution: {integrity: sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==}
|
||||
|
||||
@@ -4507,9 +4487,6 @@ packages:
|
||||
hs2js@0.1.0:
|
||||
resolution: {integrity: sha512-THlUIMX8tZf6gtbz5RUZ8xQUyKJEItsx7bxEBcouFIEWjeo90376WMocj3JEz6qTv5nM+tjo3vNvLf89XruMvg==}
|
||||
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
html-escaper@3.0.3:
|
||||
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
|
||||
|
||||
@@ -4860,22 +4837,6 @@ packages:
|
||||
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
@@ -6926,10 +6887,6 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
test-exclude@7.0.1:
|
||||
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
text-encoding-shim@1.0.5:
|
||||
resolution: {integrity: sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==}
|
||||
|
||||
@@ -7548,7 +7505,6 @@ packages:
|
||||
|
||||
workbox-google-analytics@7.0.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
|
||||
@@ -8612,8 +8568,6 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.25.9
|
||||
'@babel/helper-validator-identifier': 7.25.9
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@codemirror/autocomplete@6.18.4':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.10.8
|
||||
@@ -8980,8 +8934,6 @@ snapshots:
|
||||
|
||||
'@isaacs/string-locale-compare@1.1.0': {}
|
||||
|
||||
'@istanbuljs/schema@0.1.3': {}
|
||||
|
||||
'@jest/schemas@29.6.3':
|
||||
dependencies:
|
||||
'@sinclair/typebox': 0.27.8
|
||||
@@ -10235,24 +10187,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
|
||||
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':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.0.4
|
||||
@@ -12268,8 +12202,6 @@ snapshots:
|
||||
dependencies:
|
||||
web-tree-sitter: 0.20.8
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
html-escaper@3.0.3: {}
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
@@ -12608,27 +12540,6 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
@@ -15351,12 +15262,6 @@ snapshots:
|
||||
commander: 2.20.3
|
||||
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-extensions@1.9.0: {}
|
||||
|
||||
@@ -3019,33 +3019,6 @@ 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`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:f s:sawtooth cutoff:1000 lpenv:8 fanchor:0 ]",
|
||||
@@ -4261,96 +4234,6 @@ 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`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:A3 ]",
|
||||
@@ -4393,43 +4276,6 @@ 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`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:bd pan:0 ]",
|
||||
@@ -6178,54 +6024,67 @@ exports[`runs examples > example "ply" example index 0 1`] = `
|
||||
|
||||
exports[`runs examples > example "polymeter" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | note:c ]",
|
||||
"[ 0/1 → 1/6 | note:c2 ]",
|
||||
"[ 1/6 → 1/3 | note:eb ]",
|
||||
"[ 1/6 → 1/3 | note:g2 ]",
|
||||
"[ 1/3 → 1/2 | note:g ]",
|
||||
"[ 1/3 → 1/2 | note:c2 ]",
|
||||
"[ 1/2 → 2/3 | note:c ]",
|
||||
"[ 1/2 → 2/3 | note:g2 ]",
|
||||
"[ 2/3 → 5/6 | note:eb ]",
|
||||
"[ 2/3 → 5/6 | note:c2 ]",
|
||||
"[ 5/6 → 1/1 | note:g ]",
|
||||
"[ 5/6 → 1/1 | note:g2 ]",
|
||||
"[ 1/1 → 7/6 | note:c ]",
|
||||
"[ 1/1 → 7/6 | note:c2 ]",
|
||||
"[ 7/6 → 4/3 | note:eb ]",
|
||||
"[ 7/6 → 4/3 | note:g2 ]",
|
||||
"[ 4/3 → 3/2 | note:g ]",
|
||||
"[ 4/3 → 3/2 | note:c2 ]",
|
||||
"[ 3/2 → 5/3 | note:c ]",
|
||||
"[ 3/2 → 5/3 | note:g2 ]",
|
||||
"[ 5/3 → 11/6 | note:eb ]",
|
||||
"[ 5/3 → 11/6 | note:c2 ]",
|
||||
"[ 11/6 → 2/1 | note:g ]",
|
||||
"[ 11/6 → 2/1 | note:g2 ]",
|
||||
"[ 2/1 → 13/6 | note:c ]",
|
||||
"[ 2/1 → 13/6 | note:c2 ]",
|
||||
"[ 13/6 → 7/3 | note:eb ]",
|
||||
"[ 13/6 → 7/3 | note:g2 ]",
|
||||
"[ 7/3 → 5/2 | note:g ]",
|
||||
"[ 7/3 → 5/2 | note:c2 ]",
|
||||
"[ 5/2 → 8/3 | note:c ]",
|
||||
"[ 5/2 → 8/3 | note:g2 ]",
|
||||
"[ 8/3 → 17/6 | note:eb ]",
|
||||
"[ 8/3 → 17/6 | note:c2 ]",
|
||||
"[ 17/6 → 3/1 | note:g ]",
|
||||
"[ 17/6 → 3/1 | note:g2 ]",
|
||||
"[ 3/1 → 19/6 | note:c ]",
|
||||
"[ 3/1 → 19/6 | note:c2 ]",
|
||||
"[ 19/6 → 10/3 | note:eb ]",
|
||||
"[ 19/6 → 10/3 | note:g2 ]",
|
||||
"[ 10/3 → 7/2 | note:g ]",
|
||||
"[ 10/3 → 7/2 | note:c2 ]",
|
||||
"[ 7/2 → 11/3 | note:c ]",
|
||||
"[ 7/2 → 11/3 | note:g2 ]",
|
||||
"[ 11/3 → 23/6 | note:eb ]",
|
||||
"[ 11/3 → 23/6 | note:c2 ]",
|
||||
"[ 23/6 → 4/1 | note:g ]",
|
||||
"[ 23/6 → 4/1 | note:g2 ]",
|
||||
"[ 0/1 → 1/3 | note:c ]",
|
||||
"[ 0/1 → 1/3 | note:c2 ]",
|
||||
"[ 1/3 → 2/3 | note:eb ]",
|
||||
"[ 1/3 → 2/3 | note:g2 ]",
|
||||
"[ 2/3 → 1/1 | note:g ]",
|
||||
"[ 2/3 → 1/1 | note:c2 ]",
|
||||
"[ 1/1 → 4/3 | note:c ]",
|
||||
"[ 1/1 → 4/3 | note:g2 ]",
|
||||
"[ 4/3 → 5/3 | note:eb ]",
|
||||
"[ 4/3 → 5/3 | note:c2 ]",
|
||||
"[ 5/3 → 2/1 | note:g ]",
|
||||
"[ 5/3 → 2/1 | note:g2 ]",
|
||||
"[ 2/1 → 7/3 | note:c ]",
|
||||
"[ 2/1 → 7/3 | note:c2 ]",
|
||||
"[ 7/3 → 8/3 | note:eb ]",
|
||||
"[ 7/3 → 8/3 | note:g2 ]",
|
||||
"[ 8/3 → 3/1 | note:g ]",
|
||||
"[ 8/3 → 3/1 | note:c2 ]",
|
||||
"[ 3/1 → 10/3 | note:c ]",
|
||||
"[ 3/1 → 10/3 | note:g2 ]",
|
||||
"[ 10/3 → 11/3 | note:eb ]",
|
||||
"[ 10/3 → 11/3 | note:c2 ]",
|
||||
"[ 11/3 → 4/1 | note:g ]",
|
||||
"[ 11/3 → 4/1 | note:g2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "polymeterSteps" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c ]",
|
||||
"[ 0/1 → 1/4 | note:e ]",
|
||||
"[ 1/4 → 1/2 | note:d ]",
|
||||
"[ 1/4 → 1/2 | note:f ]",
|
||||
"[ 1/2 → 3/4 | note:c ]",
|
||||
"[ 1/2 → 3/4 | note:g ]",
|
||||
"[ 3/4 → 1/1 | note:d ]",
|
||||
"[ 3/4 → 1/1 | note:e ]",
|
||||
"[ 1/1 → 5/4 | note:c ]",
|
||||
"[ 1/1 → 5/4 | note:f ]",
|
||||
"[ 5/4 → 3/2 | note:d ]",
|
||||
"[ 5/4 → 3/2 | note:g ]",
|
||||
"[ 3/2 → 7/4 | note:c ]",
|
||||
"[ 3/2 → 7/4 | note:e ]",
|
||||
"[ 7/4 → 2/1 | note:d ]",
|
||||
"[ 7/4 → 2/1 | note:f ]",
|
||||
"[ 2/1 → 9/4 | note:c ]",
|
||||
"[ 2/1 → 9/4 | note:g ]",
|
||||
"[ 9/4 → 5/2 | note:d ]",
|
||||
"[ 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 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -6631,6 +6490,33 @@ 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`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:34 s:gm_acoustic_guitar_nylon ]",
|
||||
@@ -9113,38 +8999,38 @@ exports[`runs examples > example "transpose" example index 1 1`] = `
|
||||
|
||||
exports[`runs examples > example "tri" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:C3 ]",
|
||||
"[ 1/8 → 1/4 | note:Eb3 ]",
|
||||
"[ 0/1 → 1/8 | note:C4 ]",
|
||||
"[ 1/8 → 1/4 | note:Bb3 ]",
|
||||
"[ 1/4 → 3/8 | note:G3 ]",
|
||||
"[ 3/8 → 1/2 | note:Bb3 ]",
|
||||
"[ 1/2 → 5/8 | note:C4 ]",
|
||||
"[ 5/8 → 3/4 | note:Bb3 ]",
|
||||
"[ 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:Eb3 ]",
|
||||
"[ 1/1 → 9/8 | note:C3 ]",
|
||||
"[ 9/8 → 5/4 | note:Eb3 ]",
|
||||
"[ 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:Bb3 ]",
|
||||
"[ 3/2 → 13/8 | note:C4 ]",
|
||||
"[ 13/8 → 7/4 | note:Bb3 ]",
|
||||
"[ 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:Eb3 ]",
|
||||
"[ 2/1 → 17/8 | note:C3 ]",
|
||||
"[ 17/8 → 9/4 | note:Eb3 ]",
|
||||
"[ 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:Bb3 ]",
|
||||
"[ 5/2 → 21/8 | note:C4 ]",
|
||||
"[ 21/8 → 11/4 | note:Bb3 ]",
|
||||
"[ 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:Eb3 ]",
|
||||
"[ 3/1 → 25/8 | note:C3 ]",
|
||||
"[ 25/8 → 13/4 | note:Eb3 ]",
|
||||
"[ 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:Bb3 ]",
|
||||
"[ 7/2 → 29/8 | note:C4 ]",
|
||||
"[ 29/8 → 15/4 | note:Bb3 ]",
|
||||
"[ 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:Eb3 ]",
|
||||
"[ 31/8 → 4/1 | note:Bb3 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -9674,59 +9560,6 @@ 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`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | note:c3 ]",
|
||||
|
||||
Binary file not shown.
@@ -1,7 +0,0 @@
|
||||
|
||||
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,6 +36,8 @@ export function Showcase() {
|
||||
}
|
||||
|
||||
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 - Bitrot [v10101a Edit]', id: 'z_cJMdBp67Q' },
|
||||
{ title: 'you will not steve reich your way out of it', id: 'xpILnXcWyuo' },
|
||||
@@ -56,6 +58,7 @@ let _videos = [
|
||||
},
|
||||
{ title: 'letSeaTstrudeL @ solstice stream 2023', id: 'fTiX6dVtdWQ' },
|
||||
{ title: 'totalgee (Glen F) @ solstice stream 2023', id: 'IvI6uaE3nLU' },
|
||||
{ title: 'Dan Gorelick @ solstice stream 2023', id: 'qMJEljJyPi0' },
|
||||
//
|
||||
/* { // not sure if this is copyrighted ...
|
||||
title: 'Creative Coding @ Chalmers University of Technology, video by svt.se',
|
||||
@@ -123,11 +126,6 @@ let _videos = [
|
||||
'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: '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);
|
||||
|
||||
@@ -9,11 +9,11 @@ import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'
|
||||
// }
|
||||
|
||||
export default function UdelsEditor(Props) {
|
||||
const { context, ...editorProps } = Props;
|
||||
const { context } = Props;
|
||||
const { containerRef, editorRef, error, init, pending, started, handleTogglePlay } = context;
|
||||
|
||||
return (
|
||||
<div className={'h-full flex w-full flex-col relative'} {...editorProps}>
|
||||
<div className={'h-full flex w-full flex-col relative'}>
|
||||
<Loader active={pending} />
|
||||
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
|
||||
@@ -4,7 +4,7 @@ export default function UdelsHeader(Props) {
|
||||
const { numWindows, setNumWindows } = Props;
|
||||
|
||||
return (
|
||||
<header id="header" className="flex text-white z-[100] text-lg select-none bg-neutral-800">
|
||||
<header id="header" className="flex text-white z-[100] text-lg select-none bg-neutral-900">
|
||||
<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'}>
|
||||
<div className={'mt-[1px] cursor-pointer'}>🌀</div>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
const ALLOW_MANY = ['by', 'url', 'genre', 'license'];
|
||||
|
||||
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 comments = [...raw_code.matchAll(comment_regexp)].map((c) => (c[1] || c[2] || '').trim());
|
||||
const tags = {};
|
||||
|
||||
@@ -293,6 +293,8 @@ global effects use the same chain for all events of the same orbit:
|
||||
|
||||
<JsDoc client:idle name="iresponse" h={0} />
|
||||
|
||||
Next, we'll look at strudel's support for [Csound](/learn/csound).
|
||||
|
||||
## Phaser
|
||||
|
||||
### phaser
|
||||
@@ -310,5 +312,3 @@ global effects use the same chain for all events of the same orbit:
|
||||
### phasersweep
|
||||
|
||||
<JsDoc client:idle name="phasersweep" h={0} />
|
||||
|
||||
Next, we'll look at input / output via [MIDI, OSC and other methods](/learn/input-output).
|
||||
|
||||
@@ -8,7 +8,7 @@ import { JsDoc } from '../../docs/JsDoc';
|
||||
|
||||
# Mini-notation
|
||||
|
||||
Just like [Tidal Cycles](https://tidalcycles.org/docs/reference/mini_notation/), Strudel uses a so called "Mini-Notation", which is a custom language that is designed for writing rhythmic patterns using little amounts of text.
|
||||
Just like [Tidal Cycles](https://tidalcycles.org/), Strudel uses a so called "Mini-Notation", which is a custom language that is designed for writing rhythmic patterns using little amounts of text.
|
||||
|
||||
## Note
|
||||
|
||||
@@ -156,11 +156,7 @@ But to play multiple chords in a sequence, we have to wrap them in brackets:
|
||||
|
||||
## Elongation
|
||||
|
||||
With the "\_" symbol, we can manually extend the duration of an event:
|
||||
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4] _ _ [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
|
||||
|
||||
With the "@" symbol, we can do the same thing more conveniently by specifying the temporal "weight" of a sequence child:
|
||||
With the "@" symbol, we can specify temporal "weight" of a sequence child:
|
||||
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
|
||||
|
||||
@@ -172,16 +168,6 @@ Using "!" we can repeat without speeding up:
|
||||
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
|
||||
|
||||
## Degradation
|
||||
|
||||
Using "?" we can randomly drop events from a pattern while preserving their time slot:
|
||||
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]? [a3,c3,e4]? [b3,d3,f#4]>*2")`} punchcard />
|
||||
|
||||
We can include a number after the "?" to represent the probability of being dropped, otherwise defaulting to 0.5:
|
||||
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]? [a3,c3,e4]?0.9 [b3,d3,f#4]>*2")`} punchcard />
|
||||
|
||||
## Mini-notation review
|
||||
|
||||
To recap what we've learned so far, compare the following patterns:
|
||||
@@ -193,7 +179,6 @@ To recap what we've learned so far, compare the following patterns:
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4] _ [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]?0.9 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||
|
||||
## Euclidian rhythms
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
### extend
|
||||
### repeat
|
||||
|
||||
<JsDoc client:idle name="extend" h={0} />
|
||||
<JsDoc client:idle name="repeat" h={0} />
|
||||
|
||||
### take
|
||||
|
||||
@@ -116,6 +116,10 @@ Earlier versions of many of these functions had `s_` prefixes, and the `pace` fu
|
||||
|
||||
<JsDoc client:idle name="polymeter" h={0} />
|
||||
|
||||
### polymeterSteps
|
||||
|
||||
<JsDoc client:idle name="polymeterSteps" h={0} />
|
||||
|
||||
### shrink
|
||||
|
||||
<JsDoc client:idle name="shrink" h={0} />
|
||||
|
||||
@@ -9,12 +9,10 @@ import UdelsEditor from '@components/Udels/UdelsEditor';
|
||||
import ReplEditor from './components/ReplEditor';
|
||||
import EmbeddedReplEditor from './components/EmbeddedReplEditor';
|
||||
import { useReplContext } from './useReplContext';
|
||||
import { useSettings } from '@src/settings.mjs';
|
||||
|
||||
export function Repl({ embedded = false }) {
|
||||
const isEmbedded = embedded || isIframe();
|
||||
const Editor = isUdels() ? UdelsEditor : isEmbedded ? EmbeddedReplEditor : ReplEditor;
|
||||
const context = useReplContext();
|
||||
const { fontFamily } = useSettings();
|
||||
return <Editor context={context} style={{ fontFamily }} />;
|
||||
return <Editor context={context} />;
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import { Header } from './Header';
|
||||
// }
|
||||
|
||||
export default function EmbeddedReplEditor(Props) {
|
||||
const { context, ...editorProps } = Props;
|
||||
const { context } = Props;
|
||||
const { pending, started, handleTogglePlay, containerRef, editorRef, error, init } = context;
|
||||
return (
|
||||
<div className="h-full flex flex-col relative" {...editorProps}>
|
||||
<div className="h-full flex flex-col relative">
|
||||
<Loader active={pending} />
|
||||
<Header context={context} embedded={true} />
|
||||
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
||||
|
||||
@@ -11,7 +11,7 @@ export function Header({ context, embedded = false }) {
|
||||
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } =
|
||||
context;
|
||||
const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location);
|
||||
const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings();
|
||||
const { isZen, isButtonRowHidden, isCSSAnimationDisabled } = useSettings();
|
||||
|
||||
return (
|
||||
<header
|
||||
@@ -22,7 +22,6 @@ 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',
|
||||
isEmbedded ? 'flex' : 'md:flex',
|
||||
)}
|
||||
style={{ fontFamily }}
|
||||
>
|
||||
<div className="px-4 flex space-x-2 md:pt-0 select-none">
|
||||
<h1
|
||||
@@ -47,7 +46,7 @@ export function Header({ context, embedded = false }) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="block text-foreground rotate-90">꩜</span>
|
||||
<span className="block rotate-90">꩜</span>
|
||||
</div>
|
||||
{!isZen && (
|
||||
<div className="space-x-2">
|
||||
|
||||
@@ -10,13 +10,13 @@ import { useSettings } from '@src/settings.mjs';
|
||||
// }
|
||||
|
||||
export default function ReplEditor(Props) {
|
||||
const { context, ...editorProps } = Props;
|
||||
const { context } = Props;
|
||||
const { containerRef, editorRef, error, init, pending } = context;
|
||||
const settings = useSettings();
|
||||
const { panelPosition, isZen } = settings;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col relative" {...editorProps}>
|
||||
<div className="h-full flex flex-col relative">
|
||||
<Loader active={pending} />
|
||||
<Header context={context} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Incrementor } from '../incrementor/Incrementor';
|
||||
|
||||
export function Pagination({ currPage, onPageChange, className, ...incrementorProps }) {
|
||||
return <Incrementor min={1} value={currPage} onChange={onPageChange} className={className} {...incrementorProps} />;
|
||||
}
|
||||
@@ -1,32 +1,53 @@
|
||||
import { logger } from '@strudel/core';
|
||||
import useEvent from '@src/useEvent.mjs';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useSettings } from '../../../settings.mjs';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $strudel_log_history } from '../useLogger';
|
||||
|
||||
export function ConsoleTab() {
|
||||
const log = useStore($strudel_log_history);
|
||||
const { fontFamily } = useSettings();
|
||||
const [log, setLog] = useState([]);
|
||||
const { fontFamily, fontSize } = 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 (
|
||||
<div id="console-tab" className="break-all w-full first-line:text-sm p-2 h-full" style={{ fontFamily }}>
|
||||
<div className="bg-background h-full w-full overflow-auto space-y-1 p-2 rounded-md">
|
||||
{log.map((l, i) => {
|
||||
const message = linkify(l.message);
|
||||
const color = l.data?.hap?.value?.color;
|
||||
return (
|
||||
<div
|
||||
key={l.id}
|
||||
className={cx(
|
||||
l.type === 'error' ? 'text-background bg-foreground' : 'text-foreground',
|
||||
l.type === 'highlight' && 'underline',
|
||||
)}
|
||||
style={color ? { color } : {}}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />
|
||||
{l.count ? ` (${l.count})` : ''}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
id="console-tab"
|
||||
className="break-all px-4 dark:text-white text-stone-900 text-sm py-2 space-y-1"
|
||||
style={{ fontFamily, fontSize }}
|
||||
>
|
||||
{log.map((l, i) => {
|
||||
const message = linkify(l.message);
|
||||
const color = l.data?.hap?.value?.color;
|
||||
return (
|
||||
<div
|
||||
key={l.id}
|
||||
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}
|
||||
style={color ? { color } : {}}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />
|
||||
{l.count ? ` (${l.count})` : ''}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -51,3 +72,7 @@ function linkify(inputText) {
|
||||
|
||||
return replacedText;
|
||||
}
|
||||
|
||||
function useLogger(onTrigger) {
|
||||
useEvent(logger.key, onTrigger);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { FilesTab } from './FilesTab';
|
||||
import { Reference } from './Reference';
|
||||
import { SettingsTab } from './SettingsTab';
|
||||
import { SoundsTab } from './SoundsTab';
|
||||
import { useLogger } from '../useLogger';
|
||||
import { WelcomeTab } from './WelcomeTab';
|
||||
import { PatternsTab } from './PatternsTab';
|
||||
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
|
||||
@@ -116,7 +115,6 @@ function PanelNav({ children, className, settings, ...props }) {
|
||||
}
|
||||
|
||||
function PanelContent({ context, tab }) {
|
||||
useLogger();
|
||||
switch (tab) {
|
||||
case tabNames.patterns:
|
||||
return <PatternsTab context={context} />;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
exportPatterns,
|
||||
importPatterns,
|
||||
loadAndSetFeaturedPatterns,
|
||||
loadAndSetPublicPatterns,
|
||||
patternFilterName,
|
||||
useActivePattern,
|
||||
useViewingPatternData,
|
||||
@@ -14,10 +12,10 @@ import { useExamplePatterns } from '../../useExamplePatterns.jsx';
|
||||
import { parseJSON, isUdels } from '../../util.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { Pagination } from '../pagination/Pagination.jsx';
|
||||
import { useState } from 'react';
|
||||
import { useDebounce } from '../usedebounce.jsx';
|
||||
import cx from '@src/cx.mjs';
|
||||
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
|
||||
@@ -27,19 +25,21 @@ export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||
const date = new Date(pattern.created_at);
|
||||
if (!isNaN(date)) {
|
||||
title = date.toLocaleDateString();
|
||||
} else {
|
||||
title = 'unnamed';
|
||||
}
|
||||
}
|
||||
|
||||
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 = pattern.hash;
|
||||
}
|
||||
if (title == null) {
|
||||
title = 'unnamed';
|
||||
}
|
||||
return <>{`${pattern.id}: ${title} by ${Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'}`}</>;
|
||||
}
|
||||
|
||||
function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
|
||||
return (
|
||||
<a
|
||||
className={cx(
|
||||
className={classNames(
|
||||
'mr-4 hover:opacity-50 cursor-pointer block',
|
||||
showOutline && 'outline outline-1',
|
||||
showHiglight && 'bg-selection',
|
||||
@@ -56,7 +56,7 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
|
||||
const viewingPatternData = parseJSON(viewingPatternStore);
|
||||
const viewingPatternID = viewingPatternData.id;
|
||||
return (
|
||||
<div className="">
|
||||
<div className="font-mono text-sm">
|
||||
{Object.values(patterns)
|
||||
.reverse()
|
||||
.map((pattern) => {
|
||||
@@ -84,72 +84,82 @@ function ActionButton({ children, onClick, label, labelIsHidden }) {
|
||||
);
|
||||
}
|
||||
|
||||
const updateCodeWindow = (context, patternData, reset = false) => {
|
||||
context.handleUpdate(patternData, reset);
|
||||
};
|
||||
|
||||
const autoResetPatternOnChange = !isUdels();
|
||||
|
||||
function UserPatterns({ context }) {
|
||||
export function PatternsTab({ context }) {
|
||||
const activePattern = useActivePattern();
|
||||
const viewingPatternStore = useViewingPatternData();
|
||||
const viewingPatternData = parseJSON(viewingPatternStore);
|
||||
|
||||
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 autoResetPatternOnChange = !isUdels();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
|
||||
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
|
||||
<ActionButton
|
||||
label="new"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.createAndAddToDB();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="duplicate"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.duplicate(viewingPatternData);
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="delete"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.delete(viewingPatternID);
|
||||
updateCodeWindow(context, { ...data, collection: userPattern.collection });
|
||||
}}
|
||||
/>
|
||||
<label className="hover:opacity-50 cursor-pointer">
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
multiple
|
||||
accept="text/plain,application/json"
|
||||
onChange={(e) => importPatterns(e.target.files)}
|
||||
/>
|
||||
import
|
||||
</label>
|
||||
<ActionButton label="export" onClick={exportPatterns} />
|
||||
<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">
|
||||
<ButtonGroup
|
||||
value={patternFilter}
|
||||
onChange={(value) => settingsMap.setKey('patternFilter', value)}
|
||||
items={patternFilterName}
|
||||
></ButtonGroup>
|
||||
{patternFilter === patternFilterName.user && (
|
||||
<div>
|
||||
<div className="pr-4 space-x-4 border-b border-foreground flex max-w-full overflow-x-auto">
|
||||
<ActionButton
|
||||
label="new"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.createAndAddToDB();
|
||||
updateCodeWindow(data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="duplicate"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.duplicate(viewingPatternData);
|
||||
updateCodeWindow(data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="delete"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.delete(viewingPatternID);
|
||||
updateCodeWindow({ ...data, collection: userPattern.collection });
|
||||
}}
|
||||
/>
|
||||
<label className="hover:opacity-50 cursor-pointer">
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
multiple
|
||||
accept="text/plain,application/json"
|
||||
onChange={(e) => importPatterns(e.target.files)}
|
||||
/>
|
||||
import
|
||||
</label>
|
||||
<ActionButton label="export" onClick={exportPatterns} />
|
||||
|
||||
<ActionButton
|
||||
label="delete-all"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.clearAll();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ActionButton
|
||||
label="delete-all"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.clearAll();
|
||||
updateCodeWindow(data);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-auto h-full bg-background p-2 rounded-md">
|
||||
<section className="flex overflow-y-auto max-h-full flex-grow flex-col">
|
||||
{patternFilter === patternFilterName.user && (
|
||||
<PatternButtons
|
||||
onClick={(id) =>
|
||||
updateCodeWindow(
|
||||
context,
|
||||
{ ...userPatterns[id], collection: userPattern.collection },
|
||||
autoResetPatternOnChange,
|
||||
)
|
||||
updateCodeWindow({ ...userPatterns[id], collection: userPattern.collection }, autoResetPatternOnChange)
|
||||
}
|
||||
patterns={userPatterns}
|
||||
started={context.started}
|
||||
@@ -157,111 +167,24 @@ function UserPatterns({ context }) {
|
||||
viewingPatternID={viewingPatternID}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PatternPageWithPagination({ patterns, patternOnClick, context, paginationOnChange, initialPage }) {
|
||||
const [page, setPage] = useState(initialPage);
|
||||
const debouncedPageChange = useDebounce(() => {
|
||||
paginationOnChange(page);
|
||||
});
|
||||
|
||||
const onPageChange = (pageNum) => {
|
||||
setPage(pageNum);
|
||||
debouncedPageChange();
|
||||
};
|
||||
|
||||
const activePattern = useActivePattern();
|
||||
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} />
|
||||
)}
|
||||
{patternFilter !== patternFilterName.user &&
|
||||
Array.from(collections.keys()).map((collection) => {
|
||||
const patterns = collections.get(collection);
|
||||
return (
|
||||
<section key={collection} className="py-2">
|
||||
<h2 className="text-xl mb-2">{collection}</h2>
|
||||
<div className="font-mono text-sm">
|
||||
<PatternButtons
|
||||
onClick={(id) => updateCodeWindow({ ...patterns[id], collection }, autoResetPatternOnChange)}
|
||||
started={context.started}
|
||||
patterns={patterns}
|
||||
activePattern={activePattern}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import jsdocJson from '../../../../../doc.json';
|
||||
import { Textbox } from '../textbox/Textbox';
|
||||
const availableFunctions = jsdocJson.docs
|
||||
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
|
||||
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||
@@ -26,16 +25,21 @@ export function Reference() {
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full p-2 overflow-hidden">
|
||||
<div className="flex h-full w-full p-2 text-foreground overflow-hidden">
|
||||
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
|
||||
<div class="w-full flex">
|
||||
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
|
||||
<input
|
||||
className="w-full p-1 bg-background rounded-md border-none"
|
||||
placeholder="Search"
|
||||
value={search}
|
||||
onInput={(event) => setSearch(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<a
|
||||
key={i}
|
||||
className="cursor-pointer text-foreground flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
|
||||
className="cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
|
||||
onClick={() => {
|
||||
const el = document.getElementById(`doc-${i}`);
|
||||
const container = document.getElementById('reference-container');
|
||||
@@ -75,9 +79,7 @@ export function Reference() {
|
||||
))}
|
||||
</ul>
|
||||
{entry.examples?.map((example, j) => (
|
||||
<pre className="bg-background" key={j}>
|
||||
{example}
|
||||
</pre>
|
||||
<pre key={j}>{example}</pre>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
|
||||
@@ -66,7 +66,6 @@ const themeOptions = Object.fromEntries(Object.keys(themes).map((k) => [k, k]));
|
||||
const fontFamilyOptions = {
|
||||
monospace: 'monospace',
|
||||
Courier: 'Courier',
|
||||
CutiePi: 'CutiePi',
|
||||
JetBrains: 'JetBrains',
|
||||
Hack: 'Hack',
|
||||
FiraCode: 'FiraCode',
|
||||
@@ -109,7 +108,7 @@ export function SettingsTab({ started }) {
|
||||
const shouldAlwaysSync = isUdels();
|
||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||
return (
|
||||
<div className="text-foreground p-4 space-y-4 w-full" style={{ fontFamily }}>
|
||||
<div className="text-foreground p-4 space-y-4 w-full">
|
||||
{canChangeAudioDevice && (
|
||||
<FormItem label="Audio Output Device">
|
||||
<AudioDeviceSelector
|
||||
@@ -142,7 +141,7 @@ export function SettingsTab({ started }) {
|
||||
<FormItem label="Theme">
|
||||
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
|
||||
</FormItem>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 font-sans">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormItem label="Font Family">
|
||||
<SelectInput
|
||||
options={fontFamilyOptions}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useMemo, useRef, useState } from 'react';
|
||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
||||
import { Textbox } from '../textbox/Textbox.jsx';
|
||||
|
||||
const getSamples = (samples) =>
|
||||
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
|
||||
@@ -53,8 +52,13 @@ export function SoundsTab() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
|
||||
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
|
||||
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
|
||||
<input
|
||||
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">
|
||||
<ButtonGroup
|
||||
@@ -70,7 +74,7 @@ export function SoundsTab() {
|
||||
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
|
||||
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal pb-2">
|
||||
{soundEntries.map(([name, { data, onTrigger }]) => {
|
||||
return (
|
||||
<span
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useSettings } from '@src/settings.mjs';
|
||||
import cx from '@src/cx.mjs';
|
||||
|
||||
const { BASE_URL } = import.meta.env;
|
||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||
|
||||
export function WelcomeTab({ context }) {
|
||||
const { fontFamily } = useSettings();
|
||||
return (
|
||||
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 " style={{ fontFamily }}>
|
||||
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
|
||||
<h3>꩜ welcome</h3>
|
||||
<p>
|
||||
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music
|
||||
@@ -44,8 +43,7 @@ export function WelcomeTab({ context }) {
|
||||
<a href="https://github.com/tidalcycles/strudel" target="_blank">
|
||||
github
|
||||
</a>
|
||||
. 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{' '}
|
||||
. Please consider to{' '}
|
||||
<a href="https://opencollective.com/tidalcycles" target="_blank">
|
||||
support this project
|
||||
</a>{' '}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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, patternFilterName } from '../user_pattern_utils.mjs';
|
||||
import { $featuredPatterns, $publicPatterns, collectionName } from '../user_pattern_utils.mjs';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useMemo } from 'react';
|
||||
import * as tunes from '../repl/tunes.mjs';
|
||||
@@ -12,9 +12,9 @@ export const useExamplePatterns = () => {
|
||||
const publicPatterns = useStore($publicPatterns);
|
||||
const collections = useMemo(() => {
|
||||
const pats = new Map();
|
||||
pats.set(patternFilterName.featured, featuredPatterns);
|
||||
pats.set(patternFilterName.public, publicPatterns);
|
||||
// pats.set(patternFilterName.stock, stockPatterns);
|
||||
pats.set(collectionName.featured, featuredPatterns);
|
||||
pats.set(collectionName.public, publicPatterns);
|
||||
// pats.set(collectionName.stock, stockPatterns);
|
||||
return pats;
|
||||
}, [featuredPatterns, publicPatterns]);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@font-face {
|
||||
font-family: 'PressStart';
|
||||
src: url('/fonts/PressStart2P/PressStart2P-Regular.ttf');
|
||||
size-adjust: 65%;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'BigBlueTerminal';
|
||||
@@ -15,11 +14,6 @@
|
||||
font-family: 'galactico';
|
||||
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-family: 'JetBrains';
|
||||
src: url('/fonts/JetBrains/JetBrainsMono.woff2');
|
||||
@@ -27,7 +21,6 @@
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('/fonts/Monocraft/Monocraft.ttf');
|
||||
size-adjust: 90%;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Hack';
|
||||
@@ -48,12 +41,10 @@
|
||||
@font-face {
|
||||
font-family: 'teletext';
|
||||
src: url('/fonts/teletext/EuropeanTeletext.ttf');
|
||||
size-adjust: 90%;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'mode7';
|
||||
src: url('/fonts/mode7/MODE7GX3.TTF');
|
||||
size-adjust: 82%;
|
||||
}
|
||||
|
||||
.prose > h1:not(:first-child) {
|
||||
|
||||
@@ -8,12 +8,16 @@ import { confirmDialog, parseJSON, supabase } from './repl/util.mjs';
|
||||
export let $publicPatterns = atom([]);
|
||||
export let $featuredPatterns = atom([]);
|
||||
|
||||
const patternQueryLimit = 20;
|
||||
export const patternFilterName = {
|
||||
public: 'latest',
|
||||
featured: 'featured',
|
||||
export const collectionName = {
|
||||
user: 'user',
|
||||
public: 'Last Creations',
|
||||
stock: 'Stock Examples',
|
||||
featured: 'Featured',
|
||||
};
|
||||
|
||||
export const patternFilterName = {
|
||||
community: 'community',
|
||||
user: 'user',
|
||||
// stock: 'stock examples',
|
||||
};
|
||||
|
||||
const sessionAtom = (name, initial = undefined) => {
|
||||
@@ -32,7 +36,7 @@ const sessionAtom = (name, initial = undefined) => {
|
||||
export let $viewingPatternData = sessionAtom('viewingPatternData', {
|
||||
id: '',
|
||||
code: '',
|
||||
collection: patternFilterName.user,
|
||||
collection: collectionName.user,
|
||||
created_at: Date.now(),
|
||||
});
|
||||
|
||||
@@ -47,50 +51,25 @@ export const setViewingPatternData = (data) => {
|
||||
$viewingPatternData.set(JSON.stringify(data));
|
||||
};
|
||||
|
||||
function parsePageNum(page) {
|
||||
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 loadPublicPatterns() {
|
||||
return supabase.from('code_v1').select().eq('public', true).limit(20).order('id', { ascending: false });
|
||||
}
|
||||
|
||||
export function loadFeaturedPatterns(page = 0) {
|
||||
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 function loadFeaturedPatterns() {
|
||||
return supabase.from('code_v1').select().eq('featured', true).limit(20).order('id', { ascending: false });
|
||||
}
|
||||
|
||||
export async function loadDBPatterns() {
|
||||
try {
|
||||
await loadAndSetPublicPatterns();
|
||||
await loadAndSetFeaturedPatterns();
|
||||
const { data: publicPatterns } = await loadPublicPatterns();
|
||||
const { data: featuredPatterns } = await loadFeaturedPatterns();
|
||||
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) {
|
||||
console.error('error loading patterns', err);
|
||||
}
|
||||
@@ -111,9 +90,9 @@ export function useActivePattern() {
|
||||
|
||||
export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
|
||||
|
||||
export const defaultCode = '';
|
||||
const defaultCode = '';
|
||||
export const userPattern = {
|
||||
collection: patternFilterName.user,
|
||||
collection: collectionName.user,
|
||||
getAll() {
|
||||
const patterns = parseJSON(settingsMap.get().userPatterns);
|
||||
return patterns ?? {};
|
||||
|
||||
@@ -46,29 +46,6 @@ module.exports = {
|
||||
'code::after': {
|
||||
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)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user