Compare commits

..

61 Commits

Author SHA1 Message Date
Felix Roos 235f3672aa meetup talk 2022-09-11 22:11:25 +02:00
Felix Roos 55dbcdde18 add new deck for meetup 2022-09-09 21:38:35 +02:00
Felix Roos d3f10518cc Merge branch 'talk' of https://github.com/tidalcycles/strudel into talk 2022-09-04 21:01:55 +02:00
Felix Roos 99fd5a53cd Merge remote-tracking branch 'origin/HEAD' into talk 2022-07-28 22:23:05 +02:00
Felix Roos 7e21b683fb loadScript helper 2022-07-19 08:59:46 +02:00
Felix Roos a492628b33 more default samples (ignored wav files for now) 2022-07-10 20:59:07 +02:00
Felix Roos 53bb20c516 .out error handling 2022-07-10 20:55:05 +02:00
Felix Roos 588f74123e pianoroll optimizations
- fix flickering due to global alpha mutation
- change default inactive color
- add smear option
- add playheadColor option
2022-07-10 20:54:44 +02:00
Felix Roos e3473d5e70 demo ready slides 2022-07-10 20:53:27 +02:00
Felix Roos 8ce4de0c8d style errors, fixed backdrop + hide console 2022-07-10 20:52:46 +02:00
Felix Roos b87cea834f style fixes + hide-console option
- thicker highlighting outline
- only shade actual characters, not whole lines
- strudel hide-console will now hide console
2022-07-10 20:51:38 +02:00
Felix Roos 230ebe7439 talk now works offline too 2022-07-02 00:38:10 +02:00
Felix Roos e1f017916c small fixes 2022-07-02 00:11:57 +02:00
Felix Roos 6a445ee130 kind of finished slides 2022-07-01 23:55:04 +02:00
Felix Roos 8ea0044ffa Merge commit '85d3d7392ecfdc985fa8cc93e01846bec0f16dc4' into talk 2022-07-01 13:05:32 +02:00
Felix Roos 2d76de5906 log helpers 2022-07-01 10:56:31 +02:00
Felix Roos 4cce65c9fe now using symlinked samples folder from repl 2022-07-01 10:41:11 +02:00
Felix Roos 6b3687357e move samples to dedicated samples folder 2022-07-01 10:40:33 +02:00
Felix Roos 1d6e74fc77 small fix 2022-06-30 17:14:02 +02:00
Felix Roos c9fda97534 add prebake to maxirepl 2022-06-30 17:13:59 +02:00
Felix Roos 8796bcbdc3 fix: pianoroll 2022-06-30 17:13:47 +02:00
Felix Roos 4c66d9a553 Merge commit '8a1cd32d50e6689b14a77db0eca0b9033ab55312' into talk 2022-06-30 16:50:35 +02:00
Felix Roos 85d3d7392e hide-header magic comment 2022-06-30 16:49:24 +02:00
Felix Roos 31d0225341 toMidi now understands numbers 2022-06-30 16:49:01 +02:00
Felix Roos d0acb32497 piano nowalso understands n 2022-06-30 16:48:48 +02:00
Felix Roos aa9a32c9af add freq tunes from metro.html 2022-06-30 16:48:31 +02:00
Felix Roos a8610d0078 revert changing removeUndefineds 2022-06-30 02:26:24 +02:00
Felix Roos ef5dcd0dae fix: undefined error 2022-06-30 02:23:12 +02:00
Felix Roos ed86a94f99 add 2 tunes using new samples 2022-06-30 02:22:53 +02:00
Felix Roos 7be791f2d3 add some default samples 2022-06-30 02:22:38 +02:00
Felix Roos cc31a21b6f Merge remote-tracking branch 'origin/main' into talk 2022-06-22 20:21:55 +02:00
Felix Roos 72155d24c9 ec tal 2022-06-13 21:15:53 +02:00
Felix Roos 7672ca9971 small fixes 2022-06-07 22:34:39 +02:00
Felix Roos 07120e9e3b comment out stuff + style fix 2022-06-07 21:55:31 +02:00
Felix Roos 72feb3b65a pianoroll: do NOT round 2022-06-07 21:37:52 +02:00
Felix Roos 2f5ff44dc9 default to piano 2022-06-07 21:37:43 +02:00
Felix Roos 8051286904 react: usable onEvent 2022-06-07 21:16:17 +02:00
Felix Roos 8a57333f72 more slides 2022-06-07 20:40:29 +02:00
Felix Roos 512ba03fef can now hide slides 2022-06-07 20:38:51 +02:00
Felix Roos 4f0d2f181b more slides 2022-06-07 01:38:57 +02:00
Felix Roos 1339bc3b94 fix: piano now works with midi numbers 2022-06-07 01:38:12 +02:00
Felix Roos edf2fb9781 more slides + style 2022-06-06 22:39:51 +02:00
Felix Roos cf6bfd0e30 add clapping music 2022-06-06 19:10:48 +02:00
Felix Roos a93d6e884d repl: hard stop 2022-06-06 19:10:23 +02:00
Felix Roos 1a9b57e66c add phasing example 2022-06-06 13:20:17 +02:00
Felix Roos 0aec9423ac use new pianoroll settings 2022-06-05 23:33:08 +02:00
Felix Roos 54df4edca7 pianoroll: hideNegative flag 2022-06-05 23:32:10 +02:00
Felix Roos be6310b8e2 pianoroll: flipTime + flipValues 2022-06-05 23:24:59 +02:00
Felix Roos 3ab32114a6 replace from to with cycles / playhead 2022-06-05 22:51:59 +02:00
Felix Roos da85e1920b pianoroll: autorange + fold + vertical 2022-06-05 22:45:14 +02:00
Felix Roos 352d701b7b minirepl: highlighting can now be disabled 2022-06-05 22:44:24 +02:00
Felix Roos 349c1bc5d8 test new pianoroll 2022-06-04 23:55:26 +02:00
Felix Roos 9430caeb29 more flexible pianoroll 2022-06-04 23:55:05 +02:00
Felix Roos eb0adf4194 use evaluateOnly in MaxiRepl 2022-06-04 23:54:39 +02:00
Felix Roos 6fde3f2231 add evaluateOnly 2022-06-04 23:54:33 +02:00
Felix Roos 1c77e7e5b3 more content + style 2022-06-04 15:02:04 +02:00
Felix Roos 061abecaca added some slides 2022-06-04 01:01:59 +02:00
Felix Roos 98c131df9a cooler pattern 2022-06-02 22:48:18 +02:00
Felix Roos 1af10157d6 basic slides setup 2022-06-02 22:19:35 +02:00
Felix Roos a3eeaead2a remove odd packages 2022-06-02 22:18:06 +02:00
Felix Roos ff362da23f themable codemirror + repl 2022-06-02 22:17:50 +02:00
99 changed files with 11532 additions and 328 deletions
+2 -1
View File
@@ -34,4 +34,5 @@ repl_old
tutorial.rendered.mdx
doc.json
talk/public/EmuSP12
talk/public/samples
talk/public/samples
server/samples/old
+24 -2
View File
@@ -1046,6 +1046,22 @@ export class Pattern {
onTrigger(onTrigger) {
return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger }));
}
log(func = id) {
return this._withHap((hap) =>
hap.setContext({
...hap.context,
onTrigger: (...args) => {
if (hap.context.onTrigger) {
hap.context.onTrigger(...args);
}
console.log(func(...args));
},
}),
);
}
logValues(func = id) {
return this.log((_, hap) => func(hap.value));
}
}
// TODO - adopt value.mjs fully..
@@ -1206,8 +1222,9 @@ export function pure(value) {
export function isPattern(thing) {
// thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled
const is = thing instanceof Pattern || thing._Pattern;
if (!thing instanceof Pattern) {
const is = thing instanceof Pattern || thing?._Pattern;
if (thing?._Pattern && !thing instanceof Pattern) {
console.warn(
`Found Pattern that fails "instanceof Pattern" check.
This may happen if you are using multiple versions of @strudel.cycles/core.
@@ -1550,3 +1567,8 @@ Pattern.prototype.define = (name, func, options = {}) => {
// Pattern.prototype.define('early', (a, pat) => pat.early(a), { patternified: true, composable: true });
Pattern.prototype.define('hush', (pat) => pat.hush(), { patternified: false, composable: true });
Pattern.prototype.define('bypass', (pat) => pat.bypass(on), { patternified: true, composable: true });
export async function loadScript(url) {
const code = await fetch(url).then((res) => res.text());
return eval(code);
}
+16
View File
@@ -19,6 +19,9 @@ export const tokenizeNote = (note) => {
// turns the given note into its midi number representation
export const toMidi = (note) => {
if (typeof note === 'number') {
return note;
}
const [pc, acc, oct] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
@@ -31,6 +34,19 @@ export const fromMidi = (n) => {
return Math.pow(2, (n - 69) / 12) * 440;
};
export const getFreq = (noteOrMidi) => {
if (typeof noteOrMidi === 'number') {
return fromMidi(noteOrMidi);
}
return fromMidi(toMidi(noteOrMidi));
};
export const midi2note = (n) => {
const oct = Math.floor(n / 12) - 1;
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
return pc + oct;
};
// modulo that works with negative numbers e.g. mod(-1, 3) = 2
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
export const mod = (n, m) => ((n % m) + m) % m;
+2 -2
View File
File diff suppressed because one or more lines are too long
+83 -18
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import React, { useState, useEffect, useCallback, useMemo, useRef, useLayoutEffect } from 'react';
import { CodeMirror as CodeMirror$1 } from 'react-codemirror6';
import { EditorView, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
@@ -46,7 +46,13 @@ const materialPalenightTheme = EditorView.theme(
lineHeight: '22px',
},
'.cm-line': {
background: '#2C323699',
// background: '#2C323699',
background: 'transparent',
},
'.cm-line > *': {
// background: '#2C323699',
background: '#00000090',
// background: 'transparent',
},
// done
'&.cm-focused .cm-cursor': {
@@ -71,7 +77,7 @@ const materialPalenightTheme = EditorView.theme(
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-activeLine': { backgroundColor: cursor + '50' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
@@ -193,7 +199,7 @@ const highlightField = StateField.define({
if (from > l || to > l) {
return;
}
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
return mark.range(from, to);
})).filter(Boolean), true);
}
@@ -205,7 +211,7 @@ const highlightField = StateField.define({
},
provide: (f) => EditorView.decorations.from(f)
});
function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount, theme }) {
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(CodeMirror$1, {
onViewChange: onViewChanged,
style: {
@@ -217,7 +223,7 @@ function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorD
onChange,
extensions: [
javascript(),
materialPalenight,
theme || materialPalenight,
highlightField,
flashField
]
@@ -288,8 +294,13 @@ function useCycle(props) {
await Tone.start();
Tone.getTransport().start('+0.1');
};
const stop = () => {
Tone.getTransport().pause();
const stop = (setZero = false) => {
if (setZero) {
Tone.getTransport().cancel();
Tone.getTransport().stop();
} else {
Tone.getTransport().pause();
}
setStarted(false);
};
const toggle = () => (started ? stop() : start());
@@ -351,13 +362,15 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
}
}, [activeCode, onDrawProp]);
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
// cycle hook to control scheduling
const cycle = useCycle({
onDraw,
onEvent: useCallback(
(time, event, currentTime) => {
try {
onEvent?.(event);
onEvent?.(time, event, currentTime);
if (event.context.logs?.length) {
event.context.logs.forEach(pushLog);
}
@@ -366,8 +379,10 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
} else {
throw new Error('no defaultSynth passed to useRepl.');
} else if (!onEvent) {
throw new Error(
'no defaultSynth nor onEvent passed to useRepl + event has no onTrigger. nothing happens',
);
}
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
@@ -408,16 +423,16 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
});
const activateCode = useCallback(
async (_code = code) => {
async (_code = code, evaluateOnly = false) => {
if (activeCode && !dirty) {
setError(undefined);
cycle.start();
!evaluateOnly && cycle.start();
return;
}
try {
setPending(true);
const parsed = await evaluate(_code);
cycle.start();
!evaluateOnly && cycle.start();
broadcast({ type: 'start', from: id });
setPattern(() => parsed.pattern);
if (autolink) {
@@ -448,7 +463,22 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
}
};
const stop = () => {
cycle.stop(true);
setActiveCode(undefined);
};
const evaluateOnly = () => {
activateCode(code, true);
};
useEffect(() => {
return () => stop();
}, []);
return {
hideHeader,
hideConsole,
pending,
code,
setCode,
@@ -459,8 +489,10 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
dirty,
log,
togglePlay,
stop,
setActiveCode,
activateCode,
evaluateOnly,
activeCode,
pushLog,
hash,
@@ -559,16 +591,25 @@ function Icon({ type }) {
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__ */ React.createElement("path", {
fillRule: "evenodd",
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z",
clipRule: "evenodd"
})
}[type]);
}
function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } = useRepl({
tune,
defaultSynth,
autolink: false
autolink: false,
onEvent
});
useEffect(() => {
init && evaluateOnly();
}, [tune, init]);
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01
@@ -580,7 +621,25 @@ function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
useHighlighting({ view, pattern, active: cycle.started });
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes("strudel disable-highlighting") });
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === "Enter") {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === "Period") {
cycle.stop();
e.preventDefault();
}
}
};
window.addEventListener("keydown", handleKeyPress, true);
return () => window.removeEventListener("keydown", handleKeyPress, true);
}
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
return /* @__PURE__ */ React.createElement("div", {
className: styles.container,
ref
@@ -598,11 +657,17 @@ function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
onClick: () => activateCode()
}, /* @__PURE__ */ React.createElement(Icon, {
type: "refresh"
})), /* @__PURE__ */ React.createElement("button", {
className: cx(styles.button),
onClick: () => stop(true)
}, /* @__PURE__ */ React.createElement(Icon, {
type: "stop"
}))), error && /* @__PURE__ */ React.createElement("div", {
className: styles.error
}, error.message)), /* @__PURE__ */ React.createElement("div", {
className: styles.body
}, show && /* @__PURE__ */ React.createElement(CodeMirror, {
theme,
value: code,
onChange: setCode,
onViewChanged: setView
@@ -60,7 +60,8 @@ const highlightField = StateField.define({
if (from > l || to > l) {
return; // dont mark outside of range, as it will throw an error
}
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
// const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
return mark.range(from, to);
}),
)
@@ -78,7 +79,7 @@ const highlightField = StateField.define({
provide: (f) => EditorView.decorations.from(f),
});
export default function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
export default function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount, theme }) {
return (
<>
<_CodeMirror
@@ -92,7 +93,7 @@ export default function CodeMirror({ value, onChange, onViewChanged, onCursor, o
onChange={onChange}
extensions={[
javascript(),
materialPalenight,
theme || materialPalenight,
highlightField,
flashField,
// theme, language, ...
+7
View File
@@ -26,6 +26,13 @@ export function Icon({ type }) {
clipRule="evenodd"
/>
),
stop: (
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z"
clipRule="evenodd"
/>
),
}[type]
}
</svg>
+40 -11
View File
@@ -1,20 +1,25 @@
import React, { useState, useMemo, useRef } from 'react';
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
import { useInView } from 'react-hook-inview';
import useRepl from '../hooks/useRepl.mjs';
import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs';
import CodeMirror6 from './CodeMirror6';
import CodeMirror6, { flash } from './CodeMirror6';
import 'tailwindcss/tailwind.css';
import './style.css';
import styles from './MiniRepl.module.css';
import { Icon } from './Icon';
export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
tune,
defaultSynth,
autolink: false,
});
export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
useRepl({
tune,
defaultSynth,
autolink: false,
onEvent,
});
useEffect(() => {
init && evaluateOnly();
}, [tune, init]);
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01,
@@ -26,7 +31,28 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
useHighlighting({ view, pattern, active: cycle.started });
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes('strudel disable-highlighting') });
// set active pattern on ctrl+enter
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === 'Period') {
cycle.stop();
e.preventDefault();
}
}
};
window.addEventListener('keydown', handleKeyPress, true);
return () => window.removeEventListener('keydown', handleKeyPress, true);
}
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
return (
<div className={styles.container} ref={ref}>
<div className={styles.header}>
@@ -37,11 +63,14 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
<button className={cx(dirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
<Icon type="refresh" />
</button>
<button className={cx(styles.button)} onClick={() => stop(true)}>
<Icon type="stop" />
</button>
</div>
{error && <div className={styles.error}>{error.message}</div>}
</div>
<div className={styles.body} >
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
<div className={styles.body}>
{show && <CodeMirror6 theme={theme} value={code} onChange={setCode} onViewChanged={setView} />}
</div>
</div>
);
+7 -2
View File
@@ -66,8 +66,13 @@ function useCycle(props) {
await Tone.start();
Tone.getTransport().start('+0.1');
};
const stop = () => {
Tone.getTransport().pause();
const stop = (setZero = false) => {
if (setZero) {
Tone.getTransport().cancel();
Tone.getTransport().stop();
} else {
Tone.getTransport().pause();
}
setStarted(false);
};
const toggle = () => (started ? stop() : start());
+28 -7
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 { useCallback, useState, useMemo } from 'react';
import { useCallback, useState, useMemo, useEffect } from 'react';
import { evaluate } from '@strudel.cycles/eval';
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
import useCycle from './useCycle.mjs';
@@ -36,13 +36,15 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
}
}, [activeCode, onDrawProp]);
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
// cycle hook to control scheduling
const cycle = useCycle({
onDraw,
onEvent: useCallback(
(time, event, currentTime) => {
try {
onEvent?.(event);
onEvent?.(time, event, currentTime);
if (event.context.logs?.length) {
event.context.logs.forEach(pushLog);
}
@@ -51,8 +53,10 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
} else {
throw new Error('no defaultSynth passed to useRepl.');
} else if (!onEvent) {
throw new Error(
'no defaultSynth nor onEvent passed to useRepl + event has no onTrigger. nothing happens',
);
}
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
@@ -93,16 +97,16 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
});
const activateCode = useCallback(
async (_code = code) => {
async (_code = code, evaluateOnly = false) => {
if (activeCode && !dirty) {
setError(undefined);
cycle.start();
!evaluateOnly && cycle.start();
return;
}
try {
setPending(true);
const parsed = await evaluate(_code);
cycle.start();
!evaluateOnly && cycle.start();
broadcast({ type: 'start', from: id });
setPattern(() => parsed.pattern);
if (autolink) {
@@ -135,7 +139,22 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
}
};
const stop = () => {
cycle.stop(true);
setActiveCode(undefined);
};
const evaluateOnly = () => {
activateCode(code, true);
};
useEffect(() => {
return () => stop();
}, []);
return {
hideHeader,
hideConsole,
pending,
code,
setCode,
@@ -146,8 +165,10 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
dirty,
log,
togglePlay,
stop,
setActiveCode,
activateCode,
evaluateOnly,
activeCode,
pushLog,
hash,
@@ -36,7 +36,13 @@ export const materialPalenightTheme = EditorView.theme(
lineHeight: '22px',
},
'.cm-line': {
background: '#2C323699',
// background: '#2C323699',
background: 'transparent',
},
'.cm-line > *': {
// background: '#2C323699',
background: '#00000090',
// background: 'transparent',
},
// done
'&.cm-focused .cm-cursor': {
@@ -61,7 +67,7 @@ export const materialPalenightTheme = EditorView.theme(
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-activeLine': { backgroundColor: cursor + '50' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
+24 -14
View File
@@ -22,13 +22,17 @@ Pattern.prototype.pianoroll = function ({
flipTime = 0,
flipValues = 0,
hideNegative = false,
inactive = '#C9E597',
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
minMidi = 10,
maxMidi = 90,
autorange = 0,
autorange = 1,
timeframe: timeframeProp,
fold = 0,
vertical = 0,
@@ -58,12 +62,14 @@ Pattern.prototype.pianoroll = function ({
flipTime && timeRange.reverse();
flipValues && valueRange.reverse();
const playheadPosition = scale(-from / timeExtent, ...timeRange);
this.draw(
(ctx, events, t) => {
ctx.fillStyle = background;
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
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 <= t + to && event.whole.end >= t + from;
events.filter(inFrame).forEach((event) => {
@@ -71,15 +77,6 @@ Pattern.prototype.pianoroll = function ({
ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
ctx.beginPath();
if (vertical) {
ctx.moveTo(0, playheadPosition);
ctx.lineTo(valueAxis, playheadPosition);
} else {
ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
@@ -107,6 +104,19 @@ Pattern.prototype.pianoroll = function ({
}
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();
},
{
from: from - overscan,
+5 -1
View File
@@ -62,7 +62,11 @@ Pattern.prototype.tone = function (instrument) {
} else if (instrument instanceof NoiseSynth) {
instrument.triggerAttackRelease(hap.duration.valueOf(), time); // noise has no value
} else if (instrument instanceof Piano) {
note = getPlayableNoteValue(hap);
// note = getPlayableNoteValue(hap); // piano does not understand frequencies
note = hap.value;
if (typeof note === 'number') {
note = midi2note(note);
}
instrument.keyDown({ note, time, velocity });
instrument.keyUp({ note, time: time + hap.duration.valueOf(), velocity });
} else if (instrument instanceof Sampler) {
+138 -134
View File
@@ -126,146 +126,150 @@ const splitSN = (s, n) => {
Pattern.prototype.out = function () {
return this.onTrigger(async (t, hap, ct) => {
const ac = getAudioContext();
// calculate correct time (tone.js workaround)
t = ac.currentTime + t - ct;
// destructure value
let {
freq,
s,
sf,
clip = 0, // if 1, samples will be cut off when the hap ends
n = 0,
note,
gain = 1,
cutoff,
resonance = 1,
hcutoff,
hresonance = 1,
bandf,
bandq = 1,
pan,
attack = 0.001,
decay = 0.05,
sustain = 0.5,
release = 0.001,
speed = 1, // sample playback speed
begin = 0,
end = 1,
} = hap.value;
const { velocity = 1 } = hap.context;
gain *= velocity; // legacy fix for velocity
// the chain will hold all audio nodes that connect to each other
const chain = [];
if (typeof s === 'string') {
[s, n] = splitSN(s, n);
}
if (typeof note === 'string') {
[note, n] = splitSN(note, n);
}
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
// with synths, n and note are the same thing
n = note || n;
if (typeof n === 'string') {
n = toMidi(n); // e.g. c3 => 48
try {
const ac = getAudioContext();
// calculate correct time (tone.js workaround)
t = ac.currentTime + t - ct;
// destructure value
let {
freq,
s,
sf,
clip = 0, // if 1, samples will be cut off when the hap ends
n = 0,
note,
gain = 1,
cutoff,
resonance = 1,
hcutoff,
hresonance = 1,
bandf,
bandq = 1,
pan,
attack = 0.001,
decay = 0.05,
sustain = 0.5,
release = 0.001,
speed = 1, // sample playback speed
begin = 0,
end = 1,
} = hap.value;
const { velocity = 1 } = hap.context;
gain *= velocity; // legacy fix for velocity
// the chain will hold all audio nodes that connect to each other
const chain = [];
if (typeof s === 'string') {
[s, n] = splitSN(s, n);
}
// get frequency
if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48);
if (typeof note === 'string') {
[note, n] = splitSN(note, n);
}
// make oscillator
const o = getOscillator({ t, s, freq, duration: hap.duration, release });
chain.push(o);
// level down oscillators as they are really loud compared to samples i've tested
const g = ac.createGain();
g.gain.value = 0.3;
chain.push(g);
// TODO: make adsr work with samples without pops
// envelope
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hap.duration);
chain.push(adsr);
} else {
// load sample
if (speed === 0) {
// no playback
return;
}
if (!s) {
console.warn('no sample specified');
return;
}
const soundfont = getSoundfontKey(s);
let bufferSource;
try {
if (soundfont) {
// is soundfont
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
} else {
// is sample from loaded samples(..)
bufferSource = await getSampleBufferSource(s, n, note);
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
// with synths, n and note are the same thing
n = note || n;
if (typeof n === 'string') {
n = toMidi(n); // e.g. c3 => 48
}
} catch (err) {
console.warn(err);
return;
}
// asny stuff above took too long?
if (ac.currentTime > t) {
console.warn('sample still loading:', s, n);
return;
}
if (!bufferSource) {
console.warn('no buffer source');
return;
}
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
// TODO: nudge, unit, cut, loop
let duration = soundfont || clip ? hap.duration : bufferSource.buffer.duration;
// let duration = bufferSource.buffer.duration;
const offset = begin * duration;
duration = ((end - begin) * duration) / Math.abs(speed);
if (soundfont || clip) {
bufferSource.start(t, offset); // duration does not work here for some reason
// get frequency
if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48);
}
// make oscillator
const o = getOscillator({ t, s, freq, duration: hap.duration, release });
chain.push(o);
// level down oscillators as they are really loud compared to samples i've tested
const g = ac.createGain();
g.gain.value = 0.3;
chain.push(g);
// TODO: make adsr work with samples without pops
// envelope
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hap.duration);
chain.push(adsr);
} else {
bufferSource.start(t, offset, duration);
}
chain.push(bufferSource);
if (soundfont || clip) {
const env = ac.createGain();
const releaseLength = 0.1;
env.gain.value = 0.6;
env.gain.setValueAtTime(env.gain.value, t + duration);
env.gain.linearRampToValueAtTime(0, t + duration + releaseLength);
// env.gain.linearRampToValueAtTime(0, t + duration + releaseLength);
chain.push(env);
bufferSource.stop(t + duration + releaseLength);
} else {
bufferSource.stop(t + duration);
}
}
// master out
const master = ac.createGain();
master.gain.value = gain;
chain.push(master);
// load sample
if (speed === 0) {
// no playback
return;
}
if (!s) {
console.warn('no sample specified');
return;
}
const soundfont = getSoundfontKey(s);
let bufferSource;
// filters
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
// TODO vowel
// TODO delay / delaytime / delayfeedback
// panning
if (pan !== undefined) {
const panner = ac.createStereoPanner();
panner.pan.value = 2 * pan - 1;
chain.push(panner);
}
// master out
/* const master = ac.createGain();
try {
if (soundfont) {
// is soundfont
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
} else {
// is sample from loaded samples(..)
bufferSource = await getSampleBufferSource(s, n, note);
}
} catch (err) {
console.warn(err);
return;
}
// asny stuff above took too long?
if (ac.currentTime > t) {
console.warn('sample still loading:', s, n);
return;
}
if (!bufferSource) {
console.warn('no buffer source');
return;
}
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
// TODO: nudge, unit, cut, loop
let duration = soundfont || clip ? hap.duration : bufferSource.buffer.duration;
// let duration = bufferSource.buffer.duration;
const offset = begin * duration;
duration = ((end - begin) * duration) / Math.abs(speed);
if (soundfont || clip) {
bufferSource.start(t, offset); // duration does not work here for some reason
} else {
bufferSource.start(t, offset, duration);
}
chain.push(bufferSource);
if (soundfont || clip) {
const env = ac.createGain();
const releaseLength = 0.1;
env.gain.value = 0.6;
env.gain.setValueAtTime(env.gain.value, t + duration);
env.gain.linearRampToValueAtTime(0, t + duration + releaseLength);
// env.gain.linearRampToValueAtTime(0, t + duration + releaseLength);
chain.push(env);
bufferSource.stop(t + duration + releaseLength);
} else {
bufferSource.stop(t + duration);
}
}
// master out
const master = ac.createGain();
master.gain.value = gain;
chain.push(master);
// filters
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
// TODO vowel
// TODO delay / delaytime / delayfeedback
// panning
if (pan !== undefined) {
const panner = ac.createStereoPanner();
panner.pan.value = 2 * pan - 1;
chain.push(panner);
}
// master out
/* const master = ac.createGain();
master.gain.value = 0.8 * gain;
chain.push(master); */
chain.push(ac.destination);
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
chain.push(ac.destination);
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
} catch (e) {
console.warn('.out error:', e);
}
});
};
+3 -1
View File
@@ -23,4 +23,6 @@ dist-ssr
*.sln
*.sw?
oldtunes.mjs
oldtunes.mjs
public/samples/EMU World
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -20,7 +20,7 @@ body {
.darken::before {
content: ' ';
position: absolute;
position: fixed;
top: 0;
left: 0;
width: 100vw;
+129 -120
View File
@@ -108,6 +108,8 @@ function App() {
pattern,
pushLog,
pending,
hideHeader,
hideConsole,
} = useRepl({
tune: '// LOADING...',
defaultSynth,
@@ -167,142 +169,149 @@ function App() {
return (
<div className="min-h-screen flex flex-col">
<header
id="header"
className={cx(
'flex-none w-full px-2 flex border-b border-gray-200 justify-between z-[10] bg-gray-100',
isEmbedded ? 'h-8' : 'h-14',
)}
>
<div className="flex items-center space-x-2">
<img src={logo} className={cx('Tidal-logo', isEmbedded ? 'w-6 h-6' : 'w-10 h-10')} alt="logo" />
<h1 className={isEmbedded ? 'text-l' : 'text-xl'}>Strudel {isEmbedded ? 'Mini ' : ''}REPL</h1>
</div>
<div className="flex">
<button
onClick={() => {
getAudioContext().resume(); // fixes no sound in ios webkit
togglePlay();
}}
className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
>
{!pending ? (
<span className={cx('flex items-center', isEmbedded ? 'w-16' : 'w-16')}>
{cycle.started ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<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"
/>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<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"
/>
</svg>
)}
{cycle.started ? 'pause' : 'play'}
</span>
) : (
<>loading...</>
)}
</button>
{!isEmbedded && (
{!hideHeader && (
<header
id="header"
className={cx(
'flex-none w-full px-2 flex border-b border-gray-200 justify-between z-[10] bg-gray-100',
isEmbedded ? 'h-8' : 'h-14',
)}
>
<div className="flex items-center space-x-2">
<img src={logo} className={cx('Tidal-logo', isEmbedded ? 'w-6 h-6' : 'w-10 h-10')} alt="logo" />
<h1 className={isEmbedded ? 'text-l' : 'text-xl'}>Strudel {isEmbedded ? 'Mini ' : ''}REPL</h1>
</div>
<div className="flex">
<button
className="hover:bg-gray-300 p-2"
onClick={async () => {
const _code = getRandomTune();
console.log('tune', _code); // uncomment this to debug when random code fails
setCode(_code);
cleanupDraw();
cleanupUi();
resetLoadedSamples();
prebake();
const parsed = await evaluate(_code);
setPattern(parsed.pattern);
setActiveCode(_code);
onClick={() => {
getAudioContext().resume(); // fixes no sound in ios webkit
togglePlay();
}}
className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
>
🎲 random
{!pending ? (
<span className={cx('flex items-center', isEmbedded ? 'w-16' : 'w-16')}>
{cycle.started ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<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"
/>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<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"
/>
</svg>
)}
{cycle.started ? 'pause' : 'play'}
</span>
) : (
<>loading...</>
)}
</button>
)}
{!isEmbedded && (
<button className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}>
<a href="./tutorial">📚 tutorial</a>
</button>
)}
{!isEmbedded && (
<button
className={cx('cursor-pointer hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
onClick={async () => {
const codeToShare = activeCode || code;
if (lastShared === codeToShare) {
// alert('Link already generated!');
pushLog(`Link already generated!`);
return;
}
// generate uuid in the browser
const hash = nanoid(12);
const { data, error } = await supabase.from('code').insert([{ code: codeToShare, hash }]);
if (!error) {
setLastShared(activeCode || code);
const shareUrl = window.location.origin + '?' + hash;
// copy shareUrl to clipboard
navigator.clipboard.writeText(shareUrl);
const message = `Link copied to clipboard: ${shareUrl}`;
// alert(message);
pushLog(message);
} else {
console.log('error', error);
const message = `Error: ${error.message}`;
// alert(message);
pushLog(message);
}
}}
>
📣 share{lastShared && lastShared === (activeCode || code) ? 'd!' : ''}
</button>
)}
{isEmbedded && (
<button className={cx('hover:bg-gray-300 px-2')}>
<a href={window.location.href} target="_blank" rel="noopener noreferrer" title="Open in REPL">
🚀 open
</a>
</button>
)}
{isEmbedded && (
<button className={cx('hover:bg-gray-300 px-2')}>
<a
onClick={() => {
window.location.href = initialUrl;
window.location.reload();
{!isEmbedded && (
<button
className="hover:bg-gray-300 p-2"
onClick={async () => {
const _code = getRandomTune();
console.log('tune', _code); // uncomment this to debug when random code fails
setCode(_code);
cleanupDraw();
cleanupUi();
resetLoadedSamples();
prebake();
const parsed = await evaluate(_code);
setPattern(parsed.pattern);
setActiveCode(_code);
}}
title="Reset"
>
💔 reset
</a>
</button>
)}
</div>
</header>
🎲 random
</button>
)}
{!isEmbedded && (
<button className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}>
<a href="./tutorial">📚 tutorial</a>
</button>
)}
{!isEmbedded && (
<button
className={cx('cursor-pointer hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
onClick={async () => {
const codeToShare = activeCode || code;
if (lastShared === codeToShare) {
// alert('Link already generated!');
pushLog(`Link already generated!`);
return;
}
// generate uuid in the browser
const hash = nanoid(12);
const { data, error } = await supabase.from('code').insert([{ code: codeToShare, hash }]);
if (!error) {
setLastShared(activeCode || code);
const shareUrl = window.location.origin + '?' + hash;
// copy shareUrl to clipboard
navigator.clipboard.writeText(shareUrl);
const message = `Link copied to clipboard: ${shareUrl}`;
// alert(message);
pushLog(message);
} else {
console.log('error', error);
const message = `Error: ${error.message}`;
// alert(message);
pushLog(message);
}
}}
>
📣 share{lastShared && lastShared === (activeCode || code) ? 'd!' : ''}
</button>
)}
{isEmbedded && (
<button className={cx('hover:bg-gray-300 px-2')}>
<a href={window.location.href} target="_blank" rel="noopener noreferrer" title="Open in REPL">
🚀 open
</a>
</button>
)}
{isEmbedded && (
<button className={cx('hover:bg-gray-300 px-2')}>
<a
onClick={() => {
window.location.href = initialUrl;
window.location.reload();
}}
title="Reset"
>
💔 reset
</a>
</button>
)}
</div>
</header>
)}
<section className="grow flex flex-col text-gray-100">
<div className="grow relative flex overflow-auto" id="code">
<div className="grow relative flex overflow-auto pb-8 cursor-text" id="code">
{/* onCursor={markParens} */}
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
<span className="z-[20] py-1 px-2 absolute top-0 right-0 text-xs whitespace-pre text-right pointer-events-none">
<span className="z-[20] bg-black rounded-t-md py-1 px-2 fixed bottom-0 right-1 text-xs whitespace-pre text-right pointer-events-none">
{!cycle.started ? `press ctrl+enter to play\n` : dirty ? `ctrl+enter to update\n` : 'no changes\n'}
</span>
{error && (
<div className={cx('absolute right-2 bottom-2 px-2', 'text-red-500')}>
<div
className={cx(
'rounded-md fixed pointer-events-none left-2 bottom-1 text-xs bg-black px-2 z-[20]',
'text-red-500',
)}
>
{error?.message || 'unknown error'}
</div>
)}
</div>
{!isEmbedded && (
{!isEmbedded && !hideConsole && (
<textarea
className="z-[10] h-16 border-0 text-xs bg-[transparent] border-t border-slate-600 resize-none"
value={log}
+143 -7
View File
@@ -1,4 +1,4 @@
import { Pattern, toMidi } from '@strudel.cycles/core';
import { Pattern, silence, toMidi } from '@strudel.cycles/core';
import { samples } from '@strudel.cycles/webaudio';
export function prebake() {
@@ -46,15 +46,151 @@ export function prebake() {
.then(json => samples(json, './EmuSP12/'));
}
samples({
jazzbass: {
_base: './samples/jazzbass/moog_',
c1: 'c2.mp3',
e1: 'e2.mp3',
a1: 'a2.mp3',
c2: 'c3.mp3',
e2: 'e3.mp3',
a2: 'a3.mp3',
c3: 'c4.mp3',
},
stage73: {
_base: './samples/stage73/',
c2: ['quiet/c2.mp3', 'loud/c2.mp3'],
e2: ['quiet/e2.mp3', 'loud/e2.mp3'],
a2: ['quiet/a2.mp3', 'loud/a2.mp3'],
c3: ['quiet/c3.mp3', 'loud/c3.mp3'],
e3: ['quiet/e3.mp3', 'loud/e3.mp3'],
a3: ['quiet/a3.mp3', 'loud/a3.mp3'],
c4: ['quiet/c4.mp3', 'loud/c4.mp3'],
e4: ['quiet/e4.mp3', 'loud/e4.mp3'],
a4: ['quiet/a4.mp3', 'loud/a4.mp3'],
c5: ['quiet/c5.mp3', 'loud/c5.mp3'],
e5: ['quiet/e5.mp3', 'loud/e5.mp3'],
a5: ['quiet/a5.mp3', 'loud/a5.mp3'],
c6: ['quiet/c6.mp3', 'loud/c6.mp3'],
},
flute: {
_base: './samples/flute/',
g2: 'g2.mp3',
c3: 'c3.mp3',
f3: 'f3.mp3',
c4: 'c4.mp3',
f4: 'f4.mp3',
c5: 'c5.mp3',
},
});
console.log('bake...');
samples(
{
bd: 'bd.mp3',
sd: ['sd.mp3', 'sd2.mp3'],
hh: ['hh.mp3'],
snap: ['snap.mp3'],
oh: ['oh.mp3'],
},
'./samples/president/president_',
);
const times = (length, func) => Array.from({ length }, (_, i) => func(i));
samples({ earth: times((i) => `Earth Kit (${i + 1}).wav`) }, './samples/EMU World/Earth Kit/');
samples({ bottle: times((i) => `Bottle (${i + 1}).wav`) }, './samples/EMU World/Bottle/');
samples({ shekere: times(32, (i) => `Shekere (${i + 1}).wav`) }, './samples/EMU World/Shekere/');
samples({ block: times(5, (i) => `Block (${i + 1}).wav`) }, './samples/EMU World/Block/');
samples(
{
'Brazilian Kit': [
'BK Agogo 2.wav',
'BK Agogo 3.wav',
'BK Agogo.wav',
'BK Bass Drum.wav',
'BK Bass Tom.wav',
'BK Block 2.wav',
'BK Block 3.wav',
'BK Block.wav',
'BK ClapTamb 2.wav',
'BK ClapTamb.wav',
'BK Conga 2.wav',
'BK Conga.wav',
'BK Cowbell 2.wav',
'BK Cowbell 3.wav',
'BK Cowbell.wav',
'BK CrashTamb.wav',
'BK Cup 2.wav',
'BK Cup.wav',
'BK Cymb 2.wav',
'BK Cymb 3.wav',
'BK Cymbal.wav',
'BK High Tom 2.wav',
'BK High Tom.wav',
'BK Low Tom 2.wav',
'BK Low Tom.wav',
'BK Mid Tom 2.wav',
'BK Mid Tom.wav',
'BK Mute Cymb.wav',
'BK Rattle.wav',
'BK Ride 2.wav',
'BK Ride.wav',
'BK Rimshot.wav',
'BK Scraper 2.wav',
'BK Scraper.wav',
'BK Shaker 2.wav',
'BK Shaker 3.wav',
'BK Shaker.wav',
'BK Snare.wav',
'BK SnareTamb 2.wav',
'BK SnareTamb.wav',
'BK Triangle 2.wav',
'BK Triangle 3.wav',
'BK Triangle 4.wav',
'BK Triangle 5.wav',
'BK Triangle 6.wav',
'BK Triangle.wav',
'BK Whistle 2.wav',
'BK Whistle 3.wav',
'BK Whistle.wav',
'BK Woop 2.wav',
'BK Woop.wav',
],
},
'./samples/EMU World/Brazilian Kit/',
);
const maxPan = toMidi('C8');
const panwidth = (pan, width) => pan * width + (1 - width) / 2;
Pattern.prototype.piano = function () {
return this.clip(1)
.s('piano')
.fmap((value) => {
Pattern.prototype.panByPitch = function () {
return this.fmap((value) => {
try {
// pan by pitch
const pan = panwidth(Math.min(toMidi(value.note) / maxPan, 1), 0.5);
const pan = panwidth(Math.min(toMidi(value.note || value.n) / maxPan, 1), 0.5);
return { ...value, pan: (value.pan || 1) * pan };
});
} catch (e) {
console.log('.panByPitch error', e);
return silence;
}
});
};
Pattern.prototype.piano = function () {
return this.clip(1).s('piano').panByPitch().gain(0.6);
};
Pattern.prototype.rhodes = function () {
return this.clip(1).s('stage73').panByPitch(); //.gain(1.5);
};
Pattern.prototype.jazzbass = function () {
return this.clip(1).s('jazzbass').gain(0.6);
};
Pattern.prototype.flute = function () {
return this.clip(1).s('flute');
};
Pattern.prototype.block = function () {
return this.clip(1).s('block');
};
+109
View File
@@ -1019,3 +1019,112 @@ x=>x.add(7).color('steelblue')
.stack(s("bd:1*2,~ sd:0,[~ hh:0]*2"))
.out()
.pianoroll({vertical:1})`;
export const root73 = `const root = 'E'
stack(
stack(
s("<bd(5,12) bd(3,12)>"),
s("hh!6").echo(2,1/12+.02,.2), // .begin(0.04),
s("~!5 oh"), // hush(),
s("~ <sd!7 [sd@3 sd@2 sd]>")
).slow(1) //.hcutoff(2000)
,
"<0(3,9) [2 <-3 3>]>"
.scale(root+'2 minor')
.legato(1).note().fast(2).s('jazzbass'),
"0 2 4 1".euclid(7,12)
.off(1/16, x=>x.add(2).color('steelblue'))
.off(1/12, x=>x.add(7).color('orange'))
.degradeBy(0.3)
.slow(4)
.scale(root+'4 minor').note()
.gain(.9)
.s('stage73').n(rand.range(0,.7).round())
.legato(perlin.range(.1,.5))
.velocity(perlin.range(.5,.8))
.echo(4, 1/6, .5), //.hcutoff(1000).hresonance(10),
mini(root+'m7@5 <Ab7 Eb7>').slow(2).voicings().transpose(0)
//.euclid(5,12,1)
.euclidLegato(5,12,1)
.note()
.gain(0.5)
.echo(1, 1/12, .5)
.slow(1)
.s('stage73')
.legato(.1)
.color('tomato')
.cutoff("300@2 600".fast(2))
//.resonance("10 20 25 10")
.n(1)
).slow(4/2).clip(1)
.out()
.pianoroll({autorange:0,vertical:1})
// strudel disable-highlighting`;
export const flute73 = `
const harmony = x=>x.scale(cat('E major','F# minor').slow(4)).note().clip(1)
stack(
// flute
"<0 2 5 4>"
.off(1/6, add(2))
.off(1/12, add(5))
.layer(harmony)
.legato("<1 .1>/16")
.echo("<1 3>/16",1/3,.5)
.s('flute')
.cutoff(perlin.range(500,2000))
.color('darkseagreen')
,
// bass
"<-7 -5 -9 -5>(3,9)"
.legato("<2.5 1.5>/16")
.layer(harmony)
.gain("1 .8 1")
.s('stage73')
.color('brown'),
// drums
s("bd*3, [~@2 <hh!3 hh*3>]*2,~ snap".slow(2)).gain(.4)
)
//.hcutoff(800)
.out()
.pianoroll({vertical:1,autorange:0})
`;
export const frequencyTest = `sequence(880, [440, 660], 440, 660)
.div(slowcat(3,2).slow(2))
.off(1/8,mul(2))
.off(1/4,mul(3))
.freq()
.legato(2)
.slow(2)
.s('sawtooth').cutoff(1000)
.out()
`;
export const gameboyParty = `
stack(
freq(
sequence(1,2).mul(55/2) // frequencies
.mul(slowcat(1,2))
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
.fast(3)
.velocity(.5)
)
.s(cat('sawtooth','square').fast(2))
.attack(.01).decay(.02).sustain(.5).release(.1)
.cutoff(2000) //.resonance(25)
,
freq(
sequence(1,[3/2,4/3],5/3)
.off(1/6, x=>x.mul(2).velocity(.5))
.iter(3)
.fast(1)
.mul(220)
)
.gain(.5)
.s(cat('sawtooth','square').slow(8))
.legato(sine.slow(16).add(.5).div(2))
.echo(2,1/12,.6)
.attack(.01).decay(.02).sustain(.5).release(.1)
)
.out()`;
+26
View File
@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
samples
+6
View File
@@ -0,0 +1,6 @@
function ImgTile({ src, width, height }) {
return (
<div style={{ background: `url(${src})`, backgroundSize: 'cover', backgroundPosition: 'center', width, height }} />
);
}
export default ImgTile;
+95
View File
@@ -0,0 +1,95 @@
import { evalScope } from '@strudel.cycles/eval';
import { MiniRepl as _MiniRepl } from '@strudel.cycles/react';
import controls from '@strudel.cycles/core/controls.mjs';
import { loadWebDirt } from '@strudel.cycles/webdirt';
import { materialPalenightLarge } from './materialPalenightThemeLarge';
import { useState, useEffect } from 'react';
import { cleanupDraw, cleanupUi, Tone, piano } from '@strudel.cycles/tone';
import { midi2note } from '@strudel.cycles/core';
import { prebake } from '../repl/src/prebake.mjs';
let defaultPiano;
piano().then((instrument) => {
defaultPiano = instrument.toDestination();
});
const init = evalScope(
Tone,
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tone'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
import('@strudel.cycles/webdirt'),
);
loadWebDirt({
sampleMapUrl: './samples/EmuSP12.json',
sampleFolder: './samples/EmuSP12/',
});
prebake();
export function MaxiRepl({ code, canvasHeight = 500 }) {
const [exampleIndex, setExampleIndex] = useState(0);
const examples = Array.isArray(code) ? code : [code];
const [ready, setReady] = useState(false);
useEffect(() => {
init.then(() => setReady(true));
}, []);
useEffect(() => {
cleanupDraw();
cleanupUi();
prebake();
}, [exampleIndex]);
return (
<div className="text-left block max-w-screen relative">
{examples.length > 1 && (
<div className="space-x-2 absolute left-[200px] top-[-3px]">
{examples.map((c, i) => (
<button
key={i}
onClick={() => setExampleIndex(i)}
className={`rounded-full ${exampleIndex === i ? 'bg-gray-200 w-5 h-5' : 'bg-gray-500 w-5 h-5'}`}
></button>
))}
</div>
)}
<_MiniRepl
enableKeyboard={true}
key={exampleIndex}
tune={examples[exampleIndex]}
hideOutsideView={true}
theme={materialPalenightLarge}
init={ready}
onEvent={(time, hap) => {
if (hap.context.onTrigger) {
// dont need default synth
return;
}
let velocity = hap.context?.velocity ?? 0.75;
note = hap.value;
if (typeof note === 'number') {
note = midi2note(note);
}
defaultPiano.keyDown({ note, time, velocity });
defaultPiano.keyUp({ note, time: time + hap.duration.valueOf(), velocity });
}}
/>
<canvas
id="test-canvas"
className="w-full pointer-events-none"
height={canvasHeight}
ref={(el) => {
if (el) {
el.width = el.clientWidth;
}
}}
></canvas>
</div>
);
}
+6
View File
@@ -0,0 +1,6 @@
```sh
npm create vite@latest slides -- --template react
cd slides
npm i vite-plugin-mdx --save-dev
npm run dev
```
+130
View File
@@ -0,0 +1,130 @@
import { useState, useLayoutEffect, useEffect, useCallback, useMemo } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
const getUrlIndex = () => parseInt(window.location.hash.split('#')[1] || 0, 10);
export function Slides({ children }) {
const visible = useMemo(() => children.filter((child) => !child.props.hidden), [children]);
const [slide, setSlide] = useState(getUrlIndex() % visible.length);
useEffect(() => {
window.location.hash = '#' + slide;
}, [slide]);
const next = useCallback(() => setSlide((s) => (s + 1) % visible.length), [visible]);
const prev = useCallback(() => setSlide((s) => (s + visible.length - 1) % visible.length), [visible]);
useLayoutEffect(() => {
const handleKeyPress = async (e) => {
if (e.ctrlKey && e.altKey) {
if (e.code === 'ArrowRight') {
next();
} else if (e.code === 'ArrowLeft') {
prev();
}
e.preventDefault();
}
};
window.addEventListener('keydown', handleKeyPress, true);
return () => window.removeEventListener('keydown', handleKeyPress, true);
}, [slide]);
return (
<div className="bg-slate-900">
<AnimatePresence exitBeforeEnter>
<motion.div
key={slide}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="w-screen h-screen"
>
<div className="flex justify-center px-6 min-h-full overflow-auto">
<div
className={`px-8 prose grow max-w-[1280px]
text-gray-200 font-serif
prose-headings:text-gray-200
prose-a:text-indigo-300
prose-blockquote:text-gray-200
prose-em:text-[1.3em]
prose-strong:text-gray-200
prose-li:text-[1.4em]
prose-headings:font-sans
prose-code:text-gray-300
prose-headings:mt-12
prose-2xl
prose-slate`}
>
<center>{visible[slide]}</center>
</div>
</div>
</motion.div>
</AnimatePresence>
<SlideNav onNext={next} onPrev={prev} slides={visible} />
</div>
);
}
export function Slide({ children }) {
return children;
}
function SlideNav({ onNext, onPrev }) {
return (
<div className="flex absolute top-1/2 space-x-2 w-full justify-between items-center z-1">
<button onClick={() => onPrev()} className="absolute left-0 my-auto">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-12 w-12 text-gray-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
</button>
<button onClick={() => onNext()} className="absolute right-0 my-auto">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-12 w-12 text-gray-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
);
}
/*
<div className="flex absolute bottom-8 space-x-2 w-full justify-center items-center z-1">
<button onClick={() => setSlide((s) => (s + slides.length - 1) % slides.length)}>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-gray-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
</button>
{slides.map((_, i) => (
<div key={i} className={`rounded-full ${slide === i ? 'bg-gray-200 w-3 h-3' : 'bg-gray-500 w-2 h-2'}`}></div>
))}
<button onClick={() => setSlide((s) => (s + 1) % slides.length)}>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-gray-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
</div> */
+981
View File
@@ -0,0 +1,981 @@
import { Slides, Slide } from './Slides.jsx';
import { MaxiRepl } from './MaxiRepl';
import ImgTile from './ImgTile';
<Slides>
<Slide>
<div className="w-full h-full flex justify-center items-center">
<div>
# Strudel
## Algorithmic Patterns for the Web
![whirlpool](./strudel_icon.png)
Alex McLean, Felix Roos & Tidal Cycles Community
</div>
</div>
</Slide><Slide hidden>
<img src="./ada.jpg" className="h-[600px] rounded-md" />
</Slide><Slide hidden>
<img src="./ada.jpg" className="h-[400px] rounded-md" />
<div className="flex space-x-2 items-center">
<em className="hidden">
"The Analytical Engine weaves algebraic patterns, just as the Jacquard loom weaves flowers and leaves." - Ada
Lovelace
</em>
<em>
“[The Analytical Engine] might act upon other things besides <strong>number</strong>, were objects found whose
mutual fundamental relations could be expressed by those of the abstract science of <strong>operations</strong>{' '}
[...]
<span className="hidden">
, and which should be also susceptible of adaptations to the action of the operating notation and mechanism of the
engine.{' '}
</span>
<br />
Supposing, for instance, that the fundamental relations of pitched sounds in the science of harmony and of musical composition
were susceptible of such expression and adaptations, the [Analytical] Engine might compose elaborate and scientific pieces
of music of any degree of complexity or extent."
<br />
-- Ada Lovelace, (1815 - 1852)
</em>
</div>
</Slide><Slide>
## Strudel is..
<div className="flex">
<div className="text-left">
- zero-install live coding editor for the web
- Tidal Cycles port to JavaScript
- a terse language for algorithmic patterns
- for dynamic music sequencing
- algorithmic composition
- live improvisation
- a set of reusable packages
- probably more exciting than your regular DAW
</div>
![whirlpool](./strudel_icon.png)
</div>
</Slide><Slide hidden>
<img src="./codeandmusic.png" className="h-[700px] rounded-md -my-24" />
<div className="text-left">
- How to represent and control music with code?
- How to leverage unique features of computing?
- How to break loose from old conventions?
- How to improvise electronic music?
- Answer: Live Coding?
</div>
</Slide><Slide>
## About Me
<div className="text-left">
- 💻 working as a frontend developer by day, studied Computer Science
- 🎺 Trumpet Player, studied Jazz and Popular Music
- 💿 ocassionally publishes music with Lui Mafuta & Puste
- 🔌 Modular Synth & Electronics Lover, Dilettante DJ
- 💌 Coding Blog: [loophole-letters.vercel.app](https://loophole-letters.vercel.app/)
- 👀 Loves visualizations
<div className="grid grid-cols-5 gap-4 mt-12">
<ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Flambdoma.png&w=2048&q=75" />
{/* <ImgTile
height={300}
src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fharmonic-spiral.png&w=2048&q=75"
/> */}
<ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fpiano-roll.png&w=2048&q=75" />
<ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fcof.png&w=2048&q=75" />
<ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2F251graph.png&w=2048&q=75" />
<ImgTile
height={200}
src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fdiy-modular-synth%2Fmidi2cv-styleshot.png&w=2048&q=75"
/>
{/* <ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fcolorpiano.png&w=2048&q=75" /> */}
{/*
<ImgTile
height={300}
src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fvoicing-permutation.png&w=2048&q=75"
/> */}
</div>
</div>
</Slide><Slide>
## Some of my other Projects loosely related to Strudel
## A Taste of Strudel
<MaxiRepl
canvasHeight={600}
code={[
`"<0 2 [4 6](3,4,1) 3*2>"
.color('salmon')
.off(1/4, x=>x.add(2).color('green'))
.off(1/2, x=>x.add(6).color('steelblue'))
.scale('D minor')
.legato(.5)
.echo(2, 1/8, .5)
.pianoroll()
.note().piano().out()`,
//
`stack(
s("bd,[~ <sd!3 sd(3,4,2)>],hh(3,4)") // 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"
.decay(.15).sustain(0) // make each note of equal length
.s('sawtooth') // waveform
.gain(.4) // turn down
.cutoff(sine.slow(7).range(300,5000)) // automate cutoff
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings() // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.s('sawtooth') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
.attack(1) // slowly fade in
,"a4 c5 <e6 a6>".struct("x(5,8)")
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.decay(.1).sustain(0) // make notes short
.s('triangle') // waveform
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
.echoWith(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes
)
.out()
.slow(3/2)`,
/* `// froos - strudel schneckno
samples({
clubkick: 'clubkick/2.wav',
sd: ['808sd/SD0010.WAV','808sd/SD0050.WAV'],
hh: 'hh/000_hh3closedhh.wav',
clak: 'clak/000_clak1.wav',
jvbass: ['jvbass/000_01.wav','jvbass/001_02.wav','jvbass/003_04.wav','jvbass/004_05.wav','jvbass/005_06.wav']
}, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
stack(
s("<clubkick*2>,[~ <sd!3 sd(3,4,2)>],hh(3,4,2)")
.n("<0 1 2>").speed(.8),
n("<0 1*2 4(3,8) 2>").speed(.6)
.off(1/8, x => x.speed(1.2).degradeBy(.5))
.s("jvbass")
).reset("<x@7 <x(5,8,3) x(3,8) x(3,4,1)>>")
.cutoff(perlin.slow(7).range(100,20000))
.webdirt().slow(3/2)`,*/
/* `stack( // drum pattern by yaxu
"24*2 [~ 25]".chunk(4, x => x.sub("12").fast(2))
.tone(membrane().toDestination()),
"~ x ~ x".every(2, x => x.fast(2)).iter(4)
.tone(noise().toDestination()),
"c4*8".add(saw.mul(12).slow(1.5)).degrade(0.05)
.tone(metal().set(adsr(0,.06,0,0)).chain(vol(0.5),out()))
)`,*/
/* `stack(
note("<[0 2 4 2] [0 3 5 3]>".scale('D3 dorian'))
.s('jazzbass'),
note("0,2,4".add("<0 1 2 1>").scale('D3 dorian'))
.s('stage73')
).clip(1).out()`, */
]}
/>
</Slide><Slide>
## Live Coding
<i className="text-3xl">
"In live coding performance, performers create time-based works by programming them while these same works are being
executed." <br />
Roberts & Wakefield, 2016
</i>
<img src="./hydra-screenshot.png" className="h-[600px] rounded-md" />
<p>
Screenshot: "Olivia Jack -{' '}
<a href="https://hydra.ojack.xyz/" target="_blank">
Hydra
</a>
, Live Coding Visuals in the Browser".{' '}
</p>
</Slide><Slide>
## Tidal Cycles
<div className="text-left">
- A popular software for live coding music, developed since 2009
- a powerful pattern language, inspired by classical music
- functional reactive programming (FRP)
</div>
<img src="./tidal-screenshot2.png" className="h-[500px] rounded-md" />
<p>
Screenshot:{' '}
<a href="https://youtu.be/tY8BmTnlUA0?t=594" target="_blank">
"Yaxu & hellocatfood - LIVE code for Noise Quest"
</a>
</p>
</Slide><Slide hidden>
## What is Strudel?
<div className="text-left">
- a port of Tidal Cycles to JavaScript
- a browser based editor for zero-install live coding: <a href="https://strudel.tidalcycles.org/" target="_blank" className="text-indigo-400">strudel.tidalcycles.org</a>
- many ways to make sound: Web Audio, OSC, MIDI, Speech ...
<div className="hidden">
- "Strudel" = internationally known as delicious pastry, but also "swirl" / "whirlpool" in german
- (a tidal can cause a whirlpool!)
</div>
</div>
<img src="./strudel-screenshot.png" className="h-[500px] rounded-md mb-0" />
<p>
Screenshot:{' '}
<a href="https://youtu.be/IcMSocdKwvw?t=330" target="_blank">
"Algorave 10th Birthday March 2022 - froos"
</a>
</p>
</Slide><Slide>
## Strudel: How it started
<div className="text-left mb-0">
<blockquote>
"Any application that can be written in JavaScript, will eventually be written in JavaScript."
<br />
Jeff Atwood, 2009{' '}
</blockquote>
![tidal-vortex-strudel](./tidal-vortex-strudel.png)
</div>
<div className="text-left">
![strudel port quote](./strudel-port.png)
- Started by Alex McLean in January 2022
- based on a port of a rewrite of Tidal
- I implemented the REPL + additional strudel packages
- Help from Tidal Community Members
- Inspired by: Tidal Vortex, Gibber, Estuary, Hydra, WAGS and Feedforward.
</div>
</Slide><Slide>
## High Level Overview
<div className="absolute top-[250px] text-8xl">➡️</div>
<img src="./strudelflow.png" className="h-[800px] mt-24" />
</Slide><Slide hidden>
## REPL = Read Eval Play Loop
<div className="text-left">
1. Read: User code is parsed, syntax sugar is dissolved
2. Eval: Transformed code is evaluated, creating a **Pattern**
3. Play: The **Pattern** is queried for events, which trigger sounds and effects
4. Loop: The new output is fed back into the ears of the human, informing the next move
</div>
<img src="./repl.png" className="-my-16" />
</Slide><Slide>
## User Code
<div className="text-left">
- JavaScript + syntax sugar (transpilation with shift-ast)
- "Mini Notation" for terse rhythm notation
- `seq(c3, [e3, g3])` === `"c3 [e3 g3]"`
- All Parameters can be a Pattern
</div>
<MaxiRepl
code={[
`note("c3 [e3 g3]")
.piano().out().pianoroll()`,
`// mini notation also works with samples
s("bd,[~ sd],hh*4").out()`,
`\`[[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5]
[c5 a4 a4 ~]
[[~ d5] [~ f5] a5 [g5 f5]]
[e5 [~ c5] e5 [d5 c5]]
[b4 [b4 c5] d5 e5]
[c5 a4 a4 ~]],
[[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]\`.slow(16)
.note().out()
.pianoroll({ fold:1, cycles:16 })`,
`"-7, 0 2 4 7".add("<0 1 2 1>")
.scale(cat('F#2 major','A2 major').slow(8))
.note().piano().out().pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'})`,
/* `stack(
// melody
\`<
[e5 ~] [[d5@2 c5] [~@2 e5]] ~ [~ [c5@2 d5]] [e5 e5] [d5 c5] [e5 f5] [g5 a5]
[~ c5] [c5 d5] [e5 [c5@2 c5]] [~ c5] [f5 e5] [c5 d5] [~ g6] [g6 ~]
[e5 ~] [[d5@2 c5] [~@2 e5]] ~ [~ [c5@2 d5]] [e5 e5] [d5 c5] [a5 g5] [c6 [e5@2 d5]]
[~ c5] [c5 d5] [e5 [c5@2 c5]] [~ c5] [f5 e5] [c5 d5] [~ [g6@2 ~] ~@2] [g5 ~]
[~ a5] [b5 c6] [b5@2 ~@2 g5] ~
[f5 ~] [[g5@2 f5] ~] [[e5 ~] [f5 ~]] [[f#5 ~] [g5 ~]]
[~ a5] [b5 c6] [b5@2 ~@2 g5] ~
[eb6 d6] [~ c6] ~!2
>\`
.legato(.95),
// sub melody
\`<
[~ g4]!2 [~ ab4]!2 [~ a4]!2 [~ bb4]!2
[~ a4]!2 [~ g4]!2 [d4 e4] [f4 gb4] ~!2
[~ g4]!2 [~ ab4]!2 [~ a4]!2 [~ bb4]!2
[~ a4]!2 [~ g4]!2 [d4 e4] [f4 gb4] ~!2
[~ c5]!4 [~ a4]!2 [[c4 ~] [d4 ~]] [[eb4 ~] [e4 ~]]
[~ c5]!4 [~ eb5]!2 [g4*2 [f4 ~]] [[e4 ~] [d4 ~]]
>\`,
// bass
\`<
c3!7 a3 f3!2
e3!2 ~!4
c3!7 a3 f3!2
e3!2 ~!4
f3!2 e3!2 d3!2 ~!2
f3!2 e3!2 ab3!2 ~!2
>\`
.legato(.5)
).fast(2).pianoroll({fold:1,cycles:8,overscan:8})
.note().piano().out()`,
`stack(
"c2 <g2 g1>",
"<[e3,[g3 a3 bb3 a3]]!11 [e3,g3]>".slow(2)
.struct("<[[x@2 x]*4]!11 [[~@2 x] [[x@2 ~]*9]@3]>".slow(2))
)
.velocity("[.7@2 .9]*2".add(rand.range(-.1,.1)))
.transpose("<1 6 1@2 6@2 1@2 8 6 1 8>/2")
.pianoroll({ fold:1, cycles:16 })
.note().piano().out()`, */
]}
/>
</Slide><Slide>
## 🍭 Syntax Sugar
JavaScript Transpilation: converting valid JavaScript to valid JavaScript
<div className="hidden">https://github.com/tidalcycles/strudel/blob/main/packages/eval/shapeshifter.mjs#L40</div>
<img src="./shiftflow.png" className="-my-16" />
<div className="text-left">
<div className="grid grid-cols-2 gap-4 mt-4">
<span>🍭 Before (User Code)</span>
<span>🤖 After (Ready to Eval)</span>
<pre>"c3 [e3 g3]".fast(2)</pre>
```js
mini('c3 [e3 g3]')
.withMiniLocation([1, 0, 0], [1, 11, 11]) // source location
.fast(2);
```
</div>
</div>
<div className="hidden">
[Blog Post](https://loophole-letters.vercel.app/shift-ast), [AST Explorer](https://astexplorer.net/)
</div>
</Slide><Slide>
## 👁 Parsing Mini Notation
<div className="text-left">
1. write [peg](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), generate [parser](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill-parser.js) with [parser generator (peggyjs)](https://peggyjs.org/)
2. pass string to generated parser: `mini('c3 [e3 g3]')`
3. obtain Abstract Syntax Tree (AST)
4. [transform](https://github.com/tidalcycles/strudel/blob/main/packages/mini/mini.mjs#L76) AST into Pattern: `seq('c3', ['e3', 'g3'])`
</div>
<img src="./pegflow.png" className="-my-16" />
[OhmJS Editor](https://ohmjs.org/editor/)
</Slide><Slide>
## High Level Overview
<div className="absolute top-[545px] text-8xl">➡️</div>
<img src="./strudelflow.png" className="h-[800px] mt-24" />
</Slide><Slide hidden>
## AST of `c3 [e3 g3]`
<div className="text-left">
```js
{
"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 } }
}
]
},
}
]
}
```
</div>
= `seq(c3, seq(e3, g3))`
</Slide><Slide>
## 🕵🏽 Querying Events
Asking a Pattern for Events within a certain time span
<div className="text-left">
```js
seq('c3', ['e3', 'g3']) // <--- Pattern
.queryArc(0, 2) // query events within 0 and 2 seconds
.map((event) => event.showWhole()); // make readable
```
```js
[
'0/1 -> 1/2: c3',
'1/2 -> 3/4: e3',
'3/4 -> 1/1: g3',
'1/1 -> 3/2: c3', // second time
'3/2 -> 7/4: e3',
'7/4 -> 2/1: g3',
];
```
</div>
The scheduler will query the active pattern at regular intervals!
</Slide><Slide>
## 🗓️ Scheduling
Query events repeatedly
```js
let step = 0.5; // query interval in seconds
let tick = 0; // how many intervals have passed
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
setInterval(() => {
const events = pattern.queryArc(tick * step, ++tick * step);
events.forEach((event) => {
console.log(event.showWhole());
const o = getAudioContext().createOscillator();
o.frequency.value = getFreq(event.value);
o.start(event.whole.begin);
o.stop(event.whole.begin + event.duration);
o.connect(getAudioContext().destination);
});
}, step * 1000); // query each "step" seconds
```
</Slide><Slide>
## High Level Overview
<div className="absolute top-[855px] text-8xl">➡️</div>
<img src="./strudelflow.png" className="h-[800px] mt-24" />
</Slide><Slide>
## Sound Output
Patterns are wrapped with param functions to compose different properties of the sound.
<MaxiRepl
canvasHeight={0}
code={[
`note("[c2(3,8) [<eb2 g1> bb1]]") // sets frequency
.s("<sawtooth square>") // sound source
.gain(.5) // turn down volume
.cutoff(sine.range(200,1000).slow(4)) // modulated cutoff
.slow(2)
.out().logValues()`,
]}
/>
```js
{ note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 }
```
![waa nodes](./waa-nodes.png)
Supports Web Audio API, Web MIDI, OSC, Speech, Web Serial, ... ?
</Slide><Slide>
## 🔭 Future Outlook
<div className="text-left">
- Alternative Sound Backends
- Collaborative Mode
- Context Aware Documentation / Completion / Visualization
- Port more Tidal Features
</div>
## 🌀 Try it out now
<div className="text-left">
- Strudel REPL: [strudel.tidalcycles.org](https://strudel.tidalcycles.org/?EeNsQ8hdNZwN) (-> [offline version](http://localhost:3000/#Y29uc3QgZGVsYXkgPSBuZXcgRmVlZGJhY2tEZWxheSgxLzMsIC41KS5jaGFpbih2b2woLjIpLCBvdXQoKSkKbGV0IGthbGltYmEgPSBhd2FpdCBzYW1wbGVyKHsKICBDNTogJy4vc2FtcGxlcy9rYWxpbWJhL2thbGltYmEtYzUubXAzJwp9KQprYWxpbWJhID0ga2FsaW1iYS5jaGFpbih2b2woMC42KS5jb25uZWN0KGRlbGF5KSxvdXQoKSk7CmNvbnN0IHNjYWxlcyA9IGNhdCgnQyBtYWpvcicsICdDIG1peG9seWRpYW4nLCAnRiBseWRpYW4nLCBbJ0YgbWlub3InLCAnRGIgbWFqb3InXSkKCnN0YWNrKAogICJbMCAyIDQgNiA5IDIgMCAtMl0qMyIKICAuYWRkKCI8MCAyPi80IikKICAuc2NhbGUoc2NhbGVzKQogIC5zdHJ1Y3QoIngqOCIpCiAgLnZlbG9jaXR5KCI8LjggLjMgLjY%2BKjgiKQogIC5zbG93KDIpCiAgLnRvbmUoa2FsaW1iYSksCiAgIjxjMiBjMiBmMiBbW0YyIEMyXSBkYjJdPiIKICAuc2NhbGUoc2NhbGVzKQogIC5zY2FsZVRyYW5zcG9zZSgiWzAgPDIgND5dKjIiKQogIC5zdHJ1Y3QoIngqNCIpCiAgLnZlbG9jaXR5KCI8LjggLjU%2BKjQiKQogIC52ZWxvY2l0eSgwLjgpCiAgLnNsb3coMikKICAudG9uZShrYWxpbWJhKQopCiAgLmxlZ2F0bygiPC40IC44IDEgMS4yIDEuNCAxLjYgMS44IDI%2BLzgiKQogIC5mYXN0KDEpCi5waWFub3JvbGwoe2F1dG9yYW5nZTowfSk%3D))
- Interactive Tutorial: [strudel.tidalcycles.org/tutorial](https://strudel.tidalcycles.org/)
- Stay Tuned: [@tidalcycles](https://twitter.com/tidalcycles)
- Github: [https://github.com/tidalcycles/strudel](https://github.com/tidalcycles/strudel)
</div>
### Happy Strudeling!
</Slide><Slide>
## More?
<MaxiRepl
code={[
`stack(
s("<bd [bd ~ bd]> ~, ~ sd, hh*3")
//.cutoff(perlin.range(100,900))
//.sometimes(x=>x.speed("2"))
,
note(
"Cm7 <Dm7 C#m7>".voicings()
).rhodes().slow(4)
,
note(
"[c2 <eb2 g1>](3,9)".slow(2)
//.superimpose(add(0.02))
)
.s("<sawtooth square>").gain(.5)
.cutoff(800),
note(
"0 2 4 1".iter(4)
.off(1/12, add("6"))
.degradeBy(.5)
.scale('C5 minor')
.legato(.05)
.echo(2, 1/6, .5)
)
.piano().hcutoff(2000)
).out()
//.cutoff(800)
//.reset("<x@3 x*2>")
`,
]}
/>
</Slide><Slide hidden>
## Strudel Workshop
![whirlpool](./strudel_icon.png)
</Slide><Slide>
## Pattern Factories
seq, cat, stack
<MaxiRepl
code={[
`seq(c3,e3,g3) // "c3 e3 g3"
.pianoroll()
.note().piano().out()`,
`cat(c3,e3,g3) // "<c3 e3 g3>"
.pianoroll()
.note().piano().out()`,
`stack(c3,e3,g3) // "c3,e3,g3"
.pianoroll()
.note().piano().out()`,
`stack(c3, seq(e3, cat(g3, c4))) // "c3,[e3,<g3 c4>]"
.pianoroll()
.note().piano().out()`,
]}
/>
</Slide><Slide>
## Time Control
fast / slow, early / late, rev, palindrome
<MaxiRepl
code={[
`"c3 e3 g3".fast(2) // "[c3 e3 g3]*2"
.pianoroll()
.note().piano().out()`,
`"c3 e3 g3".slow(2) // "[c3 e3 g3]/2"
.pianoroll()
.note().piano().out()`,
`stack(
"c3 e3".color('steelblue'),
"<g3 a3 bb3 a3>".early(.16)
).late(.16)
.pianoroll()
.note().piano().out()`,
`"c3 e3 g3 bb3".rev()
.pianoroll()
.note().piano().out()`,
]}
/>
</Slide><Slide>
## Arithmetic & Patternified Params
add, sub, mul, div
<MaxiRepl
code={[
`"30,42 45 49 54".add(0).slow(2)
.pianoroll({autorange:0,minMidi:28,maxMidi:56})
.note().piano().out()`,
`"30,42 45 49 54".add("<0 1 2 1>").slow(2)
.pianoroll({fold:1,cycles:8})
.note().piano().out()`,
]}
/>
</Slide><Slide>
## Scales
generating notes from numbers
<MaxiRepl
code={[
`"-7,0 2 4 7".add("<0 1 2 1>")
.scale('F#2 major')
.pianoroll({autorange:1})
.note().piano().out()`,
`"-7, 0 2 4 7".add("<0 1 2 1>")
.scale(cat('F#2 major','A2 major').slow(8))
.pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'})
.note().piano().out()`,
]}
/>
</Slide><Slide>
## Articulation
velocity, legato
<MaxiRepl
code={[
`"-7, 0 2 4 7".add("<0 1 2 1>")
.scale(cat('F#2 major','A2 major').slow(8))
.legato(.25)
.velocity(.5)
.pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'})
.note().piano().out()`,
`"-7, 0 2 4 7".add("<0 1 2 1>")
.scale(cat('F#2 major','A2 major').slow(8))
.legato("<0.25 2>/8")
.velocity("<1 .8>/8")
.pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'})
.note().piano().out()`,
]}
/>
</Slide><Slide>
## Continuous Signals / Randomness
sine / saw / tri / square / rand / perlin, range, segment
<MaxiRepl
code={[
`sine
.range(42,54).slow(4).segment(8)
.pianoroll({ minMidi:42, maxMidi: 54, autorange:0,
cycles:8,fold:0,inactive:'orange'})
.s('triangle').out()`,
`s("hh*8").speed(sine.range(.5,2).slow(4))
.out()`,
`sine.range(42,50).color('#00ff0050')
.superimpose(x=>x.fast(3/2).color('#ff00ff50'))
.fast(1)
.s('triangle').out()
.segment(64)
.pianoroll({fold:0,vertical:0,cycles:2})
// strudel disable-highlighting`,
]}
/>
</Slide><Slide>
## Humanization
"articulate randomness"
<MaxiRepl
code={[
`"-7, 0 2 4 7".add("<0 1 2 1>")
.scale(cat('F#2 major','A2 major').slow(8))
.legato(perlin.range(.5,2))
.velocity("<0.4 .3 .7>*3")
.late(perlin.range(0,.05))
.pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'})`,
]}
/>
</Slide><Slide hidden>
## Shuffling
iter
<MaxiRepl
code={[
`"c3 e3 g3 c4".iter(4)
.pianoroll({autorange:1,cycles:8,fold:1,inactive:'indigo'})`,
`stack("-7", "0 2 4 7".iter(4)).add("<0 1 2 1>")
.scale(cat('F#2 major','A2 major').slow(8))
.legato(perlin.range(.5,2))
.velocity(rand.range(.6,.9))
.pianoroll({autorange:1,cycles:16,fold:1,inactive:'indigo'})`,
]}
/>
</Slide><Slide>
## Superposition
superimpose, off, echo, echoWith
<MaxiRepl
code={[
`"0 [2 1] 4 7"
.superimpose(x=>x.add(5).color('green'))
.scale('C major').slow(2)
.pianoroll({autorange:1,cycles:8,fold:0,inactive:'indigo'})`,
`"0 2 4 7"
//.off(1/6, x=>x.add(9).color('green'))
//.off(1/3, x=>x.add(7).color('brown'))
.scale('C major').slow(2)
.pianoroll({autorange:1,cycles:8,fold:0,inactive:'indigo'})`,
`"0 2 4 7".add("<0 1>/8")
//.echo(6,.125,.8)
.scale('C major').slow(2)
.color("indigo steelblue orange")
.pianoroll({autorange:1,cycles:8,fold:1,inactive:'indigo'})`,
]}
/>
</Slide><Slide>
## Phasing
Oscillator Detune in slow motion
<MaxiRepl
code={[
`// Steve Reich - Piano Phase (1967)
"E4 F#4 B4 C#5 D5 F#4 E4 C#5 B4 F#4 D5 C#5".color('#00ff0050')
.superimpose(x=>x.fast(1.01).color('#ff00ff50'))
.slow(2)
.pianoroll({fold:1,vertical:0,cycles:8})`,
`// Steve Reich - Clapping Music
s("cp").struct("x!3 ~ x!2 ~ x ~ x!2 ~").speed(.8)
.jux(x=>x.speed(1.2)
.late("<0 1 2 3 4 5 6 7 8 9 10 11>/8".div(12))
).slow(2).webdirt()`,
]}
/>
</Slide><Slide>
## Chord Voicings
chord dictionaries + smooth voice leading (<a href="https://loophole-letters.vercel.app/rhythmical-chords" target="_blank">blog post</a>)
<MaxiRepl
code={[
`"Am7 [Bm7b5 E7b9]"
.struct("x(3,8)".fast(2))
.voicings(['E3', 'G4'])
.stack("A2 E2 <B2 B1> E2")
.slow(4)
.pianoroll({cycles:8,fold:1,inactive:'orange'})`,
`// froos - giant steps, stride version
stack(
// melody
seq(
"[F#5 D5] [B4 G4] Bb4 [B4 A4]",
"[D5 Bb4] [G4 Eb4] F#4 [G4 F4]",
"Bb4 [B4 A4] D5 [D#5 C#5]",
"F#5 [G5 F5] Bb5 [F#5 [F#5 ~@3]]",
),
// chords
seq(
"[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]",
"[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]",
"Eb^7 [Am7 D7] G^7 [C#m7 F#7]",
"B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]"
)
.struct("~ [x ~]".fast(4*8))
.voicings(['E3', 'G4']),
// bass
seq(
"[B2 D2] [G2 D2] [Eb2 Bb2] [A2 D2]",
"[G2 Bb2] [Eb2 F#2] [B2 F#2] [F2 Bb2]",
"[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]",
"[B2 F#2] [F2 Bb2] [Eb2 Bb2] [C#2 F#2]"
)
.struct("x ~".fast(4*8))
).slow(25)
.pianoroll({cycles:8,fold:1,inactive:'orange'})`]}/>
</Slide><Slide>
## Speech
<MaxiRepl code={['"<hello world>".speak()']} />
</Slide><Slide>
## Strudel Packages
`npm i @strudel.cycles/*`, where \* is one of:
<div className="text-left">
- core: tidal pattern engine
- mini: mini notation parser + core binding
- eval: user code evaluator. syntax sugar + highlighting
- tone: bindings for Tone.js instruments and effects
- osc: bindings to communicate via OSC
- midi: webmidi bindings
- serial: webserial bindings
- tonal: tonal functions
- xen: microtonal / xenharmonic functions
- ... there are [more](https://github.com/tidalcycles/strudel/tree/main/packages)
</div>
</Slide><Slide hidden>
## Workflow
<div className="text-left">
- Everything is in one [monorepo](https://github.com/tidalcycles/strudel)
- Development via npm workspaces
- Packages are published with lerna
- REPL is Deployed to github pages
- Features are added with Pull Requests
- Automated tests run for every PR
- Snapshot tests
</div>
</Slide><Slide hidden>
## Tech Stack
<div className="text-left">
- REPL: Codemirror 6 + react + tailwind
- Everything is built with vite
- In source documentation with jsdoc
- Tutorial written with MDX
- jsdoc is inserted into MDX with nunjucks
</div>
</Slide>
</Slides>
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Strudel Slides</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.jsx"></script>
</body>
</html>
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
// import Deck from './deck.mdx';
import Deck from './meetup.mdx';
import './style.css';
import '@strudel.cycles/react/dist/style.css';
import 'prismjs/themes/prism-tomorrow.css';
ReactDOM.render(
<React.StrictMode>
<div className="w-screen h-screen overflow-auto bg-slate-900">
<Deck />
</div>
</React.StrictMode>,
document.getElementById('root'),
);
+137
View File
@@ -0,0 +1,137 @@
import { EditorView } from '@codemirror/view';
import { HighlightStyle, tags as t } from '@codemirror/highlight';
/*
Credits for color palette:
Author: Mattia Astorino (http://github.com/equinusocio)
Website: https://material-theme.site/
*/
const ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
invalid = '#ffffff',
darkBackground = '#21252b',
highlightBackground = 'rgba(0, 0, 0, 0.5)',
// background = '#292d3e',
background = 'transparent',
tooltipBackground = '#353a42',
selection = 'rgba(128, 203, 196, 0.5)',
cursor = '#ffcc00';
/// The editor theme styles for Material Palenight.
export const materialPalenightThemeLarge = EditorView.theme(
{
// done
'&': {
color: '#ffffff',
backgroundColor: background,
fontSize: '30px',
'z-index': 11,
},
// done
'.cm-content': {
caretColor: cursor,
lineHeight: '40px',
},
'.cm-line': {
background: '#2C323699',
},
// done
'&.cm-focused .cm-cursor': {
backgroundColor: cursor,
width: '3px',
},
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
backgroundColor: selection,
},
'.cm-panels': { backgroundColor: darkBackground, color: '#ffffff' },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
// done, use onedarktheme
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
outline: '1px solid #515a6b',
},
// done
'.cm-gutters': {
background: 'transparent',
color: '#676e95',
border: 'none',
display: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: true },
);
/// The highlighting style for code in the Material Palenight theme.
export const materialPalenightHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' },
{ tag: t.special(t.variableName), color: '#eeffff' },
{ tag: t.typeName, color: '#f07178' },
{ tag: t.atom, color: '#f78c6c' },
{ tag: t.number, color: '#ff5370' },
{ tag: t.definition(t.variableName), color: '#82aaff' },
{ tag: t.string, color: '#c3e88d' },
{ tag: t.special(t.string), color: '#f07178' },
{ tag: t.comment, color: stone },
{ tag: t.variableName, color: '#f07178' },
{ tag: t.tagName, color: '#ff5370' },
{ tag: t.bracket, color: '#a2a1a4' },
{ tag: t.meta, color: '#ffcb6b' },
{ tag: t.attributeName, color: '#c792ea' },
{ tag: t.propertyName, color: '#c792ea' },
{ tag: t.className, color: '#decb6b' },
{ tag: t.invalid, color: invalid },
]);
/// Extension to enable the Material Palenight theme (both the editor theme and
/// the highlight style).
// : Extension
export const materialPalenightLarge = [materialPalenightThemeLarge, materialPalenightHighlightStyle];
+1124
View File
File diff suppressed because it is too large Load Diff
+7963
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
{
"name": "@strudel.cycles/tutorial",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite --port 4321",
"start": "vite",
"build": "vite build",
"preview": "vite preview",
"symlink-samples": "ln -s ../../repl/public/samples/ ./public/"
},
"type": "module",
"dependencies": {
"framer-motion": "^6.3.6",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@mdx-js/mdx": "^1.6.22",
"@mdx-js/react": "^1.6.22",
"@tailwindcss/typography": "^0.5.2",
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2",
"@vitejs/plugin-react": "^1.3.0",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.13",
"rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1",
"remark-prism": "^1.3.6",
"remark-toc": "^8.0.1",
"sass": "^1.51.0",
"tailwindcss": "^3.0.24",
"vite": "^2.9.9",
"vite-plugin-mdx": "^3.5.10"
}
}
+12
View File
@@ -0,0 +1,12 @@
/*
postcss.config.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/postcss.config.js>
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/>.
*/
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+17
View File
@@ -0,0 +1,17 @@
{
"bd": ["bd/Bassdrum-01.wav","bd/Bassdrum-02.wav","bd/Bassdrum-03.wav","bd/Bassdrum-04.wav","bd/Bassdrum-05.wav","bd/Bassdrum-06.wav","bd/Bassdrum-07.wav","bd/Bassdrum-08.wav","bd/Bassdrum-09.wav","bd/Bassdrum-10.wav","bd/Bassdrum-11.wav","bd/Bassdrum-12.wav","bd/Bassdrum-13.wav","bd/Bassdrum-14.wav"],
"cb": ["cb/Cowbell.wav"],
"cp": ["cp/Clap.wav"],
"cr": ["cr/Crash.wav"],
"hh": ["hh/Hat Closed-01.wav","hh/Hat Closed-02.wav"],
"ht": ["ht/Tom H-01.wav","ht/Tom H-02.wav","ht/Tom H-03.wav","ht/Tom H-04.wav","ht/Tom H-05.wav","ht/Tom H-06.wav"],
"lt": ["lt/Tom L-01.wav","lt/Tom L-02.wav","lt/Tom L-03.wav","lt/Tom L-04.wav","lt/Tom L-05.wav","lt/Tom L-06.wav"],
"misc": ["misc/Metal-01.wav","misc/Metal-02.wav","misc/Metal-03.wav","misc/Scratch.wav","misc/Shot-01.wav","misc/Shot-02.wav","misc/Shot-03.wav"],
"mt": ["mt/Tom M-01.wav","mt/Tom M-02.wav","mt/Tom M-03.wav","mt/Tom M-05.wav"],
"oh": ["oh/Hhopen1.wav"],
"perc": ["perc/Blow1.wav"],
"rd": ["rd/Ride.wav"],
"rim": ["rim/zRim Shot-01.wav","rim/zRim Shot-02.wav"],
"sd": ["sd/Snaredrum-01.wav","sd/Snaredrum-02.wav","sd/Snaredrum-03.wav","sd/Snaredrum-04.wav","sd/Snaredrum-05.wav","sd/Snaredrum-06.wav","sd/Snaredrum-07.wav","sd/Snaredrum-08.wav","sd/Snaredrum-09.wav","sd/Snaredrum-10.wav","sd/Snaredrum-11.wav","sd/Snaredrum-12.wav","sd/Snaredrum-13.wav","sd/Snaredrum-14.wav","sd/Snaredrum-15.wav","sd/Snaredrum-16.wav","sd/Snaredrum-17.wav","sd/Snaredrum-18.wav","sd/Snaredrum-19.wav","sd/Snaredrum-20.wav","sd/Snaredrum-21.wav"]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+145
View File
@@ -0,0 +1,145 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* cyrillic-ext */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_bZF3gTD_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_bZF3g3D_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_bZF3gbD_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_bZF3gfD_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_bZF3gnD_vx3rCs.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_epG3gTD_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_epG3g3D_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_epG3gbD_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_epG3gfD_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_epG3gnD_vx3rCs.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* @import url('https://fonts.googleapis.com/css2?family=Merriweather&display=swap'); */
/* cyrillic-ext */
@font-face {
font-family: 'Merriweather';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/merriweather/v28/u-440qyriQwlOrhSvowK_l5-cSZMdeX3rsHo.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Merriweather';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/merriweather/v28/u-440qyriQwlOrhSvowK_l5-eCZMdeX3rsHo.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Merriweather';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/merriweather/v28/u-440qyriQwlOrhSvowK_l5-cyZMdeX3rsHo.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Merriweather';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/merriweather/v28/u-440qyriQwlOrhSvowK_l5-ciZMdeX3rsHo.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Merriweather';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/merriweather/v28/u-440qyriQwlOrhSvowK_l5-fCZMdeX3rg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
+17
View File
@@ -0,0 +1,17 @@
/*
tailwind.config.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/tailwind.config.js>
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/>.
*/
module.exports = {
content: ['./**/*.{js,jsx,ts,tsx,mdx}'],
theme: {
extend: {},
fontFamily: {
sans: ['Montserrat'],
serif: ['Merriweather'],
},
},
plugins: [require('@tailwindcss/typography')],
};
+34
View File
@@ -0,0 +1,34 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import _mdx from 'vite-plugin-mdx';
import remarkPrism from 'remark-prism';
/* import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings'; */
const mdx = _mdx.default || _mdx;
// `options` are passed to `@mdx-js/mdx`
const options = {
// See https://mdxjs.com/advanced/plugins
remarkPlugins: [
remarkPrism,
// remarkToc,
// E.g. `remark-frontmatter`
],
rehypePlugins: [
// rehypeSlug, rehypeAutolinkHeadings
],
};
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), mdx(options)],
build: {
outDir: '../out/tutorial',
},
base: '/',
});
// jsxRuntime:'classic' to prevent "jsxDevRuntime.exports.jsxDEV is not a function" for dev mode
// mode: 'development',
// react({ jsxRuntime: 'classic' }),
-2
View File
@@ -23,8 +23,6 @@
"@types/react-dom": "^17.0.2",
"@vitejs/plugin-react": "^1.3.0",
"autoprefixer": "^10.4.7",
"install": "^0.13.0",
"npm": "^8.10.0",
"nunjucks": "^3.2.3",
"postcss": "^8.4.13",
"rehype-autolink-headings": "^6.1.1",