Compare commits

..

1 Commits

Author SHA1 Message Date
Felix Roos 0b7ff17e01 move samples to dedicated samples folder 2022-07-28 22:35:27 +02:00
99 changed files with 328 additions and 11532 deletions
+1 -2
View File
@@ -34,5 +34,4 @@ repl_old
tutorial.rendered.mdx
doc.json
talk/public/EmuSP12
talk/public/samples
server/samples/old
talk/public/samples
+2 -24
View File
@@ -1046,22 +1046,6 @@ 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..
@@ -1222,9 +1206,8 @@ 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?._Pattern && !thing instanceof Pattern) {
const is = thing instanceof Pattern || thing._Pattern;
if (!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.
@@ -1567,8 +1550,3 @@ Pattern.prototype.define = (name, func, options = {}) => {
// Pattern.prototype.define('early', (a, pat) => pat.early(a), { patternified: true, composable: true });
Pattern.prototype.define('hush', (pat) => pat.hush(), { patternified: false, composable: true });
Pattern.prototype.define('bypass', (pat) => pat.bypass(on), { patternified: true, composable: true });
export async function loadScript(url) {
const code = await fetch(url).then((res) => res.text());
return eval(code);
}
-16
View File
@@ -19,9 +19,6 @@ 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 + '"');
@@ -34,19 +31,6 @@ export const fromMidi = (n) => {
return Math.pow(2, (n - 69) / 12) * 440;
};
export const getFreq = (noteOrMidi) => {
if (typeof noteOrMidi === 'number') {
return fromMidi(noteOrMidi);
}
return fromMidi(toMidi(noteOrMidi));
};
export const midi2note = (n) => {
const oct = Math.floor(n / 12) - 1;
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
return pc + oct;
};
// modulo that works with negative numbers e.g. mod(-1, 3) = 2
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
export const mod = (n, m) => ((n % m) + m) % m;
+2 -2
View File
File diff suppressed because one or more lines are too long
+18 -83
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useMemo, useRef, useLayoutEffect } from 'react';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { CodeMirror as CodeMirror$1 } from 'react-codemirror6';
import { EditorView, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
@@ -46,13 +46,7 @@ const materialPalenightTheme = EditorView.theme(
lineHeight: '22px',
},
'.cm-line': {
// background: '#2C323699',
background: 'transparent',
},
'.cm-line > *': {
// background: '#2C323699',
background: '#00000090',
// background: 'transparent',
background: '#2C323699',
},
// done
'&.cm-focused .cm-cursor': {
@@ -77,7 +71,7 @@ const materialPalenightTheme = EditorView.theme(
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: cursor + '50' },
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
@@ -199,7 +193,7 @@ const highlightField = StateField.define({
if (from > l || to > l) {
return;
}
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
return mark.range(from, to);
})).filter(Boolean), true);
}
@@ -211,7 +205,7 @@ const highlightField = StateField.define({
},
provide: (f) => EditorView.decorations.from(f)
});
function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount, theme }) {
function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(CodeMirror$1, {
onViewChange: onViewChanged,
style: {
@@ -223,7 +217,7 @@ function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorD
onChange,
extensions: [
javascript(),
theme || materialPalenight,
materialPalenight,
highlightField,
flashField
]
@@ -294,13 +288,8 @@ function useCycle(props) {
await Tone.start();
Tone.getTransport().start('+0.1');
};
const stop = (setZero = false) => {
if (setZero) {
Tone.getTransport().cancel();
Tone.getTransport().stop();
} else {
Tone.getTransport().pause();
}
const stop = () => {
Tone.getTransport().pause();
setStarted(false);
};
const toggle = () => (started ? stop() : start());
@@ -362,15 +351,13 @@ 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?.(time, event, currentTime);
onEvent?.(event);
if (event.context.logs?.length) {
event.context.logs.forEach(pushLog);
}
@@ -379,10 +366,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
} else if (!onEvent) {
throw new Error(
'no defaultSynth nor onEvent passed to useRepl + event has no onTrigger. nothing happens',
);
} else {
throw new Error('no defaultSynth passed to useRepl.');
}
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
@@ -423,16 +408,16 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
});
const activateCode = useCallback(
async (_code = code, evaluateOnly = false) => {
async (_code = code) => {
if (activeCode && !dirty) {
setError(undefined);
!evaluateOnly && cycle.start();
cycle.start();
return;
}
try {
setPending(true);
const parsed = await evaluate(_code);
!evaluateOnly && cycle.start();
cycle.start();
broadcast({ type: 'start', from: id });
setPattern(() => parsed.pattern);
if (autolink) {
@@ -463,22 +448,7 @@ 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,
@@ -489,10 +459,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
dirty,
log,
togglePlay,
stop,
setActiveCode,
activateCode,
evaluateOnly,
activeCode,
pushLog,
hash,
@@ -591,25 +559,16 @@ 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, theme, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } = useRepl({
function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
tune,
defaultSynth,
autolink: false,
onEvent
autolink: false
});
useEffect(() => {
init && evaluateOnly();
}, [tune, init]);
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01
@@ -621,25 +580,7 @@ function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, on
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
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]);
useHighlighting({ view, pattern, active: cycle.started });
return /* @__PURE__ */ React.createElement("div", {
className: styles.container,
ref
@@ -657,17 +598,11 @@ function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, on
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,8 +60,7 @@ 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: 1.5px solid ${color};` } });
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
return mark.range(from, to);
}),
)
@@ -79,7 +78,7 @@ const highlightField = StateField.define({
provide: (f) => EditorView.decorations.from(f),
});
export default function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount, theme }) {
export default function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
return (
<>
<_CodeMirror
@@ -93,7 +92,7 @@ export default function CodeMirror({ value, onChange, onViewChanged, onCursor, o
onChange={onChange}
extensions={[
javascript(),
theme || materialPalenight,
materialPalenight,
highlightField,
flashField,
// theme, language, ...
-7
View File
@@ -26,13 +26,6 @@ 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>
+11 -40
View File
@@ -1,25 +1,20 @@
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
import React, { useState, useMemo, useRef } 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, { flash } from './CodeMirror6';
import CodeMirror6 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, 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]);
export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
tune,
defaultSynth,
autolink: false,
});
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01,
@@ -31,28 +26,7 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, i
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
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]);
useHighlighting({ view, pattern, active: cycle.started });
return (
<div className={styles.container} ref={ref}>
<div className={styles.header}>
@@ -63,14 +37,11 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, i
<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 theme={theme} value={code} onChange={setCode} onViewChanged={setView} />}
<div className={styles.body} >
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</div>
</div>
);
+2 -7
View File
@@ -66,13 +66,8 @@ function useCycle(props) {
await Tone.start();
Tone.getTransport().start('+0.1');
};
const stop = (setZero = false) => {
if (setZero) {
Tone.getTransport().cancel();
Tone.getTransport().stop();
} else {
Tone.getTransport().pause();
}
const stop = () => {
Tone.getTransport().pause();
setStarted(false);
};
const toggle = () => (started ? stop() : start());
+7 -28
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useCallback, useState, useMemo, useEffect } from 'react';
import { useCallback, useState, useMemo } from 'react';
import { evaluate } from '@strudel.cycles/eval';
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
import useCycle from './useCycle.mjs';
@@ -36,15 +36,13 @@ 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?.(time, event, currentTime);
onEvent?.(event);
if (event.context.logs?.length) {
event.context.logs.forEach(pushLog);
}
@@ -53,10 +51,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
} else if (!onEvent) {
throw new Error(
'no defaultSynth nor onEvent passed to useRepl + event has no onTrigger. nothing happens',
);
} else {
throw new Error('no defaultSynth passed to useRepl.');
}
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
@@ -97,16 +93,16 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
});
const activateCode = useCallback(
async (_code = code, evaluateOnly = false) => {
async (_code = code) => {
if (activeCode && !dirty) {
setError(undefined);
!evaluateOnly && cycle.start();
cycle.start();
return;
}
try {
setPending(true);
const parsed = await evaluate(_code);
!evaluateOnly && cycle.start();
cycle.start();
broadcast({ type: 'start', from: id });
setPattern(() => parsed.pattern);
if (autolink) {
@@ -139,22 +135,7 @@ 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,
@@ -165,10 +146,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
dirty,
log,
togglePlay,
stop,
setActiveCode,
activateCode,
evaluateOnly,
activeCode,
pushLog,
hash,
@@ -36,13 +36,7 @@ export const materialPalenightTheme = EditorView.theme(
lineHeight: '22px',
},
'.cm-line': {
// background: '#2C323699',
background: 'transparent',
},
'.cm-line > *': {
// background: '#2C323699',
background: '#00000090',
// background: 'transparent',
background: '#2C323699',
},
// done
'&.cm-focused .cm-cursor': {
@@ -67,7 +61,7 @@ export const materialPalenightTheme = EditorView.theme(
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: cursor + '50' },
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
+14 -24
View File
@@ -22,17 +22,13 @@ Pattern.prototype.pianoroll = function ({
flipTime = 0,
flipValues = 0,
hideNegative = false,
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
inactive = '#C9E597',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
minMidi = 10,
maxMidi = 90,
autorange = 1,
autorange = 0,
timeframe: timeframeProp,
fold = 0,
vertical = 0,
@@ -62,14 +58,12 @@ 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.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
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) => {
@@ -77,6 +71,15 @@ 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);
@@ -104,19 +107,6 @@ 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,
+1 -5
View File
@@ -62,11 +62,7 @@ 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); // piano does not understand frequencies
note = hap.value;
if (typeof note === 'number') {
note = midi2note(note);
}
note = getPlayableNoteValue(hap);
instrument.keyDown({ note, time, velocity });
instrument.keyUp({ note, time: time + hap.duration.valueOf(), velocity });
} else if (instrument instanceof Sampler) {
+133 -137
View File
@@ -126,150 +126,146 @@ const splitSN = (s, n) => {
Pattern.prototype.out = function () {
return this.onTrigger(async (t, hap, ct) => {
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);
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
}
if (typeof note === 'string') {
[note, n] = splitSN(note, n);
// get frequency
if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48);
}
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
// 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);
}
// 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);
} 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 {
// 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);
}
} 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);
}
bufferSource.start(t, offset, 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);
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 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]);
} catch (e) {
console.warn('.out error:', e);
}
chain.push(ac.destination);
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
});
};
+1 -3
View File
@@ -23,6 +23,4 @@ dist-ssr
*.sln
*.sw?
oldtunes.mjs
public/samples/EMU World
oldtunes.mjs
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -20,7 +20,7 @@ body {
.darken::before {
content: ' ';
position: fixed;
position: absolute;
top: 0;
left: 0;
width: 100vw;
+120 -129
View File
@@ -108,8 +108,6 @@ function App() {
pattern,
pushLog,
pending,
hideHeader,
hideConsole,
} = useRepl({
tune: '// LOADING...',
defaultSynth,
@@ -169,149 +167,142 @@ function App() {
return (
<div className="min-h-screen flex flex-col">
{!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">
<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 && (
<button
onClick={() => {
getAudioContext().resume(); // fixes no sound in ios webkit
togglePlay();
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);
}}
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...</>
)}
🎲 random
</button>
{!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);
)}
{!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"
>
🎲 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>
)}
💔 reset
</a>
</button>
)}
</div>
</header>
<section className="grow flex flex-col text-gray-100">
<div className="grow relative flex overflow-auto pb-8 cursor-text" id="code">
<div className="grow relative flex overflow-auto" id="code">
{/* onCursor={markParens} */}
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
<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">
<span className="z-[20] py-1 px-2 absolute top-0 right-0 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(
'rounded-md fixed pointer-events-none left-2 bottom-1 text-xs bg-black px-2 z-[20]',
'text-red-500',
)}
>
<div className={cx('absolute right-2 bottom-2 px-2', 'text-red-500')}>
{error?.message || 'unknown error'}
</div>
)}
</div>
{!isEmbedded && !hideConsole && (
{!isEmbedded && (
<textarea
className="z-[10] h-16 border-0 text-xs bg-[transparent] border-t border-slate-600 resize-none"
value={log}
+8 -144
View File
@@ -1,4 +1,4 @@
import { Pattern, silence, toMidi } from '@strudel.cycles/core';
import { Pattern, toMidi } from '@strudel.cycles/core';
import { samples } from '@strudel.cycles/webaudio';
export function prebake() {
@@ -46,151 +46,15 @@ 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.panByPitch = function () {
return this.fmap((value) => {
try {
// pan by pitch
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');
return this.clip(1)
.s('piano')
.fmap((value) => {
// pan by pitch
const pan = panwidth(Math.min(toMidi(value.note) / maxPan, 1), 0.5);
return { ...value, pan: (value.pan || 1) * pan };
});
};
-109
View File
@@ -1019,112 +1019,3 @@ x=>x.add(7).color('steelblue')
.stack(s("bd:1*2,~ sd:0,[~ hh:0]*2"))
.out()
.pianoroll({vertical:1})`;
export const root73 = `const root = 'E'
stack(
stack(
s("<bd(5,12) bd(3,12)>"),
s("hh!6").echo(2,1/12+.02,.2), // .begin(0.04),
s("~!5 oh"), // hush(),
s("~ <sd!7 [sd@3 sd@2 sd]>")
).slow(1) //.hcutoff(2000)
,
"<0(3,9) [2 <-3 3>]>"
.scale(root+'2 minor')
.legato(1).note().fast(2).s('jazzbass'),
"0 2 4 1".euclid(7,12)
.off(1/16, x=>x.add(2).color('steelblue'))
.off(1/12, x=>x.add(7).color('orange'))
.degradeBy(0.3)
.slow(4)
.scale(root+'4 minor').note()
.gain(.9)
.s('stage73').n(rand.range(0,.7).round())
.legato(perlin.range(.1,.5))
.velocity(perlin.range(.5,.8))
.echo(4, 1/6, .5), //.hcutoff(1000).hresonance(10),
mini(root+'m7@5 <Ab7 Eb7>').slow(2).voicings().transpose(0)
//.euclid(5,12,1)
.euclidLegato(5,12,1)
.note()
.gain(0.5)
.echo(1, 1/12, .5)
.slow(1)
.s('stage73')
.legato(.1)
.color('tomato')
.cutoff("300@2 600".fast(2))
//.resonance("10 20 25 10")
.n(1)
).slow(4/2).clip(1)
.out()
.pianoroll({autorange:0,vertical:1})
// strudel disable-highlighting`;
export const flute73 = `
const harmony = x=>x.scale(cat('E major','F# minor').slow(4)).note().clip(1)
stack(
// flute
"<0 2 5 4>"
.off(1/6, add(2))
.off(1/12, add(5))
.layer(harmony)
.legato("<1 .1>/16")
.echo("<1 3>/16",1/3,.5)
.s('flute')
.cutoff(perlin.range(500,2000))
.color('darkseagreen')
,
// bass
"<-7 -5 -9 -5>(3,9)"
.legato("<2.5 1.5>/16")
.layer(harmony)
.gain("1 .8 1")
.s('stage73')
.color('brown'),
// drums
s("bd*3, [~@2 <hh!3 hh*3>]*2,~ snap".slow(2)).gain(.4)
)
//.hcutoff(800)
.out()
.pianoroll({vertical:1,autorange:0})
`;
export const frequencyTest = `sequence(880, [440, 660], 440, 660)
.div(slowcat(3,2).slow(2))
.off(1/8,mul(2))
.off(1/4,mul(3))
.freq()
.legato(2)
.slow(2)
.s('sawtooth').cutoff(1000)
.out()
`;
export const gameboyParty = `
stack(
freq(
sequence(1,2).mul(55/2) // frequencies
.mul(slowcat(1,2))
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
.fast(3)
.velocity(.5)
)
.s(cat('sawtooth','square').fast(2))
.attack(.01).decay(.02).sustain(.5).release(.1)
.cutoff(2000) //.resonance(25)
,
freq(
sequence(1,[3/2,4/3],5/3)
.off(1/6, x=>x.mul(2).velocity(.5))
.iter(3)
.fast(1)
.mul(220)
)
.gain(.5)
.s(cat('sawtooth','square').slow(8))
.legato(sine.slow(16).add(.5).div(2))
.echo(2,1/12,.6)
.attack(.01).decay(.02).sustain(.5).release(.1)
)
.out()`;
-26
View File
@@ -1,26 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
samples
-6
View File
@@ -1,6 +0,0 @@
function ImgTile({ src, width, height }) {
return (
<div style={{ background: `url(${src})`, backgroundSize: 'cover', backgroundPosition: 'center', width, height }} />
);
}
export default ImgTile;
-95
View File
@@ -1,95 +0,0 @@
import { evalScope } from '@strudel.cycles/eval';
import { MiniRepl as _MiniRepl } from '@strudel.cycles/react';
import controls from '@strudel.cycles/core/controls.mjs';
import { loadWebDirt } from '@strudel.cycles/webdirt';
import { materialPalenightLarge } from './materialPalenightThemeLarge';
import { useState, useEffect } from 'react';
import { cleanupDraw, cleanupUi, Tone, piano } from '@strudel.cycles/tone';
import { midi2note } from '@strudel.cycles/core';
import { prebake } from '../repl/src/prebake.mjs';
let defaultPiano;
piano().then((instrument) => {
defaultPiano = instrument.toDestination();
});
const init = evalScope(
Tone,
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tone'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
import('@strudel.cycles/webdirt'),
);
loadWebDirt({
sampleMapUrl: './samples/EmuSP12.json',
sampleFolder: './samples/EmuSP12/',
});
prebake();
export function MaxiRepl({ code, canvasHeight = 500 }) {
const [exampleIndex, setExampleIndex] = useState(0);
const examples = Array.isArray(code) ? code : [code];
const [ready, setReady] = useState(false);
useEffect(() => {
init.then(() => setReady(true));
}, []);
useEffect(() => {
cleanupDraw();
cleanupUi();
prebake();
}, [exampleIndex]);
return (
<div className="text-left block max-w-screen relative">
{examples.length > 1 && (
<div className="space-x-2 absolute left-[200px] top-[-3px]">
{examples.map((c, i) => (
<button
key={i}
onClick={() => setExampleIndex(i)}
className={`rounded-full ${exampleIndex === i ? 'bg-gray-200 w-5 h-5' : 'bg-gray-500 w-5 h-5'}`}
></button>
))}
</div>
)}
<_MiniRepl
enableKeyboard={true}
key={exampleIndex}
tune={examples[exampleIndex]}
hideOutsideView={true}
theme={materialPalenightLarge}
init={ready}
onEvent={(time, hap) => {
if (hap.context.onTrigger) {
// dont need default synth
return;
}
let velocity = hap.context?.velocity ?? 0.75;
note = hap.value;
if (typeof note === 'number') {
note = midi2note(note);
}
defaultPiano.keyDown({ note, time, velocity });
defaultPiano.keyUp({ note, time: time + hap.duration.valueOf(), velocity });
}}
/>
<canvas
id="test-canvas"
className="w-full pointer-events-none"
height={canvasHeight}
ref={(el) => {
if (el) {
el.width = el.clientWidth;
}
}}
></canvas>
</div>
);
}
-6
View File
@@ -1,6 +0,0 @@
```sh
npm create vite@latest slides -- --template react
cd slides
npm i vite-plugin-mdx --save-dev
npm run dev
```
-130
View File
@@ -1,130 +0,0 @@
import { useState, useLayoutEffect, useEffect, useCallback, useMemo } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
const getUrlIndex = () => parseInt(window.location.hash.split('#')[1] || 0, 10);
export function Slides({ children }) {
const visible = useMemo(() => children.filter((child) => !child.props.hidden), [children]);
const [slide, setSlide] = useState(getUrlIndex() % visible.length);
useEffect(() => {
window.location.hash = '#' + slide;
}, [slide]);
const next = useCallback(() => setSlide((s) => (s + 1) % visible.length), [visible]);
const prev = useCallback(() => setSlide((s) => (s + visible.length - 1) % visible.length), [visible]);
useLayoutEffect(() => {
const handleKeyPress = async (e) => {
if (e.ctrlKey && e.altKey) {
if (e.code === 'ArrowRight') {
next();
} else if (e.code === 'ArrowLeft') {
prev();
}
e.preventDefault();
}
};
window.addEventListener('keydown', handleKeyPress, true);
return () => window.removeEventListener('keydown', handleKeyPress, true);
}, [slide]);
return (
<div className="bg-slate-900">
<AnimatePresence exitBeforeEnter>
<motion.div
key={slide}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="w-screen h-screen"
>
<div className="flex justify-center px-6 min-h-full overflow-auto">
<div
className={`px-8 prose grow max-w-[1280px]
text-gray-200 font-serif
prose-headings:text-gray-200
prose-a:text-indigo-300
prose-blockquote:text-gray-200
prose-em:text-[1.3em]
prose-strong:text-gray-200
prose-li:text-[1.4em]
prose-headings:font-sans
prose-code:text-gray-300
prose-headings:mt-12
prose-2xl
prose-slate`}
>
<center>{visible[slide]}</center>
</div>
</div>
</motion.div>
</AnimatePresence>
<SlideNav onNext={next} onPrev={prev} slides={visible} />
</div>
);
}
export function Slide({ children }) {
return children;
}
function SlideNav({ onNext, onPrev }) {
return (
<div className="flex absolute top-1/2 space-x-2 w-full justify-between items-center z-1">
<button onClick={() => onPrev()} className="absolute left-0 my-auto">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-12 w-12 text-gray-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
</button>
<button onClick={() => onNext()} className="absolute right-0 my-auto">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-12 w-12 text-gray-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
);
}
/*
<div className="flex absolute bottom-8 space-x-2 w-full justify-center items-center z-1">
<button onClick={() => setSlide((s) => (s + slides.length - 1) % slides.length)}>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-gray-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
</button>
{slides.map((_, i) => (
<div key={i} className={`rounded-full ${slide === i ? 'bg-gray-200 w-3 h-3' : 'bg-gray-500 w-2 h-2'}`}></div>
))}
<button onClick={() => setSlide((s) => (s + 1) % slides.length)}>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-gray-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
</div> */
-981
View File
@@ -1,981 +0,0 @@
import { Slides, Slide } from './Slides.jsx';
import { MaxiRepl } from './MaxiRepl';
import ImgTile from './ImgTile';
<Slides>
<Slide>
<div className="w-full h-full flex justify-center items-center">
<div>
# Strudel
## Algorithmic Patterns for the Web
![whirlpool](./strudel_icon.png)
Alex McLean, Felix Roos & Tidal Cycles Community
</div>
</div>
</Slide><Slide hidden>
<img src="./ada.jpg" className="h-[600px] rounded-md" />
</Slide><Slide hidden>
<img src="./ada.jpg" className="h-[400px] rounded-md" />
<div className="flex space-x-2 items-center">
<em className="hidden">
"The Analytical Engine weaves algebraic patterns, just as the Jacquard loom weaves flowers and leaves." - Ada
Lovelace
</em>
<em>
“[The Analytical Engine] might act upon other things besides <strong>number</strong>, were objects found whose
mutual fundamental relations could be expressed by those of the abstract science of <strong>operations</strong>{' '}
[...]
<span className="hidden">
, and which should be also susceptible of adaptations to the action of the operating notation and mechanism of the
engine.{' '}
</span>
<br />
Supposing, for instance, that the fundamental relations of pitched sounds in the science of harmony and of musical composition
were susceptible of such expression and adaptations, the [Analytical] Engine might compose elaborate and scientific pieces
of music of any degree of complexity or extent."
<br />
-- Ada Lovelace, (1815 - 1852)
</em>
</div>
</Slide><Slide>
## Strudel is..
<div className="flex">
<div className="text-left">
- zero-install live coding editor for the web
- Tidal Cycles port to JavaScript
- a terse language for algorithmic patterns
- for dynamic music sequencing
- algorithmic composition
- live improvisation
- a set of reusable packages
- probably more exciting than your regular DAW
</div>
![whirlpool](./strudel_icon.png)
</div>
</Slide><Slide hidden>
<img src="./codeandmusic.png" className="h-[700px] rounded-md -my-24" />
<div className="text-left">
- How to represent and control music with code?
- How to leverage unique features of computing?
- How to break loose from old conventions?
- How to improvise electronic music?
- Answer: Live Coding?
</div>
</Slide><Slide>
## About Me
<div className="text-left">
- 💻 working as a frontend developer by day, studied Computer Science
- 🎺 Trumpet Player, studied Jazz and Popular Music
- 💿 ocassionally publishes music with Lui Mafuta & Puste
- 🔌 Modular Synth & Electronics Lover, Dilettante DJ
- 💌 Coding Blog: [loophole-letters.vercel.app](https://loophole-letters.vercel.app/)
- 👀 Loves visualizations
<div className="grid grid-cols-5 gap-4 mt-12">
<ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Flambdoma.png&w=2048&q=75" />
{/* <ImgTile
height={300}
src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fharmonic-spiral.png&w=2048&q=75"
/> */}
<ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fpiano-roll.png&w=2048&q=75" />
<ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fcof.png&w=2048&q=75" />
<ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2F251graph.png&w=2048&q=75" />
<ImgTile
height={200}
src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fdiy-modular-synth%2Fmidi2cv-styleshot.png&w=2048&q=75"
/>
{/* <ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fcolorpiano.png&w=2048&q=75" /> */}
{/*
<ImgTile
height={300}
src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fvoicing-permutation.png&w=2048&q=75"
/> */}
</div>
</div>
</Slide><Slide>
## Some of my other Projects loosely related to Strudel
## A Taste of Strudel
<MaxiRepl
canvasHeight={600}
code={[
`"<0 2 [4 6](3,4,1) 3*2>"
.color('salmon')
.off(1/4, x=>x.add(2).color('green'))
.off(1/2, x=>x.add(6).color('steelblue'))
.scale('D minor')
.legato(.5)
.echo(2, 1/8, .5)
.pianoroll()
.note().piano().out()`,
//
`stack(
s("bd,[~ <sd!3 sd(3,4,2)>],hh(3,4)") // drums
.speed(perlin.range(.7,.9)) // random sample speed variation
,"<a1 b1*2 a1(3,8) e2>" // bassline
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
.add(perlin.range(0,.5)) // random pitch variation
.superimpose(add(.05)) // add second, slightly detuned voice
.n() // wrap in "n"
.decay(.15).sustain(0) // make each note of equal length
.s('sawtooth') // waveform
.gain(.4) // turn down
.cutoff(sine.slow(7).range(300,5000)) // automate cutoff
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings() // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.s('sawtooth') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
.attack(1) // slowly fade in
,"a4 c5 <e6 a6>".struct("x(5,8)")
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.decay(.1).sustain(0) // make notes short
.s('triangle') // waveform
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
.echoWith(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes
)
.out()
.slow(3/2)`,
/* `// froos - strudel schneckno
samples({
clubkick: 'clubkick/2.wav',
sd: ['808sd/SD0010.WAV','808sd/SD0050.WAV'],
hh: 'hh/000_hh3closedhh.wav',
clak: 'clak/000_clak1.wav',
jvbass: ['jvbass/000_01.wav','jvbass/001_02.wav','jvbass/003_04.wav','jvbass/004_05.wav','jvbass/005_06.wav']
}, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
stack(
s("<clubkick*2>,[~ <sd!3 sd(3,4,2)>],hh(3,4,2)")
.n("<0 1 2>").speed(.8),
n("<0 1*2 4(3,8) 2>").speed(.6)
.off(1/8, x => x.speed(1.2).degradeBy(.5))
.s("jvbass")
).reset("<x@7 <x(5,8,3) x(3,8) x(3,4,1)>>")
.cutoff(perlin.slow(7).range(100,20000))
.webdirt().slow(3/2)`,*/
/* `stack( // drum pattern by yaxu
"24*2 [~ 25]".chunk(4, x => x.sub("12").fast(2))
.tone(membrane().toDestination()),
"~ x ~ x".every(2, x => x.fast(2)).iter(4)
.tone(noise().toDestination()),
"c4*8".add(saw.mul(12).slow(1.5)).degrade(0.05)
.tone(metal().set(adsr(0,.06,0,0)).chain(vol(0.5),out()))
)`,*/
/* `stack(
note("<[0 2 4 2] [0 3 5 3]>".scale('D3 dorian'))
.s('jazzbass'),
note("0,2,4".add("<0 1 2 1>").scale('D3 dorian'))
.s('stage73')
).clip(1).out()`, */
]}
/>
</Slide><Slide>
## Live Coding
<i className="text-3xl">
"In live coding performance, performers create time-based works by programming them while these same works are being
executed." <br />
Roberts & Wakefield, 2016
</i>
<img src="./hydra-screenshot.png" className="h-[600px] rounded-md" />
<p>
Screenshot: "Olivia Jack -{' '}
<a href="https://hydra.ojack.xyz/" target="_blank">
Hydra
</a>
, Live Coding Visuals in the Browser".{' '}
</p>
</Slide><Slide>
## Tidal Cycles
<div className="text-left">
- A popular software for live coding music, developed since 2009
- a powerful pattern language, inspired by classical music
- functional reactive programming (FRP)
</div>
<img src="./tidal-screenshot2.png" className="h-[500px] rounded-md" />
<p>
Screenshot:{' '}
<a href="https://youtu.be/tY8BmTnlUA0?t=594" target="_blank">
"Yaxu & hellocatfood - LIVE code for Noise Quest"
</a>
</p>
</Slide><Slide hidden>
## What is Strudel?
<div className="text-left">
- a port of Tidal Cycles to JavaScript
- a browser based editor for zero-install live coding: <a href="https://strudel.tidalcycles.org/" target="_blank" className="text-indigo-400">strudel.tidalcycles.org</a>
- many ways to make sound: Web Audio, OSC, MIDI, Speech ...
<div className="hidden">
- "Strudel" = internationally known as delicious pastry, but also "swirl" / "whirlpool" in german
- (a tidal can cause a whirlpool!)
</div>
</div>
<img src="./strudel-screenshot.png" className="h-[500px] rounded-md mb-0" />
<p>
Screenshot:{' '}
<a href="https://youtu.be/IcMSocdKwvw?t=330" target="_blank">
"Algorave 10th Birthday March 2022 - froos"
</a>
</p>
</Slide><Slide>
## Strudel: How it started
<div className="text-left mb-0">
<blockquote>
"Any application that can be written in JavaScript, will eventually be written in JavaScript."
<br />
Jeff Atwood, 2009{' '}
</blockquote>
![tidal-vortex-strudel](./tidal-vortex-strudel.png)
</div>
<div className="text-left">
![strudel port quote](./strudel-port.png)
- Started by Alex McLean in January 2022
- based on a port of a rewrite of Tidal
- I implemented the REPL + additional strudel packages
- Help from Tidal Community Members
- Inspired by: Tidal Vortex, Gibber, Estuary, Hydra, WAGS and Feedforward.
</div>
</Slide><Slide>
## High Level Overview
<div className="absolute top-[250px] text-8xl">➡️</div>
<img src="./strudelflow.png" className="h-[800px] mt-24" />
</Slide><Slide hidden>
## REPL = Read Eval Play Loop
<div className="text-left">
1. Read: User code is parsed, syntax sugar is dissolved
2. Eval: Transformed code is evaluated, creating a **Pattern**
3. Play: The **Pattern** is queried for events, which trigger sounds and effects
4. Loop: The new output is fed back into the ears of the human, informing the next move
</div>
<img src="./repl.png" className="-my-16" />
</Slide><Slide>
## User Code
<div className="text-left">
- JavaScript + syntax sugar (transpilation with shift-ast)
- "Mini Notation" for terse rhythm notation
- `seq(c3, [e3, g3])` === `"c3 [e3 g3]"`
- All Parameters can be a Pattern
</div>
<MaxiRepl
code={[
`note("c3 [e3 g3]")
.piano().out().pianoroll()`,
`// mini notation also works with samples
s("bd,[~ sd],hh*4").out()`,
`\`[[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5]
[c5 a4 a4 ~]
[[~ d5] [~ f5] a5 [g5 f5]]
[e5 [~ c5] e5 [d5 c5]]
[b4 [b4 c5] d5 e5]
[c5 a4 a4 ~]],
[[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]\`.slow(16)
.note().out()
.pianoroll({ fold:1, cycles:16 })`,
`"-7, 0 2 4 7".add("<0 1 2 1>")
.scale(cat('F#2 major','A2 major').slow(8))
.note().piano().out().pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'})`,
/* `stack(
// melody
\`<
[e5 ~] [[d5@2 c5] [~@2 e5]] ~ [~ [c5@2 d5]] [e5 e5] [d5 c5] [e5 f5] [g5 a5]
[~ c5] [c5 d5] [e5 [c5@2 c5]] [~ c5] [f5 e5] [c5 d5] [~ g6] [g6 ~]
[e5 ~] [[d5@2 c5] [~@2 e5]] ~ [~ [c5@2 d5]] [e5 e5] [d5 c5] [a5 g5] [c6 [e5@2 d5]]
[~ c5] [c5 d5] [e5 [c5@2 c5]] [~ c5] [f5 e5] [c5 d5] [~ [g6@2 ~] ~@2] [g5 ~]
[~ a5] [b5 c6] [b5@2 ~@2 g5] ~
[f5 ~] [[g5@2 f5] ~] [[e5 ~] [f5 ~]] [[f#5 ~] [g5 ~]]
[~ a5] [b5 c6] [b5@2 ~@2 g5] ~
[eb6 d6] [~ c6] ~!2
>\`
.legato(.95),
// sub melody
\`<
[~ g4]!2 [~ ab4]!2 [~ a4]!2 [~ bb4]!2
[~ a4]!2 [~ g4]!2 [d4 e4] [f4 gb4] ~!2
[~ g4]!2 [~ ab4]!2 [~ a4]!2 [~ bb4]!2
[~ a4]!2 [~ g4]!2 [d4 e4] [f4 gb4] ~!2
[~ c5]!4 [~ a4]!2 [[c4 ~] [d4 ~]] [[eb4 ~] [e4 ~]]
[~ c5]!4 [~ eb5]!2 [g4*2 [f4 ~]] [[e4 ~] [d4 ~]]
>\`,
// bass
\`<
c3!7 a3 f3!2
e3!2 ~!4
c3!7 a3 f3!2
e3!2 ~!4
f3!2 e3!2 d3!2 ~!2
f3!2 e3!2 ab3!2 ~!2
>\`
.legato(.5)
).fast(2).pianoroll({fold:1,cycles:8,overscan:8})
.note().piano().out()`,
`stack(
"c2 <g2 g1>",
"<[e3,[g3 a3 bb3 a3]]!11 [e3,g3]>".slow(2)
.struct("<[[x@2 x]*4]!11 [[~@2 x] [[x@2 ~]*9]@3]>".slow(2))
)
.velocity("[.7@2 .9]*2".add(rand.range(-.1,.1)))
.transpose("<1 6 1@2 6@2 1@2 8 6 1 8>/2")
.pianoroll({ fold:1, cycles:16 })
.note().piano().out()`, */
]}
/>
</Slide><Slide>
## 🍭 Syntax Sugar
JavaScript Transpilation: converting valid JavaScript to valid JavaScript
<div className="hidden">https://github.com/tidalcycles/strudel/blob/main/packages/eval/shapeshifter.mjs#L40</div>
<img src="./shiftflow.png" className="-my-16" />
<div className="text-left">
<div className="grid grid-cols-2 gap-4 mt-4">
<span>🍭 Before (User Code)</span>
<span>🤖 After (Ready to Eval)</span>
<pre>"c3 [e3 g3]".fast(2)</pre>
```js
mini('c3 [e3 g3]')
.withMiniLocation([1, 0, 0], [1, 11, 11]) // source location
.fast(2);
```
</div>
</div>
<div className="hidden">
[Blog Post](https://loophole-letters.vercel.app/shift-ast), [AST Explorer](https://astexplorer.net/)
</div>
</Slide><Slide>
## 👁 Parsing Mini Notation
<div className="text-left">
1. write [peg](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), generate [parser](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill-parser.js) with [parser generator (peggyjs)](https://peggyjs.org/)
2. pass string to generated parser: `mini('c3 [e3 g3]')`
3. obtain Abstract Syntax Tree (AST)
4. [transform](https://github.com/tidalcycles/strudel/blob/main/packages/mini/mini.mjs#L76) AST into Pattern: `seq('c3', ['e3', 'g3'])`
</div>
<img src="./pegflow.png" className="-my-16" />
[OhmJS Editor](https://ohmjs.org/editor/)
</Slide><Slide>
## High Level Overview
<div className="absolute top-[545px] text-8xl">➡️</div>
<img src="./strudelflow.png" className="h-[800px] mt-24" />
</Slide><Slide hidden>
## AST of `c3 [e3 g3]`
<div className="text-left">
```js
{
"type_": "pattern",
"arguments_": { "alignment": "h" },
"source_": [
{
"type_": "element", "source_": "c3",
"location_": { "start": { "offset": 1, "line": 1, "column": 2 }, "end": { "offset": 4, "line": 1, "column": 5 } }
},
{
"type_": "element",
"location_": { "start": { "offset": 4, "line": 1, "column": 5 }, "end": { "offset": 11, "line": 1, "column": 12 } }
"source_": {
"type_": "pattern", "arguments_": { "alignment": "h" },
"source_": [
{
"type_": "element", "source_": "e3",
"location_": { "start": { "offset": 5, "line": 1, "column": 6 }, "end": { "offset": 8, "line": 1, "column": 9 } }
},
{
"type_": "element", "source_": "g3",
"location_": { "start": { "offset": 8, "line": 1, "column": 9 }, "end": { "offset": 10, "line": 1, "column": 11 } }
}
]
},
}
]
}
```
</div>
= `seq(c3, seq(e3, g3))`
</Slide><Slide>
## 🕵🏽 Querying Events
Asking a Pattern for Events within a certain time span
<div className="text-left">
```js
seq('c3', ['e3', 'g3']) // <--- Pattern
.queryArc(0, 2) // query events within 0 and 2 seconds
.map((event) => event.showWhole()); // make readable
```
```js
[
'0/1 -> 1/2: c3',
'1/2 -> 3/4: e3',
'3/4 -> 1/1: g3',
'1/1 -> 3/2: c3', // second time
'3/2 -> 7/4: e3',
'7/4 -> 2/1: g3',
];
```
</div>
The scheduler will query the active pattern at regular intervals!
</Slide><Slide>
## 🗓️ Scheduling
Query events repeatedly
```js
let step = 0.5; // query interval in seconds
let tick = 0; // how many intervals have passed
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
setInterval(() => {
const events = pattern.queryArc(tick * step, ++tick * step);
events.forEach((event) => {
console.log(event.showWhole());
const o = getAudioContext().createOscillator();
o.frequency.value = getFreq(event.value);
o.start(event.whole.begin);
o.stop(event.whole.begin + event.duration);
o.connect(getAudioContext().destination);
});
}, step * 1000); // query each "step" seconds
```
</Slide><Slide>
## High Level Overview
<div className="absolute top-[855px] text-8xl">➡️</div>
<img src="./strudelflow.png" className="h-[800px] mt-24" />
</Slide><Slide>
## Sound Output
Patterns are wrapped with param functions to compose different properties of the sound.
<MaxiRepl
canvasHeight={0}
code={[
`note("[c2(3,8) [<eb2 g1> bb1]]") // sets frequency
.s("<sawtooth square>") // sound source
.gain(.5) // turn down volume
.cutoff(sine.range(200,1000).slow(4)) // modulated cutoff
.slow(2)
.out().logValues()`,
]}
/>
```js
{ note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 }
```
![waa nodes](./waa-nodes.png)
Supports Web Audio API, Web MIDI, OSC, Speech, Web Serial, ... ?
</Slide><Slide>
## 🔭 Future Outlook
<div className="text-left">
- Alternative Sound Backends
- Collaborative Mode
- Context Aware Documentation / Completion / Visualization
- Port more Tidal Features
</div>
## 🌀 Try it out now
<div className="text-left">
- Strudel REPL: [strudel.tidalcycles.org](https://strudel.tidalcycles.org/?EeNsQ8hdNZwN) (-> [offline version](http://localhost:3000/#Y29uc3QgZGVsYXkgPSBuZXcgRmVlZGJhY2tEZWxheSgxLzMsIC41KS5jaGFpbih2b2woLjIpLCBvdXQoKSkKbGV0IGthbGltYmEgPSBhd2FpdCBzYW1wbGVyKHsKICBDNTogJy4vc2FtcGxlcy9rYWxpbWJhL2thbGltYmEtYzUubXAzJwp9KQprYWxpbWJhID0ga2FsaW1iYS5jaGFpbih2b2woMC42KS5jb25uZWN0KGRlbGF5KSxvdXQoKSk7CmNvbnN0IHNjYWxlcyA9IGNhdCgnQyBtYWpvcicsICdDIG1peG9seWRpYW4nLCAnRiBseWRpYW4nLCBbJ0YgbWlub3InLCAnRGIgbWFqb3InXSkKCnN0YWNrKAogICJbMCAyIDQgNiA5IDIgMCAtMl0qMyIKICAuYWRkKCI8MCAyPi80IikKICAuc2NhbGUoc2NhbGVzKQogIC5zdHJ1Y3QoIngqOCIpCiAgLnZlbG9jaXR5KCI8LjggLjMgLjY%2BKjgiKQogIC5zbG93KDIpCiAgLnRvbmUoa2FsaW1iYSksCiAgIjxjMiBjMiBmMiBbW0YyIEMyXSBkYjJdPiIKICAuc2NhbGUoc2NhbGVzKQogIC5zY2FsZVRyYW5zcG9zZSgiWzAgPDIgND5dKjIiKQogIC5zdHJ1Y3QoIngqNCIpCiAgLnZlbG9jaXR5KCI8LjggLjU%2BKjQiKQogIC52ZWxvY2l0eSgwLjgpCiAgLnNsb3coMikKICAudG9uZShrYWxpbWJhKQopCiAgLmxlZ2F0bygiPC40IC44IDEgMS4yIDEuNCAxLjYgMS44IDI%2BLzgiKQogIC5mYXN0KDEpCi5waWFub3JvbGwoe2F1dG9yYW5nZTowfSk%3D))
- Interactive Tutorial: [strudel.tidalcycles.org/tutorial](https://strudel.tidalcycles.org/)
- Stay Tuned: [@tidalcycles](https://twitter.com/tidalcycles)
- Github: [https://github.com/tidalcycles/strudel](https://github.com/tidalcycles/strudel)
</div>
### Happy Strudeling!
</Slide><Slide>
## More?
<MaxiRepl
code={[
`stack(
s("<bd [bd ~ bd]> ~, ~ sd, hh*3")
//.cutoff(perlin.range(100,900))
//.sometimes(x=>x.speed("2"))
,
note(
"Cm7 <Dm7 C#m7>".voicings()
).rhodes().slow(4)
,
note(
"[c2 <eb2 g1>](3,9)".slow(2)
//.superimpose(add(0.02))
)
.s("<sawtooth square>").gain(.5)
.cutoff(800),
note(
"0 2 4 1".iter(4)
.off(1/12, add("6"))
.degradeBy(.5)
.scale('C5 minor')
.legato(.05)
.echo(2, 1/6, .5)
)
.piano().hcutoff(2000)
).out()
//.cutoff(800)
//.reset("<x@3 x*2>")
`,
]}
/>
</Slide><Slide hidden>
## Strudel Workshop
![whirlpool](./strudel_icon.png)
</Slide><Slide>
## Pattern Factories
seq, cat, stack
<MaxiRepl
code={[
`seq(c3,e3,g3) // "c3 e3 g3"
.pianoroll()
.note().piano().out()`,
`cat(c3,e3,g3) // "<c3 e3 g3>"
.pianoroll()
.note().piano().out()`,
`stack(c3,e3,g3) // "c3,e3,g3"
.pianoroll()
.note().piano().out()`,
`stack(c3, seq(e3, cat(g3, c4))) // "c3,[e3,<g3 c4>]"
.pianoroll()
.note().piano().out()`,
]}
/>
</Slide><Slide>
## Time Control
fast / slow, early / late, rev, palindrome
<MaxiRepl
code={[
`"c3 e3 g3".fast(2) // "[c3 e3 g3]*2"
.pianoroll()
.note().piano().out()`,
`"c3 e3 g3".slow(2) // "[c3 e3 g3]/2"
.pianoroll()
.note().piano().out()`,
`stack(
"c3 e3".color('steelblue'),
"<g3 a3 bb3 a3>".early(.16)
).late(.16)
.pianoroll()
.note().piano().out()`,
`"c3 e3 g3 bb3".rev()
.pianoroll()
.note().piano().out()`,
]}
/>
</Slide><Slide>
## Arithmetic & Patternified Params
add, sub, mul, div
<MaxiRepl
code={[
`"30,42 45 49 54".add(0).slow(2)
.pianoroll({autorange:0,minMidi:28,maxMidi:56})
.note().piano().out()`,
`"30,42 45 49 54".add("<0 1 2 1>").slow(2)
.pianoroll({fold:1,cycles:8})
.note().piano().out()`,
]}
/>
</Slide><Slide>
## Scales
generating notes from numbers
<MaxiRepl
code={[
`"-7,0 2 4 7".add("<0 1 2 1>")
.scale('F#2 major')
.pianoroll({autorange:1})
.note().piano().out()`,
`"-7, 0 2 4 7".add("<0 1 2 1>")
.scale(cat('F#2 major','A2 major').slow(8))
.pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'})
.note().piano().out()`,
]}
/>
</Slide><Slide>
## Articulation
velocity, legato
<MaxiRepl
code={[
`"-7, 0 2 4 7".add("<0 1 2 1>")
.scale(cat('F#2 major','A2 major').slow(8))
.legato(.25)
.velocity(.5)
.pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'})
.note().piano().out()`,
`"-7, 0 2 4 7".add("<0 1 2 1>")
.scale(cat('F#2 major','A2 major').slow(8))
.legato("<0.25 2>/8")
.velocity("<1 .8>/8")
.pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'})
.note().piano().out()`,
]}
/>
</Slide><Slide>
## Continuous Signals / Randomness
sine / saw / tri / square / rand / perlin, range, segment
<MaxiRepl
code={[
`sine
.range(42,54).slow(4).segment(8)
.pianoroll({ minMidi:42, maxMidi: 54, autorange:0,
cycles:8,fold:0,inactive:'orange'})
.s('triangle').out()`,
`s("hh*8").speed(sine.range(.5,2).slow(4))
.out()`,
`sine.range(42,50).color('#00ff0050')
.superimpose(x=>x.fast(3/2).color('#ff00ff50'))
.fast(1)
.s('triangle').out()
.segment(64)
.pianoroll({fold:0,vertical:0,cycles:2})
// strudel disable-highlighting`,
]}
/>
</Slide><Slide>
## Humanization
"articulate randomness"
<MaxiRepl
code={[
`"-7, 0 2 4 7".add("<0 1 2 1>")
.scale(cat('F#2 major','A2 major').slow(8))
.legato(perlin.range(.5,2))
.velocity("<0.4 .3 .7>*3")
.late(perlin.range(0,.05))
.pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'})`,
]}
/>
</Slide><Slide hidden>
## Shuffling
iter
<MaxiRepl
code={[
`"c3 e3 g3 c4".iter(4)
.pianoroll({autorange:1,cycles:8,fold:1,inactive:'indigo'})`,
`stack("-7", "0 2 4 7".iter(4)).add("<0 1 2 1>")
.scale(cat('F#2 major','A2 major').slow(8))
.legato(perlin.range(.5,2))
.velocity(rand.range(.6,.9))
.pianoroll({autorange:1,cycles:16,fold:1,inactive:'indigo'})`,
]}
/>
</Slide><Slide>
## Superposition
superimpose, off, echo, echoWith
<MaxiRepl
code={[
`"0 [2 1] 4 7"
.superimpose(x=>x.add(5).color('green'))
.scale('C major').slow(2)
.pianoroll({autorange:1,cycles:8,fold:0,inactive:'indigo'})`,
`"0 2 4 7"
//.off(1/6, x=>x.add(9).color('green'))
//.off(1/3, x=>x.add(7).color('brown'))
.scale('C major').slow(2)
.pianoroll({autorange:1,cycles:8,fold:0,inactive:'indigo'})`,
`"0 2 4 7".add("<0 1>/8")
//.echo(6,.125,.8)
.scale('C major').slow(2)
.color("indigo steelblue orange")
.pianoroll({autorange:1,cycles:8,fold:1,inactive:'indigo'})`,
]}
/>
</Slide><Slide>
## Phasing
Oscillator Detune in slow motion
<MaxiRepl
code={[
`// Steve Reich - Piano Phase (1967)
"E4 F#4 B4 C#5 D5 F#4 E4 C#5 B4 F#4 D5 C#5".color('#00ff0050')
.superimpose(x=>x.fast(1.01).color('#ff00ff50'))
.slow(2)
.pianoroll({fold:1,vertical:0,cycles:8})`,
`// Steve Reich - Clapping Music
s("cp").struct("x!3 ~ x!2 ~ x ~ x!2 ~").speed(.8)
.jux(x=>x.speed(1.2)
.late("<0 1 2 3 4 5 6 7 8 9 10 11>/8".div(12))
).slow(2).webdirt()`,
]}
/>
</Slide><Slide>
## Chord Voicings
chord dictionaries + smooth voice leading (<a href="https://loophole-letters.vercel.app/rhythmical-chords" target="_blank">blog post</a>)
<MaxiRepl
code={[
`"Am7 [Bm7b5 E7b9]"
.struct("x(3,8)".fast(2))
.voicings(['E3', 'G4'])
.stack("A2 E2 <B2 B1> E2")
.slow(4)
.pianoroll({cycles:8,fold:1,inactive:'orange'})`,
`// froos - giant steps, stride version
stack(
// melody
seq(
"[F#5 D5] [B4 G4] Bb4 [B4 A4]",
"[D5 Bb4] [G4 Eb4] F#4 [G4 F4]",
"Bb4 [B4 A4] D5 [D#5 C#5]",
"F#5 [G5 F5] Bb5 [F#5 [F#5 ~@3]]",
),
// chords
seq(
"[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]",
"[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]",
"Eb^7 [Am7 D7] G^7 [C#m7 F#7]",
"B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]"
)
.struct("~ [x ~]".fast(4*8))
.voicings(['E3', 'G4']),
// bass
seq(
"[B2 D2] [G2 D2] [Eb2 Bb2] [A2 D2]",
"[G2 Bb2] [Eb2 F#2] [B2 F#2] [F2 Bb2]",
"[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]",
"[B2 F#2] [F2 Bb2] [Eb2 Bb2] [C#2 F#2]"
)
.struct("x ~".fast(4*8))
).slow(25)
.pianoroll({cycles:8,fold:1,inactive:'orange'})`]}/>
</Slide><Slide>
## Speech
<MaxiRepl code={['"<hello world>".speak()']} />
</Slide><Slide>
## Strudel Packages
`npm i @strudel.cycles/*`, where \* is one of:
<div className="text-left">
- core: tidal pattern engine
- mini: mini notation parser + core binding
- eval: user code evaluator. syntax sugar + highlighting
- tone: bindings for Tone.js instruments and effects
- osc: bindings to communicate via OSC
- midi: webmidi bindings
- serial: webserial bindings
- tonal: tonal functions
- xen: microtonal / xenharmonic functions
- ... there are [more](https://github.com/tidalcycles/strudel/tree/main/packages)
</div>
</Slide><Slide hidden>
## Workflow
<div className="text-left">
- Everything is in one [monorepo](https://github.com/tidalcycles/strudel)
- Development via npm workspaces
- Packages are published with lerna
- REPL is Deployed to github pages
- Features are added with Pull Requests
- Automated tests run for every PR
- Snapshot tests
</div>
</Slide><Slide hidden>
## Tech Stack
<div className="text-left">
- REPL: Codemirror 6 + react + tailwind
- Everything is built with vite
- In source documentation with jsdoc
- Tutorial written with MDX
- jsdoc is inserted into MDX with nunjucks
</div>
</Slide>
</Slides>
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Strudel Slides</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.jsx"></script>
</body>
</html>
-17
View File
@@ -1,17 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
// import Deck from './deck.mdx';
import Deck from './meetup.mdx';
import './style.css';
import '@strudel.cycles/react/dist/style.css';
import 'prismjs/themes/prism-tomorrow.css';
ReactDOM.render(
<React.StrictMode>
<div className="w-screen h-screen overflow-auto bg-slate-900">
<Deck />
</div>
</React.StrictMode>,
document.getElementById('root'),
);
-137
View File
@@ -1,137 +0,0 @@
import { EditorView } from '@codemirror/view';
import { HighlightStyle, tags as t } from '@codemirror/highlight';
/*
Credits for color palette:
Author: Mattia Astorino (http://github.com/equinusocio)
Website: https://material-theme.site/
*/
const ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
invalid = '#ffffff',
darkBackground = '#21252b',
highlightBackground = 'rgba(0, 0, 0, 0.5)',
// background = '#292d3e',
background = 'transparent',
tooltipBackground = '#353a42',
selection = 'rgba(128, 203, 196, 0.5)',
cursor = '#ffcc00';
/// The editor theme styles for Material Palenight.
export const materialPalenightThemeLarge = EditorView.theme(
{
// done
'&': {
color: '#ffffff',
backgroundColor: background,
fontSize: '30px',
'z-index': 11,
},
// done
'.cm-content': {
caretColor: cursor,
lineHeight: '40px',
},
'.cm-line': {
background: '#2C323699',
},
// done
'&.cm-focused .cm-cursor': {
backgroundColor: cursor,
width: '3px',
},
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
backgroundColor: selection,
},
'.cm-panels': { backgroundColor: darkBackground, color: '#ffffff' },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
// done, use onedarktheme
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
outline: '1px solid #515a6b',
},
// done
'.cm-gutters': {
background: 'transparent',
color: '#676e95',
border: 'none',
display: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: true },
);
/// The highlighting style for code in the Material Palenight theme.
export const materialPalenightHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' },
{ tag: t.special(t.variableName), color: '#eeffff' },
{ tag: t.typeName, color: '#f07178' },
{ tag: t.atom, color: '#f78c6c' },
{ tag: t.number, color: '#ff5370' },
{ tag: t.definition(t.variableName), color: '#82aaff' },
{ tag: t.string, color: '#c3e88d' },
{ tag: t.special(t.string), color: '#f07178' },
{ tag: t.comment, color: stone },
{ tag: t.variableName, color: '#f07178' },
{ tag: t.tagName, color: '#ff5370' },
{ tag: t.bracket, color: '#a2a1a4' },
{ tag: t.meta, color: '#ffcb6b' },
{ tag: t.attributeName, color: '#c792ea' },
{ tag: t.propertyName, color: '#c792ea' },
{ tag: t.className, color: '#decb6b' },
{ tag: t.invalid, color: invalid },
]);
/// Extension to enable the Material Palenight theme (both the editor theme and
/// the highlight style).
// : Extension
export const materialPalenightLarge = [materialPalenightThemeLarge, materialPalenightHighlightStyle];
-1124
View File
File diff suppressed because it is too large Load Diff
-7963
View File
File diff suppressed because it is too large Load Diff
-36
View File
@@ -1,36 +0,0 @@
{
"name": "@strudel.cycles/tutorial",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite --port 4321",
"start": "vite",
"build": "vite build",
"preview": "vite preview",
"symlink-samples": "ln -s ../../repl/public/samples/ ./public/"
},
"type": "module",
"dependencies": {
"framer-motion": "^6.3.6",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@mdx-js/mdx": "^1.6.22",
"@mdx-js/react": "^1.6.22",
"@tailwindcss/typography": "^0.5.2",
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2",
"@vitejs/plugin-react": "^1.3.0",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.13",
"rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1",
"remark-prism": "^1.3.6",
"remark-toc": "^8.0.1",
"sass": "^1.51.0",
"tailwindcss": "^3.0.24",
"vite": "^2.9.9",
"vite-plugin-mdx": "^3.5.10"
}
}
-12
View File
@@ -1,12 +0,0 @@
/*
postcss.config.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/postcss.config.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

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

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

-145
View File
@@ -1,145 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* cyrillic-ext */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_bZF3gTD_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_bZF3g3D_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_bZF3gbD_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_bZF3gfD_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_bZF3gnD_vx3rCs.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_epG3gTD_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_epG3g3D_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_epG3gbD_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_epG3gfD_vx3rCubqg.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_epG3gnD_vx3rCs.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* @import url('https://fonts.googleapis.com/css2?family=Merriweather&display=swap'); */
/* cyrillic-ext */
@font-face {
font-family: 'Merriweather';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/merriweather/v28/u-440qyriQwlOrhSvowK_l5-cSZMdeX3rsHo.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Merriweather';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/merriweather/v28/u-440qyriQwlOrhSvowK_l5-eCZMdeX3rsHo.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Merriweather';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/merriweather/v28/u-440qyriQwlOrhSvowK_l5-cyZMdeX3rsHo.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Merriweather';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/merriweather/v28/u-440qyriQwlOrhSvowK_l5-ciZMdeX3rsHo.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Merriweather';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/merriweather/v28/u-440qyriQwlOrhSvowK_l5-fCZMdeX3rg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
-17
View File
@@ -1,17 +0,0 @@
/*
tailwind.config.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/tailwind.config.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
module.exports = {
content: ['./**/*.{js,jsx,ts,tsx,mdx}'],
theme: {
extend: {},
fontFamily: {
sans: ['Montserrat'],
serif: ['Merriweather'],
},
},
plugins: [require('@tailwindcss/typography')],
};
-34
View File
@@ -1,34 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import _mdx from 'vite-plugin-mdx';
import remarkPrism from 'remark-prism';
/* import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings'; */
const mdx = _mdx.default || _mdx;
// `options` are passed to `@mdx-js/mdx`
const options = {
// See https://mdxjs.com/advanced/plugins
remarkPlugins: [
remarkPrism,
// remarkToc,
// E.g. `remark-frontmatter`
],
rehypePlugins: [
// rehypeSlug, rehypeAutolinkHeadings
],
};
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), mdx(options)],
build: {
outDir: '../out/tutorial',
},
base: '/',
});
// jsxRuntime:'classic' to prevent "jsxDevRuntime.exports.jsxDEV is not a function" for dev mode
// mode: 'development',
// react({ jsxRuntime: 'classic' }),
+2
View File
@@ -23,6 +23,8 @@
"@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",