Merge remote-tracking branch 'upstream/main' into branch-daria

This commit is contained in:
Felix Roos 2023-06-11 19:54:55 +02:00
commit 623661f8d2
91 changed files with 5130 additions and 296 deletions

View File

@ -0,0 +1,3 @@
# @strudel/codemirror
This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it.

View File

@ -0,0 +1,199 @@
import { EditorState } from '@codemirror/state';
import { EditorView, keymap, Decoration, lineNumbers, highlightActiveLineGutter } from '@codemirror/view';
import { defaultKeymap } from '@codemirror/commands';
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
import { javascript } from '@codemirror/lang-javascript';
import { StateField, StateEffect } from '@codemirror/state';
import { oneDark } from './themes/one-dark';
import { repl, Drawer } from '@strudel.cycles/core';
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
let state = EditorState.create({
doc: initialCode,
extensions: [
theme,
javascript(),
lineNumbers(),
highlightField,
highlightActiveLineGutter(),
syntaxHighlighting(defaultHighlightStyle),
keymap.of(defaultKeymap),
flashField,
EditorView.updateListener.of((v) => onChange(v)),
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate(),
},
{
key: 'Ctrl-.',
run: () => onStop(),
},
]),
],
});
return new EditorView({
state,
parent: root,
});
}
//
// highlighting
//
export const setHighlights = StateEffect.define();
export const highlightField = StateField.define({
create() {
return Decoration.none;
},
update(highlights, tr) {
try {
for (let e of tr.effects) {
if (e.is(setHighlights)) {
const { haps } = e.value;
const marks =
haps
.map((hap) =>
(hap.context.locations || []).map(({ start, end }) => {
// const color = hap.context.color || e.value.color || '#FFCA28';
let from = tr.newDoc.line(start.line).from + start.column;
let to = tr.newDoc.line(end.line).from + end.column;
const l = tr.newDoc.length;
if (from > l || to > l) {
return; // dont mark outside of range, as it will throw an error
}
const mark = Decoration.mark({
attributes: { style: `outline: 2px solid #FFCA28;` },
});
return mark.range(from, to);
}),
)
.flat()
.filter(Boolean) || [];
highlights = Decoration.set(marks, true);
}
}
return highlights;
} catch (err) {
// console.warn('highlighting error', err);
return Decoration.set([]);
}
},
provide: (f) => EditorView.decorations.from(f),
});
// helper to simply trigger highlighting for given haps
export function highlightHaps(view, haps) {
view.dispatch({ effects: setHighlights.of({ haps }) });
}
//
// flash
//
export const setFlash = StateEffect.define();
const flashField = StateField.define({
create() {
return Decoration.none;
},
update(flash, tr) {
try {
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash = Decoration.set([]);
}
}
}
return flash;
} catch (err) {
console.warn('flash error', err);
return flash;
}
},
provide: (f) => EditorView.decorations.from(f),
});
export const flash = (view, ms = 200) => {
view.dispatch({ effects: setFlash.of(true) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, ms);
};
export class StrudelMirror {
constructor(options) {
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
this.code = initialCode;
this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end);
this.highlight(currentFrame);
onDraw?.(haps, time, currentFrame);
}, drawTime);
const prebaked = prebake();
prebaked.then(async () => {
if (!onDraw) {
return;
}
const { scheduler, evaluate } = await this.repl;
// draw first frame instantly
prebaked.then(async () => {
await evaluate(this.code, false);
this.drawer.invalidate(scheduler);
onDraw?.(this.drawer.visibleHaps, 0, []);
});
});
this.repl = repl({
...replOptions,
onToggle: async (started) => {
replOptions?.onToggle?.(started);
const { scheduler } = await this.repl;
if (started) {
this.drawer.start(scheduler);
} else {
this.drawer.stop();
}
},
beforeEval: async () => {
await prebaked;
},
afterEval: (options) => {
replOptions?.afterEval?.(options);
this.drawer.invalidate();
},
});
this.editor = initEditor({
root,
initialCode,
onChange: (v) => {
this.code = v.state.doc.toString();
},
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
});
}
async evaluate() {
const { evaluate } = await this.repl;
this.flash();
await evaluate(this.code);
}
async stop() {
const { scheduler } = await this.repl;
scheduler.stop();
}
flash(ms) {
flash(this.editor, ms);
}
highlight(haps) {
highlightHaps(this.editor, haps);
}
}

View File

@ -0,0 +1,47 @@
{
"name": "@strudel/codemirror",
"version": "0.8.3",
"description": "Codemirror Extensions for Strudel",
"main": "codemirror.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": [
"Alex McLean <alex@slab.org>"
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/language": "^6.6.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^4.3.3"
}
}

139
packages/codemirror/themes/one-dark.mjs vendored Normal file
View File

@ -0,0 +1,139 @@
import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags as t } from '@lezer/highlight';
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
const chalky = '#e5c07b',
coral = '#e06c75',
cyan = '#56b6c2',
invalid = '#ffffff',
ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
malibu = '#61afef',
sage = '#98c379',
whiskey = '#d19a66',
violet = '#c678dd',
darkBackground = '#21252b',
highlightBackground = '#2c313a',
background = '#282c34',
tooltipBackground = '#353a42',
selection = '#3E4451',
cursor = '#528bff';
/// The colors used in the theme, as CSS color strings.
export const color = {
chalky,
coral,
cyan,
invalid,
ivory,
stone,
malibu,
sage,
whiskey,
violet,
darkBackground,
highlightBackground,
background,
tooltipBackground,
selection,
cursor,
};
/// The editor theme styles for One Dark.
export const oneDarkTheme = EditorView.theme(
{
'&': {
color: ivory,
backgroundColor: background,
},
'.cm-content': {
caretColor: cursor,
},
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
{ backgroundColor: selection },
'.cm-panels': { backgroundColor: darkBackground, color: ivory },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
},
'.cm-gutters': {
backgroundColor: background,
color: stone,
border: '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 One Dark theme.
export const oneDarkHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: violet },
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
{ tag: [t.definition(t.name), t.separator], color: ivory },
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
{ tag: [t.meta, t.comment], color: stone },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.link, color: stone, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: coral },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
{ tag: t.invalid, color: invalid },
]);
/// Extension to enable the One Dark theme (both the editor theme and
/// the highlight style).
export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];

View File

@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'codemirror.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});

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 { Pattern, sequence } from './pattern.mjs';
import { Pattern, register, sequence } from './pattern.mjs';
import { zipWith } from './util.mjs';
const controls = {};
@ -810,4 +810,15 @@ generic_params.forEach(([names, ...aliases]) => {
controls.createParams = (...names) =>
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
controls.adsr = register('adsr', (adsr, pat) => {
adsr = !Array.isArray(adsr) ? [adsr] : adsr;
const [attack, decay, sustain, release] = adsr;
return pat.set({ attack, decay, sustain, release });
});
controls.ds = register('ds', (ds, pat) => {
ds = !Array.isArray(ds) ? [ds] : ds;
const [decay, sustain] = ds;
return pat.set({ decay, sustain });
});
export default controls;

View File

@ -1,6 +1,6 @@
/*
draw.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/draw.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.mjs>
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/>.
*/
@ -13,7 +13,7 @@ export const getDrawContext = (id = 'test-canvas') => {
canvas.id = id;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0;z-index:5';
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
document.body.prepend(canvas);
}
return canvas.getContext('2d');
@ -65,3 +65,97 @@ Pattern.prototype.onPaint = function (onPaint) {
this.context = { onPaint };
return this;
};
// const round = (x) => Math.round(x * 1000) / 1000;
// encapsulates starting and stopping animation frames
export class Framer {
constructor(onFrame, onError) {
this.onFrame = onFrame;
this.onError = onError;
}
start() {
const self = this;
let frame = requestAnimationFrame(function updateHighlights(time) {
try {
self.onFrame(time);
} catch (err) {
self.onError(err);
}
frame = requestAnimationFrame(updateHighlights);
});
self.cancel = () => {
cancelAnimationFrame(frame);
};
}
stop() {
if (this.cancel) {
this.cancel();
}
}
}
// syncs animation frames to a cyclist scheduler
// see vite-vanilla-repl-cm6 for an example
export class Drawer {
constructor(onDraw, drawTime) {
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
lookbehind = Math.abs(lookbehind);
this.visibleHaps = [];
this.lastFrame = null;
this.drawTime = drawTime;
this.framer = new Framer(
() => {
if (!this.scheduler) {
console.warn('Drawer: no scheduler');
return;
}
// calculate current frame time (think right side of screen for pianoroll)
const phase = this.scheduler.now() + lookahead;
// first frame just captures the phase
if (this.lastFrame === null) {
this.lastFrame = phase;
return;
}
// query haps from last frame till now. take last 100ms max
const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase);
this.lastFrame = phase;
this.visibleHaps = (this.visibleHaps || [])
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
.filter((h) => h.whole.end >= phase - lookbehind - lookahead)
// add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead;
onDraw(this.visibleHaps, time, this);
},
(err) => {
console.warn('draw error', err);
},
);
}
invalidate(scheduler = this.scheduler) {
if (!scheduler) {
return;
}
this.scheduler = scheduler;
const t = scheduler.now();
let [_, lookahead] = this.drawTime;
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
// remove all future haps
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
// query future haps
const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query..
// append future haps
this.visibleHaps = this.visibleHaps.concat(futureHaps);
}
start(scheduler) {
this.scheduler = scheduler;
this.invalidate();
this.framer.start();
}
stop() {
if (this.framer) {
this.framer.stop();
}
}
}

View File

@ -0,0 +1,24 @@
# 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?

View File

@ -0,0 +1,8 @@
# vite-vanilla-repl-cm6
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
```sh
npm i
npm run dev
```

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite Vanilla Strudel REPL</title>
</head>
<body>
<main>
<nav>
<button id="play">eval</button>
<button id="stop">stop</button>
</nav>
<div class="container">
<div id="editor"></div>
<div id="output"></div>
</div>
<canvas id="roll"></canvas>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>

View File

@ -0,0 +1,39 @@
import { StrudelMirror } from '@strudel/codemirror';
import { funk42 } from './tunes';
import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
import './style.css';
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
// init canvas
const canvas = document.getElementById('roll');
canvas.width = canvas.width * 2;
canvas.height = canvas.height * 2;
const drawContext = canvas.getContext('2d');
const drawTime = [-2, 2]; // time window of drawn haps
const editor = new StrudelMirror({
defaultOutput: webaudioOutput,
getTime: () => getAudioContext().currentTime,
transpiler,
root: document.getElementById('editor'),
initialCode: funk42,
drawTime,
onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }),
prebake: async () => {
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
const loadModules = evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
},
});
document.getElementById('play').addEventListener('click', () => editor.evaluate());
document.getElementById('stop').addEventListener('click', () => editor.stop());

View File

@ -0,0 +1,23 @@
{
"name": "vite-vanilla-repl-cm6",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.3.2"
},
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
}
}

View File

@ -0,0 +1,31 @@
body,
html {
margin: 0;
height: 100%;
background: #282c34;
}
main {
height: 100%;
display: flex;
flex-direction: column;
}
.container {
flex-grow: 1;
max-height: 100%;
position: relative;
overflow: auto;
}
#editor {
overflow: auto;
}
.cm-editor {
height: 100%;
}
#roll {
height: 300px;
}

View File

@ -0,0 +1,112 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
// make it 22 edo
.fmap(v => Math.pow(2,v/22))
// mess with the base frequency
.mul("<300 [300@3 200]>/8").freq()
.layer(
// chords
x=>x.div(freq(2)).s("flute").euclidLegato("<3 2>",8)
.shape(.4).lpf(sine.range(800,4000).slow(8)),
// adlibs
x=>x.arp("{0 3 2 [1 3]}%1.5")
.s('xylo').mul(freq(2))
.delay(.5).delayfeedback(.4).juxBy(.5, rev)
.hpf(sine.range(200,3000).slow(8)),
// bass
x=>x.arp("[0 [2 1?]](5,8)").s('sawtooth').div(freq(4))
.lpf(sine.range(400,2000).slow(8)).lpq(8).shape(.4)
.off(1/8, x=>x.mul(freq(2)).degradeBy(.5)).gain(.3)
).clip(1).release(.2)
.stack(
// drums
s("bd sd:<2 1>, [~ hh]*2, [~ rim]").bank('RolandTR909')
.off(1/8, x=>x.speed(2).gain(.4)).sometimes(ply(2)).gain(.8)
.mask("<0@4 1@12>/4")
.reset("<x@15 [x(3,8) x*[4 8]]>")
// wait for it...
).fast(2/3)
//.crush(6) // remove "//" if you dare`;
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', {
m11: ['2M 3m 4P 7m'],
'^7#11': ['3M 4A 5P 7M'],
}, ['C4', 'C6'])
stack(
stack(
"<Bbm11 A^7#11>/2".voicings('hip').note()
.s("gm_epiano1:2")
.arp("[<[0 1 2 3] [3 2 1 0]> ~@5]/2")
.release(2).late(.25).lpf(2000),
"<Bb1 A1>/2".note().s('gm_acoustic_bass'),
n("<0 2 3>(3,8)".off(1/8, add(4)))
.scale("<Bb4:minor A4:lydian>/2")
.s('gm_electric_guitar_jazz')
.decay(sine.range(.05, .2).slow(32)).sustain(0)
.delay(.5).lpf(sine.range(100,5000).slow(64))
.gain(.7).room(.5).pan(sine.range(0,1).slow(11))
).add(perlin.range(0,.25).note()),
stack(
s("bd:1(3,8) rim").bank('RolandTR707').slow(2).room("<0 <.1 .6>>")
.when("<0@7 1>",x=>x.echoWith(3, .0625, (x,i) => x.speed(1+i*.24))),
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
)
)
.late("[0 .05]*2").late(12)
`;
export const funk42 = `// froos - how to funk in 42 lines of code
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5)
let drums = stack(
s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"),
s("hh*4").begin(.2).release(.02).end(.25).release(.02)
.gain(.3).bank('RolandTR707').late(.02).room(.5),
//s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02)
).fast(2)
let wurli = note(\`<
[[a2,g3,[b3 c4],e4] ~ [g3,c4,e4](3,8)@4 ~@2]!3
[[e2,e3,a3,b3,e4]@3 [e2,e3,ab3,b3,e4]@5]>\`)
.s("gm_epiano1:5").decay(.2).sustain("<[1 0@7]!3 1>")
.gain("<[.8@2 .4@14]!3 .7>").room(.3)
let organ = note("<[~@3 [a3,d4,f#4]@2 [[a3,c4,e4]@2 ~] ~@2]!3 ~>".add(12))
.s("gm_percussive_organ:2").gain(.6).lpf(1800).pan(.2).room(.3);
let clav = note(\`<
[~@3 a2 [g3,[b3 c4],e4]@2 ~ a2 [g3,b3,e4] ~@2 [g3,c4,e4] ~@4]!3
[~@3 e3 [[a3 b3],c3,e3]@2 ~ e2 [e3,a3]@3 [b3,e3] ~@2 [b3,e3]@2]>\`)
.s("gm_clavinet:1").decay("<.25!3 [.25 .4]>").sustain(0)
.gain(.7).pan(.8).room(.2);
let bass = note(\`<
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
[a1 [~ [g2 a2]] [e3 d3] [c3 [g3 a3]]]
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
[e2@6 e1@5 e1 [[d2 e3] g1]@4]
>\`).s("gm_electric_bass_pick:1").release(.1)
stack(
drums
,wurli
,organ
,clav
,bass
)`;

View File

@ -6,5 +6,3 @@ This folder demonstrates how to set up a strudel repl using vite and vanilla JS.
npm i
npm run dev
```
or view it [live on githack](https://rawcdn.githack.com/tidalcycles/strudel/5fb36acb046ead7cd6ad3cd10f532e7f585f536a/packages/core/examples/vite-vanilla-repl/dist/index.html)

View File

@ -18,6 +18,7 @@ export * from './util.mjs';
export * from './speak.mjs';
export * from './evaluate.mjs';
export * from './repl.mjs';
export * from './cyclist.mjs';
export * from './logger.mjs';
export * from './time.mjs';
export * from './draw.mjs';

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.7.2",
"version": "0.8.1",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",

View File

@ -1677,6 +1677,9 @@ export const ply = register('ply', function (factor, pat) {
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
*/
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
if (factor === 0) {
return silence;
}
factor = Fraction(factor);
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
return fastQuery.withHapTime((t) => t.div(factor));
@ -1703,6 +1706,9 @@ export const hurry = register('hurry', function (r, pat) {
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
*/
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
if (factor === 0) {
return silence;
}
return pat._fast(Fraction(1).div(factor));
});

View File

@ -50,6 +50,7 @@ Pattern.prototype.pianoroll = function ({
timeframe: timeframeProp,
fold = 0,
vertical = 0,
labels = 0,
} = {}) {
const ctx = getDrawContext();
const w = ctx.canvas.width;
@ -87,7 +88,7 @@ Pattern.prototype.pianoroll = function ({
const isActive = event.whole.begin <= t && event.whole.end > t;
ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
@ -114,6 +115,14 @@ Pattern.prototype.pianoroll = function ({
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
@ -181,6 +190,7 @@ export function pianoroll({
timeframe: timeframeProp,
fold = 0,
vertical = 0,
labels = false,
ctx,
} = {}) {
const w = ctx.canvas.width;
@ -240,7 +250,7 @@ export function pianoroll({
const color = event.value?.color || event.context?.color;
ctx.fillStyle = color || inactive;
ctx.strokeStyle = color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
@ -267,6 +277,14 @@ export function pianoroll({
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
@ -284,7 +302,7 @@ export function pianoroll({
return this;
}
function getOptions(drawTime, options = {}) {
export function getDrawOptions(drawTime, options = {}) {
let [lookbehind, lookahead] = drawTime;
lookbehind = Math.abs(lookbehind);
const cycles = lookahead + lookbehind;
@ -293,5 +311,18 @@ function getOptions(drawTime, options = {}) {
}
Pattern.prototype.punchcard = function (options) {
return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }),
);
};
/* Pattern.prototype.pianoroll = function (options) {
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),
);
}; */
export function drawPianoroll(options) {
const { drawTime, ...rest } = options;
pianoroll({ ...getDrawOptions(drawTime), ...rest });
}

View File

@ -18,23 +18,15 @@ export function repl({
}) {
const scheduler = new Cyclist({
interval,
onTrigger: async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
},
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
onToggle,
});
const setPattern = (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
};
setTime(() => scheduler.now()); // TODO: refactor?
const evaluate = async (code, autostart = true) => {
if (!code) {
@ -45,8 +37,7 @@ export function repl({
let { pattern } = await _evaluate(code, transpiler);
logger(`[eval] code updated`);
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
setPattern(pattern, autostart);
afterEval?.({ code, pattern });
return pattern;
} catch (err) {
@ -63,5 +54,21 @@ export function repl({
setCps,
setcps: setCps,
});
return { scheduler, evaluate, start, stop, pause, setCps };
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
}
export const getTrigger =
({ getTime, defaultOutput }) =>
async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
};

View File

@ -6,12 +6,12 @@ This program is free software: you can redistribute it and/or modify it under th
// returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]?$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9])?$/)?.slice(1) || [];
if (!pc) {
return [];
}
@ -25,7 +25,7 @@ export const noteToMidi = (note) => {
throw new Error('not a note: "' + note + '"');
}
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1, f: -1 }[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const midiToFreq = (n) => {
@ -67,14 +67,9 @@ export const getFreq = (noteOrMidi) => {
return midiToFreq(noteToMidi(noteOrMidi));
};
/**
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
* @noAutocomplete
*/
/* added code from here to solve issue 302*/
export const midi2note = (n, notation = 'letters') => {
const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/
/* added code from here to solve issue 302*/
const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/
const indian = [
'Sa',
'Re',
@ -108,6 +103,11 @@ export const midi2note = (n, notation = 'letters') => {
'He',
'To',
]; /*traditional japanese musical notes, seems like they do not use falts or sharps*/
const english = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']
export const sol2note = (n, notation = 'letters') => {
const pc =
notation === 'solfeggio'
? solfeggio /*check if its is any of the following*/
@ -119,11 +119,24 @@ export const midi2note = (n, notation = 'letters') => {
? byzantine
: notation === 'japanese'
? japanese
: ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']; /*if not use standard version*/
: english; /*if not use standard version*/
const note = pc[n % 12]; /*calculating the midi value to the note*/
const oct = Math.floor(n / 12) - 1;
return note + oct;
};
const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
/**
* @deprecated only used in workshop atm
* @noAutocomplete
*/
export const midi2note = (n, notation = 'letters') => {
const oct = Math.floor(n / 12) - 1;
//return note + oct;
const pc = pcs[n % 12];
return pc + oct;
};
/*testing if the function works by using the file solmization.test.mjs
in the test folder*/
@ -262,3 +275,5 @@ export const splitAt = function (index, value) {
};
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);

View File

@ -190,3 +190,8 @@ export function minify(thing) {
}
return strudel.reify(thing);
}
// calling this function will cause patterns to parse strings as mini notation by default
export function miniAllStrings() {
strudel.setStringParser(mini);
}

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.7.2",
"version": "0.8.1",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",

View File

@ -15,6 +15,7 @@
"@strudel.cycles/osc": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/react": "workspace:*"

View File

@ -1,22 +1,34 @@
import { controls, evalScope } from '@strudel.cycles/core';
import { CodeMirror, useHighlighting, useKeydown, useStrudel, flash } from '@strudel.cycles/react';
import { getAudioContext, initAudioOnFirstClick, panic, webaudioOutput } from '@strudel.cycles/webaudio';
import {
getAudioContext,
initAudioOnFirstClick,
panic,
webaudioOutput,
registerSynthSounds,
} from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { useCallback, useState } from 'react';
import './style.css';
// import { prebake } from '../../../../../repl/src/prebake.mjs';
initAudioOnFirstClick();
// TODO: only import stuff when play is pressed?
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
);
async function init() {
// TODO: only import stuff when play is pressed?
const loadModules = evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
}
init();
const defaultTune = `samples({
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
@ -31,7 +43,7 @@ stack(
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
.add(perlin.range(0,.5)) // random pitch variation
.superimpose(add(.05)) // add second, slightly detuned voice
.n() // wrap in "n"
.note() // wrap in "note"
.decay(.15).sustain(0) // make each note of equal length
.s('sawtooth') // waveform
.gain(.4) // turn down
@ -40,7 +52,7 @@ stack(
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.note() // wrap in "n"
.s('square') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
@ -49,7 +61,7 @@ stack(
,"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"
.note() // wrap in "note"
.decay(.1).sustain(0) // make notes short
.s('triangle') // waveform
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
@ -103,7 +115,7 @@ function App() {
}
}
},
[scheduler, evaluate, view],
[scheduler, evaluate, view, code],
),
);
return (

View File

@ -67,7 +67,7 @@ const highlightField = StateField.define({
}
let mark;
if (color) {
mark = Decoration.mark({ attributes: { style: `outline: 4px solid ${color};` } });
mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
} else {
mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
}
@ -104,6 +104,7 @@ export default function CodeMirror({
onSelectionChange,
theme,
keybindings,
isLineNumbersDisplayed,
fontSize = 18,
fontFamily = 'monospace',
options,
@ -148,6 +149,7 @@ export default function CodeMirror({
onCreateEditor={handleOnCreateEditor}
onUpdate={handleOnUpdate}
extensions={extensions}
basicSetup={{ lineNumbers: isLineNumbersDisplayed }}
/>
</div>
);

View File

@ -18,16 +18,24 @@ export function MiniRepl({
tune,
hideOutsideView = false,
enableKeyboard,
onTrigger,
drawTime,
punchcard,
punchcardLabels,
onPaint,
canvasHeight = 200,
fontSize = 18,
fontFamily,
hideHeader = false,
theme,
keybindings,
isLineNumbersDisplayed,
}) {
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
const evalOnMount = !!drawTime;
const drawContext = useCallback(
!!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
[drawTime],
punchcard ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
[punchcard],
);
const {
code,
@ -47,7 +55,18 @@ export function MiniRepl({
} = useStrudel({
initialCode: tune,
defaultOutput: webaudioOutput,
editPattern: (pat) => (punchcard ? pat.punchcard() : pat),
editPattern: (pat, id) => {
//pat = pat.withContext((ctx) => ({ ...ctx, id }));
if (onTrigger) {
pat = pat.onTrigger(onTrigger, false);
}
if (onPaint) {
pat = pat.onPaint(onPaint);
} else if (punchcard) {
pat = pat.punchcard({ labels: punchcardLabels });
}
return pat;
},
getTime,
evalOnMount,
drawContext,
@ -82,7 +101,7 @@ export function MiniRepl({
e.preventDefault();
flash(view);
await activateCode();
} else if (e.key === '.') {
} else if (e.key === '.' || e.code === 'Period') {
stop();
e.preventDefault();
}
@ -101,7 +120,7 @@ export function MiniRepl({
// const logId = data?.pattern?.meta?.id;
if (logId === replId) {
setLog((l) => {
return l.concat([e.detail]).slice(-10);
return l.concat([e.detail]).slice(-8);
});
}
}, []),
@ -109,33 +128,46 @@ export function MiniRepl({
return (
<div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight" ref={ref}>
<div className="flex justify-between bg-lineHighlight">
<div className="flex">
<button
className={cx(
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
started ? 'animate-pulse' : '',
)}
onClick={() => togglePlay()}
>
<Icon type={started ? 'stop' : 'play'} />
</button>
<button
className={cx(
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
)}
onClick={() => activateCode()}
>
<Icon type="refresh" />
</button>
{!hideHeader && (
<div className="flex justify-between bg-lineHighlight">
<div className="flex">
<button
className={cx(
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
started ? 'animate-pulse' : '',
)}
onClick={() => togglePlay()}
>
<Icon type={started ? 'stop' : 'play'} />
</button>
<button
className={cx(
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
)}
onClick={() => activateCode()}
>
<Icon type="refresh" />
</button>
</div>
</div>
{error && <div className="text-right p-1 text-sm text-red-200">{error.message}</div>}
</div>
)}
<div className="overflow-auto relative">
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} theme={theme} />}
{show && (
<CodeMirror6
value={code}
onChange={setCode}
onViewChanged={setView}
theme={theme}
fontFamily={fontFamily}
fontSize={fontSize}
keybindings={keybindings}
isLineNumbersDisplayed={isLineNumbersDisplayed}
/>
)}
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
</div>
{drawTime && (
{punchcard && (
<canvas
id={canvasId}
className="w-full pointer-events-none"

View File

@ -1,6 +1,6 @@
:root {
--background: #222;
--lineBackground: #22222250;
--lineBackground: #22222299;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);

View File

@ -29,7 +29,8 @@ function useStrudel({
const [pattern, setPattern] = useState();
const [started, setStarted] = useState(false);
const isDirty = code !== activeCode;
const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
//const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
const shouldPaint = useCallback((pat) => !!pat?.context?.onPaint, []);
// TODO: make sure this hook reruns when scheduler.started changes
const { scheduler, evaluate, start, stop, pause, setCps } = useMemo(

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.7.1",
"version": "0.8.1",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.7.1",
"version": "0.8.1",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/transpiler",
"version": "0.7.1",
"version": "0.8.1",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"publishConfig": {

82
packages/web/README.md Normal file
View File

@ -0,0 +1,82 @@
# @strudel/web
This package provides an easy to use bundle of multiple strudel packages for the web.
## Usage
Save this code as a `.html` file and double click it:
```html
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
```
With the help of [skypack](https://www.skypack.dev/), you don't need a bundler nor a server.
As soon as you call `initStrudel()`, all strudel functions are made available.
In this case, we are using the `note` function to create a pattern.
To actually play the pattern, you have to append `.play()` to the end.
Note: Due to the [Autoplay policy](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy), you can only play audio in a browser after a click event.
### Via npm
If you're using a bundler, you can install the package via `npm i @strudel/web`, then just import it like:
```js
import { initStrudel } from '@strudel/web';
```
The rest of the code should be the same. Check out [vite](https://vitejs.dev/) for a good bundler / dev server.
### Loading samples
By default, no external samples are loaded, but you can add them like this:
```js
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
document.getElementById('play').addEventListener('click',
() => s("bd sd").play()
)
```
You can learn [more about the `samples` function here](https://strudel.tidalcycles.org/learn/samples#loading-custom-samples).
### Evaluating Code
Instead of creating patterns directly in JS, you might also want to take in user input and turn that into a pattern.
This is called evaluation: Taking a piece of code and executing it on the fly.
To do that, you can use the `evaluate` function:
```js
initStrudel();
document.getElementById('play').addEventListener('click',
() => evaluate('note("c a f e").jux(rev)')
);
document.getElementById('play').addEventListener('stop',
() => hush()
);
```
### Double vs Single Quotes
There is a tiny difference between the [Strudel REPL](https://strudel.tidalcycles.org/) and `@strudel/web`.
In the REPL you can use 'single quotes' for regular JS strings and "double quotes" for mini notation patterns.
In `@strudel/web`, it does not matter which types of quotes you're using.
There will probably be an escapte hatch for that in the future.
## More Examples
Check out the examples folder for more examples, both using plain html and vite!

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
document.getElementById('play').addEventListener('stop', () => hush());
</script>

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<button id="a">A</button>
<button id="b">B</button>
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
click('stop', () => hush());
</script>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>

View File

@ -0,0 +1,24 @@
# 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?

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="https://strudel.tidalcycles.org/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@strudel/web REPL Example</title>
</head>
<body>
<div id="app"></div>
<button id="a">A</button>
<button id="b">B</button>
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from '@strudel/web';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
click('stop', () => hush());
</script>
</body>
</html>

View File

@ -0,0 +1,18 @@
{
"name": "repl-example",
"private": true,
"version": "0.0.0",
"type": "module",
"license": "AGPL-3.0-or-later",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.3.2"
},
"dependencies": {
"@strudel/web": "workspace:*"
}
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
document.getElementById('play').addEventListener('click', () => s('[bd sd](3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>

45
packages/web/package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "@strudel/web",
"version": "0.8.2",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"main": "web.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": [
"Alex McLean <alex@slab.org>"
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*"
},
"devDependencies": {
"vite": "^4.3.3"
}
}

View File

@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'web.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});

66
packages/web/web.mjs Normal file
View File

@ -0,0 +1,66 @@
export * from '@strudel.cycles/core';
export * from '@strudel.cycles/webaudio';
//export * from '@strudel.cycles/soundfonts';
export * from '@strudel.cycles/transpiler';
export * from '@strudel.cycles/mini';
export * from '@strudel.cycles/tonal';
export * from '@strudel.cycles/webaudio';
import { Pattern, evalScope, controls } from '@strudel.cycles/core';
import { initAudioOnFirstClick, registerSynthSounds, webaudioScheduler } from '@strudel.cycles/webaudio';
// import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { evaluate as _evaluate } from '@strudel.cycles/transpiler';
import { miniAllStrings } from '@strudel.cycles/mini';
// init logic
export async function defaultPrebake() {
const loadModules = evalScope(
evalScope,
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
{ hush, evaluate },
);
await Promise.all([loadModules, registerSynthSounds() /* , registerSoundfonts() */]);
}
// when this function finishes, everything is initialized
let initDone;
let scheduler;
export function initStrudel(options = {}) {
initAudioOnFirstClick();
miniAllStrings();
const { prebake, ...schedulerOptions } = options;
initDone = (async () => {
await defaultPrebake();
await prebake?.();
})();
scheduler = webaudioScheduler(schedulerOptions);
}
window.initStrudel = initStrudel;
// this method will play the pattern on the default scheduler
Pattern.prototype.play = function () {
if (!scheduler) {
throw new Error('.play: no scheduler found. Have you called init?');
}
initDone.then(() => {
scheduler.setPattern(this, true);
});
return this;
};
// stop playback
export function hush() {
scheduler.stop();
}
// evaluate and play the given code using the transpiler
export async function evaluate(code, autoplay = true) {
const { pattern } = await _evaluate(code);
autoplay && pattern.play();
}

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.7.1",
"version": "0.8.1",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",

View File

@ -85,7 +85,12 @@ export async function initAudioOnFirstClick() {
}
let delays = {};
const maxfeedback = 0.98;
function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = strudel.clamp(delayfeedback, 0, 0.98);
if (!delays[orbit]) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
@ -243,3 +248,16 @@ Pattern.prototype.webaudio = function () {
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
return this.onTrigger(webaudioOutputTrigger);
};
export function webaudioScheduler(options = {}) {
options = {
getTime: () => getAudioContext().currentTime,
defaultOutput: webaudioOutput,
...options,
};
const { defaultOutput, getTime } = options;
return new strudel.Cyclist({
...options,
onTrigger: strudel.getTrigger({ defaultOutput, getTime }),
});
}

160
pnpm-lock.yaml generated
View File

@ -66,6 +66,34 @@ importers:
specifier: ^0.28.0
version: 0.28.0(@vitest/ui@0.28.0)
packages/codemirror:
dependencies:
'@codemirror/commands':
specifier: ^6.2.4
version: 6.2.4
'@codemirror/lang-javascript':
specifier: ^6.1.7
version: 6.1.7
'@codemirror/language':
specifier: ^6.6.0
version: 6.6.0
'@codemirror/state':
specifier: ^6.2.0
version: 6.2.0
'@codemirror/view':
specifier: ^6.10.0
version: 6.10.0
'@lezer/highlight':
specifier: ^1.1.4
version: 1.1.4
'@strudel.cycles/core':
specifier: workspace:*
version: link:../core
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.11.18)
packages/core:
dependencies:
fraction.js:
@ -74,7 +102,7 @@ importers:
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
vitest:
specifier: ^0.28.0
version: 0.28.0(@vitest/ui@0.28.0)
@ -99,7 +127,35 @@ importers:
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
packages/core/examples/vite-vanilla-repl-cm6:
dependencies:
'@strudel.cycles/core':
specifier: workspace:*
version: link:../..
'@strudel.cycles/mini':
specifier: workspace:*
version: link:../../../mini
'@strudel.cycles/soundfonts':
specifier: workspace:*
version: link:../../../soundfonts
'@strudel.cycles/tonal':
specifier: workspace:*
version: link:../../../tonal
'@strudel.cycles/transpiler':
specifier: workspace:*
version: link:../../../transpiler
'@strudel.cycles/webaudio':
specifier: workspace:*
version: link:../../../webaudio
'@strudel/codemirror':
specifier: workspace:*
version: link:../../../codemirror
devDependencies:
vite:
specifier: ^4.3.2
version: 4.3.3(@types/node@18.11.18)
packages/csound:
dependencies:
@ -115,7 +171,7 @@ importers:
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
packages/embed: {}
@ -148,7 +204,7 @@ importers:
version: link:../mini
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
vitest:
specifier: ^0.28.0
version: 0.28.0(@vitest/ui@0.28.0)
@ -167,7 +223,7 @@ importers:
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
packages/mini:
dependencies:
@ -180,7 +236,7 @@ importers:
version: 3.0.2
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
vitest:
specifier: ^0.28.0
version: 0.28.0(@vitest/ui@0.28.0)
@ -199,7 +255,7 @@ importers:
version: 5.8.1
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
packages/react:
dependencies:
@ -220,10 +276,10 @@ importers:
version: 1.1.4
'@replit/codemirror-emacs':
specifier: ^6.0.1
version: 6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
version: 6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
'@replit/codemirror-vim':
specifier: ^6.0.14
version: 6.0.14(@codemirror/commands@6.2.0)(@codemirror/language@6.6.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
version: 6.0.14(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
'@strudel.cycles/core':
specifier: workspace:*
version: link:../core
@ -269,7 +325,7 @@ importers:
version: 3.3.2
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
packages/react/examples/nano-repl:
dependencies:
@ -285,6 +341,9 @@ importers:
'@strudel.cycles/react':
specifier: workspace:*
version: link:../..
'@strudel.cycles/soundfonts':
specifier: workspace:*
version: link:../../../soundfonts
'@strudel.cycles/tonal':
specifier: workspace:*
version: link:../../../tonal
@ -321,7 +380,7 @@ importers:
version: 3.3.2
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
packages/serial:
dependencies:
@ -331,7 +390,7 @@ importers:
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
packages/soundfonts:
dependencies:
@ -353,7 +412,7 @@ importers:
version: 3.3.1
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
packages/tonal:
dependencies:
@ -372,7 +431,7 @@ importers:
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
vitest:
specifier: ^0.28.0
version: 0.28.0(@vitest/ui@0.28.0)
@ -388,7 +447,7 @@ importers:
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
vitest:
specifier: ^0.28.0
version: 0.28.0(@vitest/ui@0.28.0)
@ -410,11 +469,43 @@ importers:
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
vitest:
specifier: ^0.28.0
version: 0.28.0(@vitest/ui@0.28.0)
packages/web:
dependencies:
'@strudel.cycles/core':
specifier: workspace:*
version: link:../core
'@strudel.cycles/mini':
specifier: workspace:*
version: link:../mini
'@strudel.cycles/tonal':
specifier: workspace:*
version: link:../tonal
'@strudel.cycles/transpiler':
specifier: workspace:*
version: link:../transpiler
'@strudel.cycles/webaudio':
specifier: workspace:*
version: link:../webaudio
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.11.18)
packages/web/examples/repl-example:
dependencies:
'@strudel/web':
specifier: workspace:*
version: link:../..
devDependencies:
vite:
specifier: ^4.3.2
version: 4.3.3(@types/node@18.11.18)
packages/webaudio:
dependencies:
'@strudel.cycles/core':
@ -426,7 +517,7 @@ importers:
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
packages/webdirt:
dependencies:
@ -442,7 +533,7 @@ importers:
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
vitest:
specifier: ^0.28.0
version: 0.28.0(@vitest/ui@0.28.0)
@ -455,7 +546,7 @@ importers:
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
version: 4.3.3(@types/node@18.11.18)
vitest:
specifier: ^0.28.0
version: 0.28.0(@vitest/ui@0.28.0)
@ -555,6 +646,9 @@ importers:
canvas:
specifier: ^2.11.2
version: 2.11.2
claviature:
specifier: ^0.1.0
version: 0.1.0
fraction.js:
specifier: ^4.2.0
version: 4.2.0
@ -2268,8 +2362,8 @@ packages:
'@lezer/common': 1.0.2
dev: false
/@codemirror/commands@6.2.0:
resolution: {integrity: sha512-+00smmZBradoGFEkRjliN7BjqPh/Hx0KCHWOEibUmflUqZz2RwBTU0MrVovEEHozhx3AUSGcO/rl3/5f9e9Biw==}
/@codemirror/commands@6.2.4:
resolution: {integrity: sha512-42lmDqVH0ttfilLShReLXsDfASKLXzfyC36bzwcqzox9PlHulMcsUOfHXNo2X2aFMVNUoQ7j+d4q5bnfseYoOA==}
dependencies:
'@codemirror/language': 6.6.0
'@codemirror/state': 6.2.0
@ -3452,7 +3546,7 @@ packages:
escalade: 3.1.1
dev: false
/@replit/codemirror-emacs@6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0):
/@replit/codemirror-emacs@6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0):
resolution: {integrity: sha512-2WYkODZGH1QVAXWuOxTMCwktkoZyv/BjYdJi2A5w4fRrmOQFuIACzb6pO9dgU3J+Pm2naeiX2C8veZr/3/r6AA==}
peerDependencies:
'@codemirror/autocomplete': ^6.0.2
@ -3462,13 +3556,13 @@ packages:
'@codemirror/view': ^6.3.0
dependencies:
'@codemirror/autocomplete': 6.6.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)(@lezer/common@1.0.2)
'@codemirror/commands': 6.2.0
'@codemirror/commands': 6.2.4
'@codemirror/search': 6.2.3
'@codemirror/state': 6.2.0
'@codemirror/view': 6.10.0
dev: false
/@replit/codemirror-vim@6.0.14(@codemirror/commands@6.2.0)(@codemirror/language@6.6.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0):
/@replit/codemirror-vim@6.0.14(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0):
resolution: {integrity: sha512-wwhqhvL76FdRTdwfUWpKCbv0hkp2fvivfMosDVlL/popqOiNLtUhL02ThgHZH8mus/NkVr5Mj582lyFZqQrjOA==}
peerDependencies:
'@codemirror/commands': ^6.0.0
@ -3477,7 +3571,7 @@ packages:
'@codemirror/state': ^6.0.1
'@codemirror/view': ^6.0.3
dependencies:
'@codemirror/commands': 6.2.0
'@codemirror/commands': 6.2.4
'@codemirror/language': 6.6.0
'@codemirror/search': 6.2.3
'@codemirror/state': 6.2.0
@ -4073,7 +4167,7 @@ packages:
eslint-visitor-keys: 3.3.0
dev: false
/@uiw/codemirror-extensions-basic-setup@4.19.16(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.0)(@codemirror/language@6.6.0)(@codemirror/lint@6.1.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0):
/@uiw/codemirror-extensions-basic-setup@4.19.16(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/lint@6.1.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0):
resolution: {integrity: sha512-Xm0RDpyYVZ/8hWqaBs3+wZwi4uLwZUBwp/uCt89X80FeR6mr3BFuC+a+gcDO4dBu3l+WQE3jJdhjKjB2TCY/PQ==}
peerDependencies:
'@codemirror/autocomplete': '>=6.0.0'
@ -4085,7 +4179,7 @@ packages:
'@codemirror/view': '>=6.0.0'
dependencies:
'@codemirror/autocomplete': 6.6.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)(@lezer/common@1.0.2)
'@codemirror/commands': 6.2.0
'@codemirror/commands': 6.2.4
'@codemirror/language': 6.6.0
'@codemirror/lint': 6.1.0
'@codemirror/search': 6.2.3
@ -4380,11 +4474,11 @@ packages:
react-dom: '>=16.8.0'
dependencies:
'@babel/runtime': 7.20.13
'@codemirror/commands': 6.2.0
'@codemirror/commands': 6.2.4
'@codemirror/state': 6.2.0
'@codemirror/theme-one-dark': 6.1.0
'@codemirror/view': 6.10.0
'@uiw/codemirror-extensions-basic-setup': 4.19.16(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.0)(@codemirror/language@6.6.0)(@codemirror/lint@6.1.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
'@uiw/codemirror-extensions-basic-setup': 4.19.16(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/lint@6.1.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
codemirror: 6.0.1(@lezer/common@1.0.2)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -4415,7 +4509,7 @@ packages:
'@babel/plugin-transform-react-jsx-self': 7.21.0(@babel/core@7.21.5)
'@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.21.5)
react-refresh: 0.14.0
vite: 4.3.3(@types/node@18.16.3)
vite: 4.3.3(@types/node@18.11.18)
transitivePeerDependencies:
- supports-color
dev: true
@ -5338,6 +5432,10 @@ packages:
resolution: {integrity: sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==}
engines: {node: '>=8'}
/claviature@0.1.0:
resolution: {integrity: sha512-Ai12axNwQ7x/F9QAj64RYKsgvi5Y33+X3GUSKAC/9s/adEws8TSSc0efeiqhKNGKBo6rT/c+CSCwSXzXxwxZzQ==}
dev: false
/clean-stack@2.2.0:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
engines: {node: '>=6'}
@ -5458,7 +5556,7 @@ packages:
resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
dependencies:
'@codemirror/autocomplete': 6.6.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)(@lezer/common@1.0.2)
'@codemirror/commands': 6.2.0
'@codemirror/commands': 6.2.4
'@codemirror/language': 6.6.0
'@codemirror/lint': 6.1.0
'@codemirror/search': 6.2.3

View File

@ -3,4 +3,6 @@ packages:
- "packages/*"
- "website/"
- "packages/core/examples/vite-vanilla-repl"
- "packages/react/examples/nano-repl"
- "packages/core/examples/vite-vanilla-repl-cm6"
- "packages/react/examples/nano-repl"
- "packages/web/examples/repl-example"

246
test/metadata.test.mjs Normal file
View File

@ -0,0 +1,246 @@
import { describe, expect, it } from 'vitest';
import { getMetadata } from '../website/src/pages/metadata_parser';
describe.concurrent('Metadata parser', () => {
it('loads a tag from inline comment', async () => {
const tune = `// @title Awesome song`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
});
});
it('loads many tags from inline comments', async () => {
const tune = `// @title Awesome song
// @by Sam`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam'],
});
});
it('loads many tags from one inline comment', async () => {
const tune = `// @title Awesome song @by Sam`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam'],
});
});
it('loads a tag from a block comment', async () => {
const tune = `/* @title Awesome song */`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
});
});
it('loads many tags from a block comment', async () => {
const tune = `/*
@title Awesome song
@by Sam
*/`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam'],
});
});
it('loads many tags from many block comments', async () => {
const tune = `/* @title Awesome song */
/* @by Sam */`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam'],
});
});
it('loads many tags from mixed comments', async () => {
const tune = `/* @title Awesome song */
// @by Sam
`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam'],
});
});
it('loads a title tag with quotes syntax', async () => {
const tune = `// "Awesome song"`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
});
});
it('loads a title tag with quotes syntax among other tags', async () => {
const tune = `// "Awesome song" made @by Sam`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam'],
});
});
it('loads a title tag with quotes syntax from block comment', async () => {
const tune = `/* "Awesome song"
@by Sam */`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam'],
});
});
it('does not load a title tag with quotes syntax after a prefix', async () => {
const tune = `// I don't care about those "metadata".`;
expect(getMetadata(tune)).toStrictEqual({});
});
it('does not load a title tag with quotes syntax after an other comment', async () => {
const tune = `// I don't care about those
// "metadata"`;
expect(getMetadata(tune)).toStrictEqual({});
});
it('does not load a title tag with quotes syntax after other tags', async () => {
const tune = `/*
@by Sam aka "Lady Strudel"
"Sandyyy"
*/`;
expect(getMetadata(tune)).toStrictEqual({
by: ['Sam aka "Lady Strudel"', '"Sandyyy"'],
});
});
it('loads a tag list with comma-separated values syntax', async () => {
const tune = `// @by Sam, Sandy`;
expect(getMetadata(tune)).toStrictEqual({
by: ['Sam', 'Sandy'],
});
});
it('loads a tag list with duplicate keys syntax', async () => {
const tune = `// @by Sam
// @by Sandy`;
expect(getMetadata(tune)).toStrictEqual({
by: ['Sam', 'Sandy'],
});
});
it('loads a tag list with duplicate keys syntax, with prefixes', async () => {
const tune = `// song @by Sam
// samples @by Sandy`;
expect(getMetadata(tune)).toStrictEqual({
by: ['Sam', 'Sandy'],
});
});
it('loads many tag lists with duplicate keys syntax, within code', async () => {
const tune = `note("a3 c#4 e4 a4") // @by Sam @license CC0
s("bd hh sd hh") // @by Sandy @license CC BY-NC-SA`;
expect(getMetadata(tune)).toStrictEqual({
by: ['Sam', 'Sandy'],
license: ['CC0', 'CC BY-NC-SA'],
});
});
it('loads a tag list with duplicate keys syntax from block comment', async () => {
const tune = `/* @by Sam
@by Sandy */`;
expect(getMetadata(tune)).toStrictEqual({
by: ['Sam', 'Sandy'],
});
});
it('loads a tag list with newline syntax', async () => {
const tune = `/*
@by Sam
Sandy */`;
expect(getMetadata(tune)).toStrictEqual({
by: ['Sam', 'Sandy'],
});
});
it('loads a multiline tag from block comment', async () => {
const tune = `/*
@details I wrote this song in February 19th, 2023.
It was around midnight and I was lying on
the sofa in the living room.
*/`;
expect(getMetadata(tune)).toStrictEqual({
details:
'I wrote this song in February 19th, 2023. ' +
'It was around midnight and I was lying on the sofa in the living room.',
});
});
it('loads a multiline tag from block comment with duplicate keys', async () => {
const tune = `/*
@details I wrote this song in February 19th, 2023.
@details It was around midnight and I was lying on
the sofa in the living room.
*/`;
expect(getMetadata(tune)).toStrictEqual({
details:
'I wrote this song in February 19th, 2023. ' +
'It was around midnight and I was lying on the sofa in the living room.',
});
});
it('loads a multiline tag from inline comments', async () => {
const tune = `// @details I wrote this song in February 19th, 2023.
// @details It was around midnight and I was lying on
// @details the sofa in the living room.
*/`;
expect(getMetadata(tune)).toStrictEqual({
details:
'I wrote this song in February 19th, 2023. ' +
'It was around midnight and I was lying on the sofa in the living room.',
});
});
it('loads empty tags from inline comments', async () => {
const tune = `// @title
// @by`;
expect(getMetadata(tune)).toStrictEqual({
title: '',
by: [],
});
});
it('loads tags with whitespaces from inline comments', async () => {
const tune = ` // @title Awesome song
// @by Sam Tagada `;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam Tagada'],
});
});
it('loads tags with whitespaces from block comment', async () => {
const tune = ` /* @title Awesome song
@by Sam Tagada */ `;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam Tagada'],
});
});
it('loads empty tags from block comment', async () => {
const tune = `/* @title
@by */`;
expect(getMetadata(tune)).toStrictEqual({
title: '',
by: [],
});
});
it('does not load tags if there is not', async () => {
const tune = `note("a3 c#4 e4 a4")`;
expect(getMetadata(tune)).toStrictEqual({});
});
it('does not load code that looks like a metadata tag', async () => {
const tune = `const str1 = '@title Awesome song'`;
// need a lexer to avoid this one, but it's a pretty rare use case:
// const tune = `const str1 = '// @title Awesome song'`;
expect(getMetadata(tune)).toStrictEqual({});
});
});

View File

@ -4,16 +4,16 @@
"version": "0.6.0",
"private": true,
"scripts": {
"dev": "astro dev",
"dev": "astro dev --host 0.0.0.0",
"start": "astro dev",
"check": "astro check && tsc",
"build": "astro build",
"preview": "astro preview",
"preview": "astro preview --port 3009 --host 0.0.0.0",
"astro": "astro"
},
"dependencies": {
"@algolia/client-search": "^4.17.0",
"@astrojs/mdx": "^0.19.0",
"@astrojs/mdx": "^0.19.0",
"@astrojs/react": "^2.1.1",
"@astrojs/tailwind": "^3.1.1",
"@docsearch/css": "^3.3.4",
@ -43,6 +43,7 @@
"@uiw/codemirror-themes-all": "^4.19.16",
"astro": "^2.3.2",
"canvas": "^2.11.2",
"claviature": "^0.1.0",
"fraction.js": "^4.2.0",
"nanoid": "^4.0.2",
"nanostores": "^0.8.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,10 @@
---
import LightBulbIcon from '@heroicons/react/20/solid/LightBulbIcon';
//import MusicalNoteIcon from '@heroicons/react/20/solid/MusicalNoteIcon';
---
<div class="py-1 px-6 pr-10 bg-lineHighlight relative mb-4">
<div><slot /></div>
<LightBulbIcon className="w-5 h-5 absolute top-4 right-4" />
<!-- <MusicalNoteIcon className="w-5 h-5 absolute top-4 right-4" /> -->
</div>

View File

@ -0,0 +1,24 @@
import { getClaviature } from 'claviature';
import React from 'react';
export default function Claviature({ options, onClick, onMouseDown, onMouseUp, onMouseLeave }) {
const svg = getClaviature({
options,
onClick,
onMouseDown,
onMouseUp,
onMouseLeave,
});
return (
<svg {...svg.attributes}>
{svg.children.map((el, i) => {
const TagName = el.name;
return (
<TagName key={`${el.name}-${i}`} {...el.attributes}>
{el.value}
</TagName>
);
})}
</svg>
);
}

View File

@ -33,7 +33,7 @@ const base = BASE_URL;
<style is:global>
:root {
--background: #222;
--lineBackground: #22222250;
--lineBackground: #22222299;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);

View File

@ -1,5 +1,5 @@
---
// import { getLanguageFromURL } from '../../languages';
import { getLanguageFromURL } from '../../languages';
import { SIDEBAR } from '../../config';
type Props = {
@ -10,7 +10,7 @@ const { currentPage } = Astro.props as Props;
const { BASE_URL } = import.meta.env;
let currentPageMatch = currentPage.slice(BASE_URL.length, currentPage.endsWith('/') ? -1 : undefined);
const langCode = 'en'; // getLanguageFromURL(currentPage);
const langCode = getLanguageFromURL(currentPage) || 'en';
const sidebar = SIDEBAR[langCode];
---

View File

@ -0,0 +1,19 @@
import ChevronDownIcon from '@heroicons/react/20/solid/ChevronDownIcon';
import ChevronUpIcon from '@heroicons/react/20/solid/ChevronUpIcon';
import React from 'react';
import { useState } from 'react';
export default function QA({ children, q }) {
const [visible, setVisible] = useState(false);
return (
<div className="py-4 px-6 pr-10 bg-lineHighlight relative mb-4">
<div className="cursor-pointer" onClick={() => setVisible((v) => !v)}>
<div>{q}</div>
<a className="p-1 absolute top-4 right-4">
{visible ? <ChevronUpIcon className="w-5 h-5" /> : <ChevronDownIcon className="w-5 h-5" />}
</a>
</div>
{visible && <div>{children}</div>}
</div>
);
}

View File

@ -24,6 +24,7 @@ export type Frontmatter = {
export const KNOWN_LANGUAGES = {
English: 'en',
German: 'de',
} as const;
export const KNOWN_LANGUAGE_CODES = Object.values(KNOWN_LANGUAGES);
@ -38,25 +39,49 @@ export const ALGOLIA = {
apiKey: 'd5044f9d21b80e7721e5b0067a8730b1',
};
export type Sidebar = Record<(typeof KNOWN_LANGUAGE_CODES)[number], Record<string, { text: string; link: string }[]>>;
export type SidebarLang = Record<string, { text: string; link: string }[]>;
export type Sidebar = Record<(typeof KNOWN_LANGUAGE_CODES)[number], SidebarLang>;
export const SIDEBAR: Sidebar = {
de: {
Workshop: [
{ text: 'Intro', link: 'de/workshop/getting-started' },
{ text: 'Erste Sounds', link: 'de/workshop/first-sounds' },
{ text: 'Erste Töne', link: 'de/workshop/first-notes' },
{ text: 'Erste Effekte', link: 'de/workshop/first-effects' },
{ text: 'Pattern Effekte', link: 'de/workshop/pattern-effects' },
{ text: 'Rückblick', link: 'de/workshop/recap' },
{ text: 'Mehr Seiten auf Englisch', link: 'workshop/getting-started' },
],
},
en: {
Tutorial: [
{ text: 'Getting Started', link: 'learn/getting-started' },
{ text: 'Notes', link: 'learn/notes' },
{ text: 'Sounds', link: 'learn/sounds' },
{ text: 'Coding syntax', link: 'learn/code' },
{ text: 'Mini-Notation', link: 'learn/mini-notation' },
Workshop: [
{ text: 'Getting Started', link: 'workshop/getting-started' },
{ text: 'First Sounds', link: 'workshop/first-sounds' },
{ text: 'First Notes', link: 'workshop/first-notes' },
{ text: 'First Effects', link: 'workshop/first-effects' },
{ text: 'Pattern Effects', link: 'workshop/pattern-effects' },
{ text: 'Recap', link: 'workshop/recap' },
{ text: 'Workshop in German', link: 'de/workshop/getting-started' },
],
'Making Sound': [
{ text: 'Samples', link: 'learn/samples' },
{ text: 'Synths', link: 'learn/synths' },
{ text: 'Audio Effects', link: 'learn/effects' },
{ text: 'MIDI & OSC', link: 'learn/input-output' },
],
More: [
{ text: 'Mini-Notation', link: 'learn/mini-notation' },
{ text: 'Coding syntax', link: 'learn/code' },
{ text: 'Offline', link: 'learn/pwa' },
{ text: 'Patterns', link: 'technical-manual/patterns' },
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
{ text: 'Strudel vs Tidal', link: 'learn/strudel-vs-tidal' },
{ text: 'Music metadata', link: 'learn/metadata' },
{ text: 'CSound', link: 'learn/csound' },
],
'Pattern Functions': [
{ text: 'Introduction', link: 'functions/intro' },
{ text: 'Pattern Constructors', link: 'learn/factories' },
{ text: 'Creating Patterns', link: 'learn/factories' },
{ text: 'Time Modifiers', link: 'learn/time-modifiers' },
{ text: 'Control Parameters', link: 'functions/value-modifiers' },
{ text: 'Signals', link: 'learn/signals' },
@ -64,13 +89,6 @@ export const SIDEBAR: Sidebar = {
{ text: 'Accumulation', link: 'learn/accumulation' },
{ text: 'Tonal Modifiers', link: 'learn/tonal' },
],
More: [
{ text: 'MIDI & OSC', link: 'learn/input-output' },
{ text: 'Offline', link: 'learn/pwa' },
{ text: 'Patterns', link: 'technical-manual/patterns' },
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
{ text: 'Strudel vs Tidal', link: 'learn/strudel-vs-tidal' },
],
Development: [
{ text: 'REPL', link: 'technical-manual/repl' },
{ text: 'Sounds', link: 'technical-manual/sounds' },

View File

@ -1,4 +1,5 @@
.cm-activeLine {
.cm-activeLine,
.cm-activeLineGutter {
background-color: transparent !important;
}
@ -7,3 +8,11 @@
border: 1px solid var(--lineHighlight);
padding: 2px;
}
.cm-scroller {
font-family: inherit !important;
}
.cm-gutters {
display: none !important;
}

View File

@ -1,10 +1,11 @@
import { evalScope, controls } from '@strudel.cycles/core';
import { evalScope, controls, noteToMidi } from '@strudel.cycles/core';
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { useEffect, useState } from 'react';
import { prebake } from '../repl/prebake';
import { themes, settings } from '../repl/themes.mjs';
import './MiniRepl.css';
import { useSettings } from '../settings.mjs';
import Claviature from '@components/Claviature';
let modules;
if (typeof window !== 'undefined') {
@ -27,9 +28,20 @@ if (typeof window !== 'undefined') {
prebake();
}
export function MiniRepl({ tune, drawTime, punchcard, canvasHeight = 100 }) {
export function MiniRepl({
tune,
drawTime,
punchcard,
punchcardLabels = true,
span = [0, 4],
canvasHeight = 100,
hideHeader,
claviature,
claviatureLabels,
}) {
const [Repl, setRepl] = useState();
const { theme } = useSettings();
const { theme, keybindings, fontSize, fontFamily, isLineNumbersDisplayed } = useSettings();
const [activeNotes, setActiveNotes] = useState([]);
useEffect(() => {
// we have to load this package on the client
// because codemirror throws an error on the server
@ -42,11 +54,39 @@ export function MiniRepl({ tune, drawTime, punchcard, canvasHeight = 100 }) {
<Repl
tune={tune}
hideOutsideView={true}
drawTime={drawTime}
drawTime={claviature ? [0, 0] : drawTime}
punchcard={punchcard}
punchcardLabels={punchcardLabels}
span={span}
canvasHeight={canvasHeight}
theme={themes[theme]}
hideHeader={hideHeader}
keybindings={keybindings}
fontFamily={fontFamily}
fontSize={fontSize}
isLineNumbersDisplayed={isLineNumbersDisplayed}
onPaint={
claviature
? (ctx, time, haps, drawTime) => {
const active = haps
.map((hap) => hap.value.note)
.filter(Boolean)
.map((n) => (typeof n === 'string' ? noteToMidi(n) : n));
setActiveNotes(active);
}
: undefined
}
/>
{claviature && (
<Claviature
options={{
range: ['C2', 'C6'],
scaleY: 0.75,
colorize: [{ keys: activeNotes, color: 'steelblue' }],
labels: claviatureLabels || {},
}}
/>
)}
</div>
) : (
<pre>{tune}</pre>

View File

@ -0,0 +1,336 @@
---
title: Erste Effekte
layout: ../../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../../docs/MiniRepl';
import QA from '@components/QA';
# Erste Effekte
import Box from '@components/Box.astro';
## Ein paar grundlegende Effekte
**low-pass filter**
<MiniRepl
hideHeader
client:visible
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
.sound("sawtooth").lpf(800)`}
/>
<Box>
lpf = **l**ow **p**ass **f**ilter
- Ändere `lpf` in 200. Hörst du wie der Bass dumpfer klingt? Es klingt so ähnlich als würde die Musik hinter einer geschlossenen Tür laufen 🚪
- Lass uns nun die Tür öffnen: Ändere `lpf` in 5000. Der Klang wird dadurch viel heller und schärfer ✨🪩
</Box>
**filter automatisieren**
<MiniRepl
hideHeader
client:visible
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
.sound("sawtooth").lpf("200 1000")`}
/>
<Box>
- Füg noch mehr `lpf` Werte hinzu
- Das pattern in `lpf` ändert nicht den Rhythmus der Bassline
Später sehen wir wie man mit Wellenformen Dinge automatisieren kann.
</Box>
**vowel = Vokal**
<MiniRepl
hideHeader
client:visible
tune={`note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>/2")
.sound("sawtooth").vowel("<a e i o>/2")`}
/>
**gain = Verstärkung**
<MiniRepl
hideHeader
client:visible
tune={`stack(
sound("hh*8").gain("[.25 1]*2"),
sound("bd*2,~ sd:1")
) `}
punchcard
/>
<Box>
Bei Rhythmen ist die Dynamik (= Veränderungen der Lautstärke) sehr wichtig.
- Entferne `.gain(...)` und achte darauf wie es viel flacher klingt.
- Mach es rückgängig (strg+z dann strg+enter)
</Box>
**stacks in stacks**
Lass uns die obigen Beispiele kombinieren:
<MiniRepl
hideHeader
client:visible
tune={`stack(
stack(
sound("hh*8").gain("[.25 1]*2"),
sound("bd*2,~ sd:1")
),
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
.sound("sawtooth").lpf("200 1000"),
note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>/2")
.sound("sawtooth").vowel("<a e i o>/2")
) `}
/>
<Box>
Versuche die einzelnen Teile innerhalb `stack` zu erkennen, schau dir an wie die Kommas gesetzt sind.
Die 3 Teile (Drums, Bass, Akkorde) sind genau wie vorher, nur in einem `stack`, getrennt durch Kommas
</Box>
**Den Sound formen mit ADSR Hüllkurve**
<MiniRepl
hideHeader
client:visible
tune={`note("<c3 bb2 f3 eb3>")
.sound("sawtooth").lpf(600)
.attack(.1)
.decay(.1)
.sustain(.25)
.release(.2)`}
/>
<Box>
Versuche herauszufinden was die Zahlen machen. Probier folgendes:
- attack: `.5` vs `0`
- decay: `.5` vs `0`
- sustain: `1` vs `.25` vs `0`
- release: `0` vs `.5` vs `1`
Kannst du erraten was die einzelnen Werte machen?
</Box>
<QA q="Lösung anzeigen" client:visible>
- attack (anschlagen): Zeit des Aufbaus
- decay (zerfallen): Zeit des Abfalls
- sustain (erhalten): Lautstärke nach Abfall
- release (loslassen): Zeit des Abfalls nach dem Ende
![ADSR](https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/ADSR_parameter.svg/1920px-ADSR_parameter.svg.png)
</QA>
**adsr Kurznotation**
<MiniRepl
hideHeader
client:visible
tune={`note("<c3 bb2 f3 eb3>")
.sound("sawtooth").lpf(600)
.adsr(".1:.1:.5:.2")
`}
/>
**delay = Verzögerung**
<MiniRepl
hideHeader
client:visible
tune={`stack(
note("~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]")
.sound("gm_electric_guitar_muted"),
sound("<bd rim>").bank("RolandTR707")
).delay(".5")`}
/>
<Box>
Probier verschiedene `delay` Werte zwischen 0 und 1. Übrigens: `.5` ist kurz für `0.5`.
Was passiert wenn du `.delay(".8:.125")` schreibst? Kannst du erraten was die zweite Zahl macht?
Was passiert wenn du `.delay(".8:.06:.8")` schreibst? Kannst du erraten was die dritte Zahl macht?
</Box>
<QA q="Lösung anzeigen" client:visible>
`delay("a:b:c")`:
- a: Lautstärke des Delays
- b: Verzögerungszeit
- c: Feedback (je kleiner desto schneller verschwindet das Delay)
</QA>
**room aka reverb = Hall**
<MiniRepl
hideHeader
client:visible
tune={`n("<4 [3@3 4] [<2 0> ~@16] ~>/2")
.scale("D4:minor").sound("gm_accordion:2")
.room(2)`}
/>
<Box>
Spiel mit verschiedenen Werten.
Füg auch ein Delay hinzu!
</Box>
**kleiner dub tune**
<MiniRepl
hideHeader
client:visible
tune={`stack(
note("~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]")
.sound("gm_electric_guitar_muted").delay(.5),
sound("<bd rim>").bank("RolandTR707").delay(.5),
n("<4 [3@3 4] [<2 0> ~@16] ~>/2")
.scale("D4:minor").sound("gm_accordion:2")
.room(2).gain(.5)
)`}
/>
Für echten Dub fehlt noch der Bass:
<MiniRepl
hideHeader
client:visible
tune={`stack(
note("~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]")
.sound("gm_electric_guitar_muted").delay(.5),
sound("<bd rim>").bank("RolandTR707").delay(.5),
n("<4 [3@3 4] [<2 0> ~@16] ~>/2")
.scale("D4:minor").sound("gm_accordion:2")
.room(2).gain(.4),
n("<0 [~ 0] 4 [3 2] [0 ~] [0 ~] <0 2> ~>*2")
.scale("D2:minor")
.sound("sawtooth,triangle").lpf(800)
)`}
/>
<Box>
Füg `.hush()` ans ende eines Patterns im stack...
</Box>
**pan = Panorama**
<MiniRepl
hideHeader
client:visible
tune={`sound("numbers:1 numbers:2 numbers:3 numbers:4")
.pan("0 0.3 .6 1")
.slow(2)`}
/>
**speed = Geschwindigkeit**
<MiniRepl hideHeader client:visible tune={`sound("bd rim").speed("<1 2 -1 -2>").room(.2)`} />
**fast and slow = schnell und langsam**
Mit `fast` und `slow` kann man das tempo eines patterns außerhalb der Mini-Notation ändern:
<MiniRepl hideHeader client:visible tune={`sound("bd*2,~ rim").slow(2)`} />
<Box>
Ändere den `slow` Wert. Tausche `slow` durch `fast`.
Was passiert wenn du den Wert automatisierst? z.b. `.fast("<1 [2 4]>")` ?
</Box>
Übrigens, innerhalb der Mini-Notation, `fast` ist `*` und `slow` ist `/`.
<MiniRepl hideHeader client:visible tune={`sound("[bd*2,~ rim]*<1 [2 4]>")`} />
## Automation mit Signalen
Anstatt Werte schrittweise zu automatisieren können wir auch sogenannte Signale benutzen:
<MiniRepl hideHeader client:visible tune={`sound("hh*16").gain(sine)`} punchcard punchcardLabels={false} />
<Box>
Die grundlegenden Wellenformen sind `sine`, `saw`, `square`, `tri` 🌊
Probiere auch die zufälligen Signale `rand` und `perlin`!
Der `gain`-Wert (Verstärkung) wird in der Visualisierung als Transparenz dargestellt.
</Box>
**Bereich ändern mit `range`**
Signale bewegen sich standardmäßig zwischen 0 und 1. Wir können das mit `range` ändern:
<MiniRepl hideHeader client:visible tune={`sound("hh*8").lpf(saw.range(500, 2000))`} />
`range` ist nützlich wenn wir Funktionen mit einem anderen Wertebereich als 0 und 1 automatisieren wollen (z.b. lpf)
<Box>
Was passiert wenn du die beiden Werte vertauschst?
</Box>
Wir können die Geschwindigkeit der Automation mit slow / fast ändern:
<MiniRepl
hideHeader
client:visible
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
.sound("sawtooth")
.lpf(sine.range(100, 2000).slow(8))`}
/>
<Box>
Die ganze Automation braucht nun 8 cycle bis sie sich wiederholt.
</Box>
## Rückblick
| name | example |
| ----- | -------------------------------------------------------------------------------------------------- |
| lpf | <MiniRepl hideHeader client:visible tune={`note("c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
| vowel | <MiniRepl hideHeader client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | <MiniRepl hideHeader client:visible tune={`s("hh*8").gain("[.25 1]*2")`} /> |
| delay | <MiniRepl hideHeader client:visible tune={`s("bd rim").delay(.5)`} /> |
| room | <MiniRepl hideHeader client:visible tune={`s("bd rim").room(.5)`} /> |
| pan | <MiniRepl hideHeader client:visible tune={`s("bd rim").pan("0 1")`} /> |
| speed | <MiniRepl hideHeader client:visible tune={`s("bd rim").speed("<1 2 -1 -2>")`} /> |
| range | <MiniRepl hideHeader client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
Lass uns nun die für Tidal typischen [Pattern Effekte anschauen](/de/workshop/pattern-effects).

View File

@ -0,0 +1,408 @@
---
title: Erste Töne
layout: ../../../layouts/MainLayout.astro
---
import { MiniRepl } from '@src/docs/MiniRepl';
import { midi2note } from '@strudel.cycles/core/';
import Box from '@components/Box.astro';
import QA from '@components/QA';
# Erste Töne
Jetzt schauen wir uns an wie man mit Tönen mit der `note` Funktion spielt.
## Zahlen und Noten
**Töne mit Zahlen**
<MiniRepl
hideHeader
client:visible
tune={`note("48 52 55 59").sound("piano")`}
claviature
claviatureLabels={Object.fromEntries(
Array(49)
.fill()
.map((_, i) => [midi2note(i + 36), i + 36]),
)}
/>
<Box>
Probiere verschiedene Zahlen aus!
Versuch auch mal Kommazahlen, z.B. 55.5 (beachte die englische Schreibweise von Kommazahlen mit "." anstatt ",")
</Box>
**Töne mit Buchstaben**
<MiniRepl
hideHeader
client:visible
tune={`note("c e g b").sound("piano")`}
claviature
claviatureLabels={Object.fromEntries(['c3', 'd3', 'e3', 'f3', 'g3', 'a3', 'b3'].map((n) => [n, n.split('')[0]]))}
/>
<Box>
Versuch verschiedene Buchstaben aus (a - g).
Findest du Melodien die auch gleichzeitig ein Wort sind? Tipp: ☕ 🙈 🧚
</Box>
**Vorzeichen**
<MiniRepl
hideHeader
client:visible
tune={`note("db eb gb ab bb").sound("piano")`}
claviature
claviatureLabels={Object.fromEntries(
['db3', 'eb3', 'gb3', 'ab3', 'bb3'].map((n) => [n, n.split('').slice(0, 2).join('')]),
)}
/>
<MiniRepl
hideHeader
client:visible
tune={`note("c# d# f# g# a#").sound("piano")`}
claviature
claviatureLabels={Object.fromEntries(
['c#3', 'd#3', 'f#3', 'g#3', 'a#3'].map((n) => [n, n.split('').slice(0, 2).join('')]),
)}
/>
**Andere Oktaven**
<MiniRepl
hideHeader
client:visible
tune={`note("c2 e3 g4 b5").sound("piano")`}
claviature
claviatureLabels={Object.fromEntries(['c1', 'c2', 'c3', 'c4', 'c5'].map((n) => [n, n]))}
claviatureLabels={Object.fromEntries(
Array(49)
.fill()
.map((_, i) => [midi2note(i + 36), midi2note(i + 36)]),
)}
/>
<Box>
Probiere verschiedene Oktaven aus (1-8)
</Box>
Normalerweise kommen Leute die keine Noten besser mit Zahlen anstatt mit Buchstaben zurecht.
Daher benutzen die folgenden Beispiele meistens Zahlen.
Später sehen wir auch noch ein paar Tricks die es uns erleichtern Töne zu spielen die zueinander passen.
## Den Sound verändern
Genau wie bei geräuschhaften Sounds können wir den Klang unserer Töne mit `sound` verändern:
<MiniRepl hideHeader client:visible tune={`note("36 43, 52 59 62 64").sound("piano")`} />
<Box>
Probier ein paar sounds aus:
- gm_electric_guitar_muted - E-Gitarre
- gm_acoustic_bass - Kontrabass
- gm_voice_oohs - Chords
- gm_blown_bottle - Flasche
- sawtooth - Sägezahn-Welle
- square - Rechteck-Welle
- triangle - Dreieck-Welle
- Was ist mit bd, sd oder hh?
- Entferne `.sound('...')` komplett
</Box>
**Zwischen Sounds hin und her wechseln**
<MiniRepl
hideHeader
client:visible
tune={`note("48 67 63 [62, 58]")
.sound("piano gm_electric_guitar_muted")`}
/>
**Gleichzeitige Sounds**
<MiniRepl
hideHeader
client:visible
tune={`note("48 67 63 [62, 58]")
.sound("piano, gm_electric_guitar_muted")`}
/>
<Box>
Die patterns in `note` und `sound` werden kombiniert!
Wir schauen uns später noch mehr Möglichkeiten an wie man patterns kombiniert.
</Box>
## Längere Sequenzen
**Sequenzen verlangsamen mit `/`**
{/* [c2 bb1 f2 eb2] */}
<MiniRepl hideHeader client:visible tune={`note("[36 34 41 39]/4").sound("gm_acoustic_bass")`} punchcard />
<Box>
Das `/4` spielt die Sequenz 4 mal so langsam, also insgesamt 4 cycles = 4s.
Jede Note ist nun also 1s lang.
Schreib noch mehr Töne in die Klammern und achte darauf dass es schneller wird.
</Box>
Wenn eine Sequenz unabhängig von ihrem Inhalt immer gleich schnell bleiben soll, gibt es noch eine andere Art Klammern:
**Eins pro Cycle per \< \>**
<MiniRepl hideHeader client:visible tune={`note("<36 34 41 39>").sound("gm_acoustic_bass")`} punchcard />
<Box>
Füg noch mehr Töne hinzu und achte darauf wie das Tempo gleich bleibt!
Tatsächlich sind diese Klammern nur eine Abkürzung:
`<a b c>` = `[a b c]/3`
`<a b c d>` = `[a b c d]/4`
usw..
</Box>
**Eine Sequenz pro Cycle**
<MiniRepl
hideHeader
client:visible
tune={`note("<[36 48] [34 46] [41 53] [39 51]>")
.sound("gm_acoustic_bass")`}
punchcard
/>
oder auch...
<MiniRepl
hideHeader
client:visible
tune={`note("<[36 48]*4 [34 46]*4 [41 53]*4 [39 51]*4>/2")
.sound("gm_acoustic_bass")`}
punchcard
/>
**Alternativen**
Ähnlich wie Unter-Sequenzen, kann auch `<...>` innerhalb einer Sequenz verwendet werden:
<MiniRepl
hideHeader
client:visible
tune={`note("60 <63 62 65 63>")
.sound("gm_xylophone")`}
punchcard
/>
Das ist auch praktisch für atonale Sounds:
<MiniRepl
hideHeader
client:visible
tune={`sound("bd*2, ~ <sd cp>, [~ hh]*2")
.bank("RolandTR909")`}
punchcard
/>
## Skalen
Es kann mühsam sein die richtigen Noten zu finden wenn man alle zur Verfügung hat.
Mit Skalen ist das einfacher:
<MiniRepl
hideHeader
client:visible
tune={`n("0 2 4 <[6,8] [7,9]>")
.scale("C:minor").sound("piano")`}
punchcard
/>
<Box>
Probier verschiedene Zahlen aus. Jede klingt gut!
Probier verschiedene Skalen:
- C:major
- A2:minor
- D:dorian
- G:mixolydian
- A2:minor:pentatonic
- F:major:pentatonic
</Box>
**Automatisierte Skalen**
Wie alle Funktionen können auch Skalen mit einem Pattern automatisiert werden:
<MiniRepl
hideHeader
client:visible
tune={`n("<0 -3>, 2 4 <[6,8] [7,9]>")
.scale("<C:major D:mixolydian>/4")
.sound("piano")`}
punchcard
/>
<Box>
Wenn du keine Ahnung hast was die Skalen bedeuten, keine Sorge.
Es sind einfach nur Namen für verschiedene Gruppen von Tönen die gut zusammenpassen.
Nimm dir Zeit um herauszufinden welche Skalen du magst.
</Box>
## Wiederholen und Verlängern
**Verlängern mit @**
<MiniRepl hideHeader client:visible tune={`note("c@3 eb").sound("gm_acoustic_bass")`} punchcard />
<Box>
Ein Element ohne `@` ist gleichbedeutend mit `@1`. Im Beispiel ist `c` drei Einheiten lang, `eb` nur eine.
Spiel mit der Länge!
</Box>
**Unter-Sequenzen verlängern**
<MiniRepl
hideHeader
client:visible
tune={`n("<[4@2 4] [5@2 5] [6@2 6] [5@2 5]>*2")
.scale("<C2:mixolydian F2:mixolydian>/4")
.sound("gm_acoustic_bass")`}
punchcard
/>
<Box>
Dieser Groove wird auch `shuffle` genannt.
Jeder Schlag enthält 2 Töne, wobei der erste doppelt so lang wie der zweite ist.
Das nennt man auch manchmal `triolen swing`. Es ist ein typischer Rhythmus im Blues und Jazz.
</Box>
**Wiederholen**
<MiniRepl hideHeader client:visible tune={`note("c!2 [eb,<g a bb a>]").sound("piano")`} punchcard />
<Box>
Wechsel zwischen `!`, `*` und `@` hin und her.
Was ist der Unterschied?
</Box>
## Rückblick
Das haben wir in diesem Kapitel gelernt:
| Concept | Syntax | Example |
| ------------ | ------ | ------------------------------------------------------------------- |
| Verlangsamen | \/ | <MiniRepl hideHeader client:visible tune={`note("[c a f e]/2")`} /> |
| Alternativen | \<\> | <MiniRepl hideHeader client:visible tune={`note("c <e g>")`} /> |
| Verlängern | @ | <MiniRepl hideHeader client:visible tune={`note("c@3 e")`} /> |
| Wiederholen | ! | <MiniRepl hideHeader client:visible tune={`note("c!3 e")`} /> |
Neue Funktionen:
| Name | Description | Example |
| ----- | --------------------------------------- | -------------------------------------------------------------------------------------------- |
| note | Tonhöhe als Buchstabe oder Zahl | <MiniRepl hideHeader client:visible tune={`note("b g e c").sound("piano")`} /> |
| scale | Interpretiert `n` als Skalenstufe | <MiniRepl hideHeader client:visible tune={`n("6 4 2 0").scale("C:minor").sound("piano")`} /> |
| stack | Spiele mehrere Patterns parallel (s.u.) | <MiniRepl hideHeader client:visible tune={`stack(s("bd sd"),note("c eb g"))`} /> |
## Beispiele
**Bassline**
<MiniRepl
hideHeader
client:visible
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
.sound("gm_synth_bass_1")
.lpf(800) // <-- we'll learn about this soon`}
/>
**Melodie**
<MiniRepl
hideHeader
client:visible
tune={`n(\`<
[~ 0] 2 [0 2] [~ 2]
[~ 0] 1 [0 1] [~ 1]
[~ 0] 3 [0 3] [~ 3]
[~ 0] 2 [0 2] [~ 2]
>*2\`).scale("C4:minor")
.sound("gm_synth_strings_1")`}
/>
**Drums**
<MiniRepl
hideHeader
client:visible
tune={`sound("bd*2, ~ <sd cp>, [~ hh]*2")
.bank("RolandTR909")`}
/>
**Wenn es doch nur einen Weg gäbe das alles gleichzeitig zu spielen.......**
<Box>
Das geht mit `stack` 😙
</Box>
<MiniRepl
hideHeader
client:visible
tune={`stack(
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
.sound("gm_synth_bass_1").lpf(800),
n(\`<
[~ 0] 2 [0 2] [~ 2]
[~ 0] 1 [0 1] [~ 1]
[~ 0] 3 [0 3] [~ 3]
[~ 0] 2 [0 2] [~ 2]
>*2\`).scale("C4:minor")
.sound("gm_synth_strings_1"),
sound("bd*2, ~ <sd cp>, [~ hh]*2")
.bank("RolandTR909")
)`}
/>
Das hört sich doch langsam nach echter Musik an!
Wir haben Sounds, wir haben Töne.. noch ein Puzzleteil fehlt: [Effekte](/de/workshop/first-effects)

View File

@ -0,0 +1,363 @@
---
title: Erste Sounds
layout: ../../../layouts/MainLayout.astro
---
import { MiniRepl } from '@src/docs/MiniRepl';
import Box from '@components/Box.astro';
import QA from '@components/QA';
# Erste Sounds
Dies ist das erste Kapitel im Strudel Workshop, schön dich an Bord zu haben!
## Textfelder
Der Workshop ist voller interaktiver Textfelder. Lass uns lernen wie sie funktionieren. Hier ist eins:
<MiniRepl hideHeader client:visible tune={`sound("casio")`} />
<Box>
1. ⬆️ Klicke in das obige Textfeld ⬆️
2. Drücke `Strg`+`Enter` zum Abspielen
3. Ändere `casio` in `metal`
4. Drücke `Strg`+`Enter` zum Aktualisieren
5. Drücke `Strg`+`Punkt` zum Stoppen
Mac: `Strg` = `control` oder auch `option`
</Box>
Glückwunsch, du kannst jetzt live coden!
## Sounds
Gerade haben wir schon den ersten sound mit `sound` abgespielt:
<MiniRepl hideHeader client:visible tune={`sound("casio")`} />
<Box>
`casio` ist einer von vielen Standard Sounds.
Probier ein paar andere Sounds aus:
```
insect wind jazz metal east crow casio space numbers
```
Es kann sein, dass du kurz nichts hörst während ein neuer Sound lädt.
</Box>
**Sample Nummer ändern mit :**
Ein Sound kann mehrere Samples (Audio Dateien) enthalten.
Du kannst ein anderes Sample wählen, indem du `:` und eine Zahl an den Sound-Namen anhängst:
<MiniRepl hideHeader client:visible tune={`sound("casio:1")`} hideHeader />
<Box>
Probiere verschiedene Sound / Zahlen Kombinationen.
Ohne Zahl ist gleichbedeutend mit `:0`
</Box>
Jetzt weißt du wie man Sounds abspielt und ihre Sample Nummer einstellt.
Vorerst bleiben wir bei den voreingestellten Sounds, später erfahren wir noch wie man eigene benutzt.
## Drum Sounds
Strudel kommt von Haus aus mit einer breiten Auswahl an Drum Sounds:
<MiniRepl hideHeader client:visible tune={`sound("bd hh sd oh")`} hideHeader />
<Box>
Diese 2-Buchstaben Kombinationen stehen für verschiedene Teile eines Schlagzeugs:
- `bd` = **b**ass **d**rum - Basstrommel
- `sd` = **s**nare **d**rum - Schnarrtrommel
- `rim` = **rim**shot - Rahmenschlag
- `hh` = **h**i**h**at - Hallo Hut
- `oh` = **o**pen **h**ihat - Offener Hallo Hut
Probier verschiedene Sounds aus!
</Box>
Wir können den Charakter des Drum Sounds verändern, indem wir mit `bank` die Drum Machine auswählen:
<MiniRepl hideHeader client:visible tune={`sound("bd hh sd oh").bank("RolandTR909")`} hideHeader />
In diesem Beispiel ist `RolandTR909` der Name der Drum Machine, die eine prägende Rolle für House und Techno Musik spielte.
<Box>
Ändere `RolandTR909` in
- `AkaiLinn`
- `RhythmAce`
- `RolandTR808`
- `RolandTR707`
- `ViscoSpaceDrum`
Es gibt noch viel mehr, aber das sollte fürs Erste reichen..
🦥 Tipp für faule: Mach Doppel-Klick auf einen Namen um ihn zu markieren.
Dann kannst du ihn mit `Strg`+`C` kopieren und mit `Strg`+`V` einfügen.
</Box>
## Sequenzen / Sequences
Im letzten Beispiel haben wir schon gesehen dass man mehrere Sounds hintereinander abspielen kann wenn man sie durch Leerzeichen trennt:
<MiniRepl hideHeader client:visible tune={`sound("bd hh sd hh")`} punchcard />
Beachte wie der aktuell gespielte Sound im Code markiert und auch darunter visualisiert wird.
<Box>
Versuch noch mehr Sounds hinzuzfügen!
</Box>
**Je länger die Sequence, desto schneller**
<MiniRepl hideHeader client:visible tune={`sound("bd bd hh bd rim bd hh bd")`} punchcard />
Der Inhalt einer Sequence wird in einen sogenannten Cycle (=Zyklus) zusammengequetscht.
**Tempo ändern mit `cpm`**
<MiniRepl hideHeader client:visible tune={`sound("bd bd hh bd rim bd hh bd").cpm(40)`} punchcard />
<Box>
cpm = **c**ycles per **m**inute = Cycles pro Minute
Das Tempo ist standardmäßig auf 60cpm eingestellt, also 1 Cycle pro Sekunde.
`cpm` ist angelehnt an `bpm` (=beats per minute).
</Box>
Wir werden später noch mehr Möglichkeiten kennen lernen das Tempo zu verändern.
**Pausen mit '~'**
<MiniRepl hideHeader client:visible tune={`sound("bd hh ~ rim")`} punchcard />
<Box>
Tilde tippen:
- Windows / Linux: `Alt Gr` + `~`
- Mac: `option` + `N`
</Box>
**Unter-Sequenzen mit [Klammern]**
<MiniRepl hideHeader client:visible tune={`sound("bd [hh hh] rim [hh hh]")`} punchcard />
<Box>
Der Inhalt der Klammer wird ebenfalls zusammengequetscht!
Füge noch mehr Sounds in die Klammern ein!
</Box>
Genau wie bei der ganzen Sequence wird eine Unter-Sequence schneller je mehr drin ist.
**Multiplikation: Dinge schneller machen**
<MiniRepl hideHeader client:visible tune={`sound("bd hh*2 sd hh*3")`} punchcard />
**Multiplikation: Vieeeeeeel schneller**
<MiniRepl hideHeader client:visible tune={`sound("bd hh*16 sd hh*8")`} punchcard />
<Box>
Tonhöhe = sehr schneller Rhythmus
</Box>
**Multiplikation: Ganze Unter-Sequences schneller machen**
<MiniRepl hideHeader client:visible tune={`sound("bd [sd hh]*2")`} punchcard />
Bolero:
<MiniRepl
hideHeader
client:visible
tune={`sound("sd sd*3 sd sd*3 sd sd sd sd*3 sd sd*3 sd*3 sd*3")
.cpm(10)`}
punchcard
/>
**Unter-Unter-Sequenzen mit [[Klammern]]**
<MiniRepl hideHeader client:visible tune={`sound("bd [[rim rim] hh]")`} punchcard />
<Box>
Du kannst so tief verschachteln wie du willst!
</Box>
**Parallele Sequenzen mit Komma**
<MiniRepl hideHeader client:visible tune={`sound("hh hh hh, bd casio")`} punchcard />
Du kannst so viele Kommas benutzen wie du möchtest:
<MiniRepl hideHeader client:visible tune={`sound("hh hh hh, bd bd, ~ casio")`} punchcard />
Kommas können auch in Unter-Sequenzen verwendet werden:
<MiniRepl hideHeader client:visible tune={`sound("hh hh hh, bd [bd,casio]")`} punchcard />
<Box>
Ist dir aufgefallen dass sich die letzten beiden Beispiele gleich anhören?
Es kommt öfter vor, dass man die gleiche Idee auf verschiedene Arten ausdrücken kann.
</Box>
**Mehrere Zeilen schreiben mit \` (backtick)**
<MiniRepl
hideHeader
client:visible
tune={`sound(\`bd*2, ~ cp,
~ ~ ~ oh, hh*4,
[~ casio]*2\`)`}
punchcard
/>
<Box>
Ob man " oder \` benutzt ist nur eine Frage der Übersichtlichkeit.
</Box>
**Sample Nummer separat auswählen**
Benutzt man nur einen Sound mit unterschiedlichen Sample Nummer sieht das so aus:
<MiniRepl hideHeader client:visible tune={`sound("jazz:0 jazz:1 [jazz:4 jazz:2] jazz:3*2")`} punchcard />
Das gleiche kann man auch so schreiben:
<MiniRepl hideHeader client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
## Rückblick
Wir haben jetzt die Grundlagen der sogenannten Mini-Notation gelernt, der Rhythmus-Sprache von Tidal.
Das haben wir bisher gelernt:
| Concept | Syntax | Example |
| --------------------- | ----------- | -------------------------------------------------------------------------------- |
| Sequenz | Leerzeichen | <MiniRepl hideHeader client:visible tune={`sound("bd bd sd hh")`} /> |
| Sound Nummer | :x | <MiniRepl hideHeader client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Pausen | ~ | <MiniRepl hideHeader client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Unter-Sequenzen | \[\] | <MiniRepl hideHeader client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Unter-Unter-Sequenzen | \[\[\]\] | <MiniRepl hideHeader client:visible tune={`sound("bd [metal [jazz sd]]")`} /> |
| Schneller | \* | <MiniRepl hideHeader client:visible tune={`sound("bd sd*2 cp*3")`} /> |
| Parallel | , | <MiniRepl hideHeader client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> |
Die mit Apostrophen umgebene Mini-Notation benutzt man normalerweise in eine sogenannten Funktion.
Die folgenden Funktionen haben wir bereits gesehen:
| Name | Description | Example |
| ----- | -------------------------------------- | ---------------------------------------------------------------------------------- |
| sound | Spielt den Sound mit dem Namen | <MiniRepl hideHeader client:visible tune={`sound("bd sd")`} /> |
| bank | Wählt die Soundbank / Drum Machine | <MiniRepl hideHeader client:visible tune={`sound("bd sd").bank("RolandTR909")`} /> |
| cpm | Tempo in **C**ycles **p**ro **M**inute | <MiniRepl hideHeader client:visible tune={`sound("bd sd").cpm(90)`} /> |
| n | Sample Nummer | <MiniRepl hideHeader client:visible tune={`n("0 1 4 2").sound("jazz")`} /> |
## Beispiele
**Einfacher Rock Beat**
<MiniRepl hideHeader client:visible tune={`sound("bd sd, hh*4").bank("RolandTR505").cpm(100/2)`} punchcard />
**Klassischer House**
<MiniRepl hideHeader client:visible tune={`sound("bd*2, ~ cp, [~ hh]*2").bank("RolandTR909")`} punchcard />
<Box>
Ist die aufgefallen dass die letzten 2 Patterns extrem ähnlich sind?
Bestimmte Drum Patterns werden oft genreübergreifend wiederverwendet.
</Box>
We Will Rock you
<MiniRepl hideHeader client:visible tune={`sound("bd*2 cp").bank("RolandTR707").cpm(81/2)`} punchcard />
**Yellow Magic Orchestra - Firecracker**
<MiniRepl
hideHeader
client:visible
tune={`sound("bd sd, ~ ~ ~ hh ~ hh ~ ~, ~ perc ~ perc:1*2")
.bank("RolandCompurhythm1000")`}
punchcard
/>
**Nachahmung eines 16 step sequencers**
<MiniRepl
hideHeader
client:visible
tune={`sound(\`
[~ ~ oh ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ],
[hh hh ~ ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ],
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [cp ~ ~ ~ ],
[bd ~ ~ ~ ] [~ ~ ~ bd] [~ ~ bd ~ ] [~ ~ ~ bd]
\`).cpm(90/4)`}
punchcard
/>
**Noch eins**
<MiniRepl
hideHeader
client:visible
tune={`sound(\`
[~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ oh:1 ~ ],
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh ~ ~ ],
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [~ cp ~ ~ ],
[bd bd ~ ~ ] [~ ~ bd ~ ] [bd bd ~ bd ] [~ ~ ~ ~ ]
\`).bank("RolandTR808").cpm(88/4)`}
punchcard
/>
**Nicht so typische Drums**
<MiniRepl
hideHeader
client:visible
tune={`s(\`jazz*2,
insect [crow metal] ~ ~,
~ space:4 ~ space:1,
~ wind\`)
.cpm(100/2)`}
punchcard
/>
Jetzt haben wir eine grundlegende Ahnung davon wie man mit Strudel Beats baut!
Im nächsten Kapitel werden wir ein paar [Töne spielen](/de/workshop/first-notes).

View File

@ -0,0 +1,74 @@
---
title: Intro
layout: ../../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../../docs/MiniRepl';
# Willkommen
<img src="/icons/strudel_icon.png" className="w-32 animate-pulse md:float-right ml-8" />
Willkommen zum Strudel Workshop!
Du hast den richtigen Ort gefunden wenn du lernen möchtest wie man mit Code Musik macht.
## Was ist Strudel
Mit Strudel kann man dynamische Musikstücke in Echtzeit schreiben.
Es ist eine in JavaScript entwickelte Version von [Tidal Cycles](https://tidalcycles.org/) und wurde 2022 von Alex McLean und Felix Roos initiiert.
Tidal Cycles, auch bekannt unter dem Namen Tidal, ist eine Computersprache für algorithmische Muster.
Obwohl sie meistens für die Erzeugung von Musik eingesetzt wird, kann sie für jede Art von Tätigkeit eingesetzt werden,
in der Muster eine Rolle spielen.
Du brauchst keine Erfahrung in JavaScript oder Tidal Cycles um mit Strudel Musik zu machen.
Dieser interaktive Workshop leitet dich spielerisch durch die Grundlagen von Strudel.
Der beste Ort um mit Strudel Musik zu machen ist das [Strudel REPL](https://strudel.tidalcycles.org/).
## Was kann man mit Strudel machen?
- Musik Live Coding: In Echtzeit mit Code Musik machen
- Algorithmische Komposition: Schreibe Musik mithilfe Tidals einzigartiger Sprache für Muster
- Lehren: Strudel eignet sich gut für Lehrende, da keine Installation nötig ist und die Sprache kein theoretisches Vorwissen erfordert.
- Mit anderen Musik-Anwendungen kombinieren: Per MIDI oder OSC kann Strudel als flexibler Sequencer mit jedem Setup kombiniert werden
## Beispiel
Hier ist ein Beispiel wie Strudel klingen kann:
<MiniRepl
client:idle
tune={`stack(
// drums
s("bd,[~ <sd!3 sd(3,4,2)>],hh*8")
.speed(perlin.range(.8,.9)), // random sample speed variation
// bassline
"<a1 b1\*2 a1(3,8) e2>"
.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
.note() // wrap in "note"
.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
// chords
"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand')
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.note() // wrap in "note"
.s('sawtooth') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
.attack(1) // slowly fade in
)
.slow(3/2)`}
/>
Mehr Beispiele gibt es [hier](/examples).
Du kannst auch im [Strudel REPL](https://strudel.tidalcycles.org/) auf `shuffle` klicken um ein zufälliges Beispiel zu hören.
## Workshop
Der beste Weg um Strudel zu lernen ist der nun folgende Workshop.
Wenn du bereit bist, lass uns loslegen mit deinen [ersten Sounds](/de/workshop/first-sounds).

View File

@ -0,0 +1,3 @@
<script is:inline>
window.location.pathname = `/workshop/intro`;
</script>

View File

@ -0,0 +1,183 @@
---
title: Pattern Effekte
layout: ../../../layouts/MainLayout.astro
---
import { MiniRepl } from '@src/docs/MiniRepl';
import Box from '@components/Box.astro';
import QA from '@components/QA';
# Pattern Effekte
Bis jetzt sind die meisten Funktionen die wir kennengelernt haben ähnlich wie Funktionen in anderen Musik Programmen: Sequencing von Sounds, Noten und Effekten.
In diesem Kapitel beschäftigen wir uns mit Funktionen die weniger herkömmlich oder auch enzigartig sind.
**rev = rückwärts abspielen**
<MiniRepl hideHeader client:visible tune={`n("0 1 [4 3] 2").sound("jazz").rev()`} />
**jux = einen stereo kanal modifizieren**
<MiniRepl hideHeader client:visible tune={`n("0 1 [4 3] 2").sound("jazz").jux(rev)`} />
So würde man das ohne jux schreiben:
<MiniRepl
hideHeader
client:visible
tune={`stack(
n("0 1 [4 3] 2").sound("jazz").pan(0),
n("0 1 [4 3] 2").sound("jazz").pan(1).rev()
)`}
/>
Lass uns visualisieren was hier passiert:
<MiniRepl
hideHeader
client:visible
tune={`stack(
n("0 1 [4 3] 2").sound("jazz").pan(0).color("cyan"),
n("0 1 [4 3] 2").sound("jazz").pan(1).color("magenta").rev()
)`}
punchcard
/>
<Box>
Schreibe `//` vor eine der beiden Zeilen im `stack`!
</Box>
**mehrere tempos**
<MiniRepl hideHeader client:visible tune={`note("c2, eb3 g3 [bb3 c4]").sound("piano").slow("1,2,3")`} />
Das hat den gleichen Effekt wie:
<MiniRepl
hideHeader
client:visible
tune={`stack(
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(1).color('cyan'),
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(2).color('magenta'),
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(3).color('yellow')
)`}
punchcard
/>
<Box>
Schreibe wieder `//` vor eine oder mehrere Zeilen im `stack`.
</Box>
**add = addieren**
<MiniRepl
hideHeader
client:visible
tune={`note("c2 [eb3,g3]".add("<0 <1 -1>>"))
.color("<cyan <magenta yellow>>").adsr("[.1 0]:.2:[1 0]")
.sound("gm_acoustic_bass").room(.5)`}
punchcard
/>
<Box>
Addiert man eine Zahl zu einer Note, verhält sich diese wie eine Zahl.
z.B. c4 = 60, also ist c4 + 2 = 62
</Box>
Man kann auch mehrmals addieren:
<MiniRepl
hideHeader
client:visible
tune={`note("c2 [eb3,g3]".add("<0 <1 -1>>").add("0,7"))
.color("<cyan <magenta yellow>>").adsr("[.1 0]:.2:[1 0]")
.sound("gm_acoustic_bass").room(.5)`}
punchcard
/>
**add + scale**
<MiniRepl
hideHeader
client:visible
tune={`n("<0 [2 4] <3 5> [~ <4 1>]>*2".add("<0 [0,2,4]>/4"))
.scale("C5:minor").release(.5)
.sound("gm_xylophone").room(.5)`}
punchcard
/>
**Alles zusammen**
<MiniRepl
hideHeader
client:visible
tune={`stack(
n("<0 [2 4] <3 5> [~ <4 1>]>*2".add("<0 [0,2,4]>/4"))
.scale("C5:minor")
.sound("gm_xylophone")
.room(.4).delay(.125),
note("c2 [eb3,g3]".add("<0 <1 -1>>"))
.adsr("[.1 0]:.2:[1 0]")
.sound("gm_acoustic_bass")
.room(.5),
n("0 1 [2 3] 2").sound("jazz").jux(rev).slow(2)
)`}
/>
**ply**
<MiniRepl hideHeader client:visible tune={`sound("hh, bd rim").bank("RolandTR707").ply(2)`} punchcard />
das ist wie:
<MiniRepl hideHeader client:visible tune={`sound("hh*2, bd*2 rim*2").bank("RolandTR707")`} punchcard />
<Box>
Probier `ply` mit einem pattern zu automatisieren, z.b. `"<1 2 1 3>"`
</Box>
**off**
<MiniRepl
hideHeader
client:visible
tune={`n("<0 [4 <3 2>] <2 3> [~ 1]>"
.off(1/8, x=>x.add(4))
//.off(1/4, x=>x.add(7))
).scale("<C5:minor Db5:mixolydian>/4")
.s("triangle").room(.5).ds(".1:0").delay(.5)`}
punchcard
/>
<Box>
In der notation `x=>x.`, das `x` ist das Pattern das wir bearbeiten.
</Box>
`off` ist auch nützlich für sounds:
<MiniRepl
hideHeader
client:visible
tune={`s("bd sd,[~ hh]*2").bank("CasioRZ1")
.off(1/8, x=>x.speed(1.5).gain(.25))`}
/>
| name | description | example |
| ---- | --------------------------------- | ---------------------------------------------------------------------------------------------- |
| rev | rückwärts | <MiniRepl hideHeader client:visible tune={`n("0 2 4 6").scale("C:minor").rev()`} /> |
| jux | ein stereo-kanal modifizieren | <MiniRepl hideHeader client:visible tune={`n("0 2 4 6").scale("C:minor").jux(rev)`} /> |
| add | addiert zahlen oder noten | <MiniRepl hideHeader client:visible tune={`n("0 2 4 6".add("<0 1 2 1>")).scale("C:minor")`} /> |
| ply | multipliziert jedes element x mal | <MiniRepl hideHeader client:visible tune={`s("bd sd").ply("<1 2 3>")`} /> |
| off | verzögert eine modifizierte kopie | <MiniRepl hideHeader client:visible tune={`s("bd sd, hh*4").off(1/8, x=>x.speed(2))`} /> |

View File

@ -0,0 +1,68 @@
---
title: Recap
layout: ../../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../../docs/MiniRepl';
# Workshop Rückblick
Diese Seite ist eine Auflistung aller im Workshop enthaltenen Funktionen.
## Mini Notation
| Concept | Syntax | Example |
| --------------------- | -------- | -------------------------------------------------------------------------------- |
| Sequence | space | <MiniRepl hideHeader client:visible tune={`sound("bd bd sn hh")`} /> |
| Sample Nummer | :x | <MiniRepl hideHeader client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Pausen | ~ | <MiniRepl hideHeader client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Unter-Sequences | \[\] | <MiniRepl hideHeader client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Unter-Unter-Sequences | \[\[\]\] | <MiniRepl hideHeader client:visible tune={`sound("bd [metal [jazz sn]]")`} /> |
| Schneller | \* | <MiniRepl hideHeader client:visible tune={`sound("bd sn*2 cp*3")`} /> |
| Slow down | \/ | <MiniRepl hideHeader client:visible tune={`note("[c a f e]/2")`} /> |
| Parallel | , | <MiniRepl hideHeader client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> |
| Alternieren | \<\> | <MiniRepl hideHeader client:visible tune={`note("c <e g>")`} /> |
| Verlängern | @ | <MiniRepl hideHeader client:visible tune={`note("c@3 e")`} /> |
| Wiederholen | ! | <MiniRepl hideHeader client:visible tune={`note("c!3 e")`} /> |
## Sounds
| Name | Description | Example |
| ----- | -------------------------- | ---------------------------------------------------------------------------------- |
| sound | spielt den sound mit namen | <MiniRepl hideHeader client:visible tune={`sound("bd sd")`} /> |
| bank | wählt die soundbank | <MiniRepl hideHeader client:visible tune={`sound("bd sd").bank("RolandTR909")`} /> |
| n | wählt sample mit nummer | <MiniRepl hideHeader client:visible tune={`n("0 1 4 2").sound("jazz")`} /> |
## Notes
| Name | Description | Example |
| --------- | ---------------------------------- | -------------------------------------------------------------------------------------------- |
| note | wählt note per zahl oder buchstabe | <MiniRepl hideHeader client:visible tune={`note("b g e c").sound("piano")`} /> |
| n + scale | wählt note n in skala | <MiniRepl hideHeader client:visible tune={`n("6 4 2 0").scale("C:minor").sound("piano")`} /> |
| stack | spielt mehrere patterns parallel | <MiniRepl hideHeader client:visible tune={`stack(s("bd sd"),note("c eb g"))`} /> |
## Audio Effekte
| name | example |
| ----- | -------------------------------------------------------------------------------------------------- |
| lpf | <MiniRepl hideHeader client:visible tune={`note("c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
| vowel | <MiniRepl hideHeader client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | <MiniRepl hideHeader client:visible tune={`s("hh*8").gain("[.25 1]*2")`} /> |
| delay | <MiniRepl hideHeader client:visible tune={`s("bd rim").delay(.5)`} /> |
| room | <MiniRepl hideHeader client:visible tune={`s("bd rim").room(.5)`} /> |
| pan | <MiniRepl hideHeader client:visible tune={`s("bd rim").pan("0 1")`} /> |
| speed | <MiniRepl hideHeader client:visible tune={`s("bd rim").speed("<1 2 -1 -2>")`} /> |
| range | <MiniRepl hideHeader client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
## Pattern Effects
| name | description | example |
| ---- | --------------------------------- | ---------------------------------------------------------------------------------------------- |
| cpm | tempo in cycles pro minute | <MiniRepl hideHeader client:visible tune={`sound("bd sd").cpm(90)`} /> |
| fast | schneller | <MiniRepl hideHeader client:visible tune={`sound("bd sd").fast(2)`} /> |
| slow | langsamer | <MiniRepl hideHeader client:visible tune={`sound("bd sd").slow(2)`} /> |
| rev | rückwärts | <MiniRepl hideHeader client:visible tune={`n("0 2 4 6").scale("C:minor").rev()`} /> |
| jux | ein stereo-kanal modifizieren | <MiniRepl hideHeader client:visible tune={`n("0 2 4 6").scale("C:minor").jux(rev)`} /> |
| add | addiert zahlen oder noten | <MiniRepl hideHeader client:visible tune={`n("0 2 4 6".add("<0 1 2 1>")).scale("C:minor")`} /> |
| ply | jedes element schneller machen | <MiniRepl hideHeader client:visible tune={`s("bd sd").ply("<1 2 3>")`} /> |
| off | verzögert eine modifizierte kopie | <MiniRepl hideHeader client:visible tune={`s("bd sd, hh*4").off(1/8, x=>x.speed(2))`} /> |

View File

@ -1,6 +1,8 @@
---
import * as tunes from '../../../src/repl/tunes.mjs';
import HeadCommon from '../../components/HeadCommon.astro';
import { getMetadata } from '../metadata_parser';
---
<head>
@ -12,7 +14,7 @@ import HeadCommon from '../../components/HeadCommon.astro';
Object.entries(tunes).map(([name, tune]) => (
<a class="rounded-md bg-slate-900 hover:bg-slate-700 cursor-pointer relative" href={`./#${btoa(tune)}`}>
<div class="absolute w-full h-full flex justify-center items-center">
<span class="bg-slate-800 p-2 rounded-md text-white">{name}</span>
<span class="bg-slate-800 p-2 rounded-md text-white">{getMetadata(tune)['title'] || name}</span>
</div>
<img src={`./img/example-${name}.png`} />
</a>

View File

@ -6,23 +6,24 @@ layout: ../../layouts/MainLayout.astro
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# Functional JavaScript API
# Pattern Functions
While the mini notation is powerful on its own, there is much more to discover.
Internally, the mini notation will expand to use the actual functional JavaScript API.
Let's learn all about functions to create and modify patterns.
At the core of Strudel, everything is made of functions.
For example, this Pattern in Mini Notation:
For example, everything you can do with the Mini-Notation can also be done with a function.
This Pattern in Mini Notation:
<MiniRepl client:only="react" tune={`note("c3 eb3 g3")`} />
is equivalent to this Pattern without Mini Notation:
<MiniRepl client:only="react" tune={`note(seq(c3, eb3, g3))`} />
<MiniRepl client:only="react" tune={`note(seq("c3", "eb3", "g3"))`} />
Similarly, there is an equivalent function for every aspect of the mini notation.
Which representation to use is a matter of context. As a rule of thumb, you can think of the JavaScript API
to fit better for the larger context, while mini notation is more practical for individiual rhythms.
Which representation to use is a matter of context. As a rule of thumb, functions
are better suited in a larger context, while mini notation is more practical for individiual rhythms.
## Limits of Mini Notation
@ -46,10 +47,10 @@ You can freely mix JS patterns, mini patterns and values! For example, this patt
<MiniRepl
client:idle
tune={`cat(
stack(g3,b3,e4),
stack(a3,c3,e4),
stack(b3,d3,fs4),
stack(b3,e4,g4)
stack("g3","b3","e4"),
stack("a3","c3","e4"),
stack("b3","d3","fs4"),
stack("b3","e4","g4")
).note()`}
/>
@ -72,4 +73,4 @@ You can freely mix JS patterns, mini patterns and values! For example, this patt
While mini notation is almost always shorter, it only has a handful of modifiers: \* / ! @.
When using JS patterns, there is a lot more you can do.
What [Pattern Constructors](/learn/factories) does Strudel offer?
Next, let's look at how you can [create patterns](/learn/factories)

View File

@ -6,31 +6,31 @@ layout: ../../layouts/MainLayout.astro
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# Strudel Code
# Coding Syntax
Now that we have played some notes using different sounds, let's take a step back and look how we actually achieved this using _code_.
Let's take a step back and understand how the syntax in Strudel works.
Let's look at this simple example again. What do we notice?
Take a look at this simple example:
<MiniRepl client:idle tune={`freq("220 275 330 440").s("sine")`} />
<MiniRepl client:idle tune={`note("c a f e").s("piano")`} />
- We have a word `freq` which is followed by some brackets `()` with some words/letters/numbers inside, surrounded by quotes `"220 275 330 440"` (corresponding to the pitches a3, c#4, e4, a4).
- Then we have a dot `.` followed by another similar piece of code `s("sine")`.
- We can also see these texts are _highlighted_ using colours: word `freq` is purple, the brackets `()` are grey, and the content inside the `""` are green.
- We have a word `note` which is followed by some brackets `()` with some words/letters/numbers inside, surrounded by quotes `"c a f e"`
- Then we have a dot `.` followed by another similar piece of code `s("piano")`.
- We can also see these texts are _highlighted_ using colours: word `note` is purple, the brackets `()` are grey, and the content inside the `""` are green. (The colors could be different if you've changed the default theme)
What happens if we try to 'break' this pattern in different ways?
<MiniRepl client:idle tune={`freq(220 275 330 440).s(sine)`} />
<MiniRepl client:idle tune={`note(c a f e).s(piano)`} />
<MiniRepl client:idle tune={`freq("220 275 330 440")s("sine")`} />
<MiniRepl client:idle tune={`note("c a f e")s("piano")`} />
<MiniRepl client:idle tune={`freq["220 275 330 440"].s{"sine"}`} />
<MiniRepl client:idle tune={`note["c a f e"].s{"piano"}`} />
Ok, none of these seem to work...
<MiniRepl client:idle tune={`s("sine").freq("220 275 330 440")`} />
<MiniRepl client:idle tune={`s("piano").note("c a f e")`} />
This one does work, but now we can't hear the four different events and frequencies anymore.
This one does work, but now we only hear the first note...
So what is going on here?
@ -67,16 +67,17 @@ It is a handy way to quickly turn code on and off.
Try uncommenting this line by deleting `//` and refreshing the pattern.
You can also use the keyboard shortcut `cmd-/` to toggle comments on and off.
You might noticed that some comments in the REPL samples include some words starting with a "@", like `@by` or `@license`.
Those are just a convention to define some information about the music. We will talk about it in the [Music metadata](/learn/metadata) section.
# Strings
Ok, so what about the content inside the quotes (e.g. `"a3 c#4 e4 a4"`)?
Ok, so what about the content inside the quotes (e.g. `"c a f e"`)?
In JavaScript, as in most programming languages, this content is referred to as being a [_string_](<https://en.wikipedia.org/wiki/String_(computer_science)>).
A string is simply a sequence of individual characters.
In TidalCycles, double quoted strings are used to write _patterns_ using the mini-notation, and you may hear the phrase _pattern string_ from time to time.
If you want to create a regular string and not a pattern, you can use single quotes, e.g. `'C minor'` will not be parsed as Mini Notation.
The good news is, that this covers 99% of the JavaScript syntax needed for Strudel!
Let's now look at the way we can express [Rhythms](/learn/mini-notation)...
The good news is, that this covers most of the JavaScript syntax needed for Strudel!
<br />

View File

@ -1,12 +1,12 @@
---
title: Pattern Constructors
title: Creating Patterns
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# Pattern Constructors
# Creating Patterns
The following functions will return a pattern.
These are the equivalents used by the Mini Notation:

View File

@ -0,0 +1,94 @@
---
title: Music metadata
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# Music metadata
You can optionally add some music metadata in your Strudel code, by using tags in code comments:
```js
// @title Hey Hoo
// @by Sam Tagada
// @license CC BY-NC-SA
```
Like other comments, those are ignored by Strudel, but it can be used by other tools to retrieve some information about the music.
It is for instance used by the [swatch tool](https://github.com/tidalcycles/strudel/tree/main/my-patterns) to display pattern titles in the [examples page](https://strudel.tidalcycles.org/examples/).
## Alternative syntax
You can also use comment blocks:
```js
/*
@title Hey Hoo
@by Sam Tagada
@license CC BY-NC-SA
*/
```
Or define multiple tags in one line:
```js
// @title Hey Hoo @by Sam Tagada @license CC BY-NC-SA
```
The `title` tag has an alternative syntax using quotes (must be defined at the very begining):
```js
// "Hey Hoo" @by Sam Tagada
```
## Tags list
Available tags are:
- `@title`: music title
- `@by`: music author(s), separated by comma, eventually followed with a link in `<>` (ex: `@by John Doe <https://example.com>`)
- `@license`: music license(s)
- `@details`: some additional information about the music
- `@url`: web page(s) related to the music (git repo, soundcloud link, etc.)
- `@genre`: music genre(s) (pop, jazz, etc)
- `@album`: music album name
## Multiple values
Some of them accepts several values, using the comma or new line separator, or duplicating the tag:
```js
/*
@by Sam Tagada
Jimmy
@genre pop, jazz
@url https://example.com
@url https://example.org
*/
```
You can also add optional prefixes and use tags where you want:
```js
/*
song @by Sam Tagada
samples @by Jimmy
*/
...
note("a3 c#4 e4 a4") // @by Sandy
```
## Multiline
If a tag doesn't accept a list, it can take multi-line values:
```js
/*
@details I wrote this song in February 19th, 2023.
It was around midnight and I was lying on
the sofa in the living room.
*/
```

View File

@ -8,11 +8,17 @@ import { JsDoc } from '../../docs/JsDoc';
# Mini-notation
Similar to [Haskell Tidal Cycles](https://tidalcycles.org/docs/), Strudel has an "embedded mini-notation" (also called a [domain-specific language, or DSL](https://en.wikipedia.org/wiki/Domain-specific_language)) that is designed for writing rhythmic patterns using little amounts of text.
If you've seen any Tidal code before, you may have noticed the mini-notation and wondered what it's all about.
It's one of the main features of Tidal, and although it might look like a strange way to notate music and other patterns, you will soon see how powerful it can be.
Just like [Tidal Cycles](https://tidalcycles.org/), Strudel uses a so called "Mini-Notation", which is a custom language that is designed for writing rhythmic patterns using little amounts of text.
Before diving deeper into the details, here is a flavour of how the mini-notation looks like:
## Note
This page just explains the entirety of the Mini-Notation syntax.
If you are just getting started with Strudel, you can learn the basics of the Mini-Notation in a more practical manner in the [workshop](http://localhost:3000/workshop/first-sounds).
After that, you can come back here if you want to understand every little detail.
## Example
Before diving deeper into the details, here is a flavour of how the Mini-Notation looks like:
<MiniRepl
client:idle

View File

@ -24,7 +24,7 @@ Instead of numbers, scientific interval notation can be used as well:
### scale(name)
Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like scaleTranpose.
Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like scaleTranspose.
<MiniRepl
client:idle

View File

@ -0,0 +1,34 @@
const ALLOW_MANY = ['by', 'url', 'genre', 'license'];
export function getMetadata(raw_code) {
const comment_regexp = /\/\*([\s\S]*?)\*\/|\/\/(.*)$/gm;
const comments = [...raw_code.matchAll(comment_regexp)].map((c) => (c[1] || c[2] || '').trim());
const tags = {};
const [prefix, title] = (comments[0] || '').split('"');
if (prefix.trim() === '' && title !== undefined) {
tags['title'] = title;
}
for (const comment of comments) {
const tag_matches = comment.split('@').slice(1);
for (const tag_match of tag_matches) {
let [tag, tag_value] = tag_match.split(/ (.*)/s);
tag = tag.trim();
tag_value = (tag_value || '').replaceAll(/ +/g, ' ').trim();
if (ALLOW_MANY.includes(tag)) {
const tag_list = tag_value
.split(/[,\n]/)
.map((t) => t.trim())
.filter((t) => t !== '');
tags[tag] = tag in tags ? tags[tag].concat(tag_list) : tag_list;
} else {
tag_value = tag_value.replaceAll(/\s+/g, ' ');
tags[tag] = tag in tags ? tags[tag] + ' ' + tag_value : tag_value;
}
}
}
return tags;
}

View File

@ -1,9 +1,11 @@
import { getMetadata } from '../metadata_parser';
export function getMyPatterns() {
const my = import.meta.glob('../../../../my-patterns/**', { as: 'raw', eager: true });
return Object.fromEntries(
Object.entries(my) //
.filter(([name]) => name.endsWith('.txt')) //
.map(([name, raw]) => [name.split('/').slice(-1), raw]), //
Object.entries(my)
.filter(([name]) => name.endsWith('.txt'))
.map(([name, raw]) => [getMetadata(raw)['title'] || name.split('/').slice(-1), raw]),
);
}

View File

@ -0,0 +1,335 @@
---
title: First Effects
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import QA from '@components/QA';
# First Effects
import Box from '@components/Box.astro';
We have sounds, we have notes, now let's look at effects!
## Some basic effects
**low-pass filter**
<MiniRepl
hideHeader
client:visible
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
.sound("sawtooth").lpf(800)`}
/>
<Box>
lpf = **l**ow **p**ass **f**ilter
- Change lpf to 200. Notice how it gets muffled. Think of it as standing in front of the club with the door closed 🚪.
- Now let's open the door... change it to 5000. Notice how it gets brighter ✨🪩
</Box>
**pattern the filter**
<MiniRepl
hideHeader
client:visible
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
.sound("sawtooth").lpf("200 1000")`}
/>
<Box>
- Try adding more values
- Notice how the pattern in lpf does not change the overall rhythm
We will learn how to automate with waves later...
</Box>
**vowel**
<MiniRepl
hideHeader
client:visible
tune={`note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>/2")
.sound("sawtooth").vowel("<a e i o>/2")`}
/>
**gain**
<MiniRepl
hideHeader
client:visible
tune={`stack(
sound("hh*8").gain("[.25 1]*2"),
sound("bd*2,~ sd:1")
) `}
punchcard
/>
<Box>
Rhythm is all about dynamics!
- Remove `.gain(...)` and notice how flat it sounds.
- Bring it back by undoing (ctrl+z)
</Box>
**stacks within stacks**
Let's combine all of the above into a little tune:
<MiniRepl
hideHeader
client:visible
tune={`stack(
stack(
sound("hh*8").gain("[.25 1]*2"),
sound("bd*2,~ sd:1")
),
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
.sound("sawtooth").lpf("200 1000"),
note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>/2")
.sound("sawtooth").vowel("<a e i o>/2")
) `}
/>
<Box>
Try to identify the individual parts of the stacks, pay attention to where the commas are.
The 3 parts (drums, bassline, chords) are exactly as earlier, just stacked together, separated by comma.
</Box>
**shape the sound with an adsr envelope**
<MiniRepl
hideHeader
client:visible
tune={`note("<c3 bb2 f3 eb3>")
.sound("sawtooth").lpf(600)
.attack(.1)
.decay(.1)
.sustain(.25)
.release(.2)`}
/>
<Box>
Try to find out what the numbers do.. Compare the following
- attack: `.5` vs `0`
- decay: `.5` vs `0`
- sustain: `1` vs `.25` vs `0`
- release: `0` vs `.5` vs `1`
Can you guess what they do?
</Box>
<QA q="Click to see solution" client:visible>
- attack: time it takes to fade in
- decay: time it takes to fade to sustain
- sustain: level after decay
- release: time it takes to fade out after note is finished
![ADSR](https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/ADSR_parameter.svg/1920px-ADSR_parameter.svg.png)
</QA>
**adsr short notation**
<MiniRepl
hideHeader
client:visible
tune={`note("<c3 bb2 f3 eb3>")
.sound("sawtooth").lpf(600)
.adsr(".1:.1:.5:.2")
`}
/>
**delay**
<MiniRepl
hideHeader
client:visible
tune={`stack(
note("~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]")
.sound("gm_electric_guitar_muted"),
sound("<bd rim>").bank("RolandTR707")
).delay(".5")`}
/>
<Box>
Try some `delay` values between 0 and 1. Btw, `.5` is short for `0.5`
What happens if you use `.delay(".8:.125")` ? Can you guess what the second number does?
What happens if you use `.delay(".8:.06:.8")` ? Can you guess what the third number does?
</Box>
<QA q="Lösung anzeigen" client:visible>
`delay("a:b:c")`:
- a: delay volume
- b: delay time
- c: feedback (smaller number = quicker fade)
</QA>
**room aka reverb**
<MiniRepl
hideHeader
client:visible
tune={`n("<4 [3@3 4] [<2 0> ~@16] ~>/2")
.scale("D4:minor").sound("gm_accordion:2")
.room(2)`}
/>
<Box>
Try different values!
Add a delay too!
</Box>
**little dub tune**
<MiniRepl
hideHeader
client:visible
tune={`stack(
note("~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]")
.sound("gm_electric_guitar_muted").delay(.5),
sound("<bd rim>").bank("RolandTR707").delay(.5),
n("<4 [3@3 4] [<2 0> ~@16] ~>/2")
.scale("D4:minor").sound("gm_accordion:2")
.room(2).gain(.5)
)`}
/>
Let's add a bass to make this complete:
<MiniRepl
hideHeader
client:visible
tune={`stack(
note("~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]")
.sound("gm_electric_guitar_muted").delay(.5),
sound("<bd rim>").bank("RolandTR707").delay(.5),
n("<4 [3@3 4] [<2 0> ~@16] ~>/2")
.scale("D4:minor").sound("gm_accordion:2")
.room(2).gain(.4),
n("<0 [~ 0] 4 [3 2] [0 ~] [0 ~] <0 2> ~>*2")
.scale("D2:minor")
.sound("sawtooth,triangle").lpf(800)
)`}
/>
<Box>
Try adding `.hush()` at the end of one of the patterns in the stack...
</Box>
**pan**
<MiniRepl
hideHeader
client:visible
tune={`sound("numbers:1 numbers:2 numbers:3 numbers:4")
.pan("0 0.3 .6 1")
.slow(2)`}
/>
**speed**
<MiniRepl hideHeader client:visible tune={`sound("bd rim").speed("<1 2 -1 -2>").room(.2)`} />
**fast and slow**
We can use `fast` and `slow` to change the tempo of a pattern outside of Mini-Notation:
<MiniRepl hideHeader client:visible tune={`sound("bd*2,~ rim").slow(2)`} />
<Box>
Change the `slow` value. Try replacing it with `fast`.
What happens if you use a pattern like `.fast("<1 [2 4]>")`?
</Box>
By the way, inside Mini-Notation, `fast` is `*` and `slow` is `/`.
<MiniRepl hideHeader client:visible tune={`sound("[bd*2,~ rim]*<1 [2 4]>")`} />
## automation with signals
Instead of changing values stepwise, we can also control them with signals:
<MiniRepl hideHeader client:visible tune={`sound("hh*16").gain(sine)`} punchcard punchcardLabels={false} />
<Box>
The basic waveforms for signals are `sine`, `saw`, `square`, `tri` 🌊
Try also random signals `rand` and `perlin`!
The gain is visualized as transparency in the pianoroll.
</Box>
**setting a range**
By default, waves oscillate between 0 to 1. We can change that with `range`:
<MiniRepl hideHeader client:visible tune={`sound("hh*8").lpf(saw.range(500, 2000))`} />
<Box>
What happens if you flip the range values?
</Box>
We can change the automation speed with slow / fast:
<MiniRepl
hideHeader
client:visible
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
.sound("sawtooth")
.lpf(sine.range(100, 2000).slow(8))`}
/>
<Box>
The whole automation will now take 8 cycles to repeat.
</Box>
## Recap
| name | example |
| ----- | -------------------------------------------------------------------------------------------------- |
| lpf | <MiniRepl hideHeader client:visible tune={`note("c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
| vowel | <MiniRepl hideHeader client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | <MiniRepl hideHeader client:visible tune={`s("hh*8").gain("[.25 1]*2")`} /> |
| delay | <MiniRepl hideHeader client:visible tune={`s("bd rim").delay(.5)`} /> |
| room | <MiniRepl hideHeader client:visible tune={`s("bd rim").room(.5)`} /> |
| pan | <MiniRepl hideHeader client:visible tune={`s("bd rim").pan("0 1")`} /> |
| speed | <MiniRepl hideHeader client:visible tune={`s("bd rim").speed("<1 2 -1 -2>")`} /> |
| range | <MiniRepl hideHeader client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
Let us now take a look at some of Tidal's typical [pattern effects](/workshop/pattern-effects).

View File

@ -0,0 +1,390 @@
---
title: First Notes
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '@src/docs/MiniRepl';
import { midi2note } from '@strudel.cycles/core/';
import Box from '@components/Box.astro';
import QA from '@components/QA';
# First Notes
Let's look at how we can play notes
## numbers and notes
**play notes with numbers**
<MiniRepl
hideHeader
client:visible
tune={`note("48 52 55 59").sound("piano")`}
claviature
claviatureLabels={Object.fromEntries(
Array(49)
.fill()
.map((_, i) => [midi2note(i + 36), i + 36]),
)}
/>
<Box>
Try out different numbers!
Try decimal numbers, like 55.5
</Box>
**play notes with letters**
<MiniRepl
hideHeader
client:visible
tune={`note("c e g b").sound("piano")`}
claviature
claviatureLabels={Object.fromEntries(['c3', 'd3', 'e3', 'f3', 'g3', 'a3', 'b3'].map((n) => [n, n.split('')[0]]))}
/>
<Box>
Try out different letters (a - g).
Can you find melodies that are actual words? Hint: ☕ 😉 ⚪
</Box>
**add flats or sharps to play the black keys**
<MiniRepl
hideHeader
client:visible
tune={`note("db eb gb ab bb").sound("piano")`}
claviature
claviatureLabels={Object.fromEntries(
['db3', 'eb3', 'gb3', 'ab3', 'bb3'].map((n) => [n, n.split('').slice(0, 2).join('')]),
)}
/>
<MiniRepl
hideHeader
client:visible
tune={`note("c# d# f# g# a#").sound("piano")`}
claviature
claviatureLabels={Object.fromEntries(
['c#3', 'd#3', 'f#3', 'g#3', 'a#3'].map((n) => [n, n.split('').slice(0, 2).join('')]),
)}
/>
**play notes with letters in different octaves**
<MiniRepl
hideHeader
client:visible
tune={`note("c2 e3 g4 b5").sound("piano")`}
claviature
claviatureLabels={Object.fromEntries(['c1', 'c2', 'c3', 'c4', 'c5'].map((n) => [n, n]))}
claviatureLabels={Object.fromEntries(
Array(49)
.fill()
.map((_, i) => [midi2note(i + 36), midi2note(i + 36)]),
)}
/>
<Box>
Try out different octaves (1-8)
</Box>
If you are not comfortable with the note letter system, it should be easier to use numbers instead.
Most of the examples below will use numbers for that reason.
We will also look at ways to make it easier to play the right notes later.
## changing the sound
Just like with unpitched sounds, we can change the sound of our notes with `sound`:
<MiniRepl hideHeader client:visible tune={`note("36 43, 52 59 62 64").sound("piano")`} />
{/* c2 g2, e3 b3 d4 e4 */}
<Box>
Try out different sounds:
- gm_electric_guitar_muted
- gm_acoustic_bass
- gm_voice_oohs
- gm_blown_bottle
- sawtooth
- square
- triangle
- how about bd, sd or hh?
- remove `.sound('...')` completely
</Box>
**switch between sounds**
<MiniRepl
hideHeader
client:visible
tune={`note("48 67 63 [62, 58]")
.sound("piano gm_electric_guitar_muted")`}
/>
**stack multiple sounds**
<MiniRepl
hideHeader
client:visible
tune={`note("48 67 63 [62, 58]")
.sound("piano, gm_electric_guitar_muted")`}
/>
<Box>
The `note` and `sound` patterns are combined!
We will see more ways to combine patterns later..
</Box>
## Longer Sequences
**Divide sequences with `/` to slow them down**
{/* [c2 bb1 f2 eb2] */}
<MiniRepl hideHeader client:visible tune={`note("[36 34 41 39]/4").sound("gm_acoustic_bass")`} punchcard />
<Box>
The `/4` plays the sequence in brackets over 4 cycles (=4s).
So each of the 4 notes is 1s long.
Try adding more notes inside the brackets and notice how it gets faster.
</Box>
Because it is so common to just play one thing per cycle, you can..
**Play one per cycle with \< \>**
<MiniRepl hideHeader client:visible tune={`note("<36 34 41 39>").sound("gm_acoustic_bass")`} punchcard />
<Box>
Try adding more notes inside the brackets and notice how it does **not** get faster.
</Box>
**Play one sequence per cycle**
{/* <[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2 */}
<MiniRepl
hideHeader
client:visible
tune={`note("<[36 48]*4 [34 46]*4 [41 53]*4 [39 51]*4>/2")
.sound("gm_acoustic_bass")`}
punchcard
/>
**Alternate between multiple things**
<MiniRepl
hideHeader
client:visible
tune={`note("60 <63 62 65 63>")
.sound("gm_xylophone")`}
punchcard
/>
This is also useful for unpitched sounds:
<MiniRepl
hideHeader
client:visible
tune={`sound("bd*2, ~ <sd cp>, [~ hh]*2")
.bank("RolandTR909")`}
punchcard
/>
## Scales
Finding the right notes can be difficult.. Scales are here to help:
<MiniRepl
hideHeader
client:visible
tune={`n("0 2 4 <[6,8] [7,9]>")
.scale("C:minor").sound("piano")`}
punchcard
/>
<Box>
Try out different numbers. Any number should sound good!
Try out different scales:
- C:major
- A2:minor
- D:dorian
- G:mixolydian
- A2:minor:pentatonic
- F:major:pentatonic
</Box>
**automate scales**
Just like anything, we can automate the scale with a pattern:
<MiniRepl
hideHeader
client:visible
tune={`n("<0 -3>, 2 4 <[6,8] [7,9]>")
.scale("<C:major D:mixolydian>/4")
.sound("piano")`}
punchcard
/>
<Box>
If you have no idea what these scale mean, don't worry.
These are just labels for different sets of notes that go well together.
Take your time and you'll find scales you like!
</Box>
## Repeat & Elongate
**Elongate with @**
<MiniRepl hideHeader client:visible tune={`note("c@3 eb").sound("gm_acoustic_bass")`} punchcard />
<Box>
Not using `@` is like using `@1`. In the above example, c is 3 units long and eb is 1 unit long.
Try changing that number!
</Box>
**Elongate within sub-sequences**
<MiniRepl
hideHeader
client:visible
tune={`n("<[4@2 4] [5@2 5] [6@2 6] [5@2 5]>*2")
.scale("<C2:mixolydian F2:mixolydian>/4")
.sound("gm_acoustic_bass")`}
punchcard
/>
<Box>
This groove is called a `shuffle`.
Each beat has two notes, where the first is twice as long as the second.
This is also sometimes called triplet swing. You'll often find it in blues and jazz.
</Box>
**Replicate**
<MiniRepl hideHeader client:visible tune={`note("c!2 [eb,<g a bb a>]").sound("piano")`} punchcard />
<Box>
Try switching between `!`, `*` and `@`
What's the difference?
</Box>
## Recap
Let's recap what we've learned in this chapter:
| Concept | Syntax | Example |
| --------- | ------ | ------------------------------------------------------------------- |
| Slow down | \/ | <MiniRepl hideHeader client:visible tune={`note("[c a f e]/2")`} /> |
| Alternate | \<\> | <MiniRepl hideHeader client:visible tune={`note("c <e g>")`} /> |
| Elongate | @ | <MiniRepl hideHeader client:visible tune={`note("c@3 e")`} /> |
| Replicate | ! | <MiniRepl hideHeader client:visible tune={`note("c!3 e")`} /> |
New functions:
| Name | Description | Example |
| ----- | ----------------------------------- | -------------------------------------------------------------------------------------------- |
| note | set pitch as number or letter | <MiniRepl hideHeader client:visible tune={`note("b g e c").sound("piano")`} /> |
| scale | interpret `n` as scale degree | <MiniRepl hideHeader client:visible tune={`n("6 4 2 0").scale("C:minor").sound("piano")`} /> |
| stack | play patterns in parallel (read on) | <MiniRepl hideHeader client:visible tune={`stack(s("bd sd"),note("c eb g"))`} /> |
## Examples
**Classy Bassline**
<MiniRepl
hideHeader
client:visible
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
.sound("gm_synth_bass_1")
.lpf(800) // <-- we'll learn about this soon`}
/>
**Classy Melody**
<MiniRepl
hideHeader
client:visible
tune={`n(\`<
[~ 0] 2 [0 2] [~ 2]
[~ 0] 1 [0 1] [~ 1]
[~ 0] 3 [0 3] [~ 3]
[~ 0] 2 [0 2] [~ 2]
>*2\`).scale("C4:minor")
.sound("gm_synth_strings_1")`}
/>
**Classy Drums**
<MiniRepl
hideHeader
client:visible
tune={`sound("bd*2, ~ <sd cp>, [~ hh]*2")
.bank("RolandTR909")`}
/>
**If there just was a way to play all the above at the same time.......**
<Box>
It's called `stack` 😙
</Box>
<MiniRepl
hideHeader
client:visible
tune={`stack(
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2")
.sound("gm_synth_bass_1").lpf(800),
n(\`<
[~ 0] 2 [0 2] [~ 2]
[~ 0] 1 [0 1] [~ 1]
[~ 0] 3 [0 3] [~ 3]
[~ 0] 2 [0 2] [~ 2]
>*2\`).scale("C4:minor")
.sound("gm_synth_strings_1"),
sound("bd*2, ~ <sd cp>, [~ hh]*2")
.bank("RolandTR909")
)`}
/>
This is starting to sound like actual music! We have sounds, we have notes, now the last piece of the puzzle is missing: [effects](/workshop/first-effects)

View File

@ -0,0 +1,330 @@
---
title: First Sounds
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '@src/docs/MiniRepl';
import Box from '@components/Box.astro';
import QA from '@components/QA';
# First Sounds
This is the first chapter of the Strudel Workshop, nice to have you on board!
## Code Fields
The workshop is full of interactive code fields. Let's learn how to use those. Here is one:
<MiniRepl hideHeader client:visible tune={`sound("casio")`} dirt />
<Box>
1. ⬆️ click into the text field above ⬆️
2. press `ctrl`+`enter` to play
3. change `casio` to `metal`
4. press `ctrl`+`enter` to update
5. press `ctrl`+`.` to stop
</Box>
Congratulations, you are now live coding!
## Sounds
We have just played a sound with `sound` like this:
<MiniRepl hideHeader client:visible tune={`sound("casio")`} hideHeader />
<Box>
`casio` is one of many standard sounds.
Try out a few other sounds:
```
insect wind jazz metal east crow casio space numbers
```
You might hear a little pause while the sound is loading
</Box>
**Change Sample Number with :**
One Sound can contain multiple samples (audio files).
You can select the sample by appending `:` followed by a number to the name:
<MiniRepl hideHeader client:visible tune={`sound("casio:1")`} hideHeader />
<Box>
Try different sound / sample number combinations.
Not adding a number is like doing `:0`
</Box>
Now you know how to use different sounds.
For now we'll stick to this little selection of sounds, but we'll find out how to load your own sounds later.
## Drum Sounds
By default, Strudel comes with a wide selection of drum sounds:
<MiniRepl hideHeader client:visible tune={`sound("bd hh sd oh")`} hideHeader />
<Box>
These letter combinations stand for different parts of a drum set:
- `bd` = **b**ass **d**rum
- `sd` = **s**nare **d**rum
- `sd` = **sd**are
- `rim` = **rim**shot
- `hh` = **h**i**h**at
- `oh` = **o**pen **h**ihat
Try out different drum sounds!
</Box>
To change the sound character of our drums, we can use `bank` to change the drum machine:
<MiniRepl hideHeader client:visible tune={`sound("bd hh sd oh").bank("RolandTR909")`} hideHeader />
In this example `RolandTR909` is the name of the drum machine that we're using.
It is a famous drum machine for house and techno beats.
<Box>
Try changing `RolandTR909` to one of
- `AkaiLinn`
- `RhythmAce`
- `RolandTR808`
- `RolandTR707`
- `ViscoSpaceDrum`
There are a lot more, but let's keep it simple for now
🦥 Pro-Tip: Mark a name via double click. Then just copy and paste!
</Box>
## Sequences
In the last example, we already saw that you can play multiple sounds in a sequence by separating them with a space:
<MiniRepl hideHeader client:visible tune={`sound("bd hh sd hh")`} punchcard />
Notice how the currently playing sound is highlighted in the code and also visualized below.
<Box>
Try adding more sounds to the sequence!
</Box>
**The longer the sequence, the faster it runs**
<MiniRepl hideHeader client:visible tune={`sound("bd bd hh bd rim bd hh bd")`} punchcard />
The content of a sequence will be squished into what's called a cycle.
**One way to change the tempo is using `cpm`**
<MiniRepl hideHeader client:visible tune={`sound("bd bd hh bd rim bd hh bd").cpm(40)`} punchcard />
<Box>
cpm = cycles per minute
By default, the tempo is 60 cycles per minute = 1 cycle per second.
</Box>
We will look at other ways to change the tempo later!
**Add a rests in a sequence with '~'**
<MiniRepl hideHeader client:visible tune={`sound("bd hh ~ rim")`} punchcard />
**Sub-Sequences with [brackets]**
<MiniRepl hideHeader client:visible tune={`sound("bd [hh hh] sd [hh bd]")`} punchcard />
<Box>
Try adding more sounds inside a bracket!
</Box>
Similar to the whole sequence, the content of a sub-sequence will be squished to the its own length.
**Multiplication: Speed things up**
<MiniRepl hideHeader client:visible tune={`sound("bd hh*2 rim hh*3")`} punchcard />
**Multiplication: Speed up sequences**
<MiniRepl hideHeader client:visible tune={`sound("bd [hh rim]*2")`} punchcard />
**Multiplication: Speeeeeeeeed things up**
<MiniRepl hideHeader client:visible tune={`sound("bd hh*16 rim hh*8")`} punchcard />
<Box>
Pitch = really fast rhythm
</Box>
**Sub-Sub-Sequences with [[brackets]]**
<MiniRepl hideHeader client:visible tune={`sound("bd [[rim rim] hh]")`} punchcard />
<Box>
You can go as deep as you want!
</Box>
**Play sequences in parallel with comma**
<MiniRepl hideHeader client:visible tune={`sound("hh hh hh, bd casio")`} punchcard />
You can use as many commas as you want:
<MiniRepl hideHeader client:visible tune={`sound("hh hh hh, bd bd, ~ casio")`} punchcard />
Commas can also be used inside sub-sequences:
<MiniRepl hideHeader client:visible tune={`sound("hh hh hh, bd [bd,casio]")`} punchcard />
<Box>
Notice how the 2 above are the same?
It is quite common that there are many ways to express the same idea.
</Box>
**Multiple Lines with backticks**
<MiniRepl
hideHeader
client:visible
tune={`sound(\`bd*2, ~ cp,
~ ~ ~ oh, hh*4,
[~ casio]*2\`)`}
punchcard
/>
**selecting sample numbers separately**
Instead of using ":", we can also use the `n` function to select sample numbers:
<MiniRepl hideHeader client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
This is shorter and more readable than:
<MiniRepl hideHeader client:visible tune={`sound("jazz:0 jazz:1 [jazz:4 jazz:2] jazz:3*2")`} punchcard />
## Recap
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
This is what we've leared so far:
| Concept | Syntax | Example |
| ----------------- | -------- | -------------------------------------------------------------------------------- |
| Sequence | space | <MiniRepl hideHeader client:visible tune={`sound("bd bd sd hh")`} /> |
| Sample Number | :x | <MiniRepl hideHeader client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Rests | ~ | <MiniRepl hideHeader client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Sub-Sequences | \[\] | <MiniRepl hideHeader client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | <MiniRepl hideHeader client:visible tune={`sound("bd [metal [jazz sd]]")`} /> |
| Speed up | \* | <MiniRepl hideHeader client:visible tune={`sound("bd sd*2 cp*3")`} /> |
| Parallel | , | <MiniRepl hideHeader client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> |
The Mini-Notation is usually used inside some function. These are the functions we've seen so far:
| Name | Description | Example |
| ----- | ----------------------------------- | ---------------------------------------------------------------------------------- |
| sound | plays the sound of the given name | <MiniRepl hideHeader client:visible tune={`sound("bd sd")`} /> |
| bank | selects the sound bank | <MiniRepl hideHeader client:visible tune={`sound("bd sd").bank("RolandTR909")`} /> |
| cpm | sets the tempo in cycles per minute | <MiniRepl hideHeader client:visible tune={`sound("bd sd").cpm(90)`} /> |
| n | select sample number | <MiniRepl hideHeader client:visible tune={`n("0 1 4 2").sound("jazz")`} /> |
## Examples
**Basic rock beat**
<MiniRepl hideHeader client:visible tune={`sound("bd sd, hh*4").bank("RolandTR505").cpm(100/2)`} punchcard />
**Classic house**
<MiniRepl hideHeader client:visible tune={`sound("bd*2, ~ cp, [~ hh]*2").bank("RolandTR909")`} punchcard />
<Box>
Notice that the two patterns are extremely similar.
Certain drum patterns are reused across genres.
</Box>
We Will Rock you
<MiniRepl hideHeader client:visible tune={`sound("bd*2 cp").bank("RolandTR707").cpm(81/2)`} punchcard />
**Yellow Magic Orchestra - Firecracker**
<MiniRepl
hideHeader
client:visible
tune={`sound("bd sd, ~ ~ ~ hh ~ hh ~ ~, ~ perc ~ perc:1*2")
.bank("RolandCompurhythm1000")`}
punchcard
/>
**Imitation of a 16 step sequencer**
<MiniRepl
hideHeader
client:visible
tune={`sound(\`
[~ ~ oh ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ],
[hh hh ~ ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ],
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [cp ~ ~ ~ ],
[bd ~ ~ ~ ] [~ ~ ~ bd] [~ ~ bd ~ ] [~ ~ ~ bd]
\`).cpm(90/4)`}
punchcard
/>
**Another one**
<MiniRepl
hideHeader
client:visible
tune={`sound(\`
[~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ oh:1 ~ ],
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh ~ ~ ],
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [~ cp ~ ~ ],
[bd bd ~ ~ ] [~ ~ bd ~ ] [bd bd ~ bd ] [~ ~ ~ ~ ]
\`).bank("RolandTR808").cpm(88/4)`}
punchcard
/>
**Not your average drums**
<MiniRepl
hideHeader
client:visible
tune={`s(\`jazz*2,
insect [crow metal] ~ ~,
~ space:4 ~ space:1,
~ wind\`)
.cpm(100/2)`}
punchcard
/>
Now that we know the basics of how to make beats, let's look at how we can play [notes](/workshop/first-notes)

View File

@ -0,0 +1,70 @@
---
title: Getting Started
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
# Welcome
<img src="/icons/strudel_icon.png" className="w-32 animate-pulse md:float-right ml-8" />
Welcome to the Strudel documentation pages!
You've come to the right place if you want to learn how to make music with code.
## What is Strudel?
With Strudel, you can expressively write dynamic music pieces.<br/>
It is an official port of the [Tidal Cycles](https://tidalcycles.org/) pattern language to JavaScript.<br/>
You don't need to know JavaScript or Tidal Cycles to make music with Strudel.
This interactive tutorial will guide you through the basics of Strudel.<br/>
The best place to actually make music with Strudel is the [Strudel REPL](https://strudel.tidalcycles.org/)
<div className="clear-both" />
## What can you do with Strudel?
- live code music: make music with code in real time
- algorithmic composition: compose music using tidal's unique approach to pattern manipulation
- teaching: focussing on a low barrier of entry, Strudel is a good fit for teaching music and code at the same time.
- integrate into your existing music setup: either via MIDI or OSC, you can use Strudel as a really flexible sequencer
## Example
Here is an example of how strudel can sound:
<MiniRepl
client:idle
tune={`stack(
// drums
s("bd,[~ <sd!3 sd(3,4,2)>],hh*8")
.speed(perlin.range(.8,.9)), // random sample speed variation
// bassline
"<a1 b1\*2 a1(3,8) e2>"
.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
.note() // wrap in "note"
.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
// chords
"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand')
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.note() // wrap in "note"
.s('sawtooth') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
.attack(1) // slowly fade in
)
.slow(3/2)`}
/>
To hear more, go to the [Strudel REPL](https://strudel.tidalcycles.org/) and press shuffle to hear a random example pattern.
## Getting Started
The best way to start learning Strudel is the workshop.
If you're ready to dive in, let's start with your [first sounds](/workshop/first-sounds)

View File

@ -0,0 +1,3 @@
<script is:inline>
window.location.pathname = `/workshop/intro`;
</script>

View File

@ -0,0 +1,181 @@
---
title: Pattern Effects
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '@src/docs/MiniRepl';
import Box from '@components/Box.astro';
import QA from '@components/QA';
# Pattern Effects
Up until now, most of the functions we've seen are what other music programs are typically capable of: sequencing sounds, playing notes, controlling effects.
In this chapter, we are going to look at functions that are more unique to tidal.
**reverse patterns with rev**
<MiniRepl hideHeader client:visible tune={`n("0 1 [4 3] 2").sound("jazz").rev()`} />
**play pattern left and modify it right with jux**
<MiniRepl hideHeader client:visible tune={`n("0 1 [4 3] 2").sound("jazz").jux(rev)`} />
This is the same as:
<MiniRepl
hideHeader
client:visible
tune={`stack(
n("0 1 [4 3] 2").sound("jazz").pan(0),
n("0 1 [4 3] 2").sound("jazz").pan(1).rev()
)`}
/>
Let's visualize what happens here:
<MiniRepl
hideHeader
client:visible
tune={`stack(
n("0 1 [4 3] 2").sound("jazz").pan(0).color("cyan"),
n("0 1 [4 3] 2").sound("jazz").pan(1).color("magenta").rev()
)`}
punchcard
/>
<Box>
Try commenting out one of the two by adding `//` before a line
</Box>
**multiple tempos**
<MiniRepl hideHeader client:visible tune={`note("c2, eb3 g3 [bb3 c4]").sound("piano").slow("1,2,3")`} />
This is like doing
<MiniRepl
hideHeader
client:visible
tune={`stack(
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(1).color('cyan'),
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(2).color('magenta'),
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(3).color('yellow')
)`}
punchcard
/>
<Box>
Try commenting out one or more by adding `//` before a line
</Box>
**add**
<MiniRepl
hideHeader
client:visible
tune={`note("c2 [eb3,g3]".add("<0 <1 -1>>"))
.color("<cyan <magenta yellow>>").adsr("[.1 0]:.2:[1 0]")
.sound("gm_acoustic_bass").room(.5)`}
punchcard
/>
<Box>
If you add a number to a note, the note will be treated as if it was a number
</Box>
We can add as often as we like:
<MiniRepl
hideHeader
client:visible
tune={`note("c2 [eb3,g3]".add("<0 <1 -1>>").add("0,7"))
.color("<cyan <magenta yellow>>").adsr("[.1 0]:.2:[1 0]")
.sound("gm_acoustic_bass").room(.5)`}
punchcard
/>
**add with scale**
<MiniRepl
hideHeader
client:visible
tune={`n("<0 [2 4] <3 5> [~ <4 1>]>*2".add("<0 [0,2,4]>/4"))
.scale("C5:minor").release(.5)
.sound("gm_xylophone").room(.5)`}
punchcard
/>
**time to stack**
<MiniRepl
hideHeader
client:visible
tune={`stack(
n("<0 [2 4] <3 5> [~ <4 1>]>*2".add("<0 [0,2,4]>/4"))
.scale("C5:minor")
.sound("gm_xylophone")
.room(.4).delay(.125),
note("c2 [eb3,g3]".add("<0 <1 -1>>"))
.adsr("[.1 0]:.2:[1 0]")
.sound("gm_acoustic_bass")
.room(.5),
n("0 1 [2 3] 2").sound("jazz").jux(rev).slow(2)
)`}
/>
**ply**
<MiniRepl hideHeader client:visible tune={`sound("hh, bd rim").bank("RolandTR707").ply(2)`} punchcard />
this is like writing:
<MiniRepl hideHeader client:visible tune={`sound("hh*2, bd*2 rim*2").bank("RolandTR707")`} punchcard />
<Box>
Try patterning the `ply` function, for example using `"<1 2 1 3>"`
</Box>
**off**
<MiniRepl
hideHeader
client:visible
tune={`n("<0 [4 <3 2>] <2 3> [~ 1]>"
.off(1/8, x=>x.add(4))
//.off(1/4, x=>x.add(7))
).scale("<C5:minor Db5:mixolydian>/4")
.s("triangle").room(.5).ds(".1:0").delay(.5)`}
punchcard
/>
<Box>
In the notation `x=>x.`, the `x` is the shifted pattern, which where modifying.
</Box>
off is also useful for sounds:
<MiniRepl
hideHeader
client:visible
tune={`s("bd sd,[~ hh]*2").bank("CasioRZ1")
.off(1/8, x=>x.speed(1.5).gain(.25))`}
/>
| name | description | example |
| ---- | ------------------------------ | ---------------------------------------------------------------------------------------------- |
| rev | reverse | <MiniRepl hideHeader client:visible tune={`n("0 2 4 6").scale("C:minor").rev()`} /> |
| jux | split left/right, modify right | <MiniRepl hideHeader client:visible tune={`n("0 2 4 6").scale("C:minor").jux(rev)`} /> |
| add | add numbers / notes | <MiniRepl hideHeader client:visible tune={`n("0 2 4 6".add("<0 1 2 1>")).scale("C:minor")`} /> |
| ply | speed up each event n times | <MiniRepl hideHeader client:visible tune={`s("bd sd").ply("<1 2 3>")`} /> |
| off | copy, shift time & modify | <MiniRepl hideHeader client:visible tune={`s("bd sd, hh*4").off(1/8, x=>x.speed(2))`} /> |

View File

@ -0,0 +1,68 @@
---
title: Recap
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
# Workshop Recap
This page is just a listing of all functions covered in the workshop!
## Mini Notation
| Concept | Syntax | Example |
| ----------------- | -------- | -------------------------------------------------------------------------------- |
| Sequence | space | <MiniRepl hideHeader client:visible tune={`sound("bd bd sn hh")`} /> |
| Sample Number | :x | <MiniRepl hideHeader client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Rests | ~ | <MiniRepl hideHeader client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Sub-Sequences | \[\] | <MiniRepl hideHeader client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | <MiniRepl hideHeader client:visible tune={`sound("bd [metal [jazz sn]]")`} /> |
| Speed up | \* | <MiniRepl hideHeader client:visible tune={`sound("bd sn*2 cp*3")`} /> |
| Parallel | , | <MiniRepl hideHeader client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> |
| Slow down | \/ | <MiniRepl hideHeader client:visible tune={`note("[c a f e]/2")`} /> |
| Alternate | \<\> | <MiniRepl hideHeader client:visible tune={`note("c <e g>")`} /> |
| Elongate | @ | <MiniRepl hideHeader client:visible tune={`note("c@3 e")`} /> |
| Replicate | ! | <MiniRepl hideHeader client:visible tune={`note("c!3 e")`} /> |
## Sounds
| Name | Description | Example |
| ----- | --------------------------------- | ---------------------------------------------------------------------------------- |
| sound | plays the sound of the given name | <MiniRepl hideHeader client:visible tune={`sound("bd sd")`} /> |
| bank | selects the sound bank | <MiniRepl hideHeader client:visible tune={`sound("bd sd").bank("RolandTR909")`} /> |
| n | select sample number | <MiniRepl hideHeader client:visible tune={`n("0 1 4 2").sound("jazz")`} /> |
## Notes
| Name | Description | Example |
| --------- | ----------------------------- | -------------------------------------------------------------------------------------------- |
| note | set pitch as number or letter | <MiniRepl hideHeader client:visible tune={`note("b g e c").sound("piano")`} /> |
| n + scale | set note in scale | <MiniRepl hideHeader client:visible tune={`n("6 4 2 0").scale("C:minor").sound("piano")`} /> |
| stack | play patterns in parallel | <MiniRepl hideHeader client:visible tune={`stack(s("bd sd"),note("c eb g"))`} /> |
## Audio Effects
| name | example |
| ----- | -------------------------------------------------------------------------------------------------- |
| lpf | <MiniRepl hideHeader client:visible tune={`note("c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
| vowel | <MiniRepl hideHeader client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | <MiniRepl hideHeader client:visible tune={`s("hh*8").gain("[.25 1]*2")`} /> |
| delay | <MiniRepl hideHeader client:visible tune={`s("bd rim").delay(.5)`} /> |
| room | <MiniRepl hideHeader client:visible tune={`s("bd rim").room(.5)`} /> |
| pan | <MiniRepl hideHeader client:visible tune={`s("bd rim").pan("0 1")`} /> |
| speed | <MiniRepl hideHeader client:visible tune={`s("bd rim").speed("<1 2 -1 -2>")`} /> |
| range | <MiniRepl hideHeader client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
## Pattern Effects
| name | description | example |
| ---- | ----------------------------------- | ---------------------------------------------------------------------------------------------- |
| cpm | sets the tempo in cycles per minute | <MiniRepl hideHeader client:visible tune={`sound("bd sd").cpm(90)`} /> |
| fast | speed up | <MiniRepl hideHeader client:visible tune={`sound("bd sd").fast(2)`} /> |
| slow | slow down | <MiniRepl hideHeader client:visible tune={`sound("bd sd").slow(2)`} /> |
| rev | reverse | <MiniRepl hideHeader client:visible tune={`n("0 2 4 6").scale("C:minor").rev()`} /> |
| jux | split left/right, modify right | <MiniRepl hideHeader client:visible tune={`n("0 2 4 6").scale("C:minor").jux(rev)`} /> |
| add | add numbers / notes | <MiniRepl hideHeader client:visible tune={`n("0 2 4 6".add("<0 1 2 1>")).scale("C:minor")`} /> |
| ply | speed up each event n times | <MiniRepl hideHeader client:visible tune={`s("bd sd").ply("<1 2 3>")`} /> |
| off | copy, shift time & modify | <MiniRepl hideHeader client:visible tune={`s("bd sd, hh*4").off(1/8, x=>x.speed(2))`} /> |

View File

@ -273,6 +273,15 @@ function SoundsTab() {
);
}
function Checkbox({ label, value, onChange }) {
return (
<label>
<input type="checkbox" checked={value} onChange={onChange} />
{' ' + label}
</label>
);
}
function ButtonGroup({ value, onChange, items }) {
return (
<div className="flex max-w-lg">
@ -355,7 +364,7 @@ const fontFamilyOptions = {
};
function SettingsTab({ scheduler }) {
const { theme, keybindings, fontSize, fontFamily } = useSettings();
const { theme, keybindings, isLineNumbersDisplayed, fontSize, fontFamily } = useSettings();
return (
<div className="text-foreground p-4 space-y-4">
{/* <FormItem label="Tempo">
@ -397,13 +406,20 @@ function SettingsTab({ scheduler }) {
/>
</FormItem>
</div>
<FormItem label="Keybindings">
<ButtonGroup
value={keybindings}
onChange={(keybindings) => settingsMap.setKey('keybindings', keybindings)}
items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs' }}
></ButtonGroup>
</FormItem>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormItem label="Keybindings">
<ButtonGroup
value={keybindings}
onChange={(keybindings) => settingsMap.setKey('keybindings', keybindings)}
items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs' }}
></ButtonGroup>
</FormItem>
<Checkbox
label="Display line numbers"
onChange={(cbEvent) => settingsMap.setKey('isLineNumbersDisplayed', cbEvent.target.checked)}
value={isLineNumbersDisplayed}
/>
</div>
<FormItem label="Reset Settings">
<button
className="bg-background p-2 max-w-[300px] rounded-md hover:opacity-50"

View File

@ -122,7 +122,7 @@ export function Header({ context }) {
{!isEmbedded && (
<a
title="learn"
href="./learn/getting-started"
href="./workshop/getting-started"
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
>
<AcademicCapIcon className="w-6 h-6" />

View File

@ -1,3 +1,15 @@
:root {
--background: #222;
--lineBackground: #22222299;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
--selectionMatch: #036dd626;
--lineHighlight: #00000050;
--gutterBackground: transparent;
--gutterForeground: #8a919966;
}
.darken::before {
content: ' ';
position: fixed;
@ -10,15 +22,26 @@
opacity: 0.5;
}
#code .cm-content {
padding-top: 12px !important;
padding-left: 8px !important;
}
#code .cm-scroller {
padding-top: 10px !important;
padding-bottom: 50vh;
font-family: inherit;
}
#code .cm-line > * {
background: var(--lineBackground);
}
#code .cm-editor {
background-color: transparent !important;
height: 100%;
z-index: 11;
}
#code .cm-theme {
width: 100%;
height: 100%;
}
#code .cm-theme-light {
width: 100%;
}

View File

@ -105,7 +105,7 @@ export function Repl({ embedded = false }) {
const [lastShared, setLastShared] = useState();
const [pending, setPending] = useState(true);
const { theme, keybindings, fontSize, fontFamily } = useSettings();
const { theme, keybindings, fontSize, fontFamily, isLineNumbersDisplayed } = useSettings();
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
useStrudel({
@ -157,7 +157,7 @@ export function Repl({ embedded = false }) {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.key === '.') {
} else if (e.key === '.' || e.code === 'Period') {
stop();
e.preventDefault();
}
@ -271,6 +271,7 @@ export function Repl({ embedded = false }) {
theme={themes[theme] || themes.strudelTheme}
value={code}
keybindings={keybindings}
isLineNumbersDisplayed={isLineNumbersDisplayed}
fontSize={fontSize}
fontFamily={fontFamily}
onChange={handleChangeCode}

View File

@ -22,8 +22,96 @@ export async function prebake() {
tag: 'drum-machines',
}),
samples(`./EmuSP12.json`, `./EmuSP12/`, { prebake: true, tag: 'drum-machines' }),
// samples('github:tidalcycles/Dirt-Samples/master'),
samples(
{
casio: ['casio/high.wav', 'casio/low.wav', 'casio/noise.wav'],
crow: ['crow/000_crow.wav', 'crow/001_crow2.wav', 'crow/002_crow3.wav', 'crow/003_crow4.wav'],
insect: [
'insect/000_everglades_conehead.wav',
'insect/001_robust_shieldback.wav',
'insect/002_seashore_meadow_katydid.wav',
],
wind: [
'wind/000_wind1.wav',
'wind/001_wind10.wav',
'wind/002_wind2.wav',
'wind/003_wind3.wav',
'wind/004_wind4.wav',
'wind/005_wind5.wav',
'wind/006_wind6.wav',
'wind/007_wind7.wav',
'wind/008_wind8.wav',
'wind/009_wind9.wav',
],
jazz: [
'jazz/000_BD.wav',
'jazz/001_CB.wav',
'jazz/002_FX.wav',
'jazz/003_HH.wav',
'jazz/004_OH.wav',
'jazz/005_P1.wav',
'jazz/006_P2.wav',
'jazz/007_SN.wav',
],
metal: [
'metal/000_0.wav',
'metal/001_1.wav',
'metal/002_2.wav',
'metal/003_3.wav',
'metal/004_4.wav',
'metal/005_5.wav',
'metal/006_6.wav',
'metal/007_7.wav',
'metal/008_8.wav',
'metal/009_9.wav',
],
east: [
'east/000_nipon_wood_block.wav',
'east/001_ohkawa_mute.wav',
'east/002_ohkawa_open.wav',
'east/003_shime_hi.wav',
'east/004_shime_hi_2.wav',
'east/005_shime_mute.wav',
'east/006_taiko_1.wav',
'east/007_taiko_2.wav',
'east/008_taiko_3.wav',
],
space: [
'space/000_0.wav',
'space/001_1.wav',
'space/002_11.wav',
'space/003_12.wav',
'space/004_13.wav',
'space/005_14.wav',
'space/006_15.wav',
'space/007_16.wav',
'space/008_17.wav',
'space/009_18.wav',
'space/010_2.wav',
'space/011_3.wav',
'space/012_4.wav',
'space/013_5.wav',
'space/014_6.wav',
'space/015_7.wav',
'space/016_8.wav',
'space/017_9.wav',
],
numbers: [
'numbers/0.wav',
'numbers/1.wav',
'numbers/2.wav',
'numbers/3.wav',
'numbers/4.wav',
'numbers/5.wav',
'numbers/6.wav',
'numbers/7.wav',
'numbers/8.wav',
],
},
'github:tidalcycles/Dirt-Samples/master/',
),
]);
// await samples('github:tidalcycles/Dirt-Samples/master');
}
const maxPan = noteToMidi('C8');

View File

@ -80,7 +80,7 @@ export const themes = {
export const settings = {
strudelTheme: {
background: '#222',
lineBackground: '#22222250',
lineBackground: '#22222299',
foreground: '#fff',
// foreground: '#75baff',
caret: '#ffcc00',
@ -99,7 +99,7 @@ export const settings = {
terminal: terminalSettings,
abcdef: {
background: '#0f0f0f',
lineBackground: '#0f0f0f50',
lineBackground: '#0f0f0f99',
foreground: '#defdef',
caret: '#00FF00',
selection: '#515151',
@ -110,7 +110,7 @@ export const settings = {
},
androidstudio: {
background: '#282b2e',
lineBackground: '#282b2e50',
lineBackground: '#282b2e99',
foreground: '#a9b7c6',
caret: '#00FF00',
selection: '#343739',
@ -119,7 +119,7 @@ export const settings = {
},
atomone: {
background: '#272C35',
lineBackground: '#272C3550',
lineBackground: '#272C3599',
foreground: '#9d9b97',
caret: '#797977',
selection: '#ffffff30',
@ -131,7 +131,7 @@ export const settings = {
},
aura: {
background: '#21202e',
lineBackground: '#21202e50',
lineBackground: '#21202e99',
foreground: '#edecee',
caret: '#a277ff',
selection: '#3d375e7f',
@ -144,7 +144,7 @@ export const settings = {
bbedit: {
light: true,
background: '#FFFFFF',
lineBackground: '#FFFFFF50',
lineBackground: '#FFFFFF99',
foreground: '#000000',
caret: '#FBAC52',
selection: '#FFD420',
@ -156,7 +156,7 @@ export const settings = {
},
bespin: {
background: '#28211c',
lineBackground: '#28211c50',
lineBackground: '#28211c99',
foreground: '#9d9b97',
caret: '#797977',
selection: '#36312e',
@ -167,7 +167,7 @@ export const settings = {
},
darcula: {
background: '#2B2B2B',
lineBackground: '#2B2B2B50',
lineBackground: '#2B2B2B99',
foreground: '#f8f8f2',
caret: '#FFFFFF',
selection: 'rgba(255, 255, 255, 0.1)',
@ -179,7 +179,7 @@ export const settings = {
},
dracula: {
background: '#282a36',
lineBackground: '#282a3650',
lineBackground: '#282a3699',
foreground: '#f8f8f2',
caret: '#f8f8f0',
selection: 'rgba(255, 255, 255, 0.1)',
@ -192,7 +192,7 @@ export const settings = {
duotoneLight: {
light: true,
background: '#faf8f5',
lineBackground: '#faf8f550',
lineBackground: '#faf8f599',
foreground: '#b29762',
caret: '#93abdc',
selection: '#e3dcce',
@ -204,7 +204,7 @@ export const settings = {
},
duotoneDark: {
background: '#2a2734',
lineBackground: '#2a273450',
lineBackground: '#2a273499',
foreground: '#6c6783',
caret: '#ffad5c',
selection: 'rgba(255, 255, 255, 0.1)',
@ -215,7 +215,7 @@ export const settings = {
eclipse: {
light: true,
background: '#fff',
lineBackground: '#ffffff50',
lineBackground: '#ffffff99',
foreground: '#000',
caret: '#FFFFFF',
selection: '#d7d4f0',
@ -228,7 +228,7 @@ export const settings = {
githubLight: {
light: true,
background: '#fff',
lineBackground: '#ffffff50',
lineBackground: '#ffffff99',
foreground: '#24292e',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
@ -237,7 +237,7 @@ export const settings = {
},
githubDark: {
background: '#0d1117',
lineBackground: '#0d111750',
lineBackground: '#0d111799',
foreground: '#c9d1d9',
caret: '#c9d1d9',
selection: '#003d73',
@ -246,7 +246,7 @@ export const settings = {
},
gruvboxDark: {
background: '#282828',
lineBackground: '#28282850',
lineBackground: '#28282899',
foreground: '#ebdbb2',
caret: '#ebdbb2',
selection: '#bdae93',
@ -258,7 +258,7 @@ export const settings = {
gruvboxLight: {
light: true,
background: '#fbf1c7',
lineBackground: '#fbf1c750',
lineBackground: '#fbf1c799',
foreground: '#3c3836',
caret: '#af3a03',
selection: '#ebdbb2',
@ -270,7 +270,7 @@ export const settings = {
},
materialDark: {
background: '#2e3235',
lineBackground: '#2e323550',
lineBackground: '#2e323599',
foreground: '#bdbdbd',
caret: '#a0a4ae',
selection: '#d7d4f0',
@ -283,7 +283,7 @@ export const settings = {
materialLight: {
light: true,
background: '#FAFAFA',
lineBackground: '#FAFAFA50',
lineBackground: '#FAFAFA99',
foreground: '#90A4AE',
caret: '#272727',
selection: '#80CBC440',
@ -296,7 +296,7 @@ export const settings = {
noctisLilac: {
light: true,
background: '#f2f1f8',
lineBackground: '#f2f1f850',
lineBackground: '#f2f1f899',
foreground: '#0c006b',
caret: '#5c49e9',
selection: '#d5d1f2',
@ -307,7 +307,7 @@ export const settings = {
},
nord: {
background: '#2e3440',
lineBackground: '#2e344050',
lineBackground: '#2e344099',
foreground: '#FFFFFF',
caret: '#FFFFFF',
selection: '#3b4252',
@ -319,7 +319,7 @@ export const settings = {
},
okaidia: {
background: '#272822',
lineBackground: '#27282250',
lineBackground: '#27282299',
foreground: '#FFFFFF',
caret: '#FFFFFF',
selection: '#49483E',
@ -331,7 +331,7 @@ export const settings = {
solarizedLight: {
light: true,
background: '#fdf6e3',
lineBackground: '#fdf6e350',
lineBackground: '#fdf6e399',
foreground: '#657b83',
caret: '#586e75',
selection: '#dfd9c8',
@ -342,7 +342,7 @@ export const settings = {
},
solarizedDark: {
background: '#002b36',
lineBackground: '#002b3650',
lineBackground: '#002b3699',
foreground: '#93a1a1',
caret: '#839496',
selection: '#173541',
@ -353,7 +353,7 @@ export const settings = {
},
sublime: {
background: '#303841',
lineBackground: '#30384150',
lineBackground: '#30384199',
foreground: '#FFFFFF',
caret: '#FBAC52',
selection: '#4C5964',
@ -365,7 +365,7 @@ export const settings = {
tokyoNightDay: {
light: true,
background: '#e1e2e7',
lineBackground: '#e1e2e750',
lineBackground: '#e1e2e799',
foreground: '#3760bf',
caret: '#3760bf',
selection: '#99a7df',
@ -377,7 +377,7 @@ export const settings = {
},
tokyoNightStorm: {
background: '#24283b',
lineBackground: '#24283b50',
lineBackground: '#24283b99',
foreground: '#7982a9',
caret: '#c0caf5',
selection: '#6f7bb630',
@ -389,7 +389,7 @@ export const settings = {
},
tokyoNight: {
background: '#1a1b26',
lineBackground: '#1a1b2650',
lineBackground: '#1a1b2699',
foreground: '#787c99',
caret: '#c0caf5',
selection: '#515c7e40',
@ -401,7 +401,7 @@ export const settings = {
},
vscodeDark: {
background: '#1e1e1e',
lineBackground: '#1e1e1e50',
lineBackground: '#1e1e1e99',
foreground: '#9cdcfe',
caret: '#c6c6c6',
selection: '#6199ff2f',
@ -414,7 +414,7 @@ export const settings = {
xcodeLight: {
light: true,
background: '#fff',
lineBackground: '#ffffff50',
lineBackground: '#ffffff99',
foreground: '#3D3D3D',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
@ -424,7 +424,7 @@ export const settings = {
},
xcodeDark: {
background: '#292A30',
lineBackground: '#292A3050',
lineBackground: '#292A3099',
foreground: '#CECFD0',
caret: '#fff',
selection: '#727377',

View File

@ -121,8 +121,10 @@ stack(
.room(1)
//.pianoroll({fold:1})`;
export const caverave = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const caverave = `// "Caverave"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
const keys = x => x.s('sawtooth').cutoff(1200).gain(.5)
.attack(0).decay(.16).sustain(.3).release(.1);
@ -184,8 +186,10 @@ export const barryHarris = `// adapted from a Barry Harris excercise
.color('#00B8D4')
`;
export const blippyRhodes = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const blippyRhodes = `// "Blippy Rhodes"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
samples({
bd: 'samples/tidal/bd/BT0A0D0.wav',
sn: 'samples/tidal/sn/ST0T0S3.wav',
@ -226,8 +230,10 @@ stack(
).fast(3/2)
//.pianoroll({fold:1})`;
export const wavyKalimba = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const wavyKalimba = `// "Wavy kalimba"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
samples({
'kalimba': { c5:'https://freesound.org/data/previews/536/536549_11935698-lq.mp3' }
})
@ -256,8 +262,10 @@ stack(
.delay(.2)
`;
export const festivalOfFingers = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const festivalOfFingers = `// "Festival of fingers"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
const chords = "<Cm7 Fm7 G7 F#7>";
stack(
chords.voicings('lefthand').struct("x(3,8,-1)").velocity(.5).off(1/7,x=>x.transpose(12).velocity(.2)),
@ -271,8 +279,10 @@ stack(
.note().piano()`;
// iter, echo, echoWith
export const undergroundPlumber = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos, inspired by Friendship - Let's not talk about it (1979) :)
export const undergroundPlumber = `// "Underground plumber"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
// @details inspired by Friendship - Let's not talk about it (1979) :)
samples({ bd: 'bd/BT0A0D0.wav', sn: 'sn/ST0T0S3.wav', hh: 'hh/000_hh3closedhh.wav', cp: 'cp/HANDCLP0.wav',
}, 'https://loophole-letters.vercel.app/samples/tidal/')
@ -297,8 +307,10 @@ stack(
.fast(2/3)
.pianoroll({})`;
export const bridgeIsOver = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos, bassline by BDP - The Bridge Is Over
export const bridgeIsOver = `// "Bridge is over"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos, bassline by BDP - The Bridge Is Over
samples({mad:'https://freesound.org/data/previews/22/22274_109943-lq.mp3'})
stack(
stack(
@ -318,8 +330,10 @@ stack(
.pianoroll()
`;
export const goodTimes = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const goodTimes = `// "Good times"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
const scale = cat('C3 dorian','Bb2 major').slow(4);
stack(
"2*4".add(12).scale(scale)
@ -361,8 +375,10 @@ stack(
.pianoroll({maxMidi:100,minMidi:20})`;
*/
export const echoPiano = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const echoPiano = `// "Echo piano"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
"<0 2 [4 6](3,4,2) 3*2>"
.scale('D minor')
.color('salmon')
@ -408,8 +424,10 @@ stack(
.legato(.5)
).fast(2).note()`;
export const randomBells = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const randomBells = `// "Random bells"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
samples({
bell: { c6: 'https://freesound.org/data/previews/411/411089_5121236-lq.mp3' },
bass: { d2: 'https://freesound.org/data/previews/608/608286_13074022-lq.mp3' }
@ -431,8 +449,10 @@ stack(
.pianoroll({minMidi:20,maxMidi:120,background:'transparent'})
`;
export const waa2 = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const waa2 = `// "Waa2"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
n(
"a4 [a3 c3] a3 c3"
.sub("<7 12 5 12>".slow(2))
@ -447,8 +467,10 @@ n(
.room(.5)
`;
export const hyperpop = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const hyperpop = `// "Hyperpop"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
const lfo = cosine.slow(15);
const lfo2 = sine.slow(16);
const filter1 = x=>x.cutoff(lfo2.range(300,3000));
@ -498,8 +520,10 @@ stack(
).slow(2)
// strudel disable-highlighting`;
export const festivalOfFingers3 = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const festivalOfFingers3 = `// "Festival of fingers 3"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
"[-7*3],0,2,6,[8 7]"
.echoWith(4,1/4, (x,n)=>x
.add(n*7)
@ -516,8 +540,10 @@ export const festivalOfFingers3 = `// licensed with CC BY-NC-SA 4.0 https://crea
.note().piano()
//.pianoroll({maxMidi:160})`;
export const meltingsubmarine = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const meltingsubmarine = `// "Melting submarine"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
await samples('github:tidalcycles/Dirt-Samples/master/')
stack(
s("bd:5,[~ <sd:1!3 sd:1(3,4,3)>],hh27(3,4,1)") // drums
@ -554,8 +580,10 @@ stack(
)
.slow(3/2)`;
export const outroMusic = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const outroMusic = `// "Outro music"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
samples({
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
@ -583,8 +611,10 @@ samples({
//.pianoroll({autorange:1,vertical:1,fold:0})
`;
export const bassFuge = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const bassFuge = `// "Bass fuge"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
samples({ flbass: ['00_c2_finger_long_neck.wav','01_c2_finger_short_neck.wav','02_c2_finger_long_bridge.wav','03_c2_finger_short_bridge.wav','04_c2_pick_long.wav','05_c2_pick_short.wav','06_c2_palm_mute.wav'] },
'github:cleary/samples-flbass/main/')
samples({
@ -609,8 +639,10 @@ x=>x.add(7).color('steelblue')
.stack(s("bd:1*2,~ sd:0,[~ hh:0]*2"))
.pianoroll({vertical:1})`;
export const chop = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const chop = `// "Chop"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
samples({ p: 'https://cdn.freesound.org/previews/648/648433_11943129-lq.mp3' })
s("p")
@ -622,8 +654,10 @@ s("p")
.sustain(.6)
`;
export const delay = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const delay = `// "Delay"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
stack(
s("bd <sd cp>")
.delay("<0 .5>")
@ -631,8 +665,10 @@ stack(
.delayfeedback(".6 | .8")
).sometimes(x=>x.speed("-1"))`;
export const orbit = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const orbit = `// "Orbit"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
stack(
s("bd <sd cp>")
.delay(.5)
@ -645,8 +681,10 @@ stack(
.orbit(2)
).sometimes(x=>x.speed("-1"))`;
export const belldub = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const belldub = `// "Belldub"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
samples({ bell: {b4:'https://cdn.freesound.org/previews/339/339809_5121236-lq.mp3'}})
// "Hand Bells, B, Single.wav" by InspectorJ (www.jshaw.co.uk) of Freesound.org
stack(
@ -678,8 +716,10 @@ stack(
.mask("<1 0>/8")
).slow(5)`;
export const dinofunk = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const dinofunk = `// "Dinofunk"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
samples({bass:'https://cdn.freesound.org/previews/614/614637_2434927-hq.mp3',
dino:{b4:'https://cdn.freesound.org/previews/316/316403_5123851-hq.mp3'}})
setVoicingRange('lefthand', ['c3','a4'])
@ -699,8 +739,10 @@ note("Abm7".voicings('lefthand').struct("x(3,8,1)".slow(2))),
note("<b4 eb4>").s('dino').delay(.8).slow(8).room(.5)
)`;
export const sampleDemo = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const sampleDemo = `// "Sample demo"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
stack(
// percussion
s("[woodblock:1 woodblock:2*2] snare_rim:0,gong/8,brakedrum:1(3,8),~@3 cowbell:3")
@ -715,8 +757,10 @@ stack(
.release(.1).room(.5)
)`;
export const holyflute = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const holyflute = `// "Holy flute"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
"c3 eb3(3,8) c4/2 g3*2"
.superimpose(
x=>x.slow(2).add(12),
@ -728,8 +772,10 @@ export const holyflute = `// licensed with CC BY-NC-SA 4.0 https://creativecommo
.pianoroll({fold:0,autorange:0,vertical:0,cycles:12,smear:0,minMidi:40})
`;
export const flatrave = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const flatrave = `// "Flatrave"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
stack(
s("bd*2,~ [cp,sd]").bank('RolandTR909'),
@ -751,8 +797,10 @@ stack(
.decay(.05).sustain(0).delay(.2).degradeBy(.5).mask("<0 1>/16")
)`;
export const amensister = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const amensister = `// "Amensister"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
await samples('github:tidalcycles/Dirt-Samples/master')
stack(
@ -785,8 +833,10 @@ stack(
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
).reset("<x@7 x(5,8,-1)>")`;
export const juxUndTollerei = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const juxUndTollerei = `// "Jux und tollerei"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
note("c3 eb3 g3 bb3").palindrome()
.s('sawtooth')
.jux(x=>x.rev().color('green').s('sawtooth'))
@ -798,8 +848,10 @@ note("c3 eb3 g3 bb3").palindrome()
.delay(.5).delaytime(.1).delayfeedback(.4)
.pianoroll()`;
export const csoundDemo = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
export const csoundDemo = `// "CSound demo"
// @license with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
await loadCsound\`
instr CoolSynth
iduration = p3
@ -832,10 +884,10 @@ endin\`
//.pianoroll()
.csound('CoolSynth')`;
export const loungeSponge = `
// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
// livecode.orc by Steven Yi
export const loungeSponge = `// "Lounge sponge"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos, livecode.orc by Steven Yi
await loadOrc('github:kunstmusik/csound-live-code/master/livecode.orc')
stack(
@ -852,8 +904,10 @@ stack(
s("bd*2,[~ hh]*2,~ cp").bank('RolandTR909')
)`;
export const arpoon = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// "Arpoon" by Felix Roos
export const arpoon = `// "Arpoon"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
await samples('github:tidalcycles/Dirt-Samples/master')
"<<Am7 C^7> C7 F^7 [Fm7 E7b9]>".voicings('lefthand')

View File

@ -4,6 +4,7 @@ import { useStore } from '@nanostores/react';
export const defaultSettings = {
activeFooter: 'intro',
keybindings: 'codemirror',
isLineNumbersDisplayed: true,
theme: 'strudelTheme',
fontFamily: 'monospace',
fontSize: 18,
@ -19,6 +20,7 @@ export function useSettings() {
return {
...state,
isZen: [true, 'true'].includes(state.isZen) ? true : false,
isLineNumbersDisplayed: [true, 'true'].includes(state.isLineNumbersDisplayed) ? true : false,
fontSize: Number(state.fontSize),
};
}

View File

@ -27,10 +27,6 @@
src: url('/fonts/FiraCode/FiraCode-SemiBold.ttf');
}
.cm-gutters {
display: none !important;
}
.prose > h1:not(:first-child) {
margin-top: 30px;
}

View File

@ -7,6 +7,11 @@
"noImplicitAny": false,
"types": [
"vite-plugin-pwa/client"
]
],
"baseUrl": ".",
"paths": {
"@components/*": ["src/components/*"],
"@src/*": ["src/*"],
}
}
}