Merge remote-tracking branch 'origin/main' into docs

This commit is contained in:
Felix Roos 2023-01-21 15:57:54 +01:00
commit fea1e71a99
30 changed files with 1473 additions and 1121 deletions

View File

@ -3,18 +3,60 @@
This directory can be used to save your own patterns, which then get
made into a pattern swatch.
0. fork and clone the strudel repository
1. run `npm run setup` in the strudel folder
1. Save one or more .txt files in this folder
2. run `npm run repl` in the top-level strudel folder
3. open `http://localhost:3000/swatch/` !
Example: <https://felixroos.github.io/strudel/swatch/>
## deploy
1. in your fork, go to settings -> pages and select "Github Actions" as source
2. edit `website/public/CNAME` to contain `<your-username>.github.io/strudel`
3. edit `website/astro.config.mjs` to use site: `https://<your-username>.github.io` and base `/strudel`
4. go to Actions -> `Build and Deploy` and click `Run workflow`
5. view your patterns at `<your-username>.github.io/strudel/swatch/`
### 1. fork the [strudel repo on github](https://github.com/tidalcycles/strudel.git)
### 2. clone your fork to your machine `git clone https://github.com/<your-username>/strudel.git strudel && cd strudel`
### 3. create a separate branch like `git branch patternuary && git checkout patternuary`
### 4. save one or more .txt files in the my-patterns folder
### 5. edit `website/public/CNAME` to contain `<your-username>.github.io/strudel`
### 6. edit `website/astro.config.mjs` to use site: `https://<your-username>.github.io` and base `/strudel`, like this
```js
export default defineConfig({
/* ... rest of config ... */
site: 'https://<your-username>.github.io',
base: '/strudel',
});
```
### 7. commit & push the changes
```sh
git add . && git commit -m "site config" && git push --set-upstream origin
```
### 8. deploy to github pages
- go to settings -> pages and select "Github Actions" as source
- go to settings -> environments -> github-pages and press the edit button next to `main` and type in `patternuary` (under "Deployment branches")
- go to Actions -> `Build and Deploy` and click `Run workflow` with branch `patternuary`
### 9. view your patterns at `<your-username>.github.io/strudel/swatch/`
Alternatively, github pages allows you to use a custom domain, like https://mycooldomain.org/swatch/. [See their documentation for details](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site).
### 10. optional: automatic deployment
If you want to automatically deploy your site on push, go to `deploy.yml` and change `workflow_dispatch` to `push`.
## running locally
- install dependencies with `npm run setup`
- run dev server with `npm run repl` and open `http://localhost:3000/strudel/swatch/`
## tests fail?
Your tests might fail if the code does not follow prettiers format.
In that case, run `npm run codeformat`. To disable that, remove `npm run format-check` from `test.yml`
## updating your fork
To update your fork, you can pull the main branch and merge it into your `patternuary` branch.

View File

@ -22,11 +22,11 @@ Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } =
ctx.fillStyle = clearColor;
ctx.fillRect(0, 0, ww, wh);
frame.forEach((f) => {
let { x, y, w, h, s, r, a = 0, fill = 'darkseagreen' } = f.value;
let { x, y, w, h, s, r, angle = 0, fill = 'darkseagreen' } = f.value;
w *= ww;
h *= wh;
if (r !== undefined && a !== undefined) {
const radians = a * 2 * Math.PI;
if (r !== undefined && angle !== undefined) {
const radians = angle * 2 * Math.PI;
const [cx, cy] = [(ww - w) / 2, (wh - h) / 2];
x = cx + Math.cos(radians) * r * cx;
y = cy + Math.sin(radians) * r * cy;
@ -51,7 +51,7 @@ Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } =
return silence;
};
export const { x, y, w, h, a, r, fill, smear } = createParams('x', 'y', 'w', 'h', 'a', 'r', 'fill', 'smear');
export const { x, y, w, h, angle, r, fill, smear } = createParams('x', 'y', 'w', 'h', 'angle', 'r', 'fill', 'smear');
export const rescale = register('rescale', function (f, pat) {
return pat.mul(x(f).w(f).y(f).h(f));

View File

@ -59,3 +59,9 @@ export const cleanupDraw = (clearScreen = true) => {
clearInterval(window.strudelScheduler);
}
};
Pattern.prototype.onPaint = function (onPaint) {
// this is evil! TODO: add pattern.context
this.context = { onPaint };
return this;
};

View File

@ -46,7 +46,7 @@ const _bjork = function (n, x) {
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
};
const bjork = function (ons, steps) {
export const bjork = function (ons, steps) {
const offs = steps - ons;
const x = Array(ons).fill([1]);
const y = Array(offs).fill([0]);

View File

@ -334,7 +334,12 @@ export class Pattern {
* silence
*/
queryArc(begin, end) {
return this.query(new State(new TimeSpan(begin, end)));
try {
return this.query(new State(new TimeSpan(begin, end)));
} catch (err) {
logger(`[query]: ${err.message}`, 'error');
return [];
}
}
/**
@ -2060,6 +2065,7 @@ export const velocity = register('velocity', function (velocity, pat) {
*/
// TODO - fix
export const legato = register('legato', function (value, pat) {
value = Fraction(value);
return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value))));
});

View File

@ -283,3 +283,15 @@ export function pianoroll({
ctx.stroke();
return this;
}
function getOptions(drawTime, options = {}) {
let [lookbehind, lookahead] = drawTime;
lookbehind = Math.abs(lookbehind);
const cycles = lookahead + lookbehind;
const playhead = lookbehind / cycles;
return { fold: 1, ...options, cycles, playhead };
}
Pattern.prototype.punchcard = function (options) {
return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
};

View File

@ -12,8 +12,8 @@ export function repl({
afterEval,
getTime,
transpiler,
editPattern,
onToggle,
editPattern,
}) {
const scheduler = new Cyclist({
interval,
@ -35,7 +35,7 @@ export function repl({
getTime,
onToggle,
});
setTime(() => scheduler.getPhase()); // TODO: refactor?
setTime(() => scheduler.now()); // TODO: refactor?
const evaluate = async (code, autostart = true) => {
if (!code) {
throw new Error('no code to evaluate');

View File

@ -114,6 +114,14 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
*
*/
/**
* A discrete pattern of numbers from 0 to n-1
* @example
* run(4).scale('C4 major').note()
* // "0 1 2 3".scale('C4 major').note()
*/
export const run = (n) => saw.range(0, n).floor().segment(n);
/**
* A continuous pattern of random numbers, between 0 and 1.
*

View File

@ -44,6 +44,7 @@ import {
ply,
rev,
time,
run,
} from '../index.mjs';
import { steady } from '../signal.mjs';
@ -908,6 +909,11 @@ describe('Pattern', () => {
);
});
});
describe('run', () => {
it('Can run', () => {
expect(run(4).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
});
});
describe('linger', () => {
it('Can linger on the first quarter of a cycle', () => {
expect(sequence(0, 1, 2, 3, 4, 5, 6, 7).linger(0.25).firstCycle()).toStrictEqual(

File diff suppressed because one or more lines are too long

View File

@ -1,15 +1,15 @@
import l, { useCallback as N, useRef as k, useEffect as _, useMemo as K, useState as w, useLayoutEffect as G } from "react";
import Z from "@uiw/react-codemirror";
import { Decoration as y, EditorView as J } from "@codemirror/view";
import { StateEffect as Q, StateField as X } from "@codemirror/state";
import { javascript as ee } from "@codemirror/lang-javascript";
import { tags as s } from "@lezer/highlight";
import { createTheme as te } from "@uiw/codemirror-themes";
import { repl as re, logger as ne, pianoroll as oe } from "@strudel.cycles/core";
import { webaudioOutput as ae, getAudioContext as ce } from "@strudel.cycles/webaudio";
import { useInView as se } from "react-hook-inview";
import { transpiler as ie } from "@strudel.cycles/transpiler";
const le = te({
import d, { useCallback as E, useRef as A, useEffect as k, useMemo as Q, useState as _, useLayoutEffect as te } from "react";
import ue from "@uiw/react-codemirror";
import { Decoration as M, EditorView as re } from "@codemirror/view";
import { StateEffect as ne, StateField as oe } from "@codemirror/state";
import { javascript as de } from "@codemirror/lang-javascript";
import { tags as u } from "@lezer/highlight";
import { createTheme as fe } from "@uiw/codemirror-themes";
import { webaudioOutput as me, getAudioContext as he } from "@strudel.cycles/webaudio";
import { useInView as ge } from "react-hook-inview";
import { repl as pe, logger as ve } from "@strudel.cycles/core";
import { transpiler as be } from "@strudel.cycles/transpiler";
const Ee = fe({
theme: "dark",
settings: {
background: "#222",
@ -22,380 +22,421 @@ const le = te({
gutterForeground: "#8a919966"
},
styles: [
{ 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" }
{ tag: u.keyword, color: "#c792ea" },
{ tag: u.operator, color: "#89ddff" },
{ tag: u.special(u.variableName), color: "#eeffff" },
{ tag: u.typeName, color: "#c3e88d" },
{ tag: u.atom, color: "#f78c6c" },
{ tag: u.number, color: "#c3e88d" },
{ tag: u.definition(u.variableName), color: "#82aaff" },
{ tag: u.string, color: "#c3e88d" },
{ tag: u.special(u.string), color: "#c3e88d" },
{ tag: u.comment, color: "#7d8799" },
{ tag: u.variableName, color: "#c792ea" },
{ tag: u.tagName, color: "#c3e88d" },
{ tag: u.bracket, color: "#525154" },
{ tag: u.meta, color: "#ffcb6b" },
{ tag: u.attributeName, color: "#c792ea" },
{ tag: u.propertyName, color: "#c792ea" },
{ tag: u.className, color: "#decb6b" },
{ tag: u.invalid, color: "#ffffff" }
]
});
const j = Q.define(), ue = X.define({
const X = ne.define(), ye = oe.define({
create() {
return y.none;
return M.none;
},
update(e, r) {
update(e, t) {
try {
for (let t of r.effects)
if (t.is(j))
if (t.value) {
const a = y.mark({ attributes: { style: "background-color: #FFCA2880" } });
e = y.set([a.range(0, r.newDoc.length)]);
for (let r of t.effects)
if (r.is(X))
if (r.value) {
const n = M.mark({ attributes: { style: "background-color: #FFCA2880" } });
e = M.set([n.range(0, t.newDoc.length)]);
} else
e = y.set([]);
e = M.set([]);
return e;
} catch (t) {
return console.warn("flash error", t), e;
} catch (r) {
return console.warn("flash error", r), e;
}
},
provide: (e) => J.decorations.from(e)
}), de = (e) => {
e.dispatch({ effects: j.of(!0) }), setTimeout(() => {
e.dispatch({ effects: j.of(!1) });
provide: (e) => re.decorations.from(e)
}), we = (e) => {
e.dispatch({ effects: X.of(!0) }), setTimeout(() => {
e.dispatch({ effects: X.of(!1) });
}, 200);
}, z = Q.define(), fe = X.define({
}, B = ne.define(), ke = oe.define({
create() {
return y.none;
return M.none;
},
update(e, r) {
update(e, t) {
try {
for (let t of r.effects)
if (t.is(z)) {
const a = t.value.map(
(o) => (o.context.locations || []).map(({ start: i, end: u }) => {
const m = o.context.color || "#FFCA28";
let n = r.newDoc.line(i.line).from + i.column, d = r.newDoc.line(u.line).from + u.column;
const v = r.newDoc.length;
return n > v || d > v ? void 0 : y.mark({ attributes: { style: `outline: 1.5px solid ${m};` } }).range(n, d);
for (let r of t.effects)
if (r.is(B)) {
const n = r.value.map(
(l) => (l.context.locations || []).map(({ start: m, end: f }) => {
const c = l.context.color || "#FFCA28";
let s = t.newDoc.line(m.line).from + m.column, g = t.newDoc.line(f.line).from + f.column;
const b = t.newDoc.length;
return s > b || g > b ? void 0 : M.mark({ attributes: { style: `outline: 1.5px solid ${c};` } }).range(s, g);
})
).flat().filter(Boolean) || [];
e = y.set(a, !0);
e = M.set(n, !0);
}
return e;
} catch {
return y.set([]);
return M.set([]);
}
},
provide: (e) => J.decorations.from(e)
}), me = [ee(), le, fe, ue];
function ge({ value: e, onChange: r, onViewChanged: t, onSelectionChange: a, options: o, editorDidMount: i }) {
const u = N(
(d) => {
r?.(d);
},
[r]
), m = N(
(d) => {
t?.(d);
provide: (e) => re.decorations.from(e)
}), Fe = [de(), Ee, ke, ye];
function _e({ value: e, onChange: t, onViewChanged: r, onSelectionChange: n, options: l, editorDidMount: m }) {
const f = E(
(g) => {
t?.(g);
},
[t]
), n = N(
(d) => {
d.selectionSet && a && a?.(d.state.selection);
), c = E(
(g) => {
r?.(g);
},
[a]
[r]
), s = E(
(g) => {
g.selectionSet && n && n?.(g.state.selection);
},
[n]
);
return /* @__PURE__ */ l.createElement(l.Fragment, null, /* @__PURE__ */ l.createElement(Z, {
return /* @__PURE__ */ d.createElement(d.Fragment, null, /* @__PURE__ */ d.createElement(ue, {
value: e,
onChange: u,
onCreateEditor: m,
onUpdate: n,
extensions: me
onChange: f,
onCreateEditor: c,
onUpdate: s,
extensions: Fe
}));
}
function W(...e) {
function T(...e) {
return e.filter(Boolean).join(" ");
}
function pe({ view: e, pattern: r, active: t, getTime: a }) {
const o = k([]), i = k();
_(() => {
function Me({ view: e, pattern: t, active: r, getTime: n }) {
const l = A([]), m = A(0);
k(() => {
if (e)
if (r && t) {
let u = requestAnimationFrame(function m() {
if (t && r) {
m.current = 0;
let f = requestAnimationFrame(function c() {
try {
const n = a(), v = [Math.max(i.current || n, n - 1 / 10, 0), n + 1 / 60];
i.current = v[1], o.current = o.current.filter((p) => p.whole.end > n);
const h = r.queryArc(...v).filter((p) => p.hasOnset());
o.current = o.current.concat(h), e.dispatch({ effects: z.of(o.current) });
const s = n(), b = [Math.max(m.current ?? s, s - 1 / 10, -0.01), s + 1 / 60];
m.current = b[1], l.current = l.current.filter((h) => h.whole.end > s);
const i = t.queryArc(...b).filter((h) => h.hasOnset());
l.current = l.current.concat(i), e.dispatch({ effects: B.of(l.current) });
} catch {
e.dispatch({ effects: z.of([]) });
e.dispatch({ effects: B.of([]) });
}
u = requestAnimationFrame(m);
f = requestAnimationFrame(c);
});
return () => {
cancelAnimationFrame(u);
cancelAnimationFrame(f);
};
} else
o.current = [], e.dispatch({ effects: z.of([]) });
}, [r, t, e]);
l.current = [], e.dispatch({ effects: B.of([]) });
}, [t, r, e]);
}
function he(e, r = !1) {
const t = k(), a = k(), o = (m) => {
if (a.current !== void 0) {
const n = m - a.current;
e(m, n);
function Ae(e, t = !1) {
const r = A(), n = A(), l = (c) => {
if (n.current !== void 0) {
const s = c - n.current;
e(c, s);
}
a.current = m, t.current = requestAnimationFrame(o);
}, i = () => {
t.current = requestAnimationFrame(o);
}, u = () => {
t.current && cancelAnimationFrame(t.current), delete t.current;
n.current = c, r.current = requestAnimationFrame(l);
}, m = () => {
r.current = requestAnimationFrame(l);
}, f = () => {
r.current && cancelAnimationFrame(r.current), delete r.current;
};
return _(() => {
t.current && (u(), i());
}, [e]), _(() => (r && i(), u), []), {
start: i,
stop: u
return k(() => {
r.current && (f(), m());
}, [e]), k(() => (t && m(), f), []), {
start: m,
stop: f
};
}
function ve({ pattern: e, started: r, getTime: t, onDraw: a }) {
let o = k([]), i = k(null);
const { start: u, stop: m } = he(
N(() => {
const n = t();
if (i.current === null) {
i.current = n;
function Ne({ pattern: e, started: t, getTime: r, onDraw: n, drawTime: l = [-2, 2] }) {
let [m, f] = l;
m = Math.abs(m);
let c = A([]), s = A(null);
k(() => {
if (e && t) {
const i = r(), h = e.queryArc(Math.max(i, 0), i + f + 0.1);
c.current = c.current.filter((p) => p.whole.begin < i), c.current = c.current.concat(h);
}
}, [e, t]);
const { start: g, stop: b } = Ae(
E(() => {
const i = r() + f;
if (s.current === null) {
s.current = i;
return;
}
const d = e.queryArc(Math.max(i.current, n - 1 / 10), n), v = 4;
i.current = n, o.current = (o.current || []).filter((h) => h.whole.end > n - v).concat(d.filter((h) => h.hasOnset())), a(n, o.current);
const h = e.queryArc(Math.max(s.current, i - 1 / 10), i);
s.current = i, c.current = (c.current || []).filter((p) => p.whole.end >= i - m - f).concat(h.filter((p) => p.hasOnset())), n(e, i - f, c.current, l);
}, [e])
);
_(() => {
r ? u() : (o.current = [], m());
}, [r]);
}
function be(e) {
return _(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((r) => window.postMessage(r, "*"), []);
}
function Ee({
defaultOutput: e,
interval: r,
getTime: t,
evalOnMount: a = !1,
initialCode: o = "",
autolink: i = !1,
beforeEval: u,
afterEval: m,
editPattern: n,
onEvalError: d,
onToggle: v,
canvasId: h
}) {
const p = K(() => we(), []);
h = h || `canvas-${p}`;
const [F, A] = w(), [P, D] = w(), [b, q] = w(o), [x, V] = w(), [I, R] = w(), [L, B] = w(!1), H = b !== x, { scheduler: M, evaluate: c, start: f, stop: C, pause: O } = K(
() => re({
interval: r,
defaultOutput: e,
onSchedulerError: A,
onEvalError: (g) => {
D(g), d?.(g);
},
getTime: t,
transpiler: ie,
beforeEval: ({ code: g }) => {
q(g), u?.();
},
editPattern: n ? (g) => n(g, p) : void 0,
afterEval: ({ pattern: g, code: T }) => {
V(T), R(g), D(), A(), i && (window.location.hash = "#" + encodeURIComponent(btoa(T))), m?.();
},
onToggle: (g) => {
B(g), v?.(g);
}
}),
[e, r, t]
), Y = be(({ data: { from: g, type: T } }) => {
T === "start" && g !== p && C();
}), S = N(
async (g = !0) => {
await c(b, g), Y({ type: "start", from: p });
},
[c, b]
), U = k();
return _(() => {
!U.current && a && b && (U.current = !0, S());
}, [S, a, b]), _(() => () => {
M.stop();
}, [M]), {
id: p,
canvasId: h,
code: b,
setCode: q,
error: F || P,
schedulerError: F,
scheduler: M,
evalError: P,
evaluate: c,
activateCode: S,
activeCode: x,
isDirty: H,
pattern: I,
started: L,
start: f,
stop: C,
pause: O,
togglePlay: async () => {
L ? M.pause() : await S();
return k(() => {
t ? g() : (c.current = [], b());
}, [t]), {
clear: () => {
c.current = [];
}
};
}
function we() {
function Ce(e) {
return k(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), E((t) => window.postMessage(t, "*"), []);
}
function De({
defaultOutput: e,
interval: t,
getTime: r,
evalOnMount: n = !1,
initialCode: l = "",
autolink: m = !1,
beforeEval: f,
afterEval: c,
editPattern: s,
onEvalError: g,
onToggle: b,
canvasId: i,
drawContext: h,
drawTime: p = [-2, 2]
}) {
const D = Q(() => Re(), []);
i = i || `canvas-${D}`;
const [P, R] = _(), [z, H] = _(), [y, S] = _(l), [V, q] = _(), [x, I] = _(), [N, O] = _(!1), K = y !== V, L = E((a) => !!(a?.context?.onPaint && h), [h]), { scheduler: C, evaluate: o, start: v, stop: j, pause: U } = Q(
() => pe({
interval: t,
defaultOutput: e,
onSchedulerError: R,
onEvalError: (a) => {
H(a), g?.(a);
},
getTime: r,
drawContext: h,
transpiler: be,
editPattern: s,
beforeEval: ({ code: a }) => {
S(a), f?.();
},
afterEval: ({ pattern: a, code: w }) => {
q(w), I(a), H(), R(), m && (window.location.hash = "#" + encodeURIComponent(btoa(w))), c?.();
},
onToggle: (a) => {
O(a), b?.(a);
}
}),
[e, t, r]
), ce = Ce(({ data: { from: a, type: w } }) => {
w === "start" && a !== D && j();
}), Y = E(
async (a = !0) => {
const w = await o(y, a);
return ce({ type: "start", from: D }), w;
},
[o, y]
), W = E(
(a, w, G, J) => {
const { onPaint: ie } = a.context || {}, le = typeof h == "function" ? h(i) : h;
ie?.(le, w, G, J);
},
[h, i]
), $ = E(
(a) => {
if (L(a)) {
const [w, G] = p, J = a.queryArc(0, G);
W(a, -1e-3, J, p);
}
},
[p, W, L]
), Z = A();
k(() => {
!Z.current && n && y && (Z.current = !0, o(y, !1).then((a) => $(a)));
}, [n, y, o, $]), k(() => () => {
C.stop();
}, [C]);
const ae = async () => {
N ? (C.stop(), $(x)) : await Y();
}, se = P || z;
return Ne({
pattern: x,
started: L(x) && N,
getTime: () => C.now(),
drawTime: p,
onDraw: W
}), {
id: D,
canvasId: i,
code: y,
setCode: S,
error: se,
schedulerError: P,
scheduler: C,
evalError: z,
evaluate: o,
activateCode: Y,
activeCode: V,
isDirty: K,
pattern: x,
started: N,
start: v,
stop: j,
pause: U,
togglePlay: ae
};
}
function Re() {
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
}
function $({ type: e }) {
return /* @__PURE__ */ l.createElement("svg", {
function ee({ type: e }) {
return /* @__PURE__ */ d.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__ */ l.createElement("path", {
refresh: /* @__PURE__ */ d.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__ */ l.createElement("path", {
play: /* @__PURE__ */ d.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__ */ l.createElement("path", {
pause: /* @__PURE__ */ d.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"
}),
stop: /* @__PURE__ */ d.createElement("path", {
fillRule: "evenodd",
d: "M2 10a8 8 0 1116 0 8 8 0 01-16 0zm5-2.25A.75.75 0 017.75 7h4.5a.75.75 0 01.75.75v4.5a.75.75 0 01-.75.75h-4.5a.75.75 0 01-.75-.75v-4.5z",
clipRule: "evenodd"
})
}[e]);
}
const ye = "_container_3i85k_1", ke = "_header_3i85k_5", _e = "_buttons_3i85k_9", Fe = "_button_3i85k_9", Ce = "_buttonDisabled_3i85k_17", Ne = "_error_3i85k_21", xe = "_body_3i85k_25", E = {
container: ye,
header: ke,
buttons: _e,
button: Fe,
buttonDisabled: Ce,
error: Ne,
body: xe
}, Me = () => ce().currentTime;
function je({ tune: e, hideOutsideView: r = !1, enableKeyboard: t, withCanvas: a = !1, canvasHeight: o = 200 }) {
const {
code: i,
setCode: u,
evaluate: m,
activateCode: n,
error: d,
isDirty: v,
activeCode: h,
pattern: p,
started: F,
scheduler: A,
togglePlay: P,
stop: D,
canvasId: b,
id: q
} = Ee({
const xe = "_container_3i85k_1", Le = "_header_3i85k_5", Pe = "_buttons_3i85k_9", qe = "_button_3i85k_9", ze = "_buttonDisabled_3i85k_17", He = "_error_3i85k_21", Se = "_body_3i85k_25", F = {
container: xe,
header: Le,
buttons: Pe,
button: qe,
buttonDisabled: ze,
error: He,
body: Se
}, Ve = () => he().currentTime;
function Ze({ tune: e, hideOutsideView: t = !1, enableKeyboard: r, drawTime: n, punchcard: l, canvasHeight: m = 200 }) {
n = n || (l ? [0, 4] : void 0);
const f = !!n, c = E(
n ? (o) => document.querySelector("#" + o)?.getContext("2d") : null,
[n]
), {
code: s,
setCode: g,
evaluate: b,
activateCode: i,
error: h,
isDirty: p,
activeCode: D,
pattern: P,
started: R,
scheduler: z,
togglePlay: H,
stop: y,
canvasId: S,
id: V
} = De({
initialCode: e,
defaultOutput: ae,
getTime: Me,
editPattern: (c, f) => c.withContext((C) => ({ ...C, id: f }))
});
ve({
pattern: p,
started: a && F,
getTime: () => A.now(),
onDraw: (c, f) => {
const C = document.querySelector("#" + b).getContext("2d");
oe({ ctx: C, time: c, haps: f, autorange: 1, fold: 1, playhead: 1 });
}
});
const [x, V] = w(), [I, R] = se({
defaultOutput: me,
editPattern: (o) => l ? o.punchcard() : o,
getTime: Ve,
evalOnMount: f,
drawContext: c,
drawTime: n
}), [q, x] = _(), [I, N] = ge({
threshold: 0.01
}), L = k(), B = K(() => ((R || !r) && (L.current = !0), R || L.current), [R, r]);
pe({
view: x,
pattern: p,
active: F && !h?.includes("strudel disable-highlighting"),
getTime: () => A.getPhase()
}), G(() => {
if (t) {
const c = async (f) => {
(f.ctrlKey || f.altKey) && (f.code === "Enter" ? (f.preventDefault(), de(x), await n()) : f.code === "Period" && (D(), f.preventDefault()));
}), O = A(), K = Q(() => ((N || !t) && (O.current = !0), N || O.current), [N, t]);
Me({
view: q,
pattern: P,
active: R && !D?.includes("strudel disable-highlighting"),
getTime: () => z.now()
}), te(() => {
if (r) {
const o = async (v) => {
(v.ctrlKey || v.altKey) && (v.code === "Enter" ? (v.preventDefault(), we(q), await i()) : v.code === "Period" && (y(), v.preventDefault()));
};
return window.addEventListener("keydown", c, !0), () => window.removeEventListener("keydown", c, !0);
return window.addEventListener("keydown", o, !0), () => window.removeEventListener("keydown", o, !0);
}
}, [t, p, i, m, D, x]);
const [H, M] = w([]);
return Ae(
N((c) => {
const { data: f } = c.detail;
f?.hap?.context?.id === q && M((O) => O.concat([c.detail]).slice(-10));
}, [r, P, s, b, y, q]);
const [L, C] = _([]);
return Oe(
E((o) => {
const { data: v } = o.detail;
v?.hap?.context?.id === V && C((U) => U.concat([o.detail]).slice(-10));
}, [])
), /* @__PURE__ */ l.createElement("div", {
className: E.container,
), /* @__PURE__ */ d.createElement("div", {
className: F.container,
ref: I
}, /* @__PURE__ */ l.createElement("div", {
className: E.header
}, /* @__PURE__ */ l.createElement("div", {
className: E.buttons
}, /* @__PURE__ */ l.createElement("button", {
className: W(E.button, F ? "sc-animate-pulse" : ""),
onClick: () => P()
}, /* @__PURE__ */ l.createElement($, {
type: F ? "pause" : "play"
})), /* @__PURE__ */ l.createElement("button", {
className: W(v ? E.button : E.buttonDisabled),
onClick: () => n()
}, /* @__PURE__ */ l.createElement($, {
}, /* @__PURE__ */ d.createElement("div", {
className: F.header
}, /* @__PURE__ */ d.createElement("div", {
className: F.buttons
}, /* @__PURE__ */ d.createElement("button", {
className: T(F.button, R ? "sc-animate-pulse" : ""),
onClick: () => H()
}, /* @__PURE__ */ d.createElement(ee, {
type: R ? "stop" : "play"
})), /* @__PURE__ */ d.createElement("button", {
className: T(p ? F.button : F.buttonDisabled),
onClick: () => i()
}, /* @__PURE__ */ d.createElement(ee, {
type: "refresh"
}))), d && /* @__PURE__ */ l.createElement("div", {
className: E.error
}, d.message)), /* @__PURE__ */ l.createElement("div", {
className: E.body
}, B && /* @__PURE__ */ l.createElement(ge, {
value: i,
onChange: u,
onViewChanged: V
})), a && /* @__PURE__ */ l.createElement("canvas", {
id: b,
}))), h && /* @__PURE__ */ d.createElement("div", {
className: F.error
}, h.message)), /* @__PURE__ */ d.createElement("div", {
className: F.body
}, K && /* @__PURE__ */ d.createElement(_e, {
value: s,
onChange: g,
onViewChanged: x
})), n && /* @__PURE__ */ d.createElement("canvas", {
id: S,
className: "w-full pointer-events-none",
height: o,
ref: (c) => {
c && c.width !== c.clientWidth && (c.width = c.clientWidth);
height: m,
ref: (o) => {
o && o.width !== o.clientWidth && (o.width = o.clientWidth);
}
}), !!H.length && /* @__PURE__ */ l.createElement("div", {
}), !!L.length && /* @__PURE__ */ d.createElement("div", {
className: "sc-bg-gray-800 sc-rounded-md sc-p-2"
}, H.map(({ message: c }, f) => /* @__PURE__ */ l.createElement("div", {
key: f
}, c))));
}, L.map(({ message: o }, v) => /* @__PURE__ */ d.createElement("div", {
key: v
}, o))));
}
function Ae(e) {
De(ne.key, e);
function Oe(e) {
Be(ve.key, e);
}
function De(e, r, t = !1) {
_(() => (document.addEventListener(e, r, t), () => {
document.removeEventListener(e, r, t);
}), [r]);
function Be(e, t, r = !1) {
k(() => (document.addEventListener(e, t, r), () => {
document.removeEventListener(e, t, r);
}), [t]);
}
const Ue = (e) => G(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
const Te = (e) => te(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
export {
ge as CodeMirror,
je as MiniRepl,
W as cx,
de as flash,
pe as useHighlighting,
Ue as useKeydown,
be as usePostMessage,
Ee as useStrudel
_e as CodeMirror,
Ze as MiniRepl,
T as cx,
we as flash,
Me as useHighlighting,
Te as useKeydown,
Ce as usePostMessage,
De as useStrudel
};

View File

@ -26,6 +26,13 @@ export function Icon({ type }) {
clipRule="evenodd"
/>
),
stop: (
<path
fillRule="evenodd"
d="M2 10a8 8 0 1116 0 8 8 0 01-16 0zm5-2.25A.75.75 0 017.75 7h4.5a.75.75 0 01.75.75v4.5a.75.75 0 01-.75.75h-4.5a.75.75 0 01-.75-.75v-4.5z"
clipRule="evenodd"
/>
),
}[type]
}
</svg>

View File

@ -1,11 +1,9 @@
import { pianoroll } from '@strudel.cycles/core';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import React, { useLayoutEffect, useMemo, useRef, useState, useCallback, useEffect } from 'react';
import { useInView } from 'react-hook-inview';
import 'tailwindcss/tailwind.css';
import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs';
import usePatternFrame from '../hooks/usePatternFrame.mjs';
import useStrudel from '../hooks/useStrudel.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
import { Icon } from './Icon';
@ -15,7 +13,13 @@ import { logger } from '@strudel.cycles/core';
const getTime = () => getAudioContext().currentTime;
export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCanvas = false, canvasHeight = 200 }) {
export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTime, punchcard, canvasHeight = 200 }) {
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
const evalOnMount = !!drawTime;
const drawContext = useCallback(
!!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
[drawTime],
);
const {
code,
setCode,
@ -34,25 +38,13 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
} = useStrudel({
initialCode: tune,
defaultOutput: webaudioOutput,
editPattern: (pat) => (punchcard ? pat.punchcard() : pat),
getTime,
editPattern: (pat, id) => {
return pat.withContext((ctx) => ({ ...ctx, id }));
},
evalOnMount,
drawContext,
drawTime,
});
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]); */
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01,
@ -68,7 +60,7 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
view,
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.getPhase(),
getTime: () => scheduler.now(),
});
// set active pattern on ctrl+enter
@ -110,7 +102,7 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
<div className={styles.header}>
<div className={styles.buttons}>
<button className={cx(styles.button, started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
<Icon type={started ? 'pause' : 'play'} />
<Icon type={started ? 'stop' : 'play'} />
</button>
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
<Icon type="refresh" />
@ -121,7 +113,7 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
<div className={styles.body}>
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</div>
{withCanvas && (
{drawTime && (
<canvas
id={canvasId}
className="w-full pointer-events-none"

View File

@ -3,16 +3,17 @@ import { setHighlights } from '../components/CodeMirror6';
function useHighlighting({ view, pattern, active, getTime }) {
const highlights = useRef([]);
const lastEnd = useRef();
const lastEnd = useRef(0);
useEffect(() => {
if (view) {
if (pattern && active) {
lastEnd.current = 0;
let frame = requestAnimationFrame(function updateHighlights() {
try {
const audioTime = getTime();
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
// see https://github.com/tidalcycles/strudel/issues/108
const begin = Math.max(lastEnd.current || audioTime, audioTime - 1 / 10, 0); // negative time seems buggy
const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy
const span = [begin, audioTime + 1 / 60];
lastEnd.current = span[1];
highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active

View File

@ -2,23 +2,32 @@ import { useCallback, useEffect, useRef } from 'react';
import 'tailwindcss/tailwind.css';
import useFrame from '../hooks/useFrame.mjs';
function usePatternFrame({ pattern, started, getTime, onDraw }) {
function usePatternFrame({ pattern, started, getTime, onDraw, drawTime = [-2, 2] }) {
let [lookbehind, lookahead] = drawTime;
lookbehind = Math.abs(lookbehind);
let visibleHaps = useRef([]);
let lastFrame = useRef(null);
useEffect(() => {
if (pattern && started) {
const t = getTime();
const futureHaps = pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +0.1 = workaround for weird holes in query..
visibleHaps.current = visibleHaps.current.filter((h) => h.whole.begin < t);
visibleHaps.current = visibleHaps.current.concat(futureHaps);
}
}, [pattern, started]);
const { start: startFrame, stop: stopFrame } = useFrame(
useCallback(() => {
const phase = getTime();
const phase = getTime() + lookahead;
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
.filter((h) => h.whole.end >= phase - lookbehind - lookahead) // in frame
.concat(haps.filter((h) => h.hasOnset()));
onDraw(phase, visibleHaps.current);
onDraw(pattern, phase - lookahead, visibleHaps.current, drawTime);
}, [pattern]),
);
useEffect(() => {
@ -29,6 +38,11 @@ function usePatternFrame({ pattern, started, getTime, onDraw }) {
stopFrame();
}
}, [started]);
return {
clear: () => {
visibleHaps.current = [];
},
};
}
export default usePatternFrame;

View File

@ -1,6 +1,7 @@
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
import { repl } from '@strudel.cycles/core';
import { transpiler } from '@strudel.cycles/transpiler';
import usePatternFrame from './usePatternFrame';
import usePostMessage from './usePostMessage.mjs';
function useStrudel({
@ -16,6 +17,8 @@ function useStrudel({
onEvalError,
onToggle,
canvasId,
drawContext,
drawTime = [-2, 2],
}) {
const id = useMemo(() => s4(), []);
canvasId = canvasId || `canvas-${id}`;
@ -27,6 +30,7 @@ function useStrudel({
const [pattern, setPattern] = useState();
const [started, setStarted] = useState(false);
const isDirty = code !== activeCode;
const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
// TODO: make sure this hook reruns when scheduler.started changes
const { scheduler, evaluate, start, stop, pause } = useMemo(
@ -40,12 +44,13 @@ function useStrudel({
onEvalError?.(err);
},
getTime,
drawContext,
transpiler,
editPattern,
beforeEval: ({ code }) => {
setCode(code);
beforeEval?.();
},
editPattern: editPattern ? (pat) => editPattern(pat, id) : undefined,
afterEval: ({ pattern: _pattern, code }) => {
setActiveCode(code);
setPattern(_pattern);
@ -71,19 +76,41 @@ function useStrudel({
});
const activateCode = useCallback(
async (autostart = true) => {
await evaluate(code, autostart);
const res = await evaluate(code, autostart);
broadcast({ type: 'start', from: id });
return res;
},
[evaluate, code],
);
const onDraw = useCallback(
(pattern, time, haps, drawTime) => {
const { onPaint } = pattern.context || {};
const ctx = typeof drawContext === 'function' ? drawContext(canvasId) : drawContext;
onPaint?.(ctx, time, haps, drawTime);
},
[drawContext, canvasId],
);
const drawFirstFrame = useCallback(
(pat) => {
if (shouldPaint(pat)) {
const [_, lookahead] = drawTime;
const haps = pat.queryArc(0, lookahead);
// draw at -0.001 to avoid activating haps at 0
onDraw(pat, -0.001, haps, drawTime);
}
},
[drawTime, onDraw, shouldPaint],
);
const inited = useRef();
useEffect(() => {
if (!inited.current && evalOnMount && code) {
inited.current = true;
activateCode();
evaluate(code, false).then((pat) => drawFirstFrame(pat));
}
}, [activateCode, evalOnMount, code]);
}, [evalOnMount, code, evaluate, drawFirstFrame]);
// this will stop the scheduler when hot reloading in development
useEffect(() => {
@ -94,12 +121,22 @@ function useStrudel({
const togglePlay = async () => {
if (started) {
scheduler.pause();
scheduler.stop();
drawFirstFrame(pattern);
} else {
await activateCode();
}
};
const error = schedulerError || evalError;
usePatternFrame({
pattern,
started: shouldPaint(pattern) && started,
getTime: () => scheduler.now(),
drawTime,
onDraw,
});
return {
id,
canvasId,

View File

@ -1736,27 +1736,6 @@ exports[`runs examples > example "fastGap" example index 0 1`] = `
]
`;
exports[`runs examples > example "fastcat" example index 0 1`] = `
[
"[ 0/1 → 1/3 | e5 ]",
"[ 1/3 → 2/3 | b4 ]",
"[ 2/3 → 5/6 | d5 ]",
"[ 5/6 → 1/1 | c5 ]",
"[ 1/1 → 4/3 | e5 ]",
"[ 4/3 → 5/3 | b4 ]",
"[ 5/3 → 11/6 | d5 ]",
"[ 11/6 → 2/1 | c5 ]",
"[ 2/1 → 7/3 | e5 ]",
"[ 7/3 → 8/3 | b4 ]",
"[ 8/3 → 17/6 | d5 ]",
"[ 17/6 → 3/1 | c5 ]",
"[ 3/1 → 10/3 | e5 ]",
"[ 10/3 → 11/3 | b4 ]",
"[ 11/3 → 23/6 | d5 ]",
"[ 23/6 → 4/1 | c5 ]",
]
`;
exports[`runs examples > example "firstOf" example index 0 1`] = `
[
"[ 3/4 → 1/1 | note:c3 ]",
@ -3139,6 +3118,27 @@ exports[`runs examples > example "round" example index 0 1`] = `
]
`;
exports[`runs examples > example "run" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:C4 ]",
"[ 1/4 → 1/2 | note:D4 ]",
"[ 1/2 → 3/4 | note:E4 ]",
"[ 3/4 → 1/1 | note:F4 ]",
"[ 1/1 → 5/4 | note:C4 ]",
"[ 5/4 → 3/2 | note:D4 ]",
"[ 3/2 → 7/4 | note:E4 ]",
"[ 7/4 → 2/1 | note:F4 ]",
"[ 2/1 → 9/4 | note:C4 ]",
"[ 9/4 → 5/2 | note:D4 ]",
"[ 5/2 → 11/4 | note:E4 ]",
"[ 11/4 → 3/1 | note:F4 ]",
"[ 3/1 → 13/4 | note:C4 ]",
"[ 13/4 → 7/2 | note:D4 ]",
"[ 7/2 → 15/4 | note:E4 ]",
"[ 15/4 → 4/1 | note:F4 ]",
]
`;
exports[`runs examples > example "s" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd ]",
@ -3484,19 +3484,6 @@ exports[`runs examples > example "sine" example index 0 1`] = `
]
`;
exports[`runs examples > example "size" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.8 size:0 ]",
"[ 1/2 → 1/1 | s:sd room:0.8 size:0 ]",
"[ 1/1 → 3/2 | s:bd room:0.8 size:1 ]",
"[ 3/2 → 2/1 | s:sd room:0.8 size:1 ]",
"[ 2/1 → 5/2 | s:bd room:0.8 size:2 ]",
"[ 5/2 → 3/1 | s:sd room:0.8 size:2 ]",
"[ 3/1 → 7/2 | s:bd room:0.8 size:4 ]",
"[ 7/2 → 4/1 | s:sd room:0.8 size:4 ]",
]
`;
exports[`runs examples > example "slow" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:bd ]",

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import { MiniRepl } from './MiniRepl';
const getTag = (title, item) => item.tags?.find((t) => t.title === title)?.text;
export function JsDoc({ name, h = 3, hideDescription }) {
export function JsDoc({ name, h = 3, hideDescription, punchcard, canvasHeight }) {
const item = docs[name];
if (!item) {
console.warn('Not found: ' + name);
@ -40,7 +40,7 @@ export function JsDoc({ name, h = 3, hideDescription }) {
{item.examples?.length ? (
<div className="space-y-2">
{item.examples?.map((example, k) => (
<MiniRepl tune={example} key={k} />
<MiniRepl tune={example} key={k} {...{ punchcard, canvasHeight }} />
))}
</div>
) : (

View File

@ -3,8 +3,9 @@ import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { useEffect, useState } from 'react';
import { prebake } from '../repl/prebake';
let modules;
if (typeof window !== 'undefined') {
evalScope(
modules = evalScope(
controls,
import('@strudel.cycles/core'),
// import('@strudel.cycles/tone'),
@ -22,14 +23,20 @@ if (typeof window !== 'undefined') {
prebake();
}
export function MiniRepl({ tune, withCanvas }) {
export function MiniRepl({ tune, drawTime, punchcard, canvasHeight = 100 }) {
const [Repl, setRepl] = useState();
useEffect(() => {
// we have to load this package on the client
// because codemirror throws an error on the server
import('@strudel.cycles/react').then((res) => {
setRepl(() => res.MiniRepl);
});
Promise.all([import('@strudel.cycles/react'), modules])
.then(([res]) => setRepl(() => res.MiniRepl))
.catch((err) => console.error(err));
}, []);
return Repl ? <Repl tune={tune} hideOutsideView={true} withCanvas={withCanvas} /> : <pre>{tune}</pre>;
return Repl ? (
<div className="mb-4">
<Repl tune={tune} hideOutsideView={true} drawTime={drawTime} punchcard={punchcard} canvasHeight={canvasHeight} />
</div>
) : (
<pre>{tune}</pre>
);
}

View File

@ -61,3 +61,7 @@ As a chained function:
## silence
<JsDoc client:idle name="silence" h={0} />
## run
<JsDoc client:idle name="run" h={0} punchcard />

View File

@ -28,7 +28,7 @@ Strudel however runs directly in your web browser, does not require any custom s
The main place to actually make music with Strudel is the [Strudel REPL](https://strudel.tidalcycles.org/) ([what is a REPL?](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)), but in these pages you will also encounter interactive "MiniREPLs" where you can listen to and edit Strudel patterns.
Try clicking the play icon below:
<MiniRepl client:idle tune={`s("bd sd")`} />
<MiniRepl client:idle tune={`s("bd sd")`} punchcard />
Then edit the text so it reads `s("bd sd cp hh")` and click the refresh icon.
Congratulations, you have now live coded your first Strudel pattern!
@ -54,13 +54,13 @@ Alternatively, you can get a taste of what Strudel can do by clicking play on th
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
}, 'github:tidalcycles/Dirt-Samples/master/');
stack(
s("bd,[~ <sd!3 sd(3,4,2)>],hh(3,4)") // drums
s("bd,[~ <sd!3 sd(3,4,2)>],hh*8") // drums
.speed(perlin.range(.7,.9)) // random sample speed variation
,"<a1 b1\*2 a1(3,8) e2>" // bassline
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
.add(perlin.range(0,.5)) // random pitch variation
.superimpose(add(.05)) // add second, slightly detuned voice
.n() // wrap in "n"
.note() // wrap in "note"
.decay(.15).sustain(0) // make each note of equal length
.s('sawtooth') // waveform
.gain(.4) // turn down
@ -68,7 +68,7 @@ s("bd,[~ <sd!3 sd(3,4,2)>],hh(3,4)") // drums
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.note() // wrap in "note"
.s('sawtooth') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff

View File

@ -51,12 +51,12 @@ If you do just want to get a regular string that is _not_ parsed as mini-notatio
We can play more notes by separating them with spaces:
<MiniRepl client:idle tune={`note("e5 b4 d5 c5")`} withCanvas />
<MiniRepl client:idle tune={`note("c e g b")`} punchcard />
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")`} withCanvas />
<MiniRepl client:idle tune={`note("c d e f g a b")`} punchcard />
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!
@ -72,28 +72,28 @@ But, it will begin to make sense as we go through more elements of mini-notation
We can slow the sequence down by enclosing it in brackets and dividing it by a number (`/2`):
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]/2")`} />
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]/2")`} punchcard />
The division by two means that the sequence will be played over the course of two cycles.
You can also use decimal numbers for any tempo you like (`/2.75`).
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]/2.75")`} />
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]/2.75")`} punchcard />
## Angle Brackets
Using angle brackets `<>`, we can define the sequence length based on the number of events:
<MiniRepl client:idle tune={`note("<e5 b4 d5 c5>")`} />
<MiniRepl client:idle tune={`note("<e5 b4 d5 c5>")`} punchcard />
The above snippet is the same as:
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]/4")`} />
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]/4")`} punchcard />
The advantage of the angle brackets, is that we can add more events without needing to change the number at the end.
<MiniRepl client:idle tune={`note("<e5 b4 d5 c5 e5>")`} />
<MiniRepl client:idle tune={`note("<e5 b4 d5 c5 e5>")`} punchcard />
<MiniRepl client:idle tune={`note("<e5 b4 d5 c5 e5 b4>")`} />
<MiniRepl client:idle tune={`note("<e5 b4 d5 c5 e5 b4>")`} punchcard />
This is more similar to traditional music sequencers and piano rolls, where adding a note increases the perceived overall duration.
@ -101,15 +101,13 @@ This is more similar to traditional music sequencers and piano rolls, where addi
Contrary to division, a sequence can be sped up by multiplying it by a number using the asterisk symbol (`*`):
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]*2")`} />
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]*2")`} punchcard />
The multiplication by two here means that the sequence will play twice a cycle.
As with divisions, multiplications can be decimal (`*2.75`):
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]*2.75")`} />
Actually, this is not true, but this will be [fixed](https://github.com/tidalcycles/strudel/issues/314) :)
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]*2.75")`} punchcard />
## Subdividing time with bracket nesting
@ -133,7 +131,7 @@ Well, what this means is that in TidalCycles, not only can you divide time any w
The "~" represents a rest, and will create silence between other events:
<MiniRepl client:idle tune={`note("[b4 [~ c5] d5 e5]")`} />
<MiniRepl client:idle tune={`note("[b4 [~ c5] d5 e5]")`} punchcard />
## Parallel / polyphony
@ -141,17 +139,17 @@ Using commas, we can play chords.
The following are the same:
<MiniRepl client:idle tune={`note("[g3,b3,e4]")`} />
<MiniRepl client:idle tune={`note("g3,b3,e4")`} />
<MiniRepl client:idle tune={`note("g3,b3,e4")`} punchcard canvasHeight={80} />
But to play multiple chords in a sequence, we have to wrap them in brackets:
<MiniRepl client:idle tune={`note("<[g3,b3,e4] [a3,c3,e4] [b3,d3,f#4] [b3,e4,g4]>")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4] [a3,c3,e4] [b3,d3,f#4] [b3,e4,g4]>")`} punchcard />
## Elongation
With the "@" symbol, we can specify temporal "weight" of a sequence child:
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>")`} punchcard />
Here, the first chord has a weight of 2, making it twice the length of the other chords. The default weight is 1.
@ -159,7 +157,7 @@ Here, the first chord has a weight of 2, making it twice the length of the other
Using "!" we can repeat without speeding up:
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>")`} punchcard />
In essence, the `x!n` is like a shortcut for `[x*n]@n`.
@ -181,24 +179,24 @@ Using round brackets after an event, we can create rhythmical sub-divisions base
This algorithm can be found in many different types of music software, and is often referred to as a [Euclidean rhythm](https://en.wikipedia.org/wiki/Euclidean_rhythm) sequencer, after computer scientist Godfriend Toussaint.
Why is it interesting? Well, consider the following simple example:
<MiniRepl client:idle tune={`s("bd(3,8,0)")`} />
<MiniRepl client:idle tune={`s("bd(3,8,0)")`} punchcard canvasHeight={50} />
Sound familiar?
This is a popular Euclidian rhythm going by various names, such as "Pop Clave".
These rhythms can be found in all musical cultures, and the Euclidian rhythm algorithm allows us to express them extremely easily.
Writing this rhythm out in full require describing:
<MiniRepl client:idle tune={`s("bd ~ ~ bd ~ ~ bd ~")`} />
<MiniRepl client:idle tune={`s("bd ~ ~ bd ~ ~ bd ~")`} punchcard canvasHeight={50} />
But using the Euclidian rhythm notation, we only need to express "3 beats over 8 segments, starting on position 1".
This makes it easy to write patterns with interesting rhythmic structures and variations that still sound familiar:
<MiniRepl client:idle tune={`note("e5(2,8) b4(3,8) d5(2,8) c5(3,8)").slow(4)`} />
<MiniRepl client:idle tune={`note("e5(2,8) b4(3,8) d5(2,8) c5(3,8)").slow(4)`} punchcard canvasHeight={50} />
Note that since the example above does not use the third `offset` parameter, it can be written simply as `"(3,8)"`.
<MiniRepl client:idle tune={`s("bd(3,8)")`} />
<MiniRepl client:idle tune={`s("bd(3,8)")`} punchcard canvasHeight={50} />
Let's look at those three parameters in detail.
@ -207,26 +205,26 @@ Let's look at those three parameters in detail.
`beats`: the first parameter controls how may beats will be played.
Compare these:
<MiniRepl client:idle tune={`s("bd(2,8)")`} />
<MiniRepl client:idle tune={`s("bd(5,8)")`} />
<MiniRepl client:idle tune={`s("bd(7,8)")`} />
<MiniRepl client:idle tune={`s("bd(2,8)")`} punchcard canvasHeight={50} />
<MiniRepl client:idle tune={`s("bd(5,8)")`} punchcard canvasHeight={50} />
<MiniRepl client:idle tune={`s("bd(7,8)")`} punchcard canvasHeight={50} />
### Segments
`segments`: the second parameter controls the total amount of segments the beats will be distributed over:
<MiniRepl client:idle tune={`s("bd(3,4)")`} />
<MiniRepl client:idle tune={`s("bd(3,8)")`} />
<MiniRepl client:idle tune={`s("bd(3,13)")`} />
<MiniRepl client:idle tune={`s("bd(3,4)")`} punchcard canvasHeight={50} />
<MiniRepl client:idle tune={`s("bd(3,8)")`} punchcard canvasHeight={50} />
<MiniRepl client:idle tune={`s("bd(3,13)")`} punchcard canvasHeight={50} />
### Offsets
`offset`: the third (optional) parameter controls the starting position for distributing the beats.
We need a secondary rhythm to hear the difference:
<MiniRepl client:idle tune={`s("bd(3,8,0), hh cp")`} />
<MiniRepl client:idle tune={`s("bd(3,8,3), hh cp")`} />
<MiniRepl client:idle tune={`s("bd(3,8,5), hh cp")`} />
<MiniRepl client:idle tune={`s("bd(3,8,0), hh cp")`} punchcard />
<MiniRepl client:idle tune={`s("bd(3,8,3), hh cp")`} punchcard />
<MiniRepl client:idle tune={`s("bd(3,8,5), hh cp")`} punchcard />
## Mini-notation exercise

View File

@ -25,7 +25,7 @@ Here's the same pattern written in three different ways:
<MiniRepl client:idle tune={`freq("220 275 330 440")`} />
Let's look at `note`, `n` and `freq` in more detail...
Let's look at those in more detail...
## `note` names

View File

@ -20,15 +20,17 @@ You can learn more about both of these approaches in the pages [Synths](/learn/s
# Combining notes and sounds
In both of the above cases, we are no longer directly controlling the `note`/`n`/`freq` of the sound heard via `s`, as we were in the [Notes](/strudel/notes) page.
In both of the above cases, we are no longer directly controlling the `note`/`freq` of the sound heard via `s`, as we were in the [Notes](/strudel/notes) page.
So how can we both control the sound and the pitch? We can _combine_ `note`/`n`/`freq` with `s` to change the sound of our pitches:
So how can we both control the sound and the pitch? We can _combine_ `note`/`freq` with `s` to change the sound of our pitches:
<MiniRepl client:idle tune={`note("a3 c#4 e4 a4").s("sawtooth")`} />
<MiniRepl client:idle tune={`n("57 61 64 69").s("triangle")`} />
<MiniRepl client:idle tune={`note("57 61 64 69").s("sine")`} />
<MiniRepl client:idle tune={`freq("220 275 330 440").s("sine")`} />
<MiniRepl client:idle tune={`freq("220 275 330 440").s("triangle")`} />
The last example will actually sound the same with or without `s`, because `triangle` is the default value for `s`.
What about combining different notes with different sounds at the same time?
@ -37,5 +39,3 @@ What about combining different notes with different sounds at the same time?
Hmm, something interesting is going on there, related to there being five notes and three sounds.
Let's now take a step back and think about the Strudel [Code](/learn/code) we've been hearing so far.
<br />

View File

@ -18,13 +18,13 @@ Strudel is written in JavaScript, while Tidal is written in Haskell.
This difference is most obvious when looking at the syntax:
```hs
```haskell
iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4
```
One _could_ express that pattern to Strudel like so:
```txt
```
iter(4, every(3, add.squeeze("10 20"), n("0 1 3").s("triangle").crush(4)))
```
@ -37,7 +37,7 @@ operators, or change the meaning of existing ones.
Before you discard Strudel as an unwieldy paren monster, look at this alternative way to write the above:
```txt
```
n("0 1 3").every(3, add.squeeze("10 20")).iter(4).s("triangle").crush(4)
```
@ -114,7 +114,7 @@ Also, samples are always loaded from a URL rather than from the disk, although [
The Strudel REPL does not support [block based evaluation](https://github.com/tidalcycles/strudel/issues/34) yet.
You can use the following "workaround" to create multiple patterns that can be turned on and off:
```txt
```
let a = note("c a f e")
let b = s("bd sd")
@ -127,7 +127,7 @@ stack(
Alternatively, you could write everything as one `stack` and use `.hush()` to silence a pattern:
```txt
```
stack(
note("c a f e"),
s("bd sd").hush()
@ -141,6 +141,6 @@ Note that strudel will always use the last statement in your code as the pattern
Strudels tempo is 1 cycle per second, while tidal defaults to `0.5625`.
You can get the same tempo as tidal with:
```txt
```
note("c a f e").fast(.5625);
```

View File

@ -29,7 +29,11 @@ add a mini repl with
- `client:idle` is required to tell astro that the repl should be interactive, see [Client Directive](https://docs.astro.build/en/reference/directives-reference/#client-directives)
- `tune`: be any valid pattern code
- `withCanvas`: If set, a canvas will be rendered below that shows a pianoroll (WIP)
- `punchcard`: if added, a punchcard / pianoroll visualization is renderd
- `drawTime`: time window for drawing, defaults to `[0, 4]`
- `canvasHeight`: height of the canvas, defaults to 100px
See `mini-notation.mdx` for usage examples
## In-Source Documentation

View File

@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { cleanupDraw, cleanupUi, controls, evalScope, logger } from '@strudel.cycles/core';
import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core';
import { CodeMirror, cx, flash, useHighlighting, useStrudel } from '@strudel.cycles/react';
import {
getAudioContext,
@ -54,6 +54,12 @@ evalScope(
export let loadedSamples = [];
const presets = prebake();
let drawContext, clearCanvas;
if (typeof window !== 'undefined') {
drawContext = getDrawContext();
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
}
Promise.all([...modules, presets]).then((data) => {
// console.log('modules and sample registry loade', data);
loadedSamples = Object.entries(getLoadedSamples() || {});
@ -125,6 +131,7 @@ export function Repl({ embedded = false }) {
setPending(false);
},
onToggle: (play) => !play && cleanupDraw(false),
drawContext,
});
// init code
@ -167,7 +174,7 @@ export function Repl({ embedded = false }) {
view,
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.getPhase(),
getTime: () => scheduler.now(),
});
//
@ -203,6 +210,7 @@ export function Repl({ embedded = false }) {
const handleShuffle = async () => {
const { code, name } = getRandomTune();
logger(`[repl] ✨ loading random tune "${name}"`);
clearCanvas();
resetLoadedSamples();
await prebake(); // declare default samples
await evaluate(code, false);

View File

@ -0,0 +1,172 @@
export const lightflower = `Pattern.prototype.nest = function(n, cycles) {
n = reify(n)
return this.echo(n, pure(cycles).div(n), 1)
}
Pattern.prototype.deepimpose = function(func, times) {
if(times===0) return this;
return this.superimpose(x=>func(x).deepimpose(func, times-1))
}
angle(saw)
.fill('#aaffee12')
.r(.18)
.w(.06)
.h(.06)
.deepimpose(x=>x.mul(r(2).w(2).h(2)).late(1/12), 3)
.nest(6, 1)
.s('ellipse')
.mul(w(sine).h(sine).range(.5,1.25))
.off(.5, x=>x.fill('#ffeeaa12').rev().div(r(1.2)))
.slow(16)
.smear(0.6)
.animate({smear:0})
`;
// https://strudel.tidalcycles.org/?C31_NrcMfZEO
export const spiralflower = `const {innerWidth:ww,innerHeight:wh} = window;
const ctx = getDrawContext()
const piDiv180 = Math.PI / 180;
function fromPolar(angle, radius, cx, cy) {
const radians = (angle-90) * piDiv180
return [cx + Math.cos(radians) * radius, cy + Math.sin(radians) * radius]
}
const [w, h] = [200,200]
const [cx,cy] = [ww/2,wh/2];
function drawSpiralSegment(ctx, {angle,b,r, density = 2, color = 'darkseagreen', thick = 2, long = 1}) {
let i = angle;
ctx.beginPath();
while(i < b){
const radius = Math.max(Math.min(r - i*.2,1000),20);
const [x1,y1] = fromPolar(i, radius, cx, cy)
const [x2,y2] = fromPolar(i, radius+long, cx, cy)
ctx.lineWidth = thick;
ctx.moveTo(x1,y1);
ctx.strokeStyle= color
ctx.lineTo(x2,y2);
ctx.stroke()
i+=300/density;
}
}
const { r, angle, b, color, density,thick} =
createParams('r', 'angle', 'b', 'color','density','thick','long');
const pattern =
r(sine.range(200,800).slow(4))
.angle(cosine.range(0, 45).slow(3))
.b(perlin.range(1000, 4000).slow(5))
.thick(sine.range(2,50).slow(2))
.long(perlin.range(1,100).slow(3))
.off(1, x=>x.color('white'))
.off(2, x=>x.color('salmon'))
.off(4, x=>x.color('purple'))
.slow(4)//.mask("x(5,8)")
function onDraw(f) {
ctx.beginPath();
drawSpiralSegment(ctx, f.value);
}
// generic draw logic
window.frame && cancelAnimationFrame(window.frame);
function render(t) {
t = Math.round(t)
const frame = pattern.slow(1000).queryArc(t, t)
ctx.fillStyle='#20001005'
ctx.fillRect(0,0,ww,wh)
//ctx.clearRect(0,0,ww,wh)
ctx.stroke()
frame.forEach(onDraw)
window.frame = requestAnimationFrame(render);
};
window.frame = requestAnimationFrame(render);
silence
`;
export const syncexample = `"<0 1 2 3>/2"
.off(1/2, add(4))
.off(1, add(2))
.scale(cat('C minor','C major').slow(8))
.layer(
x=>x.note().piano(),
p=>stack(
p
.angle(p.sub('c3').div(12))
.r(.5)
.s('ellipse')
.w(.1)
.h(.1),
p.x(p.sub('c3').div(12))
.y(.9)
.w(1/12)
.h(.1)
.s('rect')
).animate({sync:true,smear:0.9})
)
`;
export const moveRescaleZoom = `
const rescale = register('rescale', function (f, pat) {
return pat.mul(x(f).w(f).y(f).h(f));
})
const move = register('move', function (dx, dy, pat) {
return pat.add(x(dx).y(dy));
})
const zoom = register('zoom', function (f, pat) {
const d = pure(1).sub(f).div(2);
return pat.rescale(f).move(d, d);
})
x(.5).y(.5).w(1).h(1)
.zoom(saw.slow(3))
.move(sine.range(-.1,.1),0)
.fill("#ffeeaa10")
.s('rect')
.echo(6,.5,1)
.animate({smear:0.5})`;
export const strudelS = `
const rescale = register('rescale', function (f, pat) {
return pat.mul(x(f).w(f).y(f).h(f));
})
const move = register('move', function (dx, dy, pat) {
return pat.add(x(dx).y(dy));
})
const flipY = register('flipY', function (pat) {
return pat.fmap(v => ({...v, y:1-v.y}))
})
const zoom = register('zoom', function (f, pat) {
const d = pure(1).sub(f).div(2);
return pat.rescale(f).move(d, d);
})
Pattern.prototype.nest = function(n, cycles) {
n = reify(n)
return this.echo(n, pure(cycles).div(n), 1)
}
x(sine.div(1)).y(cosine.range(0,.5))
.w(.1).h(.1)
.mul(w(square).h(square).slow(8))
.zoom(saw.slow(8))
.layer(
id,
_=>_.flipY().move(0,0).rev()
)
.mask("0 1@2").rev()
.nest(16,9)
.s('rect')
.fill("royalblue steelblue".fast(14))
.slow(8)
.animate({smear:.99})`;

View File

@ -466,14 +466,14 @@ samples({
stack(
"-7 0 -7 7".struct("x(5,8,1)").fast(2).sub(7)
.scale(scales)
.n()
.note()
.s("sawtooth,square")
.gain(.3).attack(0.01).decay(0.1).sustain(.5)
.apply(filter1),
"~@3 [<2 3>,<4 5>]"
.echo(4,1/16,.7)
.scale(scales)
.n()
.note()
.s('square').gain(.7)
.attack(0.01).decay(0.1).sustain(0)
.apply(filter1),
@ -484,7 +484,7 @@ stack(
.fast(2)
.echo(32, 1/8, .8)
.scale(scales)
.n()
.note()
.s("sawtooth")
.gain(sine.range(.1,.4).slow(8))
.attack(.001).decay(.2).sustain(0)
@ -527,7 +527,7 @@ stack(
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
.add(perlin.range(0,.5)) // random pitch variation
.superimpose(add(.05)) // add second, slightly detuned voice
.n() // wrap in "n"
.note() // wrap in "note"
.decay(.15).sustain(0) // make each note of equal length
.s('sawtooth') // waveform
.gain(.4) // turn down
@ -536,7 +536,7 @@ stack(
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.note() // wrap in "note"
.s('sawtooth') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
@ -545,7 +545,7 @@ stack(
,"a4 c5 <e6 a6>".struct("x(5,8,-1)")
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.note() // wrap in "note"
.decay(.1).sustain(0) // make notes short
.s('triangle') // waveform
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
@ -565,12 +565,12 @@ samples({
"C^7 Am7 Dm7 G7".slow(2).voicings('lefthand')
.stack("0@6 [<1 2> <2 0> 1]@2".scale('C5 major'))
.n().slow(4)
.note().slow(4)
.s('0040_FluidR3_GM_sf2_file')
.color('steelblue')
.stack(
"<-7 ~@2 [~@2 -7] -9 ~@2 [~@2 -9] -10!2 ~ [~@2 -10] -5 ~ [-3 -2 -10]@2>*2".scale('C3 major')
.n().s('sawtooth').color('brown')
.note().s('sawtooth').color('brown')
)
.attack(0.05).decay(.1).sustain(.7)
.cutoff(perlin.range(800,2000))