mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 21:58:31 +00:00
commit
ea59fafb52
175
packages/core/color.mjs
Normal file
175
packages/core/color.mjs
Normal 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);
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
2
packages/react/dist/index.cjs.js
vendored
2
packages/react/dist/index.cjs.js
vendored
File diff suppressed because one or more lines are too long
534
packages/react/dist/index.es.js
vendored
534
packages/react/dist/index.es.js
vendored
@ -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
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
43
packages/react/src/hooks/useFrame.mjs
Normal file
43
packages/react/src/hooks/useFrame.mjs
Normal 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;
|
||||
34
packages/react/src/hooks/usePatternFrame.mjs
Normal file
34
packages/react/src/hooks/usePatternFrame.mjs
Normal 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;
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
20
website/src/docs/Colors.jsx
Normal file
20
website/src/docs/Colors.jsx
Normal 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;
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
12
website/src/pages/learn/colors.mdx
Normal file
12
website/src/pages/learn/colors.mdx
Normal 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 />
|
||||
@ -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!
|
||||
|
||||
69
website/src/pages/recipes/arpeggios.mdx
Normal file
69
website/src/pages/recipes/arpeggios.mdx
Normal 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
|
||||
76
website/src/pages/recipes/microrhythms.mdx
Normal file
76
website/src/pages/recipes/microrhythms.mdx
Normal 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
|
||||
88
website/src/pages/recipes/rhythms.mdx
Normal file
88
website/src/pages/recipes/rhythms.mdx
Normal 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`
|
||||
3
website/src/pages/technical-manual/about.mdx
Normal file
3
website/src/pages/technical-manual/about.mdx
Normal 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
|
||||
47
website/src/pages/technical-manual/alignment.mdx
Normal file
47
website/src/pages/technical-manual/alignment.mdx
Normal 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.
|
||||
1
website/src/pages/technical-manual/docs.mdx
Normal file
1
website/src/pages/technical-manual/docs.mdx
Normal file
@ -0,0 +1 @@
|
||||
TODO
|
||||
10
website/src/pages/technical-manual/packages.mdx
Normal file
10
website/src/pages/technical-manual/packages.mdx
Normal 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
|
||||
39
website/src/pages/technical-manual/patterns.mdx
Normal file
39
website/src/pages/technical-manual/patterns.mdx
Normal 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.
|
||||
188
website/src/pages/technical-manual/repl.mdx
Normal file
188
website/src/pages/technical-manual/repl.mdx
Normal 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.
|
||||
1
website/src/pages/technical-manual/tests.mdx
Normal file
1
website/src/pages/technical-manual/tests.mdx
Normal file
@ -0,0 +1 @@
|
||||
TODO
|
||||
@ -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'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,6 @@ body {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.prose > h1 {
|
||||
padding-top: 30px;
|
||||
.prose > h1:not(:first-child) {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user