Compare commits

..

146 Commits

Author SHA1 Message Date
Felix Roos a2f3b85f38 remove notes 2023-06-11 11:20:28 +02:00
Felix Roos 8e6846a3aa add workshop file 2023-06-10 12:14:15 +02:00
Felix Roos 34ee2081d3 fix: typo 2023-06-09 23:02:48 +02:00
Felix Roos 4d8308a763 fix: link goes to wrong language 2023-06-09 21:33:32 +02:00
Felix Roos e10104d401 bring de improvements to en workshop 2023-06-09 20:59:40 +02:00
Felix Roos 576b88221d Merge remote-tracking branch 'origin/main' into workshop-new 2023-06-09 18:00:16 +02:00
Felix Roos 7dc2a0091b more detail 2023-06-09 17:59:08 +02:00
Felix Roos 89a4a85589 more friendly heading 2023-06-09 17:58:05 +02:00
Felix Roos a3baf07a5c Merge pull request #580 from roipoussiere/music_metadata
Music metadata
2023-06-09 17:31:58 +02:00
Felix Roos ae868c6372 hide gutter in mini repl 2023-06-09 00:46:04 +02:00
Felix Roos c287ff554c optimize sounds chapter after teach test 2023-06-09 00:26:40 +02:00
Felix Roos 3ccbd35fad add label support to pianoroll 2023-06-09 00:25:54 +02:00
Felix Roos 51cc69208f fix: repl link to docs was old 2023-06-09 00:25:14 +02:00
Felix Roos 1fec2f7a46 fix: build 2023-06-08 21:31:32 +02:00
Felix Roos ffffdd2d7d fix: format 2023-06-08 20:55:26 +02:00
Felix Roos f8c3e3d5aa add missing sounds by hand 2023-06-08 20:43:45 +02:00
Felix Roos eccce6fa84 remove dirt samples
+ load a small selection for workshop instead
2023-06-08 17:48:54 +02:00
Felix Roos 44be6cee2c Merge pull request #585 from srenatus/typo
learn/tonal: fix typo in "scaleTran[s]pose"
2023-06-08 09:33:34 +02:00
Stephan Renatus 799afd511a learn/tonal: fix typo in "scaleTran[s]pose" 2023-06-08 08:17:38 +02:00
Felix Roos 37c4541468 translate link 2023-06-07 21:50:26 +02:00
Felix Roos 119a66acd0 - translate getting-started
- fix de links
- link english docs / german workshop
2023-06-07 21:49:57 +02:00
Felix Roos 105193a452 replace tutorial with workshop
+ move parts of tutorial to more
+ move more before Pattern Functions
2023-06-07 21:22:01 +02:00
Felix Roos 02d793e9e0 Merge remote-tracking branch 'origin/main' into workshop-new 2023-06-07 20:19:34 +02:00
Felix Roos 90647a8ba8 Merge pull request #582 from roipoussiere/line_numbers
repl: add option to display line numbers
2023-06-07 20:19:11 +02:00
Felix Roos 06950a6bd8 translate rest of workshop to german 2023-06-07 16:50:54 +02:00
Roipoussiere 0c2147d9c5 repl: add option to display line numbers 2023-06-07 15:34:57 +02:00
Roipoussiere 51e8174689 metadata: add quotes syntax for titles 2023-06-07 12:26:23 +02:00
Roipoussiere 185318a70c metadata: fix typos 2023-06-07 11:02:38 +02:00
Felix Roos a88759ba94 translate notes chapter 2023-06-06 22:43:51 +02:00
Roipoussiere 96ac054392 metadata: improve doc 2023-06-06 21:34:53 +02:00
Roipoussiere bd7f4dd73f metadata: add tests 2023-06-06 21:34:53 +02:00
Roipoussiere 2e22152089 metadata: allow several values and one-liners 2023-06-06 21:34:53 +02:00
Roipoussiere 98a89aea11 lint: metadata parsing 2023-06-06 21:34:53 +02:00
Roipoussiere 372e33c066 doc: add section about metadata 2023-06-06 21:34:53 +02:00
Roipoussiere 52641db590 load titles from metadata in examples page 2023-06-06 21:34:53 +02:00
Roipoussiere f9ad5f0d01 add tunes metadata 2023-06-06 21:34:53 +02:00
Roipoussiere 3db1c7a521 swatch: look for song titles in code metadata 2023-06-06 21:34:53 +02:00
Felix Roos 61a9b01062 started german translation of workshop 2023-06-05 23:48:04 +02:00
Felix Roos f0faf5b42e Merge remote-tracking branch 'origin/main' into workshop-new 2023-06-05 22:48:50 +02:00
Felix Roos 11733d5e05 Merge pull request #563 from tidalcycles/vanilla-highlighting
Vanilla JS Refactoring
2023-06-05 21:35:49 +02:00
Felix Roos c4a38d9008 + pattern effects chapter
+ recap page
+ only load mini repls when visible
2023-05-30 06:16:02 +02:00
Felix Roos 0edd7e14b7 add compound adsr + ds controls 2023-05-29 12:39:36 +02:00
Felix Roos 8b7bb7b6ae add adsr section to effects chapter 2023-05-29 12:39:17 +02:00
Felix Roos 39d5955e58 add function recap to sounds chapter 2023-05-29 12:39:05 +02:00
Felix Roos 536327f403 effects chapter mostly finished 2023-05-29 02:18:36 +02:00
Felix Roos 8679dc63be pianoroll: also reflect gain in transparency 2023-05-29 02:18:27 +02:00
Felix Roos 1e9979ae18 start fleshing out effects chapter 2023-05-28 12:42:24 +02:00
Felix Roos d2dffe3186 MiniRepl: consume font settings 2023-05-28 12:42:15 +02:00
Felix Roos 9971867e2f clamp function 2023-05-28 12:41:53 +02:00
Felix Roos 8c93e578a0 clamp delayfeedback 2023-05-28 12:41:46 +02:00
Felix Roos 1ba5d2e1ca finish notes chapter 2023-05-27 21:52:44 +02:00
Felix Roos ed792fc0d4 continue notes chapter 2023-05-27 16:29:05 +02:00
Felix Roos 4e575c44b3 begin first notes page 2023-05-27 13:31:18 +02:00
Felix Roos fc06181217 - add claviature flag to minirepl
- bring back option+dot on macos
- consume more editor settings in minirepl
2023-05-27 13:30:57 +02:00
Felix Roos 0d6fcf78d8 hide mini repl headers + improve workshop 2023-05-26 16:05:53 +02:00
Felix Roos 82225f0b81 started workshop pages 2023-05-26 14:12:53 +02:00
Felix Roos 2177e13b49 Merge branch 'vanilla-highlighting' into workshop 2023-05-25 21:38:34 +02:00
Felix Roos d3240a75d0 fix: dupe style.css to Repl.css
for some reason they were missing after update
+ darken line backgrounds
+ change preview port to keep workbox out of dev server
2023-05-12 11:56:50 +02:00
Felix Roos d93e396ce5 fix: nano-repl 2023-05-12 10:21:05 +02:00
Felix Roos d74cc234c4 fix: thinner outline for highlights with .color 2023-05-12 10:06:55 +02:00
Felix Roos 0fda83d8de bump soundfonts 2023-05-12 10:00:55 +02:00
Felix Roos a1ec8545fb format 2023-05-10 13:03:06 +02:00
Felix Roos 087f574355 update web readme + add more html examples 2023-05-10 12:49:45 +02:00
Felix Roos 1de784b817 add headless-serverless-buildless example 2023-05-10 12:20:27 +02:00
Felix Roos db79c05d9a bump web pkg dependencies 2023-05-10 12:11:51 +02:00
Felix Roos 1fdb1f0da6 bump webaudio package 2023-05-10 12:09:20 +02:00
Felix Roos e2bfaa6233 remove soundfonts from web package for now 2023-05-10 12:06:16 +02:00
Felix Roos 6089849d35 rename init to initStrudel + add to window 2023-05-10 11:53:26 +02:00
Felix Roos 6e02bf59e9 Revert "refactor: remove old draw logic"
This reverts commit 95719654f3.
2023-05-10 11:44:55 +02:00
Felix Roos 12228c56d9 improve api for web package 2023-05-07 22:36:26 +02:00
Felix Roos 3b631cb6af add @strudel/web umbrella package + example 2023-05-07 11:56:35 +02:00
Felix Roos b27a58df7e bump more packages to 0.8.0 2023-05-05 23:58:47 +02:00
Felix Roos 308f139193 bump codemirror again to get new core version 2023-05-05 23:52:55 +02:00
Felix Roos 692fc4e11d revert rename core package 2023-05-05 23:52:04 +02:00
Felix Roos b52f65c4d7 rename core package + bump to 0.8.0 2023-05-05 23:50:06 +02:00
Felix Roos 299275e368 lockfile 2023-05-05 23:47:15 +02:00
Felix Roos bb71c519db bump codemirror package 2023-05-05 23:46:42 +02:00
Felix Roos 9ec7109dc8 + simplify vanilla setup drastically
+ move repl + drawer boilerplate inside StrudelMirror
2023-05-05 23:43:07 +02:00
Felix Roos 5b67fccb1b vanilla-repl-cm6: draw first frame 2023-05-05 16:17:22 +02:00
Felix Roos 9807c4b7a1 move Framer + Drawer to core
... from vite-vanilla-repl-cm6
2023-05-05 16:06:11 +02:00
Felix Roos 9039600f6d bump codemirror to 0.8.1 2023-05-05 15:31:19 +02:00
Felix Roos b5b0156e08 fix: codemirror reeval 2023-05-05 15:31:04 +02:00
Felix Roos 352e647d37 add build script for codemirror package 2023-05-05 15:15:45 +02:00
Felix Roos e6b66f2525 lockfile 2023-05-05 15:14:46 +02:00
Felix Roos f5075906e2 add codemirror package
+ use it in vite-vanilla-repl-cm6
2023-05-05 15:13:17 +02:00
Felix Roos a6f57bced8 prettier 2023-05-05 12:00:13 +02:00
Felix Roos d35bf9591c vanilla-repl-cm6: better theme 2023-05-05 11:54:09 +02:00
Felix Roos d96f3f9dd4 vanilla-repl-cm6: style + codesplitting 2023-05-05 11:09:44 +02:00
Felix Roos 1f4c2f8c5a draw scheduler in vanilla js 2023-05-05 10:52:21 +02:00
Felix Roos 95719654f3 refactor: remove old draw logic
+ pianoroll now uses .onPaint
2023-05-05 09:52:07 +02:00
Felix Roos 4b921c47f5 vanilla-repl-cm6: add flash effect 2023-05-05 09:11:40 +02:00
Felix Roos f069f53fae vanilla-repl-cm6: add keybindings 2023-05-05 09:06:27 +02:00
Felix Roos 1494cc38fc add vite-vanilla-repl-cm6 2023-05-05 08:55:43 +02:00
Felix Roos a035412956 Merge pull request #559 from tidalcycles/patchday
Patchday
2023-05-05 08:34:36 +02:00
Felix Roos 08662ea831 Merge pull request #557 from tidalcycles/some-voicings
add basic triads and guidetone voicings
2023-05-02 21:48:32 +02:00
Felix Roos d97103268b add basic triads and guidetone voicings 2023-05-02 21:40:20 +02:00
Felix Roos a959e2e34d update remaining packages 2023-04-30 08:30:29 +02:00
Felix Roos 602bfbdd79 update a bunch of minor versions 2023-04-30 08:20:51 +02:00
Felix Roos 0785bdd37a update lerna 2023-04-30 08:04:56 +02:00
Felix Roos 8b62b6e1b1 update codemirror + tailwindcss 2023-04-30 08:02:06 +02:00
Felix Roos d1c8ebd15b remove c8 2023-04-30 07:55:23 +02:00
Felix Roos 13088e4001 - remove gh-pages
- update peggy
- rebuild parser
2023-04-30 07:48:08 +02:00
Felix Roos 5c6fa1c82b update vite to v4 2023-04-29 08:10:12 +02:00
Felix Roos fd81867e7b fix: controls import broke on ssr 2023-04-29 07:45:21 +02:00
Felix Roos 78293e64dd lockfile 2023-04-29 07:45:09 +02:00
Felix Roos 930fc10aec bump some minor versions 2023-04-29 07:45:04 +02:00
Felix Roos 0b2a4fda44 update supabase-js 2023-04-29 07:24:03 +02:00
Felix Roos efdce6d4cd update astro 2023-04-29 06:55:34 +02:00
Felix Roos c0b0ea4493 remove preact 2023-04-29 06:25:19 +02:00
Felix Roos 0fcec0a0ca upgrade jsdoc 2023-04-28 13:05:50 +02:00
Felix Roos c0701d8a35 Merge remote-tracking branch 'origin/main' into patchday 2023-04-28 12:50:20 +02:00
Felix Roos ae51092044 Merge pull request #556 from tidalcycles/fix-prebake
fix: make soundfonts import dynamic
2023-04-28 12:46:56 +02:00
Felix Roos b7fcccb8db fix: make soundfonts import dynamic
- it broke the whole docs page
2023-04-28 12:42:07 +02:00
Felix Roos b21a52f5f3 update @vite-pwa/astro 2023-04-28 12:23:10 +02:00
Felix Roos 2bdcfe13e6 update @vitest/ui 2023-04-28 12:20:35 +02:00
Felix Roos 12a97ec429 update nanostores 2023-04-28 12:18:45 +02:00
Felix Roos 99298ce9f2 update vite-plugin-pwa 2023-04-28 12:14:07 +02:00
Felix Roos a0ed25d373 use vitests@^0.28.0 in packages 2023-04-28 12:12:05 +02:00
Felix Roos dc6c29c668 update vitest to ^0.28.0 2023-04-28 12:08:48 +02:00
Felix Roos 3a85f28b5c install with pnpm 8.3.1 2023-04-28 10:33:47 +02:00
Felix Roos 95c44b1ce2 Merge pull request #553 from tidalcycles/highlighting-colors
fix: colorable highlighting
2023-04-22 15:43:22 +02:00
Felix Roos b88e0ecb78 fix: colorable highlighting 2023-04-22 15:25:00 +02:00
Felix Roos c26f6b72d9 Merge pull request #550 from tidalcycles/fix-soundfont-loading
fix: load soundfonts in prebake
2023-04-06 00:11:02 +02:00
Felix Roos 6cd3cc96a3 fix: do not prebake in tests
+ pull piano function to separate file for tests
2023-04-06 00:08:23 +02:00
Felix Roos c138cf0b76 fix: load soundfonts in prebake 2023-04-06 00:00:49 +02:00
Felix Roos 8edb086178 Merge pull request #548 from tidalcycles/stop-pause
fix: reset time on stop
2023-03-29 22:23:07 +02:00
Felix Roos 0f74b5a4b1 Merge pull request #547 from tidalcycles/fix-mini-whitespace
fix: allow whitespace at the end of a mini pattern
2023-03-29 22:16:37 +02:00
Felix Roos 6de2feff05 fix: reset time on stop 2023-03-29 22:05:13 +02:00
Felix Roos a9811ee955 fix: allow whitespace at the end of a mini pattern
- fixes #546
2023-03-29 21:57:55 +02:00
Felix Roos 98b6c11a82 Merge pull request #544 from tidalcycles/fonts
add firacode font
2023-03-24 22:10:55 +01:00
Felix Roos 3df2929d12 dummy commit (github cert error :-/) 2023-03-24 22:06:40 +01:00
Felix Roos 30d1f423cb fix: remove loading state on eval error 2023-03-24 21:50:50 +01:00
Felix Roos e0c5566f35 add 2 FiraCode fonts 2023-03-24 21:23:37 +01:00
Felix Roos c5a2ae47d7 Merge pull request #542 from tidalcycles/loading-animation
feat: add loader bar to animate loading state
2023-03-24 12:54:20 +01:00
Felix Roos 17654b885b feat: add loader bar to animate loading state 2023-03-24 12:41:00 +01:00
Felix Roos acaf0ca0af Merge pull request #539 from tidalcycles/setcps-reset-only-on-shuffle
do not reset cps before eval #517
2023-03-23 22:39:26 +01:00
Felix Roos 6a201be1fd Merge remote-tracking branch 'origin/main' into setcps-reset-only-on-shuffle 2023-03-23 22:35:57 +01:00
Felix Roos 8617b9164e do not reset cps before eval #517 2023-03-23 22:34:25 +01:00
Felix Roos 991e5f1b3c Merge pull request #538 from tidalcycles/improve-loading
improve initial loading + wait before eval
2023-03-23 22:27:31 +01:00
Felix Roos 55c533c947 improve initial loading + wait before eval 2023-03-23 21:56:20 +01:00
Felix Roos d17543d5d9 Merge pull request #537 from tidalcycles/fix-keypress-period
fix period key for dvorak + remove duplicated code
2023-03-23 21:40:19 +01:00
Felix Roos 73169563c4 fix period key for dvorak + remove duplicated code 2023-03-23 21:37:38 +01:00
Felix Roos d351cb9f7f Merge pull request #535 from tidalcycles/update-lerna
Update lerna
2023-03-23 11:44:56 +01:00
Felix Roos 251be60630 update package publishing guide 2023-03-23 11:39:33 +01:00
Felix Roos 18d7222804 Publish
- @strudel.cycles/core@0.7.2
 - @strudel.cycles/csound@0.7.1
 - @strudel.cycles/eval@0.7.1
 - @strudel.cycles/midi@0.7.1
 - @strudel.cycles/mini@0.7.2
 - @strudel.cycles/osc@0.7.1
 - @strudel.cycles/react@0.7.1
 - @strudel.cycles/serial@0.7.1
 - @strudel.cycles/soundfonts@0.7.1
 - @strudel.cycles/tonal@0.7.1
 - @strudel.cycles/tone@0.7.1
 - @strudel.cycles/transpiler@0.7.1
 - @strudel.cycles/webaudio@0.7.1
 - @strudel.cycles/webdirt@0.7.1
 - @strudel.cycles/xen@0.7.1
2023-03-23 11:34:07 +01:00
Felix Roos ff1c37d361 bump core + mini to 0.7.1 2023-03-23 11:26:23 +01:00
126 changed files with 9805 additions and 4499 deletions
+4 -1
View File
@@ -39,4 +39,7 @@ server/samples/old
repl/stats.html
coverage
public/icons/apple-splash-*
dev-dist
dev-dist
Dirt-Samples
tidal-drum-machines
webaudiofontdata
+8 -1
View File
@@ -123,7 +123,14 @@ To publish all packages that have been changed since the last release, run:
```sh
npm login
npx lerna publish
# this will increment all the versions in package.json files of non private packages to selected versions
npx lerna version --no-private
# publish all packages inside /packages using pnpm! don't use lerna to publish!!
pnpm --filter "./packages/**" publish --dry-run
# the last command was only a dry-run, make sure everything looks ok, if yes, run the same command without flag
```
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
+8 -11
View File
@@ -18,7 +18,6 @@
"build": "npm run prebuild && cd website && npm run build",
"preview": "cd website && npm run preview",
"osc": "cd packages/osc && npm run server",
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
"lint": "eslint . --ext mjs,js --quiet",
@@ -54,21 +53,19 @@
"@strudel.cycles/xen": "workspace:*",
"acorn": "^8.8.1",
"dependency-tree": "^9.0.0",
"vitest": "^0.25.7"
"vitest": "^0.28.0"
},
"devDependencies": {
"@vitest/ui": "^0.25.7",
"c8": "^7.12.0",
"canvas": "^2.11.0",
"eslint": "^8.28.0",
"@vitest/ui": "^0.28.0",
"canvas": "^2.11.2",
"eslint": "^8.39.0",
"eslint-plugin-import": "^2.27.5",
"events": "^3.3.0",
"gh-pages": "^4.0.0",
"jsdoc": "^3.6.10",
"jsdoc": "^4.0.2",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^7.1.1",
"lerna": "^6.5.1",
"prettier": "^2.8.1",
"jsdoc-to-markdown": "^8.0.0",
"lerna": "^6.6.1",
"prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.8.1"
}
}
+3
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.
+199
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);
}
}
+47
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
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)];
+19
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',
},
});
+2 -1
View File
@@ -1,4 +1,5 @@
import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
import { Pattern, getDrawContext, silence, register, pure } from './index.mjs';
import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
const { createParams } = controls;
let clearColor = '#22222210';
+12 -1
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;
+1
View File
@@ -74,6 +74,7 @@ export class Cyclist {
stop() {
logger('[cyclist] stop');
this.clock.stop();
this.lastEnd = 0;
this.setStarted(false);
}
setPattern(pat, autostart = false) {
+96 -2
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();
}
}
}
@@ -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?
@@ -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
```
@@ -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>
@@ -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());
@@ -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:*"
}
}
@@ -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;
}
@@ -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
)`;
@@ -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)
@@ -10,7 +10,7 @@
"preview": "vite preview"
},
"devDependencies": {
"vite": "^3.2.0"
"vite": "^4.3.3"
},
"dependencies": {
"@strudel.cycles/core": "workspace:*",
+1
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';
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.7.0",
"version": "0.8.1",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
@@ -35,7 +35,7 @@
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+35 -4
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 });
}
+24 -28
View File
@@ -18,46 +18,26 @@ 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) {
throw new Error('no code to evaluate');
}
try {
beforeEval?.({ code });
scheduler.setCps(1); // reset cps in case the code does not contain a setCps call
// problem: when the code does contain a setCps after an awaited promise,
// the cps will be 1 until the promise resolves
// example:
/*
await new Promise(resolve => setTimeout(resolve,1000))
setCps(.5)
note("c a f e")
*/
// to make sure the setCps inside the code is called immediately,
// it has to be placed first
await beforeEval?.({ code });
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) {
@@ -74,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');
}
};
+4 -1
View File
@@ -67,13 +67,14 @@ export const getFreq = (noteOrMidi) => {
return midiToFreq(noteToMidi(noteOrMidi));
};
const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
/**
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
* @noAutocomplete
*/
export const midi2note = (n) => {
const oct = Math.floor(n / 12) - 1;
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
const pc = pcs[n % 12];
return pc + oct;
};
@@ -212,3 +213,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);
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/csound",
"version": "0.7.0",
"version": "0.7.1",
"description": "csound bindings for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -32,11 +32,11 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@csound/browser": "6.18.5",
"@csound/browser": "6.18.7",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.7.0",
"version": "0.7.1",
"description": "Code evaluator for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -44,7 +44,7 @@
},
"devDependencies": {
"@strudel.cycles/mini": "workspace:*",
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.7.0",
"version": "0.7.1",
"description": "Midi API for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -31,9 +31,9 @@
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"webmidi": "^3.0.21"
"webmidi": "^3.1.5"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
+22 -12
View File
@@ -1,4 +1,4 @@
// Generated by Peggy 2.0.1.
// Generated by Peggy 3.0.2.
//
// https://peggyjs.org/
@@ -43,16 +43,19 @@ peg$SyntaxError.prototype.format = function(sources) {
}
}
var s = this.location.start;
var loc = this.location.source + ":" + s.line + ":" + s.column;
var offset_s = (this.location.source && (typeof this.location.source.offset === "function"))
? this.location.source.offset(s)
: s;
var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
if (src) {
var e = this.location.end;
var filler = peg$padEnd("", s.line.toString().length, ' ');
var filler = peg$padEnd("", offset_s.line.toString().length, ' ');
var line = src[s.line - 1];
var last = s.line === e.line ? e.column : line.length + 1;
var hatLen = (last - s.column) || 1;
str += "\n --> " + loc + "\n"
+ filler + " |\n"
+ s.line + " | " + line + "\n"
+ offset_s.line + " | " + line + "\n"
+ filler + " | " + peg$padEnd("", s.column - 1, ' ')
+ peg$padEnd("", hatLen, "^");
} else {
@@ -422,11 +425,11 @@ function peg$parse(input, options) {
}
}
function peg$computeLocation(startPos, endPos) {
function peg$computeLocation(startPos, endPos, offset) {
var startPosDetails = peg$computePosDetails(startPos);
var endPosDetails = peg$computePosDetails(endPos);
return {
var res = {
source: peg$source,
start: {
offset: startPos,
@@ -439,6 +442,11 @@ function peg$parse(input, options) {
column: endPosDetails.column
}
};
if (offset && peg$source && (typeof peg$source.offset === "function")) {
res.start = peg$source.offset(res.start);
res.end = peg$source.offset(res.end);
}
return res;
}
function peg$fail(expected) {
@@ -1511,18 +1519,20 @@ function peg$parse(input, options) {
}
function peg$parsemini() {
var s0, s1, s2, s3, s4;
var s0, s1, s2, s3, s4, s5, s6;
s0 = peg$currPos;
s1 = peg$parsews();
s2 = peg$parsequote();
if (s2 !== peg$FAILED) {
s3 = peg$parsestack_or_choose();
if (s3 !== peg$FAILED) {
s4 = peg$parsequote();
if (s4 !== peg$FAILED) {
s3 = peg$parsews();
s4 = peg$parsestack_or_choose();
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
s6 = peg$parsequote();
if (s6 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f19(s3);
s0 = peg$f19(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
+1 -1
View File
@@ -182,7 +182,7 @@ polymeter_stack = head:sequence tail:stack_tail?
// Experimental haskellish parser begins
// mini-notation = a quoted stack
mini = ws quote sc:stack_or_choose quote
mini = ws quote ws sc:stack_or_choose ws quote
{ return sc; }
// ------------------ operators ---------------------------
+5
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);
}
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.7.0",
"version": "0.8.1",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
@@ -35,8 +35,8 @@
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"peggy": "^2.0.1",
"vite": "^3.2.2",
"vitest": "^0.25.7"
"peggy": "^3.0.2",
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.7.0",
"version": "0.7.1",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"publishConfig": {
@@ -40,7 +40,7 @@
"osc-js": "^2.4.0"
},
"devDependencies": {
"pkg": "^5.7.0",
"vite": "^3.2.2"
"pkg": "^5.8.1",
"vite": "^4.3.3"
}
}
@@ -15,17 +15,18 @@
"@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:*"
},
"devDependencies": {
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.0.1",
"autoprefixer": "^10.4.8",
"postcss": "^8.4.16",
"tailwindcss": "^3.1.8",
"vite": "^3.0.7"
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"tailwindcss": "^3.3.2",
"vite": "^4.3.3"
}
}
+27 -15
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 (
+16 -16
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/react",
"version": "0.7.0",
"version": "0.7.1",
"description": "React components for strudel",
"main": "src/index.js",
"publishConfig": {
@@ -32,18 +32,18 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.4.0",
"@codemirror/lang-javascript": "^6.1.1",
"@codemirror/autocomplete": "^6.6.0",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.7.3",
"@lezer/highlight": "^1.1.3",
"@replit/codemirror-emacs": "^6.0.0",
"@replit/codemirror-vim": "^6.0.6",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@replit/codemirror-emacs": "^6.0.1",
"@replit/codemirror-vim": "^6.0.14",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@uiw/codemirror-themes": "^4.12.4",
"@uiw/react-codemirror": "^4.12.4",
"@uiw/codemirror-themes": "^4.19.16",
"@uiw/react-codemirror": "^4.19.16",
"react-hook-inview": "^4.5.0"
},
"peerDependencies": {
@@ -51,14 +51,14 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^2.2.0",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.18",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.0.24",
"vite": "^3.2.2"
"tailwindcss": "^3.3.2",
"vite": "^4.3.3"
}
}
@@ -58,15 +58,19 @@ const highlightField = StateField.define({
haps
.map((hap) =>
(hap.context.locations || []).map(({ start, end }) => {
// const color = hap.context.color || e.value.color || '#FFCA28';
const color = hap.context.color || e.value.color;
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 ${color};` } });
const mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
let mark;
if (color) {
mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
} else {
mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
}
return mark.range(from, to);
}),
)
@@ -100,6 +104,7 @@ export default function CodeMirror({
onSelectionChange,
theme,
keybindings,
isLineNumbersDisplayed,
fontSize = 18,
fontFamily = 'monospace',
options,
@@ -144,6 +149,7 @@ export default function CodeMirror({
onCreateEditor={handleOnCreateEditor}
onUpdate={handleOnUpdate}
extensions={extensions}
basicSetup={{ lineNumbers: isLineNumbersDisplayed }}
/>
</div>
);
+61 -49
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.code === 'Period') {
} else if (e.key === '.' || e.code === 'Period') {
stop();
e.preventDefault();
}
@@ -93,26 +112,6 @@ export function MiniRepl({
),
);
// set active pattern on ctrl+enter
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === 'Period') {
stop();
e.preventDefault();
}
}
};
window.addEventListener('keydown', handleKeyPress, true);
return () => window.removeEventListener('keydown', handleKeyPress, true);
}
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
const [log, setLog] = useState([]);
useLogger(
useCallback((e) => {
@@ -121,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);
});
}
}, []),
@@ -129,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"
+1 -1
View File
@@ -1,6 +1,6 @@
:root {
--background: #222;
--lineBackground: #22222250;
--lineBackground: #22222299;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
+4 -3
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(
@@ -46,9 +47,9 @@ function useStrudel({
drawContext,
transpiler,
editPattern,
beforeEval: ({ code }) => {
beforeEval: async ({ code }) => {
setCode(code);
beforeEval?.();
await beforeEval?.();
},
afterEval: (res) => {
const { pattern: _pattern, code } = res;
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/serial",
"version": "0.7.0",
"version": "0.7.1",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"publishConfig": {
@@ -32,6 +32,6 @@
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
-2
View File
@@ -153,5 +153,3 @@ export function registerSoundfonts() {
);
});
}
registerSoundfonts();
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.7.0",
"version": "0.8.1",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -36,7 +36,7 @@
"soundfont2": "^0.4.0"
},
"devDependencies": {
"node-fetch": "^3.2.6",
"vite": "^3.2.2"
"node-fetch": "^3.3.1",
"vite": "^4.3.3"
}
}
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.7.0",
"version": "0.8.1",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -34,10 +34,10 @@
"@strudel.cycles/core": "workspace:*",
"@tonaljs/tonal": "^4.7.2",
"chord-voicings": "^0.0.1",
"webmidi": "^3.0.21"
"webmidi": "^3.1.5"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+44 -1
View File
@@ -6,10 +6,53 @@ This program is free software: you can redistribute it and/or modify it under th
import { stack, register } from '@strudel.cycles/core';
import _voicings from 'chord-voicings';
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
const { dictionaryVoicing, minTopNoteDiff } = _voicings.default || _voicings; // parcel module resolution fuckup
const lefthand = {
m7: ['3m 5P 7m 9M', '7m 9M 10m 12P'],
7: ['3M 6M 7m 9M', '7m 9M 10M 13M'],
'^7': ['3M 5P 7M 9M', '7M 9M 10M 12P'],
69: ['3M 5P 6A 9M'],
m7b5: ['3m 5d 7m 8P', '7m 8P 10m 12d'],
'7b9': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
'7b13': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
o7: ['1P 3m 5d 6M', '5d 6M 8P 10m'],
'7#11': ['7m 9M 11A 13A'],
'7#9': ['3M 7m 9A'],
mM7: ['3m 5P 7M 9M', '7M 9M 10m 12P'],
m6: ['3m 5P 6M 9M', '6M 9M 10m 12P'],
};
const guidetones = {
m7: ['3m 7m', '7m 10m'],
m9: ['3m 7m', '7m 10m'],
7: ['3M 7m', '7m 10M'],
'^7': ['3M 7M', '7M 10M'],
'^9': ['3M 7M', '7M 10M'],
69: ['3M 6M'],
6: ['3M 6M', '6M 10M'],
m7b5: ['3m 7m', '7m 10m'],
'7b9': ['3M 7m', '7m 10M'],
'7b13': ['3M 7m', '7m 10M'],
o7: ['3m 6M', '6M 10m'],
'7#11': ['3M 7m', '7m 10M'],
'7#9': ['3M 7m', '7m 10M'],
mM7: ['3m 7M', '7M 10m'],
m6: ['3m 6M', '6M 10m'],
};
const triads = {
'': ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
M: ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
m: ['1P 3m 5P', '3m 5P 8P', '5P 8P 10m'],
o: ['1P 3m 5d', '3m 5d 8P', '5d 8P 10m'],
aug: ['1P 3m 5A', '3m 5A 8P', '5A 8P 10m'],
};
export const voicingRegistry = {
lefthand: { dictionary: lefthand, range: ['F3', 'A4'] },
triads: { dictionary: triads },
guidetones: { dictionary: guidetones },
};
export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.7.0",
"version": "0.7.1",
"description": "Tone.js API for strudel",
"main": "index.mjs",
"type": "module",
@@ -34,7 +34,7 @@
"tone": "^14.7.77"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/transpiler",
"version": "0.7.0",
"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": {
@@ -36,7 +36,7 @@
"estree-walker": "^3.0.1"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.8"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+82
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!
+10
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>
@@ -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>
+10
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>
@@ -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?
@@ -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>
@@ -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:*"
}
}
+12
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
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"
}
}
+19
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
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();
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.7.0",
"version": "0.8.1",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
@@ -35,9 +35,9 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"nanostores": "^0.7.4"
"nanostores": "^0.8.1"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
+18
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 }),
});
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.7.0",
"version": "0.7.1",
"description": "WebDirt integration for Strudel",
"main": "index.mjs",
"type": "module",
@@ -35,7 +35,7 @@
"WebDirt": "github:dktr0/WebDirt"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/xen",
"version": "0.7.0",
"version": "0.7.1",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -33,7 +33,7 @@
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+4314 -3912
View File
File diff suppressed because it is too large Load Diff
+3 -1
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
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({});
});
});
+1 -1
View File
@@ -29,7 +29,7 @@ import '@strudel.cycles/xen/xen.mjs';
// import '@strudel.cycles/webaudio/webaudio.mjs';
// import '@strudel.cycles/serial/serial.mjs';
// import controls from '@strudel.cycles/core/controls.mjs';
import '../website/src/repl/prebake';
import '../website/src/repl/piano';
class MockedNode {
chain() {
+1 -5
View File
@@ -1,5 +1,4 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
import react from '@astrojs/react';
import mdx from '@astrojs/mdx';
@@ -12,7 +11,7 @@ import tailwind from '@astrojs/tailwind';
import AstroPWA from '@vite-pwa/astro';
// import { visualizer } from 'rollup-plugin-visualizer';
const site = `https://strudel.tidalcycles.org`; // root url without a path
const site = `https://strudel.tidalcycles.org/`; // root url without a path
const base = '/'; // base path of the strudel site
// this rehype plugin converts relative anchor links to absolute ones
@@ -47,9 +46,6 @@ const options = {
// https://astro.build/config
export default defineConfig({
integrations: [
// Enable Preact to support Preact JSX components.
preact(),
// Enable React for the Algolia search component.
react(),
mdx(options),
tailwind(),
+25 -26
View File
@@ -4,25 +4,24 @@
"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.13.1",
"@astrojs/mdx": "^0.13.0",
"@astrojs/preact": "^1.2.0",
"@astrojs/react": "^1.2.2",
"@astrojs/tailwind": "^2.1.3",
"@docsearch/css": "^3.1.0",
"@docsearch/react": "^3.1.0",
"@headlessui/react": "^1.7.7",
"@heroicons/react": "^2.0.13",
"@nanostores/persistent": "^0.7.0",
"@nanostores/react": "^0.4.1",
"@algolia/client-search": "^4.17.0",
"@astrojs/mdx": "^0.19.0",
"@astrojs/react": "^2.1.1",
"@astrojs/tailwind": "^3.1.1",
"@docsearch/css": "^3.3.4",
"@docsearch/react": "^3.3.4",
"@headlessui/react": "^1.7.14",
"@heroicons/react": "^2.0.17",
"@nanostores/persistent": "^0.8.0",
"@nanostores/react": "^0.5.0",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/csound": "workspace:*",
"@strudel.cycles/midi": "workspace:*",
@@ -35,31 +34,31 @@
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/xen": "workspace:*",
"@supabase/supabase-js": "^1.35.3",
"@supabase/supabase-js": "^2.21.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/node": "^18.0.0",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@uiw/codemirror-themes-all": "^4.19.8",
"astro": "^1.7.2",
"canvas": "^2.11.0",
"@types/node": "^18.16.3",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@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.0",
"nanostores": "^0.7.4",
"preact": "^10.7.3",
"nanoid": "^4.0.2",
"nanostores": "^0.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1",
"rehype-urls": "^1.1.1",
"remark-toc": "^8.0.1",
"tailwindcss": "^3.2.4"
"tailwindcss": "^3.3.2"
},
"devDependencies": {
"@vite-pwa/astro": "^0.0.1",
"@vite-pwa/astro": "^0.0.5",
"html-escaper": "^3.0.3",
"vite-plugin-pwa": "^0.14.1",
"vite-plugin-pwa": "^0.14.7",
"workbox-window": "^6.5.4"
}
}
Binary file not shown.
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright (c) 2014, The Fira Code Project Authors (https://github.com/tonsky/FiraCode)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+10
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>
+24
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>
);
}
+1 -1
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);
@@ -2,7 +2,6 @@
// import { getLanguageFromURL, KNOWN_LANGUAGE_CODES } from '../../languages';
import * as CONFIG from '../../config';
// import AstroLogo from './AstroLogo.astro';
import SidebarToggle from './SidebarToggle';
// import LanguageSelect from './LanguageSelect';
import Search from './Search';
import CommandLineIcon from '@heroicons/react/20/solid/CommandLineIcon';
@@ -23,9 +22,6 @@ const sidebar = SIDEBAR[langCode];
class="flex justify-between py-2 px-4 items-center h-14 max-h-14 bg-lineHighlight text-foreground"
title="Top Navigation"
>
<!-- <div class="menu-toggle">
<SidebarToggle client:idle />
</div> -->
<div class="flex overflow-visible items-center grow" style="overflow:visible">
<a href="/" class="flex items-center text-2xl space-x-2">
<h1 class="font-bold flex space-x-2 items-baseline text-xl">
@@ -1,39 +0,0 @@
/** @jsxImportSource preact */
import type { FunctionalComponent } from 'preact';
import { useState, useEffect } from 'preact/hooks';
const MenuToggle: FunctionalComponent = () => {
const [sidebarShown, setSidebarShown] = useState(false);
useEffect(() => {
const body = document.querySelector('body')!;
if (sidebarShown) {
body.classList.add('mobile-sidebar-toggle');
} else {
body.classList.remove('mobile-sidebar-toggle');
}
}, [sidebarShown]);
return (
<button
type="button"
aria-pressed={sidebarShown ? 'true' : 'false'}
id="menu-toggle"
onClick={() => setSidebarShown(!sidebarShown)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
<span className="sr-only">Toggle sidebar</span>
</button>
);
};
export default MenuToggle;
@@ -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];
---
@@ -18,10 +18,6 @@ const currentPage = Astro.url.pathname;
<article id="article" class="content">
<section class="main-section">
<!-- TODO: add dropdown toc on mobile -->
<!-- <nav class="block sm:hidden mb-8">
<span>On this Page:</span>
<TableOfContents client:media="(max-width: 50em)" headings={headings} currentPage={currentPage} />
</nav> -->
<div class="prose dark:prose-invert max-w-full pb-8">
<slot />
</div>
+19
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>
);
}
@@ -1,5 +1,4 @@
---
import ThemeToggleButton from './ThemeToggleButton';
import * as CONFIG from '../../config';
type Props = {
@@ -64,6 +63,3 @@ const showMoreSection = CONFIG.COMMUNITY_INVITE_URL;
)
}
</ul>
<div style="margin: 2rem 0; text-align: center;">
<!-- <ThemeToggleButton client:visible /> -->
</div>
@@ -1,17 +1,14 @@
import { unescape } from 'html-escaper';
import type { MarkdownHeading } from 'astro';
import type { FunctionalComponent } from 'preact';
import { useState, useEffect, useRef } from 'preact/hooks';
import type { FC } from 'react';
import { useState, useEffect, useRef } from 'react';
type ItemOffsets = {
id: string;
topOffset: number;
};
const TableOfContents: FunctionalComponent<{ headings: MarkdownHeading[]; currentPage: string }> = ({
headings = [],
currentPage,
}) => {
const TableOfContents: FC<{ headings: MarkdownHeading[]; currentPage: string }> = ({ headings = [], currentPage }) => {
const toc = useRef<any>();
const onThisPageID = 'on-this-page-heading';
const itemOffsets = useRef<ItemOffsets[]>([]);
@@ -73,8 +70,8 @@ const TableOfContents: FunctionalComponent<{ headings: MarkdownHeading[]; curren
<ul ref={toc}>
{headings
.filter(({ depth }) => depth >= minDepth && depth < 4)
.map((heading) => (
<li className={`w-full`}>
.map((heading, i) => (
<li className={`w-full`} key={i}>
<a
href={`${currentPage}#${heading.slug}`}
onClick={onLinkClick}
@@ -1,37 +0,0 @@
.theme-toggle {
display: inline-flex;
align-items: center;
gap: 0.25em;
padding: 0.33em 0.67em;
border-radius: 99em;
background-color: var(--theme-code-inline-bg);
}
.theme-toggle > label:focus-within {
outline: 2px solid transparent;
box-shadow: 0 0 0 0.08em var(--theme-accent), 0 0 0 0.12em white;
}
.theme-toggle > label {
color: var(--theme-code-inline-text);
position: relative;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.5;
}
.theme-toggle .checked {
color: var(--theme-accent);
opacity: 1;
}
input[name='theme-toggle'] {
position: absolute;
opacity: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: -1;
}
@@ -1,70 +0,0 @@
import type { FunctionalComponent } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import './ThemeToggleButton.css';
const themes = ['light', 'dark'];
const icons = [
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clipRule="evenodd"
/>
</svg>,
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>,
];
const ThemeToggle: FunctionalComponent = () => {
const [theme, setTheme] = useState(() => {
if (import.meta.env.SSR) {
return undefined;
}
if (typeof localStorage !== undefined && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
});
useEffect(() => {
const root = document.documentElement;
if (theme === 'light') {
root.classList.remove('theme-dark');
} else {
root.classList.add('theme-dark');
}
}, [theme]);
return (
<div className="theme-toggle">
{themes.map((t, i) => {
const icon = icons[i];
const checked = t === theme;
return (
<label className={checked ? ' checked' : ''}>
{icon}
<input
type="radio"
name="theme-toggle"
checked={checked}
value={t}
title={`Use ${t} theme`}
aria-label={`Use ${t} theme`}
onChange={() => {
localStorage.setItem('theme', t);
setTheme(t);
}}
/>
</label>
);
})}
</div>
);
};
export default ThemeToggle;
+33 -15
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' },
+10 -1
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;
}
+44 -4
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>
@@ -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).
@@ -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)
@@ -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).
@@ -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).
@@ -0,0 +1,3 @@
<script is:inline>
window.location.pathname = `/workshop/intro`;
</script>
@@ -0,0 +1,162 @@
---
title: Strudel Live Coding Workshop
layout: ../../../layouts/MainLayout.astro
---
# Strudel: Live Coding Workshop
![workshop](https://toplap-ka.de/img/2023-06-10_strudel_workshop.jpeg)
Mit: Felix Roos
Veranstaltet von TOPLAP Karlsruhe, Digitale Kunstfabrik e.V. & Die Anstoß e.V.
# Was ist Live Coding
"In live coding performance, performers create time-based works by programming them while these same works are being executed."
Roberts & Wakefield, 2016
"In einer Live Coding Performance werden künstlerische Werke mit Code kreiert, während dieselben Werke ausgeführt werden"
- Sound & Musik
- Grafiken / Visuelles
- Tanz
- Webmuster
- ...
Dieser Workshop = Musik Live Coding
![live coding](https://www.thetimes.co.uk/imageserver/image/%2Fmethode%2Ftimes%2Fprod%2Fweb%2Fbin%2F2a5b60ca-718e-11e9-a116-49ac88679a93.jpg?crop=3291%2C1851%2C573%2C722)
## TOPLAP
- gegründet 2004, heute ein weltweites Netzwerk
- Temporary Organisation for the Promotion of Live Algorithm Programming
- https://toplap.org/
- toplap karlsruhe: https://toplap-ka.de/
![alex ka](https://trxprds3.s3.amazonaws.com/uploads/2020/03/photo-by-yecto.jpg)
## Live Coding is not Software Engineering
"Live Coders are much more interested in causing problems than solving them" - Alex McLean
## Viele verschiedene Programme / Tools
- meistens frei und open source
- supercollider, orca, foxdot, sonic pi, gibber, sardine, estuary, hydra, ... tidalcycles, strudel
<div class="grid grid-cols-3">
<img
alt="orca"
src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTlZAfu-tbPdn2yzwIGIqkqMmB2HpbtKR5MHA&usqp=CAU"
/>
<img
alt="hydra"
src="https://cdn.glitch.com/9c3a06ab-0731-42ca-a1a3-3a249825d08f%2FScreen%20Shot%202018-06-24%20at%204.47.49%20PM.png?1531889983803"
/>
<img
alt="gibber"
src="https://i.vimeocdn.com/video/456734745-c00bd0c9f6a11a966a4346335f9d55b128584323d237e6d9630ab559a607dfce-d"
/>
</div>
## TidalCycles & Strudel
- TidalCycles = Computersprache für algorithmische Muster, meist für Musik verwendet
- Strudel = Version von TidalCycles für den Browser
- Keine Installation notwendig
- frei & open source
<a target="_blank" href="https://strudel.tidalcycles.org/?MCc4-cDOwIdG">
strudel.tidalcycles.org
</a>
## Videos
**Algorave Generation**
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/S2EZqikCIfY"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
**hellocatfood x yaxu**
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/Vomnc9R-7mw?start=261"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
**Heavy Lifting x Graham Dunning**
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/ca3J1cztnrc?start=719"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
**Andrew Sorensen - Five over Four**
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/FYWtlV4JV6o?start=226"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
**boggo - Live Coding Metal Djents**
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/n0rhn9-PRwE?start=226"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
**froos - Solstice Night 2022**
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/P1DDsOvcyco?start=414"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
## Strudel Demos
- [yellow magic orchestra - firecracker](https://felixroos.github.io/strudel/#Ly9jb250aW51aW5nIHRoZSB0cmFuc2NyaXB0aW9uIG9mIG9uZSBvZiBteSBmYXZvcml0ZSB0cmFja3MgaW4gZWxlY3Ryb25pYyBtdXNpYyBoaXN0b3J5CgpsZXQgZHJ1bXMgPSBzdGFjaygKICBzKCJ%2BQDMgaGggfiBoaCB%2BIH4iKS5sYXRlKC4wMikuc3BlZWQoLjgpLmdhaW4oLjIpLnJvb20oLjQpLmhjdXRvZmYoNDAwMCksCiAgcygiYmQiKSwKICBzKCJ%2BIHNkIikucm9vbSgiPDAgLjY%2BIiksCiAgcygifiBwZXJjOjEqMiB%2BIHBlcmM6MSIpLnNwZWVkKCIyIDMiKS5yb29tKC4yKS5nYWluKC4yNSkKKS5iYW5rKCdSb2xhbmRDb21wdXJoeXRobTEwMDAnKQoKbGV0IHdoaXN0bGUgPSB4PT54Lm5vdGUoKS5zKCdzaW5lJykKICAuZGVjYXkoLjEpLnN1c3RhaW4oLjIpLmdhaW4oLjIpCiAgLnBhbiguMikucm9vbSguOCkKCmxldCBiYXNzID0geD0%2BeC5hZGQoIjAsMTIsMTEuOTIiKS5ub3RlKCkKICAucygic2F3dG9vdGgsc3F1YXJlIikuY3V0b2ZmKDgwMCkKICAuZGVjYXkoLjEpLnN1c3RhaW4oLjEpLmdhaW4oLjQpCgpsZXQgYmxpcHMgPSB4PT54Lm5vdGUoKS5qdXgocmV2KQogIC5zKCdzYXd0b290aCcpLmN1dG9mZig1MDApLmdhaW4oc2luZS5yYW5nZSguMiwuOCkuc2xvdyg0KSkKICAuZGVjYXkoLjA4KS5zdXN0YWluKDApCgpsZXQgZ3Jvb3ZlQSA9ICIyIH4gMyB%2BIDQgfiB%2BIC0xIH4gLTEgMCB%2BIC0xIH4gMCB%2BIi5sYXllcigKICB4PT54LnNjYWxlKCdlNyBtaW5vcicpLmxheWVyKHdoaXN0bGUpLAogIHg9Pnguc2NhbGUoJ2UxIG1pbm9yJykubGF5ZXIoYmFzcykKKS5zdGFjaygKICAiMioxNiIuZGVncmFkZUJ5KCIxIC4yQDciKS5zY2FsZSgnZTMgbWlub3InKS5sYXllcihibGlwcykKKS5zbG93KDIpCiAgLnN0YWNrKGRydW1zKQoKbGV0IG1lbG9keUEgPSBjYXQoCiAgIjYgWzcgNl0gNCAzIDIgMCAtMSAwIiwKICAiWzkgMTBdIFs5IDddIDYgNCAzIDQgNiA3Ii5zdWIoNyksCiAgIns2IDcgNiA0IDMgfn0lMTYiLnN1Yig3KS5yZXN0YXJ0KCJ4IiksCiAgIjMqMiA0KjIgNioyIDcqMiIuc3ViKDcpCikuc2xvdygyKS5hZGQoIjAiKS5zY2FsZSgnZTQgbWlub3InKS5ub3RlKCkKLmxheWVyKAogICAgeD0%2BeC5zKCJ4eWxvcGhvbmVfaGFyZF9mZiIpLmdhaW4oMS4yKSwKICAgIHg9Pngucygic2F3dG9vdGgiKS5nYWluKC40KS5kZWNheSguMSkuc3VzdGFpbigwLjIpLmFkZChub3RlKCIxMiwxMi4wNSIpKQogICkubXVsKGdhaW4oLjUpKQoKbGV0IG1lbG9keUIgPSAiWzBAMiB%2BIDBANCAxIDJAMiB%2BIDJANV0vNCIuc3ViKCIwLDMiKQogIC5zY2FsZSgnZzQgbWFqb3InKS5ub3RlKCkubGF5ZXIoCiAgICB4PT54LnMoInJlY29yZGVyX2FsdG9fc3VzIikuY2xpcCgxKS5zaGFwZSguOCkuZ2FpbiguNzUpLAogICAgeD0%2BeC5zKCJzcXVhcmUiKS5nYWluKC4xKS5hZGQobm90ZSgxMikpCiAgKQogIC5yb29tKC42KS5yZWxlYXNlKC4yKQoKCmZ1bmN0aW9uIGFycmFuZ2UoLi4uc2VjdGlvbnMpIHsKICBsZXQgdG90YWwgPSBzZWN0aW9ucy5yZWR1Y2UoKHN1bSwgW2N5Y2xlc10pID0%2BIHN1bSArIGN5Y2xlcywgMCkKICBzZWN0aW9ucyA9IHNlY3Rpb25zLm1hcCgoW2N5Y2xlcywgc2VjdGlvbl0pID0%2BIFtjeWNsZXMsIHNlY3Rpb24uZmFzdChjeWNsZXMpXSkKICByZXR1cm4gdGltZUNhdCguLi5zZWN0aW9ucykuc2xvdyh0b3RhbCkKfQoKZ3Jvb3ZlQS5zdGFjaygKICBhcnJhbmdlKFs4LCBzaWxlbmNlXSwgWzgsIG1lbG9keUFdLCBbNCwgbWVsb2R5Ql0sIFs4LCBtZWxvZHlBXSwgWzQsIG1lbG9keUJdKQopCg%3D%3D)
- [reliefpfeier](https://felixroos.github.io/strudel/#Ly8gdGl0bGU6IHJlbGllZnBmZWlsZXIKcygiPHBlcmMgbWlzYyByaW0oMyw4KSBvaD4iKS5iYW5rKCdZYW1haGFURzMzJykuZW5kKCI8MUAzIC4wMj4iKQoubigiPDAgMSAyIDM%2BIi5vdXRzaWRlKDQsIHBhbGluZHJvbWUpKQoub2ZmKCI8MSAyIDMgND4iLmRpdigiPDIgMyA0IDY%2BLzQiKSwgeD0%2BeC5zcGVlZCgiPDIgMyA0PiIpKQogIAouY2h1bmsoNCwgcGx5KCI8MiAzIDQ%2BIikpCi5sYXN0T2YoOCwgc2VnbWVudCgiPDMgNiA4PiIpKQogIAoubGF5ZXIoCiAgeD0%2BeC5jdXRvZmYocGVybGluLnJhbmdlKDEwMDAsMjAwMDApKSwKICB4PT54LmJhbmsoJycpLnNsb3coMikucygncGlhbm8nKS5nYWluKC42KS5kZWdyYWRlKCkubXVsKHNwZWVkKC0xKSkKICAgIC5iYW5kZihzaW5lLnJhbmdlKDIwMDAsMTAwMDApLnNsb3coNykpLnJlc29uYW5jZSgxMikuYXR0YWNrKC44KQogICAgLnBhbihyYW5kKSwKKQogIAoubGFzdE9mKDYsIHg9PnguY3J1c2goIjgiKSkKLmdhaW4oc2F3LnJhbmdlKC40LC45KSkKLm91dHNpZGUoNCwganV4QnkoLjUsIHJldikpCi5ub3RlKCIwIDUgNCBbMiw3XSIuc2NhbGUoJ0cxIG1pbm9yJykpCi5yb29tKC40KS5mYXN0KDIvMyk%3D)
- [insect schneckno](https://felixroos.github.io/strudel/#Ly8gYWxzbyBoYXZpbmcgYSBmdW4gdGltZSB3aXRoICNzaGFiZGEKLy90aXRsZTogaW5zZWN0IHNjaG5lY2tubwoKYXdhaXQgc2FtcGxlcygnaHR0cHM6Ly9zaGFiZGEubmRyZS5nci9iYXNzOjQsaGloYXQ6NCxyaW1zaG90OjIuanNvbj9zdHJ1ZGVsPTEnKQoKc3RhY2soCiAgbigiezEgMiAzfSUxMiIpLnNwZWVkKHNpbmUucmFuZ2UoMSwxLjIpKQogIC5zKCJoaWhhdCIpLmVuZChzYXcucmFuZ2V4KC4wNCwuMTUpLnNsb3coOCkubGF0ZSguMjUpKQogIC5nYWluKCIuNSAuOSIuZmFzdCgyKSkKICAuYnBmKHNpbmUucmFuZ2UoNDAwMCwxMDAwMCkuc2xvdyg0KSkKICAucm9vbSguOCkuZ2FpbiguNikucGFuKHNpbmUucmFuZ2UoLjMsLjYpLnNsb3coNykpCiAgLAogIG4oIjAgWzJAMyAxQDJdIDMqOCAxIikucygiYmFzcyIpLmN1dCgxKQogICwKICBzKCJiZCoyIikuc3BlZWQoLjYpLmJhbmsoJ1JvbGFuZFRSOTA5JykKICAsCiAgcygifiBbY3Ascmltc2hvdCo8MSAyPl0iKS5vZmYoMS84LHg9Pnguc3BlZWQoIjw0IC0yPiIpKS5zbG93KDIpCiAgLAogIG5vdGUoInswIDQgNSozIDIgMSAtMSoyIC0yfSUyIi5zY2FsZSgnZzIgZG9yaWFuJykpLmxhdGUoLjI4KQogIC5kZWxheSguNSkuZGVsYXl0aW1lKC4xNjYpLnJvb20oLjUpCiAgLnMoIlJoeXRobUFjZV9wZXJjOjEiKS5vZmYoMS8yNCwgeD0+eC5hZGQobm90ZSgxMikpKS5zbG93KDIpCiAgLmp1eEJ5KC43NSwgcmV2KS5scGYocGVybGluLnJhbmdlKDgwMCw4MDAwKSkubHBxKDExKQogIC5nYWluKHNpbmUucmFuZ2UoMCwuOCkuc2xvdygxMikpCikuZmFzdCgzLzQp)
- [hip hop with sampling](https://felixroos.github.io/strudel/#Ly8gYW5vdGhlciBzdHVtYmxpbmcgYmVhdCB3aXRoIHF1aW50dXBsZXQgc3dpbmcgYW5kIHNvbWUgc2xpY2VkIHN0cmluZyBpbnN0cnVtZW50cwphd2FpdCBzYW1wbGVzKCdnaXRodWI6ZmVsaXhyb29zL3NhbXBsZXMvbWFpbicpCmF3YWl0IHNhbXBsZXMoeyAKICBqel9ndHI6J2h0dHBzOi8vY2RuLmZyZWVzb3VuZC5vcmcvcHJldmlld3MvNTkvNTk1MDBfNTM4MzgzLWhxLm1wMycsIAogIGp6X2Jhc3M6J2h0dHBzOi8vY2RuLmZyZWVzb3VuZC5vcmcvcHJldmlld3MvMTYvMTYxNzFfMTg4MTEtaHEubXAzJ30KKQpQYXR0ZXJuLnByb3RvdHlwZS5zbGljZSA9IGZ1bmN0aW9uKHNsaWNlcywgY29udHJvbCkgewogIHJldHVybiBjb250cm9sLmZtYXAoaSA9PiB0aGlzLmJlZ2luKGkvc2xpY2VzKS5lbmQoKGkrMSkvc2xpY2VzKSkub3V0ZXJKb2luKCkKfQoKc3RhY2soCiAgLy8gZ3VpdGFyIHNsaWNlcwogIHMoJ2p6X2d0cicpLnNsaWNlKDgsICJbM0AzIDNAMl0qMiAyKlszIDZdIDEqNCAwKjIiKS5zcGVlZCgiMSAtMiIpLmxhdGUoLjAwOCkKICAuc2xvdyg0KS5jdXQoMSkucmVsZWFzZSguMikuYXR0YWNrKCIwLjA1IDAuMjUiKS5wYW4oc2luZS5yYW5nZSguMiwuOCkuc2xvdyg0KSkKICAubXVsKHNwZWVkKDQvMykpLmdhaW4ocmFuZC5yYW5nZSguMywuNikpCiAgLmxwZihzaW5lLnJhbmdlKDUwMCw1MDAwKS5zbG93KDgpKS5scHEoMTIpCiAgLCAvLyBiYXNzIHNsaWNlcwogIHMoJ2p6X2Jhc3MnKS5zbGljZSg4LCAiWzAgWzdAMyA3QDJdXSBbMSA2KjhdIDIgPDUgNj4qMiIpLmRlY2F5KC4yKS5zdXN0YWluKC41KQogIC5hZGQoYmVnaW4oLjAxMikuZW5kKC4wKSkubXVsKGJlZ2luKC44OCkuZW5kKC44OSkpLnJlbGVhc2UoLjEpCiAgLnNsb3coNCkuY3V0KDIpLm5vdGUoImQyIikKICAsIC8vIGtpY2sgCiAgcygiW2JkOjAoPDIgMT4sNSw8MyAwPiksfiA8fiBbfkAzIGJkQDJdPl0gfiIpLmJhbmsoJ1lhbWFoYVJYNScpCiAgLmdhaW4ocmFuZC5yYW5nZSguNSwuOSkpLm1hc2soIjwxQDYgMEAyPiIpCiAgLCAvLyBzbmFyZQogIHMoIn4gPHNkIHNkPiIpLmJhbmsoJ1lhbWFoYVJYNScpLmdhaW4oLjgpLnJvb20oIjwwIC4xPiIpLnNwZWVkKCI8LjkgMT4iKQogIC5lbmQoIjwuMSAuMj4iKS5yZWxlYXNlKC4xKQogICwgLy8gaGloYXQKICBzKCJoaEAzIGhoQDIiLmZhc3QoMikpLm4oMSkuYmFuaygnS29yZ00xJykuc3BlZWQoLjgpLmVuZCgwKS5yZWxlYXNlKC4wNSkuZ2FpbiguNSkKICAub2ZmKDEvNSwgeD0%2BeC5tdWwoc3BlZWQoMS4yKSkuZ2FpbiguMSkuZGVncmFkZSgpKQogICwgLy8geHlsb3Bob25lCiAgbm90ZSgiPFtlNEAzIGEzXSBkNCB%2BQDI%2BLzIiLmFkZCgyNCkpCiAgLnMoInh5bG8iKS5nYWluKC40KS5sYXRlKDQpLmNsaXAoMSkKICAuaHBmKDUwMCkuaHBxKDE2KQogIC5yb29tKDEpLmRlbGF5KC41KS5kZWxheXRpbWUoLjExKS5kZWxheWZlZWRiYWNrKC45KQogIC5tYXNrKCI8MCAxPi84IikKKS5lYXJseSgyKQouZmFzdCgyLzMp)
- [fuzzy planet](https://felixroos.github.io/strudel/#Ly8gc3BlZWRydW4gbm8gMywgdGhpcyB0aW1lIGEgbW9yZSBmdXp6eSB0eXBlIG9mIGdlbnJlCgphd2FpdCBzYW1wbGVzKCdnaXRodWI6ZmVsaXhyb29zL3NhbXBsZXMvbWFpbicpCmF3YWl0IHNhbXBsZXMoJ2dpdGh1Yjp0aWRhbGN5Y2xlcy9EaXJ0LVNhbXBsZXMvbWFzdGVyJykKCnNhbXBsZXMoeyBwbHVjazoge2U0OiAnaHR0cHM6Ly9jZG4uZnJlZXNvdW5kLm9yZy9wcmV2aWV3cy81OTIvNTkyNDQyXzEwODcwODgwLWxxLm1wMyd9IH0pCgphZGRWb2ljaW5ncygnY29va2llJywgewogIDc6IFsnM00gN20gOU0gMTJQIDE1UCcsICc3bSAxME0gMTNNIDE2TSAxOVAnXSwKICAnXjcnOiBbJzNNIDZNIDlNIDEyUCAxNE0nLCAnN00gMTBNIDEzTSAxNk0gMTlQJ10sCiAgbTc6IFsnOFAgMTFQIDE0bSAxN20gMTlQJywgJzVQIDhQIDExUCAxNG0gMTdtJ10sCiAgbTdiNTogWyczbSA1ZCA4UCAxMVAgMTRtJywgJzVkIDhQIDExUCAxNG0gMTdtJ10sCiAgbzc6IFsnM20gNk0gOU0gMTFBIDE1UCddLAogICc3YWx0JzogWyczTSA3bSAxMG0gMTNtIDE1UCddLAogICc3IzExJzogWyc3bSAxMG0gMTNtIDE1UCAxN20nXSwKfSwgWydDMycsICdDNiddKQoKc3RhY2soCiAgIjxDbTchMiBEbTdiNSBDIzcjMTE%2BLzQiLmxheWVyKAogICAgeD0%2BeC52b2ljaW5ncygnY29va2llJykubm90ZSgpLnMoJ3BsdWNrJykuY2xpcCgxKQogICAgICAuYXJwKCJbMyAyIDEgMEAyMV0gWzAgMSAyIDMgNEAyMV0iLmxlZ2F0byg0MCkuZmFzdCgyKSkuZWFybHkoMS80OCkKICAgICAgLmxwZihzaW5lLnJhbmdlKDQwMCw0MDAwKS5zbG93KDE2KSkuZ2FpbigxLjIpLnBhbiguMjUpLAogICAgeD0%2BeC5yb290Tm90ZXMoMikuYWRkLm91dCgiWzBAMyAwXSBbNyAxMiB%2BIDEyXSB%2BIH4iLnNsb3coMikpLm5vdGUoKQogICAgICAucygnamF6emJhc3MnKS5jbGlwKDEpLmdhaW4oLjgpLAogICAgeD0%2BeC52b2ljaW5ncygnY29va2llJykuYWRkKCItLjEsLjEiKS5ub3RlKCkKICAgICAgLnMoJ3NpbmUnKS5hdHRhY2soLjUpLmRlY2F5KC4yKS5zdXN0YWluKC41KS5nYWluKC4xKS5wYW4oLjc1KSwKICAgIHg9Pngudm9pY2luZ3MoJ2Nvb2tpZScpLmFkZCgwKS5hcnAoInsxIFsyIDNdIDAhM30lNCIpLm5vdGUoKQogICAgICAucygnc3F1YXJlJykubHBmKHNpbmUucmFuZ2UoNDAwLDIwMDApLnNsb3coMzYpKS5kZWdyYWRlKCkKICAgICAgLmRlY2F5KC4xKS5zdXN0YWluKDApLm9mZigxLzYsIGFkZChub3RlKDEyKSkpLnJvb20oLjgpLmdhaW4oLjYpCiAgKSwKICBzdGFjaygKICAgIHMoImJkIFt%2BIDx%2BIGJkPl0iKS5uKDIpLnNwZWVkKC41KS5iYW5rKCdSb2xhbmRUUjkwOScpLmdhaW4oIi44IC41IiksCiAgICBzKCJiZCoyIi5sYXRlKC4yNSkpLnNwZWVkKC41KS5iYW5rKCdVbml2b3hNaWNyb1JoeXRobWVyMTInKS5ocGYoMjAwMCkucm9vbSguMjUpLAogICAgcygifiBzZCIpLnNwZWVkKC4yNSkuYmFuaygnVW5pdm94TWljcm9SaHl0aG1lcjEyJykucm9vbSguNSkuZW5kKCI8LjA0QDMgLjg%2BIikKICApCikKLnNsb3coMikuY3BtKDkyKQ%3D%3D)
- [wutang](https://felixroos.github.io/strudel/#YXdhaXQgc2FtcGxlcygnZ2l0aHViOmZlbGl4cm9vcy9zYW1wbGVzL21haW4nKQphd2FpdCBzYW1wbGVzKCdnaXRodWI6eWF4dS9jbGVhbi1icmVha3MvbWFpbi8nKQoKY29uc3QgcnVuID0gKG4pID0%2BIHNhdy5yYW5nZSgwLG4pLmZsb29yKCkuc2VnbWVudChuKQoKc3RhY2soCiAgbihydW4oOCkuc2xvdygyKS5hZGQoMSkpLnMoInJlbmUiKQogIC5jbGlwKDEpLmxhc3RPZig0LHJldikKICAuc29tZXRpbWVzKHBseSgyKSkKICAubGFzdE9mKDgsIHg9Pnguc3BlZWQoMS41KSksCiAgcygiYWN0aW9uIikubG9vcEF0KDIpLmNob3AoOCkuZ2FpbiguNCkKICAubHBmKHNpbmUucmFuZ2UoMTUwMCw4MDAwKS5zbG93KDgpKS5scHEoMTIpCiAgLnJhcmVseSh4PT54LnNwZWVkKCIxLDQiKS5tdWwoZ2FpbiguNSkpKQogIC5hbG1vc3ROZXZlcih4PT54LnBseSgiNCIpKQopCiAgCg%3D%3D)
- [african groove](https://felixroos.github.io/strudel/#YXdhaXQgc2FtcGxlcygnZ2l0aHViOmZlbGl4cm9vcy9zYW1wbGVzL21haW4nKQoKc3RhY2soCiAgcygiYmQgc2QsaGgoPDMgNCAzIDU%2BLDgsPDAgMSAwIFswIDFdPiksPH5AMyBvaD4iKS5iYW5rKCdwcmVzaWRlbnQnKSwKICBzdGFjaygKICAgIG5vdGUoIntjIGViIDxnIGEgYmIgYT4gZWJ9JTMqPDFAMyAyPiIuYWRkKDEyKSkucygiZ21fZWxlY3RyaWNfZ3VpdGFyX211dGVkOjAiKQogICAgLmxwZihjb3NpbmUucmFuZ2UoMjAwLDgwMDApLnNsb3coMTYpKS5wYW4oc2luZS5zbG93KDgpKSwKICAgIG5vdGUoIn4gW1tjNCA8ZzMgYTMgYmIzIGEzPiBlYjNdIH5ANF0iLmFkZCgxMikpLnMoImdtX2VsZWN0cmljX2d1aXRhcl9jbGVhbjowIikucmVsZWFzZSguNSkuZ2FpbiguMzUpCiAgICAuYnBmKHBlcmxpbi5yYW5nZSgxMDAwLDUwMDApKS5zaGFwZSguNSksCiAgICBub3RlKCI8W3tjMioyIGcyIFtlYjIgZWIzXX0lMiB%2BXSBbZWIyIH5dKDMsOCk%2BIikucygiZ21fc2xhcF9iYXNzXzE6MSIpLm9mZiguMjUsIGFkZChub3RlKDEyKSkpLmxwZig4MDApLAogICAgbm90ZSgiYzUqOCIpLmxwZihzaW5lLnJhbmdlKDIwMCw4MDAwKS5zbG93KDE2KSkubHBxKDUpLnMoJ3h5bG8nKS5kZWdyYWRlKCkuanV4KHJldikuZ2FpbiguNCkKICApLnN1Yihub3RlKCI8NUAxNiA3QDg%2BIikpCikuZmFzdCg0LzUpLnJvb20oLjUp)
- [yaxu groove](https://felixroos.github.io/strudel/#c2V0Y3BzKDEuMSkKCnMoIntiZCB%2BIHJpbSB%2BLCB%2BIH4gaGh9IikuYmFuaygnUm9sYW5kVFI5MDknKQogIC5jaHVuayg0LCBmYXN0KDIpKQogIC5qdXgocmV2KQogIC5vZmYoLjI1LCBtdWwoc3BlZWQoLjgpKSkKICAuc3VwZXJpbXBvc2UoCiAgICB4PT54LnZvd2VsKCI8YSA8ZSBpPj4vNCIpLnJvb20oMikuc2xvdygyKS5sYXRlKC4yNSkuY29sb3IoIiMwMGZmMDA1MCIpCiAgKQogIC5sYXN0T2YoOCwgbXVsKHNwZWVkKC0yKSkpCiAgLmNvbG9yKCJbIzAwMDBmZjUwICNmZmZmMDAyMF0qMyIpCiAgLnB1bmNoY2FyZCgpCgo%3D)
- [dubby techno](https://felixroos.github.io/strudel/#Ly8gaHR0cHM6Ly9zdHJ1ZGVsLnRpZGFsY3ljbGVzLm9yZy8%2FTUNjNC1jRE93SWRHCi8vIGZyb29zIC0gY3J1bWJsaW5nIGR1bXBsaW5ncwoKbGV0IGNob3JkcyA9ICI8RG0hNyA8QSEzIDxGIEM%2BPj4iCgpzdGFjaygKICBzKCI8fiExNiBiZCoyITQwPix%2BIDx%2BITMyIHNkOjIhMjQ%2BLCA8fiEyOCBbfiBoaDoxXSoyITI4PiIpCiAgLmJhbmsoJ1JvbGFuZFRSOTA5JyksCiAgCiAgcygifiBoaCwgfiByaW0gfkAyIikKICAuYmFuaygnUm9sYW5kVFI5MDknKQogIC5qdXhCeSguNSwgcmV2KQogIC5zZXQubWl4KHNwZWVkKCIxIFs8MSAyPiA8LjkgPC43NSAuOD4%2BXSIpKQogIC5sYXN0T2YoOCwgZmFzdCgiMSBbPDMgND4gMl0iKSkuZ2FpbiguOCkKICAubHBmKHNpbmUucmFuZ2UoODAwLCA4MDAwKS5zbG93KDE1KS5sYXRlKDUpKQogIC5nYWluKC41KSwKICAKICBjaG9yZHMudm9pY2luZ3MoJ3RyaWFkcycpLm5vdGUoKQogIC5hcnAoIjAgMSAyIDEiKS5hZGQobm90ZSgiPDAgMTI%2BLzE2IikpCiAgLnJhcmVseSh4PT54LmFkZChub3RlKCIxMiIpKSkKICAucygncGlhbm8nKS5jbGlwKDEpCiAgLmRlY2F5KHBlcmxpbi5yYW5nZSguMSwuNSkpLnN1c3RhaW4oMCkKICAucm9vbShzaW5lLnJhbmdlKDAsIDQpLnNsb3coMTYpKQogIC5vZmYoMS84LCBhZGQobm90ZSgxMikpKQogIC5scGYoY29zaW5lLnJhbmdlKDIwMCwyMDAwKS5zbG93KDMyKSkKICAubHBxKDgpLmp1eChyZXYpLmdhaW4oLjQpLAogIAogIGNob3Jkcy52b2ljaW5ncygndHJpYWRzJykKICAubm90ZSgpLmF0dGFjaygiPC4wMSAuMDg%2BLzMyIikuc3VzdGFpbigwKS5zdHJ1Y3QoIlt%2BIHhdKjIiKQogIC5zKCJzcXVhcmUsdHJpYW5nbGUiKQogIC5scGYocGVybGluLnJhbmdlKDQwMCw4MDApLnNsb3coMS4yKSkKICAucmFyZWx5KG9mZigxLzgseD0%2BeC5hZGQobm90ZSgiMTIiKSkubHBmKDgwMCkpKQogIC5yb29tKC41KS5nYWluKC42KSwKICAKICBjaG9yZHMucm9vdE5vdGVzKDIpLmFkZCgiMCwuMSIpLm5vdGUoKS5zKCdzYXd0b290aCcpCiAgLmRlY2F5KC4xKS5zdXN0YWluKDApCiAgLmdhaW4oIlsuMyAxXSoyIikKICAucGx5KDgpLmxwZihzaW5lLnJhbmdlKDgwMCwyMDAwKS5zbG93KDgpKQogIC5scHEoMTApLm11bChnYWluKC44KSkKKQogIC5sYXRlKCJbMCAuMDJdKjQiKQogIC5jcG0oMTAwLzIp)
@@ -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))`} /> |
+68
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))`} /> |
+3 -1
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>
+13 -12
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)
+17 -16
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 />
+2 -2
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:

Some files were not shown because too many files have changed in this diff Show More