Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 235f3672aa | |||
| 55dbcdde18 | |||
| d3f10518cc | |||
| 99fd5a53cd | |||
| 7e21b683fb | |||
| a492628b33 | |||
| 53bb20c516 | |||
| 588f74123e | |||
| e3473d5e70 | |||
| 8ce4de0c8d | |||
| b87cea834f | |||
| 230ebe7439 | |||
| e1f017916c | |||
| 6a445ee130 | |||
| 8ea0044ffa | |||
| 2d76de5906 | |||
| 4cce65c9fe | |||
| 6b3687357e | |||
| 1d6e74fc77 | |||
| c9fda97534 | |||
| 8796bcbdc3 | |||
| 4c66d9a553 | |||
| 85d3d7392e | |||
| 31d0225341 | |||
| d0acb32497 | |||
| aa9a32c9af | |||
| a8610d0078 | |||
| ef5dcd0dae | |||
| ed86a94f99 | |||
| 7be791f2d3 | |||
| cc31a21b6f | |||
| 72155d24c9 | |||
| 7672ca9971 | |||
| 07120e9e3b | |||
| 72feb3b65a | |||
| 2f5ff44dc9 | |||
| 8051286904 | |||
| 8a57333f72 | |||
| 512ba03fef | |||
| 4f0d2f181b | |||
| 1339bc3b94 | |||
| edf2fb9781 | |||
| cf6bfd0e30 | |||
| a93d6e884d | |||
| 1a9b57e66c | |||
| 0aec9423ac | |||
| 54df4edca7 | |||
| be6310b8e2 | |||
| 3ab32114a6 | |||
| da85e1920b | |||
| 352d701b7b | |||
| 349c1bc5d8 | |||
| 9430caeb29 | |||
| eb0adf4194 | |||
| 6fde3f2231 | |||
| 1c77e7e5b3 | |||
| 061abecaca | |||
| 98c131df9a | |||
| 1af10157d6 | |||
| a3eeaead2a | |||
| ff362da23f |
@@ -34,4 +34,5 @@ repl_old
|
||||
tutorial.rendered.mdx
|
||||
doc.json
|
||||
talk/public/EmuSP12
|
||||
talk/public/samples
|
||||
talk/public/samples
|
||||
server/samples/old
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, ...
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -23,4 +23,6 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
oldtunes.mjs
|
||||
oldtunes.mjs
|
||||
|
||||
public/samples/EMU World
|
||||
@@ -20,7 +20,7 @@ body {
|
||||
|
||||
.darken::before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
@@ -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()`;
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
function ImgTile({ src, width, height }) {
|
||||
return (
|
||||
<div style={{ background: `url(${src})`, backgroundSize: 'cover', backgroundPosition: 'center', width, height }} />
|
||||
);
|
||||
}
|
||||
export default ImgTile;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
```sh
|
||||
npm create vite@latest slides -- --template react
|
||||
cd slides
|
||||
npm i vite-plugin-mdx --save-dev
|
||||
npm run dev
|
||||
```
|
||||
@@ -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> */
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
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>
|
||||
|
||||

|
||||
|
||||
</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>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<div className="text-left">
|
||||
|
||||

|
||||
|
||||
- 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 }
|
||||
```
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
</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>
|
||||
@@ -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>
|
||||
@@ -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'),
|
||||
);
|
||||
@@ -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];
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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: {},
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 381 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -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"]
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
@@ -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;
|
||||
}
|
||||
@@ -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')],
|
||||
};
|
||||
@@ -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' }),
|
||||
@@ -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",
|
||||
|
||||