Merge pull request #324 from tidalcycles/docs

mini repl improvements
This commit is contained in:
Felix Roos 2022-12-26 23:29:47 +01:00 committed by GitHub
commit ea59fafb52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1299 additions and 265 deletions

175
packages/core/color.mjs Normal file
View File

@ -0,0 +1,175 @@
export const colorMap = {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgreen: '#006400',
darkgrey: '#a9a9a9',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
gold: '#ffd700',
goldenrod: '#daa520',
gray: '#808080',
green: '#008000',
greenyellow: '#adff2f',
grey: '#808080',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavender: '#e6e6fa',
lavenderblush: '#fff0f5',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgreen: '#90ee90',
lightgrey: '#d3d3d3',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370db',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#db7093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
red: '#ff0000',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd32',
};
export function convertColorToNumber(color) {
// Convert color to lowercase for easier matching
color = color.toLowerCase();
// If the color is a hex code, convert it to a number
if (color[0] === '#') {
return convertHexToNumber(color);
}
// If the color is a named color, return the corresponding number
if (colorMap[color] !== undefined) {
return convertHexToNumber(colorMap[color]);
}
// If the color is not recognized, return null
return -1;
}
export function convertHexToNumber(hex) {
// Remove the leading '#' from the hex code
hex = hex.slice(1);
// Convert the hex code to a number
return parseInt(hex, 16);
}

View File

@ -49,6 +49,9 @@ export class Cyclist {
getPhase() {
return this.getTime() - this.origin - this.latency;
}
now() {
return this.getTime() - this.origin + this.clock.minLatency;
}
setStarted(v) {
this.started = v;
this.onToggle?.(v);

View File

@ -153,3 +153,132 @@ Pattern.prototype.pianoroll = function ({
);
return this;
};
// this function allows drawing a pianoroll without ties to Pattern.prototype
// it will probably replace the above in the future
export function pianoroll({
time,
haps,
cycles = 4,
playhead = 0.5,
flipTime = 0,
flipValues = 0,
hideNegative = false,
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
minMidi = 10,
maxMidi = 90,
autorange = 0,
timeframe: timeframeProp,
fold = 0,
vertical = 0,
ctx,
} = {}) {
const w = ctx.canvas.width;
const h = ctx.canvas.height;
let from = -cycles * playhead;
let to = cycles * (1 - playhead);
if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead');
from = 0;
to = timeframeProp;
}
if (!autorange && fold) {
console.warn('disabling autorange has no effect when fold is enabled');
}
const timeAxis = vertical ? h : w;
const valueAxis = vertical ? w : h;
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
let foldValues = [];
flipTime && timeRange.reverse();
flipValues && valueRange.reverse();
// onQuery
const { min, max, values } = haps.reduce(
({ min, max, values }, e) => {
const v = getValue(e);
return {
min: v < min ? v : min,
max: v > max ? v : max,
values: values.includes(v) ? values : [...values, v],
};
},
{ min: Infinity, max: -Infinity, values: [] },
);
if (autorange) {
minMidi = min;
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
foldValues = values.sort((a, b) => a - b);
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
/* const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
haps
// .filter(inFrame)
.forEach((event) => {
const isActive = event.whole.begin <= time && event.whole.end > time;
const color = event.value?.color || event.context?.color;
ctx.fillStyle = color || inactive;
ctx.strokeStyle = color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valuePx = scale(
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
...valueRange,
);
let margin = 0;
const offset = scale(time / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
ctx.strokeStyle = playheadColor;
ctx.beginPath();
if (vertical) {
ctx.moveTo(0, playheadPosition);
ctx.lineTo(valueAxis, playheadPosition);
} else {
ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
return this;
}

View File

@ -44,6 +44,6 @@ function createClock(
};
const getPhase = () => phase;
// setCallback
return { setDuration, start, stop, pause, duration, getPhase };
return { setDuration, start, stop, pause, duration, getPhase, minLatency };
}
export default createClock;

File diff suppressed because one or more lines are too long

View File

@ -1,15 +1,15 @@
import n, { useCallback as _, useRef as H, useEffect as L, useMemo as V, useState as w, useLayoutEffect as j } from "react";
import X from "@uiw/react-codemirror";
import { Decoration as E, EditorView as U } from "@codemirror/view";
import i, { useCallback as N, useRef as E, useEffect as F, useMemo as V, useState as _, useLayoutEffect as U } from "react";
import Y from "@uiw/react-codemirror";
import { Decoration as y, EditorView as W } from "@codemirror/view";
import { StateEffect as $, StateField as G } from "@codemirror/state";
import { javascript as Y } from "@codemirror/lang-javascript";
import { tags as r } from "@lezer/highlight";
import { createTheme as Z } from "@uiw/codemirror-themes";
import { useInView as ee } from "react-hook-inview";
import { webaudioOutput as te, getAudioContext as re } from "@strudel.cycles/webaudio";
import { repl as oe } from "@strudel.cycles/core";
import { transpiler as ne } from "@strudel.cycles/transpiler";
const ae = Z({
import { javascript as Z } from "@codemirror/lang-javascript";
import { tags as s } from "@lezer/highlight";
import { createTheme as ee } from "@uiw/codemirror-themes";
import { repl as te, pianoroll as re } from "@strudel.cycles/core";
import { webaudioOutput as oe, getAudioContext as ne } from "@strudel.cycles/webaudio";
import { useInView as ae } from "react-hook-inview";
import { transpiler as se } from "@strudel.cycles/transpiler";
const ce = ee({
theme: "dark",
settings: {
background: "#222",
@ -22,299 +22,357 @@ const ae = Z({
gutterForeground: "#8a919966"
},
styles: [
{ tag: r.keyword, color: "#c792ea" },
{ tag: r.operator, color: "#89ddff" },
{ tag: r.special(r.variableName), color: "#eeffff" },
{ tag: r.typeName, color: "#c3e88d" },
{ tag: r.atom, color: "#f78c6c" },
{ tag: r.number, color: "#c3e88d" },
{ tag: r.definition(r.variableName), color: "#82aaff" },
{ tag: r.string, color: "#c3e88d" },
{ tag: r.special(r.string), color: "#c3e88d" },
{ tag: r.comment, color: "#7d8799" },
{ tag: r.variableName, color: "#c792ea" },
{ tag: r.tagName, color: "#c3e88d" },
{ tag: r.bracket, color: "#525154" },
{ tag: r.meta, color: "#ffcb6b" },
{ tag: r.attributeName, color: "#c792ea" },
{ tag: r.propertyName, color: "#c792ea" },
{ tag: r.className, color: "#decb6b" },
{ tag: r.invalid, color: "#ffffff" }
{ tag: s.keyword, color: "#c792ea" },
{ tag: s.operator, color: "#89ddff" },
{ tag: s.special(s.variableName), color: "#eeffff" },
{ tag: s.typeName, color: "#c3e88d" },
{ tag: s.atom, color: "#f78c6c" },
{ tag: s.number, color: "#c3e88d" },
{ tag: s.definition(s.variableName), color: "#82aaff" },
{ tag: s.string, color: "#c3e88d" },
{ tag: s.special(s.string), color: "#c3e88d" },
{ tag: s.comment, color: "#7d8799" },
{ tag: s.variableName, color: "#c792ea" },
{ tag: s.tagName, color: "#c3e88d" },
{ tag: s.bracket, color: "#525154" },
{ tag: s.meta, color: "#ffcb6b" },
{ tag: s.attributeName, color: "#c792ea" },
{ tag: s.propertyName, color: "#c792ea" },
{ tag: s.className, color: "#decb6b" },
{ tag: s.invalid, color: "#ffffff" }
]
});
const B = $.define(), se = G.define({
const B = $.define(), ie = G.define({
create() {
return E.none;
return y.none;
},
update(e, t) {
update(e, r) {
try {
for (let o of t.effects)
if (o.is(B))
if (o.value) {
const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } });
e = E.set([a.range(0, t.newDoc.length)]);
for (let t of r.effects)
if (t.is(B))
if (t.value) {
const a = y.mark({ attributes: { style: "background-color: #FFCA2880" } });
e = y.set([a.range(0, r.newDoc.length)]);
} else
e = E.set([]);
e = y.set([]);
return e;
} catch (o) {
return console.warn("flash error", o), e;
} catch (t) {
return console.warn("flash error", t), e;
}
},
provide: (e) => U.decorations.from(e)
}), ce = (e) => {
provide: (e) => W.decorations.from(e)
}), le = (e) => {
e.dispatch({ effects: B.of(!0) }), setTimeout(() => {
e.dispatch({ effects: B.of(!1) });
}, 200);
}, z = $.define(), ie = G.define({
}, H = $.define(), ue = G.define({
create() {
return E.none;
return y.none;
},
update(e, t) {
update(e, r) {
try {
for (let o of t.effects)
if (o.is(z)) {
const a = o.value.map(
(s) => (s.context.locations || []).map(({ start: u, end: d }) => {
const f = s.context.color || "#FFCA28";
let c = t.newDoc.line(u.line).from + u.column, i = t.newDoc.line(d.line).from + d.column;
const m = t.newDoc.length;
return c > m || i > m ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${f};` } }).range(c, i);
for (let t of r.effects)
if (t.is(H)) {
const a = t.value.map(
(n) => (n.context.locations || []).map(({ start: c, end: l }) => {
const d = n.context.color || "#FFCA28";
let o = r.newDoc.line(c.line).from + c.column, u = r.newDoc.line(l.line).from + l.column;
const f = r.newDoc.length;
return o > f || u > f ? void 0 : y.mark({ attributes: { style: `outline: 1.5px solid ${d};` } }).range(o, u);
})
).flat().filter(Boolean) || [];
e = E.set(a, !0);
e = y.set(a, !0);
}
return e;
} catch {
return E.set([]);
return y.set([]);
}
},
provide: (e) => U.decorations.from(e)
}), le = [Y(), ae, ie, se];
function de({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: u }) {
const d = _(
(i) => {
t?.(i);
provide: (e) => W.decorations.from(e)
}), de = [Z(), ce, ue, ie];
function fe({ value: e, onChange: r, onViewChanged: t, onSelectionChange: a, options: n, editorDidMount: c }) {
const l = N(
(u) => {
r?.(u);
},
[r]
), d = N(
(u) => {
t?.(u);
},
[t]
), f = _(
(i) => {
o?.(i);
},
[o]
), c = _(
(i) => {
i.selectionSet && a && a?.(i.state.selection);
), o = N(
(u) => {
u.selectionSet && a && a?.(u.state.selection);
},
[a]
);
return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(X, {
return /* @__PURE__ */ i.createElement(i.Fragment, null, /* @__PURE__ */ i.createElement(Y, {
value: e,
onChange: d,
onCreateEditor: f,
onUpdate: c,
extensions: le
onChange: l,
onCreateEditor: d,
onUpdate: o,
extensions: de
}));
}
function K(...e) {
function j(...e) {
return e.filter(Boolean).join(" ");
}
function ue({ view: e, pattern: t, active: o, getTime: a }) {
const s = H([]), u = H();
L(() => {
function me({ view: e, pattern: r, active: t, getTime: a }) {
const n = E([]), c = E();
F(() => {
if (e)
if (t && o) {
let d = requestAnimationFrame(function f() {
if (r && t) {
let l = requestAnimationFrame(function d() {
try {
const c = a(), m = [Math.max(u.current || c, c - 1 / 10, 0), c + 1 / 60];
u.current = m[1], s.current = s.current.filter((g) => g.whole.end > c);
const h = t.queryArc(...m).filter((g) => g.hasOnset());
s.current = s.current.concat(h), e.dispatch({ effects: z.of(s.current) });
const o = a(), f = [Math.max(c.current || o, o - 1 / 10, 0), o + 1 / 60];
c.current = f[1], n.current = n.current.filter((v) => v.whole.end > o);
const m = r.queryArc(...f).filter((v) => v.hasOnset());
n.current = n.current.concat(m), e.dispatch({ effects: H.of(n.current) });
} catch {
e.dispatch({ effects: z.of([]) });
e.dispatch({ effects: H.of([]) });
}
d = requestAnimationFrame(f);
l = requestAnimationFrame(d);
});
return () => {
cancelAnimationFrame(d);
cancelAnimationFrame(l);
};
} else
s.current = [], e.dispatch({ effects: z.of([]) });
}, [t, o, e]);
n.current = [], e.dispatch({ effects: H.of([]) });
}, [r, t, e]);
}
const fe = "_container_3i85k_1", me = "_header_3i85k_5", ge = "_buttons_3i85k_9", pe = "_button_3i85k_9", he = "_buttonDisabled_3i85k_17", be = "_error_3i85k_21", ve = "_body_3i85k_25", v = {
container: fe,
header: me,
buttons: ge,
button: pe,
buttonDisabled: he,
error: be,
body: ve
};
function O({ type: e }) {
return /* @__PURE__ */ n.createElement("svg", {
function ge(e, r = !1) {
const t = E(), a = E(), n = (d) => {
if (a.current !== void 0) {
const o = d - a.current;
e(d, o);
}
a.current = d, t.current = requestAnimationFrame(n);
}, c = () => {
t.current = requestAnimationFrame(n);
}, l = () => {
t.current && cancelAnimationFrame(t.current), delete t.current;
};
return F(() => {
t.current && (l(), c());
}, [e]), F(() => (r && c(), l), []), {
start: c,
stop: l
};
}
function pe({ pattern: e, started: r, getTime: t, onDraw: a }) {
let n = E([]), c = E(null);
const { start: l, stop: d } = ge(
N(() => {
const o = t();
if (c.current === null) {
c.current = o;
return;
}
const u = e.queryArc(Math.max(c.current, o - 1 / 10), o), f = 4;
c.current = o, n.current = (n.current || []).filter((m) => m.whole.end > o - f).concat(u.filter((m) => m.hasOnset())), a(o, n.current);
}, [e])
);
F(() => {
r ? l() : (n.current = [], d());
}, [r]);
}
function he(e) {
return F(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((r) => window.postMessage(r, "*"), []);
}
function ve({
defaultOutput: e,
interval: r,
getTime: t,
evalOnMount: a = !1,
initialCode: n = "",
autolink: c = !1,
beforeEval: l,
afterEval: d,
onEvalError: o,
onToggle: u,
canvasId: f
}) {
const m = V(() => be(), []);
f = f || `canvas-${m}`;
const [v, k] = _(), [M, T] = _(), [b, A] = _(n), [C, S] = _(), [z, D] = _(), [x, L] = _(!1), h = b !== C, { scheduler: g, evaluate: R, start: J, stop: O, pause: Q } = V(
() => te({
interval: r,
defaultOutput: e,
onSchedulerError: k,
onEvalError: (p) => {
T(p), o?.(p);
},
getTime: t,
transpiler: se,
beforeEval: ({ code: p }) => {
A(p), l?.();
},
afterEval: ({ pattern: p, code: q }) => {
S(q), D(p), T(), k(), c && (window.location.hash = "#" + encodeURIComponent(btoa(q))), d?.();
},
onToggle: (p) => {
L(p), u?.(p);
}
}),
[e, r, t]
), X = he(({ data: { from: p, type: q } }) => {
q === "start" && p !== m && O();
}), P = N(
async (p = !0) => {
await R(b, p), X({ type: "start", from: m });
},
[R, b]
), K = E();
return F(() => {
!K.current && a && b && (K.current = !0, P());
}, [P, a, b]), F(() => () => {
g.stop();
}, [g]), {
id: m,
canvasId: f,
code: b,
setCode: A,
error: v || M,
schedulerError: v,
scheduler: g,
evalError: M,
evaluate: R,
activateCode: P,
activeCode: C,
isDirty: h,
pattern: z,
started: x,
start: J,
stop: O,
pause: Q,
togglePlay: async () => {
x ? g.pause() : await P();
}
};
}
function be() {
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
}
function I({ type: e }) {
return /* @__PURE__ */ i.createElement("svg", {
xmlns: "http://www.w3.org/2000/svg",
className: "sc-h-5 sc-w-5",
viewBox: "0 0 20 20",
fill: "currentColor"
}, {
refresh: /* @__PURE__ */ n.createElement("path", {
refresh: /* @__PURE__ */ i.createElement("path", {
fillRule: "evenodd",
d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",
clipRule: "evenodd"
}),
play: /* @__PURE__ */ n.createElement("path", {
play: /* @__PURE__ */ i.createElement("path", {
fillRule: "evenodd",
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",
clipRule: "evenodd"
}),
pause: /* @__PURE__ */ n.createElement("path", {
pause: /* @__PURE__ */ i.createElement("path", {
fillRule: "evenodd",
d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",
clipRule: "evenodd"
})
}[e]);
}
function Ee(e) {
return L(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), _((t) => window.postMessage(t, "*"), []);
}
function we({
defaultOutput: e,
interval: t,
getTime: o,
evalOnMount: a = !1,
initialCode: s = "",
autolink: u = !1,
beforeEval: d,
afterEval: f,
onEvalError: c,
onToggle: i
}) {
const m = V(() => ye(), []), [h, g] = w(), [C, N] = w(), [p, y] = w(s), [M, S] = w(), [k, D] = w(), [F, x] = w(!1), b = p !== M, { scheduler: A, evaluate: T, start: J, stop: q, pause: Q } = V(
() => oe({
interval: t,
defaultOutput: e,
onSchedulerError: g,
onEvalError: (l) => {
N(l), c?.(l);
},
getTime: o,
transpiler: ne,
beforeEval: ({ code: l }) => {
y(l), d?.();
},
afterEval: ({ pattern: l, code: P }) => {
S(P), D(l), N(), g(), u && (window.location.hash = "#" + encodeURIComponent(btoa(P))), f?.();
},
onToggle: (l) => {
x(l), i?.(l);
}
}),
[e, t, o]
), W = Ee(({ data: { from: l, type: P } }) => {
P === "start" && l !== m && q();
}), R = _(
async (l = !0) => {
await T(p, l), W({ type: "start", from: m });
},
[T, p]
), I = H();
return L(() => {
!I.current && a && p && (I.current = !0, R());
}, [R, a, p]), L(() => () => {
A.stop();
}, [A]), {
code: p,
setCode: y,
error: h || C,
schedulerError: h,
scheduler: A,
evalError: C,
evaluate: T,
activateCode: R,
activeCode: M,
isDirty: b,
pattern: k,
started: F,
start: J,
stop: q,
pause: Q,
togglePlay: async () => {
F ? A.pause() : await R();
}
};
}
function ye() {
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
}
const ke = () => re().currentTime;
function Se({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) {
const we = "_container_3i85k_1", ye = "_header_3i85k_5", Ee = "_buttons_3i85k_9", ke = "_button_3i85k_9", _e = "_buttonDisabled_3i85k_17", Fe = "_error_3i85k_21", Ce = "_body_3i85k_25", w = {
container: we,
header: ye,
buttons: Ee,
button: ke,
buttonDisabled: _e,
error: Fe,
body: Ce
}, Ne = () => ne().currentTime;
function Be({ tune: e, hideOutsideView: r = !1, enableKeyboard: t, withCanvas: a = !1, canvasHeight: n = 200 }) {
const {
code: s,
setCode: u,
code: c,
setCode: l,
evaluate: d,
activateCode: f,
error: c,
isDirty: i,
activateCode: o,
error: u,
isDirty: f,
activeCode: m,
pattern: h,
started: g,
scheduler: C,
togglePlay: N,
stop: p
} = we({
pattern: v,
started: k,
scheduler: M,
togglePlay: T,
stop: b,
canvasId: A
} = ve({
initialCode: e,
defaultOutput: te,
getTime: ke
}), [y, M] = w(), [S, k] = ee({
threshold: 0.01
}), D = H(), F = V(() => ((k || !t) && (D.current = !0), k || D.current), [k, t]);
return ue({
view: y,
pattern: h,
active: g && !m?.includes("strudel disable-highlighting"),
getTime: () => C.getPhase()
}), j(() => {
if (a) {
const x = async (b) => {
(b.ctrlKey || b.altKey) && (b.code === "Enter" ? (b.preventDefault(), ce(y), await f()) : b.code === "Period" && (p(), b.preventDefault()));
};
return window.addEventListener("keydown", x, !0), () => window.removeEventListener("keydown", x, !0);
defaultOutput: oe,
getTime: Ne
});
pe({
pattern: v,
started: a && k,
getTime: () => M.now(),
onDraw: (h, g) => {
const R = document.querySelector("#" + A).getContext("2d");
re({ ctx: R, time: h, haps: g, autorange: 1, fold: 1, playhead: 1 });
}
}, [a, h, s, d, p, y]), /* @__PURE__ */ n.createElement("div", {
className: v.container,
ref: S
}, /* @__PURE__ */ n.createElement("div", {
className: v.header
}, /* @__PURE__ */ n.createElement("div", {
className: v.buttons
}, /* @__PURE__ */ n.createElement("button", {
className: K(v.button, g ? "sc-animate-pulse" : ""),
onClick: () => N()
}, /* @__PURE__ */ n.createElement(O, {
type: g ? "pause" : "play"
})), /* @__PURE__ */ n.createElement("button", {
className: K(i ? v.button : v.buttonDisabled),
onClick: () => f()
}, /* @__PURE__ */ n.createElement(O, {
});
const [C, S] = _(), [z, D] = ae({
threshold: 0.01
}), x = E(), L = V(() => ((D || !r) && (x.current = !0), D || x.current), [D, r]);
return me({
view: C,
pattern: v,
active: k && !m?.includes("strudel disable-highlighting"),
getTime: () => M.getPhase()
}), U(() => {
if (t) {
const h = async (g) => {
(g.ctrlKey || g.altKey) && (g.code === "Enter" ? (g.preventDefault(), le(C), await o()) : g.code === "Period" && (b(), g.preventDefault()));
};
return window.addEventListener("keydown", h, !0), () => window.removeEventListener("keydown", h, !0);
}
}, [t, v, c, d, b, C]), /* @__PURE__ */ i.createElement("div", {
className: w.container,
ref: z
}, /* @__PURE__ */ i.createElement("div", {
className: w.header
}, /* @__PURE__ */ i.createElement("div", {
className: w.buttons
}, /* @__PURE__ */ i.createElement("button", {
className: j(w.button, k ? "sc-animate-pulse" : ""),
onClick: () => T()
}, /* @__PURE__ */ i.createElement(I, {
type: k ? "pause" : "play"
})), /* @__PURE__ */ i.createElement("button", {
className: j(f ? w.button : w.buttonDisabled),
onClick: () => o()
}, /* @__PURE__ */ i.createElement(I, {
type: "refresh"
}))), c && /* @__PURE__ */ n.createElement("div", {
className: v.error
}, c.message)), /* @__PURE__ */ n.createElement("div", {
className: v.body
}, F && /* @__PURE__ */ n.createElement(de, {
value: s,
onChange: u,
onViewChanged: M
})));
}))), u && /* @__PURE__ */ i.createElement("div", {
className: w.error
}, u.message)), /* @__PURE__ */ i.createElement("div", {
className: w.body
}, L && /* @__PURE__ */ i.createElement(fe, {
value: c,
onChange: l,
onViewChanged: S
})), a && /* @__PURE__ */ i.createElement("canvas", {
id: A,
className: "w-full pointer-events-none",
height: n,
ref: (h) => {
h && h.width !== h.clientWidth && (h.width = h.clientWidth);
}
}));
}
const Te = (e) => j(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
const Oe = (e) => U(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
export {
de as CodeMirror,
Se as MiniRepl,
K as cx,
ce as flash,
ue as useHighlighting,
Te as useKeydown,
Ee as usePostMessage,
we as useStrudel
fe as CodeMirror,
Be as MiniRepl,
j as cx,
le as flash,
me as useHighlighting,
Oe as useKeydown,
he as usePostMessage,
ve as useStrudel
};

View File

@ -1,18 +1,20 @@
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
import { pianoroll } from '@strudel.cycles/core';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useInView } from 'react-hook-inview';
import 'tailwindcss/tailwind.css';
import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
import 'tailwindcss/tailwind.css';
import './style.css';
import styles from './MiniRepl.module.css';
import { Icon } from './Icon';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import usePatternFrame from '../hooks/usePatternFrame.mjs';
import useStrudel from '../hooks/useStrudel.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
import { Icon } from './Icon';
import styles from './MiniRepl.module.css';
import './style.css';
const getTime = () => getAudioContext().currentTime;
export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }) {
export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCanvas = false, canvasHeight = 200 }) {
const {
code,
setCode,
@ -26,11 +28,23 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
scheduler,
togglePlay,
stop,
canvasId,
} = useStrudel({
initialCode: tune,
defaultOutput: webaudioOutput,
getTime,
});
usePatternFrame({
pattern,
started: withCanvas && started,
getTime: () => scheduler.now(),
onDraw: (time, haps) => {
const ctx = document.querySelector('#' + canvasId).getContext('2d');
pianoroll({ ctx, time, haps, autorange: 1, fold: 1, playhead: 1 });
},
});
/* useEffect(() => {
init && activateCode();
}, [init, activateCode]); */
@ -88,6 +102,18 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
<div className={styles.body}>
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</div>
{withCanvas && (
<canvas
id={canvasId}
className="w-full pointer-events-none"
height={canvasHeight}
ref={(el) => {
if (el && el.width !== el.clientWidth) {
el.width = el.clientWidth;
}
}}
></canvas>
)}
</div>
);
}

View File

@ -0,0 +1,43 @@
import { useEffect, useRef } from 'react';
function useFrame(callback, autostart = false) {
const requestRef = useRef();
const previousTimeRef = useRef();
const animate = (time) => {
if (previousTimeRef.current !== undefined) {
const deltaTime = time - previousTimeRef.current;
callback(time, deltaTime);
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
};
const start = () => {
requestRef.current = requestAnimationFrame(animate);
};
const stop = () => {
requestRef.current && cancelAnimationFrame(requestRef.current);
delete requestRef.current;
};
useEffect(() => {
if (requestRef.current) {
stop();
start();
}
}, [callback]);
useEffect(() => {
if (autostart) {
start();
}
return stop;
}, []); // Make sure the effect only runs once
return {
start,
stop,
};
}
export default useFrame;

View File

@ -0,0 +1,34 @@
import { useCallback, useEffect, useRef } from 'react';
import 'tailwindcss/tailwind.css';
import useFrame from '../hooks/useFrame.mjs';
function usePatternFrame({ pattern, started, getTime, onDraw }) {
let visibleHaps = useRef([]);
let lastFrame = useRef(null);
const { start: startFrame, stop: stopFrame } = useFrame(
useCallback(() => {
const phase = getTime();
if (lastFrame.current === null) {
lastFrame.current = phase;
return;
}
const haps = pattern.queryArc(Math.max(lastFrame.current, phase - 1 / 10), phase);
const cycles = 4;
lastFrame.current = phase;
visibleHaps.current = (visibleHaps.current || [])
.filter((h) => h.whole.end > phase - cycles) // in frame
.concat(haps.filter((h) => h.hasOnset()));
onDraw(phase, visibleHaps.current);
}, [pattern]),
);
useEffect(() => {
if (started) {
startFrame();
} else {
visibleHaps.current = [];
stopFrame();
}
}, [started]);
}
export default usePatternFrame;

View File

@ -14,8 +14,10 @@ function useStrudel({
afterEval,
onEvalError,
onToggle,
canvasId,
}) {
const id = useMemo(() => s4(), []);
canvasId = canvasId || `canvas-${id}`;
// scheduler
const [schedulerError, setSchedulerError] = useState();
const [evalError, setEvalError] = useState();
@ -97,6 +99,8 @@ function useStrudel({
};
const error = schedulerError || evalError;
return {
id,
canvasId,
code,
setCode,
error,

View File

@ -17,7 +17,7 @@ const sidebar = SIDEBAR[langCode];
{
Object.entries(sidebar).map(([header, children]) => (
<li>
<div class="nav-group">
<div class="nav-group pb-4">
<h2>{header}</h2>
<ul>
{children.map((child) => {

View File

@ -57,5 +57,10 @@ export const SIDEBAR: Sidebar = {
{ text: 'Tonal', link: 'learn/tonal' },
{ text: 'MIDI & OSC', link: 'learn/input-output' },
],
'Technical Manual': [
{ text: 'Patterns', link: 'technical-manual/patterns' },
{ text: 'REPL', link: 'technical-manual/repl' },
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
],
},
};

View File

@ -0,0 +1,20 @@
import { colorMap } from '@strudel.cycles/core/color.mjs';
import React from 'react';
const Colors = () => {
return (
<div>
{Object.entries(colorMap).map(([name, hex]) => (
<div key={name} className="py-1">
<div className="grid gap-2 grid-cols-3">
<div>{name}</div>
<div style={{ backgroundColor: hex }}></div>
<div style={{ backgroundColor: name, color: hex }}>{name}</div>
</div>
</div>
))}
</div>
);
};
export default Colors;

View File

@ -1,12 +1,9 @@
import { evalScope, controls } from '@strudel.cycles/core';
import { samples } from '@strudel.cycles/webaudio';
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { useEffect, useState } from 'react';
import { prebake } from '../repl/prebake';
if (typeof window !== 'undefined') {
fetch('https://strudel.tidalcycles.org/EmuSP12.json')
.then((res) => res.json())
.then((json) => samples(json, 'https://strudel.tidalcycles.org/EmuSP12/'));
evalScope(
controls,
import('@strudel.cycles/core'),
@ -20,7 +17,12 @@ if (typeof window !== 'undefined') {
);
}
export function MiniRepl({ tune }) {
if (typeof window !== 'undefined') {
initAudioOnFirstClick();
prebake();
}
export function MiniRepl({ tune, withCanvas }) {
const [Repl, setRepl] = useState();
useEffect(() => {
// we have to load this package on the client
@ -29,5 +31,5 @@ export function MiniRepl({ tune }) {
setRepl(() => res.MiniRepl);
});
}, []);
return Repl ? <Repl tune={tune} hideOutsideView={true} /> : <pre>{tune}</pre>;
return Repl ? <Repl tune={tune} hideOutsideView={true} withCanvas={withCanvas} /> : <pre>{tune}</pre>;
}

View File

@ -0,0 +1,12 @@
---
title: Colors
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
import Colors from '../../docs/Colors.jsx';
# Colors
<Colors />

View File

@ -47,12 +47,12 @@ You can also use regular double quotes (`"`) for single line mini-notation, as w
We can play more notes by separating them with spaces:
<MiniRepl client:idle tune={`note("e5 b4 d5 c5")`} />
<MiniRepl client:idle tune={`note("e5 b4 d5 c5")`} withCanvas />
Here, those four notes are squashed into one cycle, so each note is a quarter second long.
Try adding or removing notes and notice how the tempo changes!
<MiniRepl client:idle tune={`note("e5 b4 d5 c5 e5 b4 d5 c5")`} />
<MiniRepl client:idle tune={`note("e5 b4 d5 c5 e5 b4 d5 c5")`} withCanvas />
Note that the overall duration of time does not change, and instead each note length descreases.
This is a key idea, as it illustrates the 'Cycle' in TidalCycles!

View File

@ -0,0 +1,69 @@
---
title: Build Arpeggios
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
Note: This has been (partly) translated from https://tidalcycles.org/docs/patternlib/howtos/buildarpeggios
# Build Arpeggios
This page will teach you how to get started writing arpeggios using different techniques. It is a good way to learn Strudel in a more intuitive way.
## Arpeggios from notes
Start with a simple sequence of notes:
<MiniRepl tune={`note("c a f e").piano().slow(2)`} client:idle />
Now, let's play one per cycle:
<MiniRepl tune={`note("<c a f e>").piano().slow(2)`} client:idle />
On top of that, put a copy of the sequence, offset in time and pitch:
<MiniRepl
tune={`"<c a f e>".off(1/8, add(7))
.note().piano().slow(2)`}
client:idle
/>
Add some structure to the original sequence:
<MiniRepl
tune={`"<c*2 a(3,8) f(3,8,2) e*2>"
.off(1/8, add(7))
.note().piano().slow(2)`}
client:idle
/>
Reverse in one speaker:
<MiniRepl
tune={`"<c*2 a(3,8) f(3,8,2) e*2>"
.off(1/8, add(7))
.note().piano()
.jux(rev).slow(2)`}
client:idle
/>
Let's add another layer:
<MiniRepl
tune={`"<c*2 a(3,8) f(3,8,2) e*2>"
.off(1/8, add(7))
.off(1/8, add(12))
.note().piano()
.jux(rev).slow(2)`}
client:idle
/>
- added slow(2) to approximate tidals cps
- n was replaced with note, because using n does not work as note for samples
- legato 2 was removed because it does not work in combination with rev (bug)
## Arpeggios from chords
TODO

View File

@ -0,0 +1,76 @@
---
title: Microrhythms
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
import { samples } from '@strudel.cycles/webaudio';
see https://strudel.tidalcycles.org?zMEo5kowGrFc
# Microrhythms
Inspired by this [Mini-Lecture on Microrhythm Notation](https://www.youtube.com/watch?v=or7B6vI3jOo), let's look at how we can express microrhythms with Strudel.
The timestamps of the first rhythm are `0 1/5 1/2 2/3 1`. We could naively express this with a stack:
<MiniRepl
client:idle
tune={`s("hh").struct(
stack(
"x", // 0
"~ x ~@3", // 1/5
"~ x", // 1/2
"~@2 x" // 2/3
))`}
/>
While this works, it has two problems:
- it is not very compact
- the durations are wrong, e.g. the first note takes up the whole cycle
In the video, the duration of a timestamp is calculated by subtracting it from the next timestamp:
- 1/5 - 0 = 1/5 = 6/30
- 1/2 - 1/5 = 3/10 = 9/30
- 2/3 - 1/2 = 1/6 = 5/30
- 1 - 2/3 = 1/3 = 10/30
Using those, we can now express the rhythm much shorter:
<MiniRepl client:idle tune={`s("hh").struct("x@6 x@9 x@5 x@10")`} />
The problems of the first notation are now fixed: it is much shorter and the durations are correct.
Still, this notation involved calculating the durations by hand, which could be automated:
<MiniRepl
client:idle
tune={`Pattern.prototype.micro = function (...timestamps) {
const durations = timestamps.map((x, i, a) => {
const next = i < a.length-1 ? a[i+1] : 1;
return next - a[i]
})
return this.struct(timeCat(...durations.map(d => [d, 1]))).late(timestamps[0])
}
s('hh').micro(0, 1/5, 1/2, 2/3)`}
/>
This notation is even shorter and it allows directly filling in the timestamps!
This is the second example of the video:
<MiniRepl
client:idle
tune={`Pattern.prototype.micro = function (...timestamps) {
const durations = timestamps.map((x, i, a) => {
const next = i < a.length-1 ? a[i+1] : 1;
return next - a[i]
})
return this.struct(timeCat(...durations.map(d => [d, 1]))).late(timestamps[0])
}
s('hh').micro(0, 1/6, 2/5, 2/3, 3/4)`}
/>
with bass: https://strudel.tidalcycles.org?sTglgJJCPIeY

View File

@ -0,0 +1,88 @@
---
title: Build Rhythms
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
import { samples } from '@strudel.cycles/webaudio';
Note:
- this has been (partly) translated from https://tidalcycles.org/docs/patternlib/howtos/buildrhythms
- this only sounds good with `samples('github:tidalcycles/Dirt-Samples/master')` in prebake
# Build Rhythms
This page will teach you how to get started writing rhythms using different techniques. It is a good way to learn Strudel in a more intuitive way.
## From a simple to a complex rhythm
Simple bass drum - snare:
<MiniRepl client:idle tune={`s("bd sd").slow(2)`} />
Let's pick a different snare sample:
<MiniRepl client:idle tune={`s("bd sd:3").slow(2)`} />
Now, we are going to change the rhythm:
<MiniRepl client:idle tune={`s("bd*2 [~ sd:3]").slow(2)`} />
And add some toms:
<MiniRepl client:idle tune={`s("bd*2 [[~ lt] sd:3] lt:1 [ht mt*2]").slow(2)`} />
Start to transform, shift a quarter cycle every other cycle:
<MiniRepl
client:idle
tune={`s("bd*2 [[~ lt] sd:3] lt:1 [ht mt*2]")
.every(2, early(.25)).slow(2)`}
/>
Pattern the shift amount:
<MiniRepl
client:idle
tune={`s("bd*2 [[~ lt] sd:3] lt:1 [ht mt*2]")
.every(2, early("<.25 .125 .5>")).slow(2)`}
/>
Add some patterned effects:
<MiniRepl
client:idle
tune={`s("bd*2 [[~ lt] sd:3] lt:1 [ht mt*2]")
.every(2, early("<.25 .125 .5>"))
.shape("<0 .5 .3>")
.room(saw.range(0,.2).slow(4))
.slow(2)`}
/>
More transformation:
<MiniRepl
client:idle
tune={`s("bd*2 [[~ lt] sd:3] lt:1 [ht mt*2]")
.every(2, early("<.25 .125 .5>"))
.shape("<0 .5 .3>")
.room(saw.range(0,.2).slow(4))
.jux(id, rev, x=>x.speed(2))
.slow(2)`}
/>
## Another rhythmic construction
Let's start with a sequence:
<MiniRepl client:idle tune={`n("0 0 [2 0] [2 3]").s("feel").speed(1.5).slow(2)`} />
We add a bit of flavour:
<MiniRepl client:idle tune={`n("0 <0 4> [2 0] [2 3]").s("feel").speed(1.5).slow(2)`} />
Swap the samples round every other cycle:
TODO: implement `rot`

View File

@ -0,0 +1,3 @@
This section introduces you to Strudel in a technical sense. If you just want to _use_ Strudel, have a look at the [Tutorial](/learn/getting-started).
TODO

View File

@ -0,0 +1,47 @@
---
title: Pattern Aligment
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
# Pattern Aligment & Combination
One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective of their structure. Its declarative approach means a live coder does not have to think about the details of _how_ this is done, only _what_ is to be done.
As a simple example, consider two number patterns `"0 [1 2] 3"`, and `"10 20"`. The first has three contiguous steps of equal lengths, with the second step broken down into two substeps, giving four events in total. There are a very large number of ways in which the structure of these two patterns could be combined, but the default method in both Strudel and Tidal is to line up the cycles of the two patterns, and then take events from the first pattern and match them with those in the second pattern. Therefore, the following two lines are equivalent:
```js
'0 [1 2] 3'.add('10 20');
('10 [11 22] 23');
```
Where the events only partially overlap, they are treated as fragments
of the event in the first pattern. This is a little difficult to
conceptualise, but lets start by comparing the two patterns in the
following example:
```js
'0 1 2'.add('10 20');
('10 [11 21] 20');
```
They are similar to the previous example in that the number `1` is split in two, with its two halves added to `10` and `20` respectively. However, the `11` 'remembers' that it is a fragment of that original `1` event, and so is treated as having a duration of a third of a cycle, despite only being active for a sixth of a cycle. Likewise, the `21` is also a fragment of that original `1` event, but a fragment of its second half. Because the start of its event is missing, it wouldn't actually trigger a sound (unless it underwent further pattern transformations/combinations).
In practice, the effect of this default, implicit method for combining two patterns is that the second pattern is added _in_ to the first one, and indeed this can be made explicit:
```js
'0 1 2'.add.in('10 20');
```
This makes way for other ways to align the pattern, and several are already defined, in particular:
- `in` - as explained above, aligns cycles, and applies values from the pattern on the right _in_ to the pattern on the left.
- `out` - as with `in`, but values are applied _out_ of the pattern on the left (i.e. _in_ to the one on the right).
- `mix` - structures from both patterns are combined, so that the new events are not fragments but are created at intersections of events from both sides.
- `squeeze` - cycles from the pattern on the right are squeezed into events on the left. So that e.g. `"0 1 2".add.squeeze("10 20")` is equivalent to `"[10 20] [11 21] [12 22]"`.
- `squeezeout` - as with `squeeze`, but cycles from the left are squeezed into events on the right. So, `"0 1 2".add.squeezeout("10 20")` is equivalent to `[10 11 12] [20 21 22]`.
- `trig` is similar to `squeezeout` in that cycles from the right are aligned with events on the left. However those cycles are not 'squeezed', rather they are truncated to fit the event. So `"0 1 2 3 4 5 6 7".add.trig("10 [20 30]")` would be equivalent to `10 11 12 13 20 21 30 31`. In effect, events on the right 'trigger' cycles on the left.
- `trigzero` is similar to `trig`, but the pattern is 'triggered' from its very first cycle, rather than from the current cycle. `trig` and `trigzero` therefore only give different results where the leftmost pattern differs from one cycle to the next.
We will save going deeper into the background, design and practicalities of these alignment functions for future publications. However in the next section, we take them as a case study for looking at the different design affordances offered by Haskell to Tidal, and JavaScript to Strudel.

View File

@ -0,0 +1 @@
TODO

View File

@ -0,0 +1,10 @@
## Strudel Packages
There are different packages for different purposes. They..
- split up the code into smaller chunks
- can be selectively used to implement some sort of time based system
Please refer to the individual README files in the [packages folder](https://github.com/tidalcycles/strudel/tree/main/packages)
TODO

View File

@ -0,0 +1,39 @@
---
title: Patterns
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# Patterns
Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern can output a set of events that happen within that time span. It depends on the structure of the Pattern how the events are located in time.
From now on, this process of generating events from a time span will be called **querying**.
Example:
<MiniRepl
client:idle
tune={`const pattern = sequence(c3, [e3, g3])
const events = pattern.queryArc(0, 1)
console.log(events.map((e) => e.show()))
silence`}
/>
In this example, we create a pattern using the `sequence` function and **query** it for the time span from `0` to `1`.
Those numbers represent units of time called **cycles**. The length of one cycle depends on the tempo, which defaults to one cycle per second.
The resulting events are:
```js
[
'[ 0/1 -> 1/2 | c3 ]', //
'[ 1/2 -> 3/4 | e3 ]',
'[ 3/4 -> 1/1 | g3 ]',
];
```
Each event has a value, a begin time and an end time, where time is represented as a fraction. In the above case, the events are placed in sequential order, where c3 takes the first half, and e3 and g3 together take the second half. This temporal placement is the result of the `sequence` function, which divides its arguments equally over one cycle. If an argument is an array, the same rule applies to that part of the cycle. In the example, e3 and g3 are divided equally over the second half of the whole cycle.
Note that the query function is not just a way to access a pattern, but true to the principles of functional programming, is the pattern itself. This means that in theory there is no way to change a pattern, it is opaque as a pure function. In practice though, Strudel and Tidal are all about transforming patterns, so how is this done? The answer is, by replacing the pattern with a new one, that calls the old one. This new one is only able to manipulate the query before passing it to the old pattern, and manipulate the results from it before returning them to caller. But, this is enough to support all the temporal and structural manipulations provided by Strudel (and Tidal's) extensive library of functions.
The above examples do not represent how Strudel is used in practice. In the live coding editor, the user only has to type in the pattern itself, the querying will be handled by the scheduler. The scheduler will repeatedly query the pattern for events, which are then scheduled as sound synthesis or other event triggers.

View File

@ -0,0 +1,188 @@
---
title: REPL
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
# REPL
{/* The [REPL](https://strudel.tidalcycles.org/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. */}
While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL^[REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel.
Besides a UI for playback control and meta information, the main part of the REPL interface is the code editor powered by CodeMirror. In it, the user can edit and evaluate pattern code live, using one of the available synthesis outputs to create music and/or sound art. The control flow of the REPL follows 3 basic steps:
1. The user writes and updates code. Each update transpiles and evaluates it to create a `Pattern` instance
2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span.
3. For each scheduling tick, all generated `Events` are triggered by calling their `onTrigger` method, which is set by the output.
<img src="https://github.com/tidalcycles/strudel/blob/talk/talk/public/strudelflow.png?raw=true" width="600" />
## User Code
To create a `Pattern` from the user code, two steps are needed:
1. Transpile the JS input code to make it functional
2. Evaluate the transpiled code
### Transpilation & Evaluation
In the JavaScript world, using transpilation is a common practise to be able to use language features that are not supported by the base language. Tools like `babel` will transpile code that contains unsupported language features into a version of the code without those features.
In the same tradition, Strudel can add a transpilation step to simplify the user code in the context of live coding. For example, the Strudel REPL lets the user create mini-notation patterns using just double quoted strings, while single quoted strings remain what they are:
```js
'c3 [e3 g3]*2';
```
is transpiled to:
```js
mini('c3 [e3 g3]*2').withMiniLocation([1, 0, 0], [1, 14, 14]);
```
Here, the string is wrapped in `mini`, which will create a pattern from a mini-notation string. Additionally, the `withMiniLocation` method passes the original source code location of the string to the pattern, which enables highlighting active events.
Other convenient features like pseudo variables, operator overloading and top level await are possible with transpilation.
After the transpilation, the code is ready to be evaluated into a `Pattern`.
Behind the scenes, the user code string is parsed with `acorn`, turning it into an Abstract Syntax Tree (AST). The AST allows changing the structure of the code before generating the transpiled version using `escodegen`.
### Mini-notation
While the transpilation allows JavaScript to express Patterns in a less verbose way, it is still preferable to use the mini-notation as a more compact way to express rhythm. Strudel aims to provide the same mini-notation features and syntax as used in Tidal.
The mini-notation parser is implemented using `peggy`, which allows generating performant parsers for Domain Specific Languages (DSLs) using a concise grammar notation. The generated parser turns the mini-notation string into an AST which is used to call the respective Strudel functions with the given structure. For example, `"c3 [e3 g3]*2"` will result in the following calls:
```js
seq(
reify('c3').withLocation([1, 1, 1], [1, 4, 4]),
seq(reify('e3').withLocation([1, 5, 5], [1, 8, 8]), reify('g3').withLocation([1, 8, 8], [1, 10, 10])).fast(2),
);
```
### Highlighting Locations
As seen in the examples above, both the JS and the mini-notation parser add source code locations using `withMiniLocation` and `withLocation` methods. While the JS parser adds locations relative to the user code as a whole, the mini-notation adds locations relative to the position of the mini-notation string. The absolute location of elements within mini-notation can be calculated by simply adding both locations together. This absolute location can be used to highlight active events in real time.
### Mini Notation
Another important part of the user code is the mini notation, which allows to express rhythms in a short manner.
- the mini notation is [implemented as a PEG grammar](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), living in the [mini package](https://github.com/tidalcycles/strudel/tree/main/packages/mini)
- it is based on [krill](https://github.com/Mdashdotdashn/krill) by Mdashdotdashn
- the peg grammar is used to generate a parser with [peggyjs](https://peggyjs.org/)
- the generated parser takes a mini notation string and outputs an AST
- the AST can then be used to construct a pattern using the regular Strudel API
Here's an example AST for `c3 [e3 g3]`
```json
{
"type_": "pattern",
"arguments_": { "alignment": "h" },
"source_": [
{
"type_": "element", "source_": "c3",
"location_": { "start": { "offset": 1, "line": 1, "column": 2 }, "end": { "offset": 4, "line": 1, "column": 5 } }
},
{
"type_": "element",
"location_": { "start": { "offset": 4, "line": 1, "column": 5 }, "end": { "offset": 11, "line": 1, "column": 12 } }
"source_": {
"type_": "pattern", "arguments_": { "alignment": "h" },
"source_": [
{
"type_": "element", "source_": "e3",
"location_": { "start": { "offset": 5, "line": 1, "column": 6 }, "end": { "offset": 8, "line": 1, "column": 9 } }
},
{
"type_": "element", "source_": "g3",
"location_": { "start": { "offset": 8, "line": 1, "column": 9 }, "end": { "offset": 10, "line": 1, "column": 11 } }
}
]
},
}
]
}
```
which translates to `seq(c3, seq(e3, g3))`
## Scheduling Events
After an instance of `Pattern` is obtained from the user code,
it is used by the scheduler to get queried for events. Once started, the scheduler runs at a fixed interval to query the active pattern for events within the current interval's time span. A simplified implementation looks like this:
```js
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
let interval = 0.5; // query interval in seconds
let time = 0; // beginning of current time span
let minLatency = 0.1; // min time before a hap should trigger
setInterval(() => {
const haps = pattern.queryArc(time, time + interval);
time += interval; // increment time
haps.forEach((hap) => {
const deadline = hap.whole.begin - time + minLatency;
onTrigger(hap, deadline, duration);
});
}, interval * 1000); // query each "interval" seconds
```
Note that the above code is simplified for illustrative purposes. The actual implementation has to work around imprecise callbacks of `setInterval`. More about the implementation details can be read in [this blog post](https://loophole-letters.vercel.app/web-audio-scheduling).
The fact that `Pattern.queryArc` is a pure function that maps a time span to a set of events allows us to choose any interval we like without changing the resulting output. It also means that when the pattern is changed from outside, the next scheduling callback will work with the new pattern, keeping its clock running.
The latency between the time the pattern is evaluated and the change is heard is between `minLatency` and `interval + minLatency`, in our example between 100ms and 600ms. In Strudel, the current query interval is 50ms with a minLatency of 100ms, meaning the latency is between 50ms and 150ms.
## Output
The last step is to trigger each event in the chosen output.
This is where the given time and value of each event is used to generate audio or any other form of time based output. The default output of the Strudel REPL is the WebAudio output. To understand what an output does, we first have to understand what control parameters are.
### Control Parameters
To be able to manipulate multiple aspects of sound in parallel, so called control parameters are used to shape the value of each event. Example:
```js
note('c3 e3')
.cutoff(1000)
.s('sawtooth')
.queryArc(0, 1)
.map((hap) => hap.value);
/* [
{ note: 'c3', cutoff: 1000, s: 'sawtooth' }
{ note: 'e3', cutoff: 1000, s: 'sawtooth' }
] */
```
Here, the control parameter functions `note`, `cutoff` and `s` are used, where each controls a different property in the value object. Each control parameter function accepts a primitive value, a list of values to be sequenced into a `Pattern`, or a `Pattern`. In the example, `note` gets a `Pattern` from a mini-notation expression (double quoted), while `cutoff` and `s` are given a `Number` and a (single quoted) `String` respectively.
Strudel comes with a large default set of control parameter functions that are based on the ones used by Tidal and SuperDirt, focusing on music and audio terminology. It is however possible to create custom control parameters for any purpose:
```js
const { x, y } = createParams('x', 'y');
x(sine.range(0, 200)).y(cosine.range(0, 200));
```
This example creates the custom control parameters `x` and `y` which are then used to form a pattern that descibes the coordinates of a circle.
### Outputs
Now that we know how the value of an event is manipulated using control parameters, we can look at how outputs can use that value to generate anything. The scheduler above was calling the `onTrigger` function which is used to implement the output. A very simple version of the web audio output could look like this:
```js
function onTrigger(hap, deadline, duration) {
const { note } = hap.value;
const time = getAudioContext().currentTime + deadline;
const o = getAudioContext().createOscillator();
o.frequency.value = getFreq(note);
o.start(time);
o.stop(time + event.duration);
o.connect(getAudioContext().destination);
}
```
The above example will create an `OscillatorNode` for each event, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.

View File

@ -0,0 +1 @@
TODO

View File

@ -1,17 +1,18 @@
import { Pattern, toMidi, valueToMidi } from '@strudel.cycles/core';
import { samples } from '@strudel.cycles/webaudio';
export async function prebake({ baseDir = '.' } = {}) {
export async function prebake({ baseDir = '' } = {}) {
// https://archive.org/details/SalamanderGrandPianoV3
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
return await Promise.all([
samples('piano.json', `${baseDir}/piano/`),
samples('/piano.json', `${baseDir}/piano/`),
// https://github.com/sgossner/VCSL/
// https://api.github.com/repositories/126427031/contents/
// LICENSE: CC0 general-purpose
samples('vcsl.json', 'github:sgossner/VCSL/master/'),
samples('tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/'),
samples('EmuSP12.json', `${baseDir}/EmuSP12/`),
samples('/vcsl.json', 'github:sgossner/VCSL/master/'),
samples('/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/'),
samples('/EmuSP12.json', `${baseDir}/EmuSP12/`),
// samples('github:tidalcycles/Dirt-Samples/master'),
]);
}

View File

@ -8,6 +8,6 @@ body {
display: none !important;
}
.prose > h1 {
padding-top: 30px;
.prose > h1:not(:first-child) {
margin-top: 30px;
}