From 1494cc38fce198d74cdd5b07aaecbad87bac3044 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 08:55:43 +0200 Subject: [PATCH 01/86] add vite-vanilla-repl-cm6 --- .../examples/vite-vanilla-repl-cm6/.gitignore | 24 ++++++ .../examples/vite-vanilla-repl-cm6/README.md | 8 ++ .../vite-vanilla-repl-cm6/codemirror.js | 83 +++++++++++++++++++ .../vite-vanilla-repl-cm6/highlighter.js | 44 ++++++++++ .../examples/vite-vanilla-repl-cm6/index.html | 21 +++++ .../examples/vite-vanilla-repl-cm6/main.js | 32 +++++++ .../vite-vanilla-repl-cm6/package.json | 27 ++++++ .../examples/vite-vanilla-repl-cm6/strudel.js | 38 +++++++++ .../examples/vite-vanilla-repl-cm6/style.css | 24 ++++++ .../examples/vite-vanilla-repl-cm6/tunes.mjs | 32 +++++++ .../core/examples/vite-vanilla-repl/README.md | 2 - pnpm-lock.yaml | 66 ++++++++++++--- pnpm-workspace.yaml | 1 + 13 files changed, 387 insertions(+), 15 deletions(-) create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/.gitignore create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/README.md create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/codemirror.js create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/highlighter.js create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/index.html create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/main.js create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/package.json create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/strudel.js create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/style.css create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs diff --git a/packages/core/examples/vite-vanilla-repl-cm6/.gitignore b/packages/core/examples/vite-vanilla-repl-cm6/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/.gitignore @@ -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? diff --git a/packages/core/examples/vite-vanilla-repl-cm6/README.md b/packages/core/examples/vite-vanilla-repl-cm6/README.md new file mode 100644 index 00000000..70d18e09 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/README.md @@ -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 +``` diff --git a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js new file mode 100644 index 00000000..7ba377ab --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js @@ -0,0 +1,83 @@ +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 './style.css'; + +// https://codemirror.net/docs/guide/ +export function initEditor(initialCode, onUpdate) { + let state = EditorState.create({ + doc: initialCode, + extensions: [ + javascript(), + lineNumbers(), + /*gutter({ + class: "cm-mygutter" + }),*/ + highlightField, + highlightActiveLineGutter(), + //markLineGutter, + syntaxHighlighting(defaultHighlightStyle), + keymap.of(defaultKeymap), + //flashField, + EditorView.updateListener.of((v) => { + onUpdate(v); + }), + ], + }); + + return new EditorView({ + state, + parent: document.getElementById('editor'), + }); +} + +// codemirror specific highlighting logic + +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 }) }); +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js b/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js new file mode 100644 index 00000000..0a306e70 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js @@ -0,0 +1,44 @@ +const round = (x) => Math.round(x * 1000) / 1000; + +// this class can be used to create a code highlighter +// it is encapsulated from the editor via the onUpdate callback +// the scheduler is expected to be an instance of Cyclist +export class Highlighter { + constructor(onUpdate) { + this.onUpdate = onUpdate; + } + start(scheduler) { + let highlights = []; + let lastEnd = 0; + this.stop(); + const self = this; + let frame = requestAnimationFrame(function updateHighlights() { + try { + const time = scheduler.now(); + // force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away + // see https://github.com/tidalcycles/strudel/issues/108 + const begin = Math.max(lastEnd ?? time, time - 1 / 10, -0.01); // negative time seems buggy + const span = [round(begin), round(time + 1 / 60)]; + lastEnd = span[1]; + highlights = highlights.filter((hap) => hap.whole.end > time); // keep only highlights that are still active + const haps = scheduler.pattern + .queryArc(...span) + .filter((hap) => hap.hasOnset()); + highlights = highlights.concat(haps); // add potential new onsets + self.onUpdate(highlights); // highlight all still active + new active haps + } catch (err) { + self.onUpdate([]); + } + frame = requestAnimationFrame(updateHighlights); + }); + self.cancel = () => { + cancelAnimationFrame(frame); + }; + } + stop() { + if (this.cancel) { + this.cancel(); + this.onUpdate([]); + } + } +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/index.html b/packages/core/examples/vite-vanilla-repl-cm6/index.html new file mode 100644 index 00000000..32381f03 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/index.html @@ -0,0 +1,21 @@ + + + + + + Vite Vanilla Strudel REPL + + +
+ +
+
+
+
+
+ + + diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js new file mode 100644 index 00000000..814f3609 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -0,0 +1,32 @@ +// moved from sandbox: https://codesandbox.io/s/vanilla-codemirror-strudel-2wb7yw?file=/index.html:114-186 + +import { initEditor, highlightHaps } from './codemirror'; +import { initStrudel } from './strudel'; +import { Highlighter } from './highlighter'; +import { bumpStreet } from './tunes'; +let code = bumpStreet; + +const view = initEditor(code, (v) => { + code = v.state.doc.toString(); +}); +const repl = initStrudel(); + +let highlighter = new Highlighter((haps) => highlightHaps(view, haps)); + +document.getElementById('play').addEventListener('click', async () => { + const { evaluate, scheduler } = await repl; + if (!scheduler.started) { + scheduler.stop(); + scheduler.lastEnd = 0; + await evaluate(code); + highlighter.start(scheduler); + } else { + await evaluate(code); + } +}); + +document.getElementById('stop').addEventListener('click', async () => { + const { stop } = await repl; + stop(); + highlighter.stop(); +}); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/package.json b/packages/core/examples/vite-vanilla-repl-cm6/package.json new file mode 100644 index 00000000..f84b23ed --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/package.json @@ -0,0 +1,27 @@ +{ + "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": { + "@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", + "@strudel.cycles/core": "workspace:*", + "@strudel.cycles/mini": "workspace:*", + "@strudel.cycles/tonal": "workspace:*", + "@strudel.cycles/transpiler": "workspace:*", + "@strudel.cycles/webaudio": "workspace:*", + "@strudel.cycles/soundfonts": "workspace:*" + } +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js new file mode 100644 index 00000000..a1e999ee --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js @@ -0,0 +1,38 @@ +import { controls, repl, evalScope } from "@strudel.cycles/core"; +import { transpiler } from "@strudel.cycles/transpiler"; +import { + getAudioContext, + webaudioOutput, + initAudioOnFirstClick, + registerSynthSounds +} from "@strudel.cycles/webaudio"; +import { registerSoundfonts } from "@strudel.cycles/soundfonts"; + +const initAudio = initAudioOnFirstClick(); +const ctx = getAudioContext(); + +const loadModules = (scope = {}) => + evalScope( + controls, + import("@strudel.cycles/core"), + import("@strudel.cycles/mini"), + import("@strudel.cycles/tonal"), + import("@strudel.cycles/webaudio"), + scope + ); + +export async function initStrudel(options = {}) { + await Promise.all([ + initAudio, + loadModules(), + registerSynthSounds(), + registerSoundfonts() + ]); + + return repl({ + defaultOutput: webaudioOutput, + getTime: () => ctx.currentTime, + transpiler, + ...options + }); +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/style.css b/packages/core/examples/vite-vanilla-repl-cm6/style.css new file mode 100644 index 00000000..20970778 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/style.css @@ -0,0 +1,24 @@ +body, +html { + margin: 0; + height: 100%; +} + +#editor { + overflow: auto; + height: 100%; +} + +.cm-editor { + height: 100%; +} + +main { + height: 100%; + display: flex; + flex-direction: column; +} + +.container { + flex-grow: 1; +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs b/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs new file mode 100644 index 00000000..4b0155e4 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs @@ -0,0 +1,32 @@ +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("") + // wait for it... +).fast(2/3) + //.crush(6) // remove "//" if you dare`; diff --git a/packages/core/examples/vite-vanilla-repl/README.md b/packages/core/examples/vite-vanilla-repl/README.md index ac2ed20f..d7f65720 100644 --- a/packages/core/examples/vite-vanilla-repl/README.md +++ b/packages/core/examples/vite-vanilla-repl/README.md @@ -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) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4df850b..40a6442f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,46 @@ importers: specifier: ^4.3.3 version: 4.3.3(@types/node@18.16.3) + packages/core/examples/vite-vanilla-repl-cm6: + dependencies: + '@codemirror/commands': + specifier: ^6.2.4 + version: 6.2.4 + '@codemirror/lang-javascript': + specifier: ^6.1.7 + version: 6.1.7 + '@codemirror/language': + specifier: ^6.6.0 + version: 6.6.0 + '@codemirror/state': + specifier: ^6.2.0 + version: 6.2.0 + '@codemirror/view': + specifier: ^6.10.0 + version: 6.10.0 + '@strudel.cycles/core': + specifier: workspace:* + version: link:../.. + '@strudel.cycles/mini': + specifier: workspace:* + version: link:../../../mini + '@strudel.cycles/soundfonts': + specifier: workspace:* + version: link:../../../soundfonts + '@strudel.cycles/tonal': + specifier: workspace:* + version: link:../../../tonal + '@strudel.cycles/transpiler': + specifier: workspace:* + version: link:../../../transpiler + '@strudel.cycles/webaudio': + specifier: workspace:* + version: link:../../../webaudio + devDependencies: + vite: + specifier: ^4.3.2 + version: 4.3.3(@types/node@18.16.3) + packages/csound: dependencies: '@csound/browser': @@ -220,10 +260,10 @@ importers: version: 1.1.4 '@replit/codemirror-emacs': specifier: ^6.0.1 - version: 6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0) + version: 6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0) '@replit/codemirror-vim': specifier: ^6.0.14 - version: 6.0.14(@codemirror/commands@6.2.0)(@codemirror/language@6.6.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0) + version: 6.0.14(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0) '@strudel.cycles/core': specifier: workspace:* version: link:../core @@ -2268,8 +2308,8 @@ packages: '@lezer/common': 1.0.2 dev: false - /@codemirror/commands@6.2.0: - resolution: {integrity: sha512-+00smmZBradoGFEkRjliN7BjqPh/Hx0KCHWOEibUmflUqZz2RwBTU0MrVovEEHozhx3AUSGcO/rl3/5f9e9Biw==} + /@codemirror/commands@6.2.4: + resolution: {integrity: sha512-42lmDqVH0ttfilLShReLXsDfASKLXzfyC36bzwcqzox9PlHulMcsUOfHXNo2X2aFMVNUoQ7j+d4q5bnfseYoOA==} dependencies: '@codemirror/language': 6.6.0 '@codemirror/state': 6.2.0 @@ -3451,7 +3491,7 @@ packages: escalade: 3.1.1 dev: false - /@replit/codemirror-emacs@6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0): + /@replit/codemirror-emacs@6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0): resolution: {integrity: sha512-2WYkODZGH1QVAXWuOxTMCwktkoZyv/BjYdJi2A5w4fRrmOQFuIACzb6pO9dgU3J+Pm2naeiX2C8veZr/3/r6AA==} peerDependencies: '@codemirror/autocomplete': ^6.0.2 @@ -3461,13 +3501,13 @@ packages: '@codemirror/view': ^6.3.0 dependencies: '@codemirror/autocomplete': 6.6.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)(@lezer/common@1.0.2) - '@codemirror/commands': 6.2.0 + '@codemirror/commands': 6.2.4 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 '@codemirror/view': 6.10.0 dev: false - /@replit/codemirror-vim@6.0.14(@codemirror/commands@6.2.0)(@codemirror/language@6.6.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0): + /@replit/codemirror-vim@6.0.14(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0): resolution: {integrity: sha512-wwhqhvL76FdRTdwfUWpKCbv0hkp2fvivfMosDVlL/popqOiNLtUhL02ThgHZH8mus/NkVr5Mj582lyFZqQrjOA==} peerDependencies: '@codemirror/commands': ^6.0.0 @@ -3476,7 +3516,7 @@ packages: '@codemirror/state': ^6.0.1 '@codemirror/view': ^6.0.3 dependencies: - '@codemirror/commands': 6.2.0 + '@codemirror/commands': 6.2.4 '@codemirror/language': 6.6.0 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 @@ -4069,7 +4109,7 @@ packages: eslint-visitor-keys: 3.3.0 dev: false - /@uiw/codemirror-extensions-basic-setup@4.19.16(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.0)(@codemirror/language@6.6.0)(@codemirror/lint@6.1.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0): + /@uiw/codemirror-extensions-basic-setup@4.19.16(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/lint@6.1.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0): resolution: {integrity: sha512-Xm0RDpyYVZ/8hWqaBs3+wZwi4uLwZUBwp/uCt89X80FeR6mr3BFuC+a+gcDO4dBu3l+WQE3jJdhjKjB2TCY/PQ==} peerDependencies: '@codemirror/autocomplete': '>=6.0.0' @@ -4081,7 +4121,7 @@ packages: '@codemirror/view': '>=6.0.0' dependencies: '@codemirror/autocomplete': 6.6.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)(@lezer/common@1.0.2) - '@codemirror/commands': 6.2.0 + '@codemirror/commands': 6.2.4 '@codemirror/language': 6.6.0 '@codemirror/lint': 6.1.0 '@codemirror/search': 6.2.3 @@ -4376,11 +4416,11 @@ packages: react-dom: '>=16.8.0' dependencies: '@babel/runtime': 7.20.13 - '@codemirror/commands': 6.2.0 + '@codemirror/commands': 6.2.4 '@codemirror/state': 6.2.0 '@codemirror/theme-one-dark': 6.1.0 '@codemirror/view': 6.10.0 - '@uiw/codemirror-extensions-basic-setup': 4.19.16(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.0)(@codemirror/language@6.6.0)(@codemirror/lint@6.1.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0) + '@uiw/codemirror-extensions-basic-setup': 4.19.16(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/lint@6.1.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0) codemirror: 6.0.1(@lezer/common@1.0.2) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5441,7 +5481,7 @@ packages: resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} dependencies: '@codemirror/autocomplete': 6.6.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)(@lezer/common@1.0.2) - '@codemirror/commands': 6.2.0 + '@codemirror/commands': 6.2.4 '@codemirror/language': 6.6.0 '@codemirror/lint': 6.1.0 '@codemirror/search': 6.2.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3b068aa6..4b821d33 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,4 +3,5 @@ packages: - "packages/*" - "website/" - "packages/core/examples/vite-vanilla-repl" + - "packages/core/examples/vite-vanilla-repl-cm6" - "packages/react/examples/nano-repl" \ No newline at end of file From f069f53faee20265c2e0c9928d1437ddff194201 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 09:06:27 +0200 Subject: [PATCH 02/86] vanilla-repl-cm6: add keybindings --- .../vite-vanilla-repl-cm6/codemirror.js | 16 ++++++--- .../examples/vite-vanilla-repl-cm6/main.js | 34 ++++++++++++------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js index 7ba377ab..7a0d95d8 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js @@ -7,7 +7,7 @@ import { StateField, StateEffect } from '@codemirror/state'; import './style.css'; // https://codemirror.net/docs/guide/ -export function initEditor(initialCode, onUpdate) { +export function initEditor({ initialCode, onChange, onEvaluate, onStop }) { let state = EditorState.create({ doc: initialCode, extensions: [ @@ -22,9 +22,17 @@ export function initEditor(initialCode, onUpdate) { syntaxHighlighting(defaultHighlightStyle), keymap.of(defaultKeymap), //flashField, - EditorView.updateListener.of((v) => { - onUpdate(v); - }), + EditorView.updateListener.of((v) => onChange(v)), + keymap.of([ + { + key: 'Ctrl-Enter', + run: () => onEvaluate(), + }, + { + key: 'Ctrl-.', + run: () => onStop(), + }, + ]), ], }); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index 814f3609..fafed909 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -5,28 +5,36 @@ import { initStrudel } from './strudel'; import { Highlighter } from './highlighter'; import { bumpStreet } from './tunes'; let code = bumpStreet; - -const view = initEditor(code, (v) => { - code = v.state.doc.toString(); -}); const repl = initStrudel(); -let highlighter = new Highlighter((haps) => highlightHaps(view, haps)); - -document.getElementById('play').addEventListener('click', async () => { +async function onEvaluate() { const { evaluate, scheduler } = await repl; if (!scheduler.started) { scheduler.stop(); - scheduler.lastEnd = 0; await evaluate(code); highlighter.start(scheduler); } else { await evaluate(code); } +} + +async function onStop() { + const { scheduler } = await repl; + scheduler.stop(); + highlighter.stop(); +} + +const view = initEditor({ + initialCode: code, + onChange: (v) => { + code = v.state.doc.toString(); + }, + onEvaluate, + onStop, }); -document.getElementById('stop').addEventListener('click', async () => { - const { stop } = await repl; - stop(); - highlighter.stop(); -}); +let highlighter = new Highlighter((haps) => highlightHaps(view, haps)); + +document.getElementById('play').addEventListener('click', () => onEvaluate()); + +document.getElementById('stop').addEventListener('click', async () => onStop()); From 4b921c47f5798b5e727ab8d2d15df8fe09721c92 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 09:11:40 +0200 Subject: [PATCH 03/86] vanilla-repl-cm6: add flash effect --- .../vite-vanilla-repl-cm6/codemirror.js | 47 ++++++++++++++++--- .../examples/vite-vanilla-repl-cm6/main.js | 21 +++++---- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js index 7a0d95d8..a79aaf4f 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js @@ -13,15 +13,11 @@ export function initEditor({ initialCode, onChange, onEvaluate, onStop }) { extensions: [ javascript(), lineNumbers(), - /*gutter({ - class: "cm-mygutter" - }),*/ highlightField, highlightActiveLineGutter(), - //markLineGutter, syntaxHighlighting(defaultHighlightStyle), keymap.of(defaultKeymap), - //flashField, + flashField, EditorView.updateListener.of((v) => onChange(v)), keymap.of([ { @@ -42,7 +38,9 @@ export function initEditor({ initialCode, onChange, onEvaluate, onStop }) { }); } -// codemirror specific highlighting logic +// +// highlighting +// export const setHighlights = StateEffect.define(); export const highlightField = StateField.define({ @@ -89,3 +87,40 @@ export const highlightField = StateField.define({ 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) => { + view.dispatch({ effects: setFlash.of(true) }); + setTimeout(() => { + view.dispatch({ effects: setFlash.of(false) }); + }, 200); +}; diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index fafed909..ff10ad64 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -1,14 +1,24 @@ // moved from sandbox: https://codesandbox.io/s/vanilla-codemirror-strudel-2wb7yw?file=/index.html:114-186 -import { initEditor, highlightHaps } from './codemirror'; +import { initEditor, highlightHaps, flash } from './codemirror'; import { initStrudel } from './strudel'; import { Highlighter } from './highlighter'; import { bumpStreet } from './tunes'; let code = bumpStreet; const repl = initStrudel(); +const view = initEditor({ + initialCode: code, + onChange: (v) => { + code = v.state.doc.toString(); + }, + onEvaluate, + onStop, +}); + async function onEvaluate() { const { evaluate, scheduler } = await repl; + flash(view); if (!scheduler.started) { scheduler.stop(); await evaluate(code); @@ -24,15 +34,6 @@ async function onStop() { highlighter.stop(); } -const view = initEditor({ - initialCode: code, - onChange: (v) => { - code = v.state.doc.toString(); - }, - onEvaluate, - onStop, -}); - let highlighter = new Highlighter((haps) => highlightHaps(view, haps)); document.getElementById('play').addEventListener('click', () => onEvaluate()); From 95719654f3bfc5d3cc98c33816b402e2c0e38bd7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 09:52:07 +0200 Subject: [PATCH 04/86] refactor: remove old draw logic + pianoroll now uses .onPaint --- packages/core/draw.mjs | 38 +--------- packages/core/index.mjs | 1 - packages/core/pianoroll.mjs | 136 ++---------------------------------- packages/core/repl.mjs | 2 - packages/core/time.mjs | 11 --- packages/core/ui.mjs | 23 ------ website/src/repl/Repl.jsx | 5 +- 7 files changed, 10 insertions(+), 206 deletions(-) delete mode 100644 packages/core/time.mjs diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 4bfd3257..81700f6c 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, getTime, State, TimeSpan } from './index.mjs'; +import { Pattern } from './index.mjs'; export const getDrawContext = (id = 'test-canvas') => { let canvas = document.querySelector('#' + id); @@ -19,45 +19,9 @@ export const getDrawContext = (id = 'test-canvas') => { return canvas.getContext('2d'); }; -Pattern.prototype.draw = function (callback, { from, to, onQuery }) { - if (window.strudelAnimation) { - cancelAnimationFrame(window.strudelAnimation); - } - const ctx = getDrawContext(); - let cycle, - events = []; - const animate = (time) => { - const t = getTime(); - if (from !== undefined && to !== undefined) { - const currentCycle = Math.floor(t); - if (cycle !== currentCycle) { - cycle = currentCycle; - const begin = currentCycle + from; - const end = currentCycle + to; - setTimeout(() => { - events = this.query(new State(new TimeSpan(begin, end))) - .filter(Boolean) - .filter((event) => event.part.begin.equals(event.whole.begin)); - onQuery?.(events); - }, 0); - } - } - callback(ctx, events, t, time); - window.strudelAnimation = requestAnimationFrame(animate); - }; - requestAnimationFrame(animate); - return this; -}; - export const cleanupDraw = (clearScreen = true) => { const ctx = getDrawContext(); clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); - if (window.strudelAnimation) { - cancelAnimationFrame(window.strudelAnimation); - } - if (window.strudelScheduler) { - clearInterval(window.strudelScheduler); - } }; Pattern.prototype.onPaint = function (onPaint) { diff --git a/packages/core/index.mjs b/packages/core/index.mjs index b6c74848..30504a5b 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -19,7 +19,6 @@ export * from './speak.mjs'; export * from './evaluate.mjs'; export * from './repl.mjs'; export * from './logger.mjs'; -export * from './time.mjs'; export * from './draw.mjs'; export * from './animate.mjs'; export * from './pianoroll.mjs'; diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 635b7fac..7f5f2fec 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs'; +import { Pattern, noteToMidi, freqToMidi } from './index.mjs'; const scale = (normalized, min, max) => normalized * (max - min) + min; const getValue = (e) => { @@ -29,134 +29,6 @@ const getValue = (e) => { return value; }; -Pattern.prototype.pianoroll = function ({ - cycles = 4, - playhead = 0.5, - overscan = 1, - flipTime = 0, - flipValues = 0, - hideNegative = false, - // inactive = '#C9E597', - // inactive = '#FFCA28', - inactive = '#7491D2', - active = '#FFCA28', - // background = '#2A3236', - background = 'transparent', - smear = 0, - playheadColor = 'white', - minMidi = 10, - maxMidi = 90, - autorange = 0, - timeframe: timeframeProp, - fold = 0, - vertical = 0, -} = {}) { - const ctx = getDrawContext(); - const w = ctx.canvas.width; - const h = ctx.canvas.height; - let from = -cycles * playhead; - let to = cycles * (1 - playhead); - - if (timeframeProp) { - console.warn('timeframe is deprecated! use from/to instead'); - from = 0; - to = timeframeProp; - } - const timeAxis = vertical ? h : w; - const valueAxis = vertical ? w : h; - let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time - const timeExtent = to - from; // number of seconds that fit inside the canvas frame - const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values - let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true - let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true - let foldValues = []; - flipTime && timeRange.reverse(); - flipValues && valueRange.reverse(); - - this.draw( - (ctx, events, t) => { - ctx.fillStyle = background; - ctx.globalAlpha = 1; // reset! - if (!smear) { - ctx.clearRect(0, 0, w, h); - ctx.fillRect(0, 0, w, h); - } - const inFrame = (event) => - (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from; - events.filter(inFrame).forEach((event) => { - 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; - const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange); - let durationPx = scale(event.duration / timeExtent, 0, timeAxis); - const value = getValue(event); - const valuePx = scale( - fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent, - ...valueRange, - ); - let margin = 0; - const offset = scale(t / timeExtent, ...timeRange); - let coords; - if (vertical) { - coords = [ - valuePx + 1 - (flipValues ? barThickness : 0), // x - timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y - barThickness - 2, // width - durationPx - 2, // height - ]; - } else { - coords = [ - timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x - valuePx + 1 - (flipValues ? 0 : barThickness), // y - durationPx - 2, // widith - barThickness - 2, // height - ]; - } - isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); - }); - ctx.globalAlpha = 1; // reset! - const playheadPosition = scale(-from / timeExtent, ...timeRange); - // draw playhead - ctx.strokeStyle = playheadColor; - ctx.beginPath(); - if (vertical) { - ctx.moveTo(0, playheadPosition); - ctx.lineTo(valueAxis, playheadPosition); - } else { - ctx.moveTo(playheadPosition, 0); - ctx.lineTo(playheadPosition, valueAxis); - } - ctx.stroke(); - }, - { - from: from - overscan, - to: to + overscan, - onQuery: (events) => { - const { min, max, values } = events.reduce( - ({ min, max, values }, e) => { - const v = getValue(e); - return { - min: v < min ? v : min, - max: v > max ? v : max, - values: values.includes(v) ? values : [...values, v], - }; - }, - { min: Infinity, max: -Infinity, values: [] }, - ); - if (autorange) { - minMidi = min; - maxMidi = max; - valueExtent = maxMidi - minMidi + 1; - } - foldValues = values.sort((a, b) => String(a).localeCompare(String(b))); - barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent; - }, - }, - ); - return this; -}; - // this function allows drawing a pianoroll without ties to Pattern.prototype // it will probably replace the above in the future export function pianoroll({ @@ -295,3 +167,9 @@ function getOptions(drawTime, options = {}) { Pattern.prototype.punchcard = function (options) { return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) })); }; + +Pattern.prototype.pianoroll = function (options) { + return this.onPaint((ctx, time, haps, drawTime) => + pianoroll({ ctx, time, haps, ...getOptions(drawTime, { fold: 0, ...options }) }), + ); +}; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 7c96bb66..de979258 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -1,7 +1,6 @@ import { Cyclist } from './cyclist.mjs'; import { evaluate as _evaluate } from './evaluate.mjs'; import { logger } from './logger.mjs'; -import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; export function repl({ @@ -35,7 +34,6 @@ export function repl({ getTime, onToggle, }); - setTime(() => scheduler.now()); // TODO: refactor? const evaluate = async (code, autostart = true) => { if (!code) { throw new Error('no code to evaluate'); diff --git a/packages/core/time.mjs b/packages/core/time.mjs deleted file mode 100644 index 80daaf53..00000000 --- a/packages/core/time.mjs +++ /dev/null @@ -1,11 +0,0 @@ -let time; -export function getTime() { - if (!time) { - throw new Error('no time set! use setTime to define a time source'); - } - return time(); -} - -export function setTime(func) { - time = func; -} diff --git a/packages/core/ui.mjs b/packages/core/ui.mjs index df8230ec..cc148553 100644 --- a/packages/core/ui.mjs +++ b/packages/core/ui.mjs @@ -4,19 +4,6 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { getTime } from './time.mjs'; - -function frame(callback) { - if (window.strudelAnimation) { - cancelAnimationFrame(window.strudelAnimation); - } - const animate = (animationTime) => { - callback(animationTime, getTime()); - window.strudelAnimation = requestAnimationFrame(animate); - }; - requestAnimationFrame(animate); -} - export const backgroundImage = function (src, animateOptions = {}) { const container = document.getElementById('code'); const bg = 'background-image:url(' + src + ');background-size:contain;'; @@ -28,18 +15,8 @@ export const backgroundImage = function (src, animateOptions = {}) { className: () => (container.className = value + ' ' + initialClassName), })[option](); }; - const funcOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'function'); const stringOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'string'); stringOptions.forEach(([option, value]) => handleOption(option, value)); - - if (funcOptions.length === 0) { - return; - } - frame((_, t) => - funcOptions.forEach(([option, value]) => { - handleOption(option, value(t)); - }), - ); }; export const cleanupUi = () => { diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 4ad387fe..d8270b16 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -50,10 +50,9 @@ const modulesLoading = evalScope( const presets = prebake(); -let drawContext, clearCanvas; +let drawContext; if (typeof window !== 'undefined') { drawContext = getDrawContext(); - clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width); } const getTime = () => getAudioContext().currentTime; @@ -208,7 +207,7 @@ export function Repl({ embedded = false }) { const handleShuffle = async () => { const { code, name } = getRandomTune(); logger(`[repl] ✨ loading random tune "${name}"`); - clearCanvas(); + cleanupDraw(); resetLoadedSounds(); scheduler.setCps(1); await prebake(); // declare default samples From 1f4c2f8c5a0d21599da4fc0cb258281ebda77ae9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 10:52:21 +0200 Subject: [PATCH 05/86] draw scheduler in vanilla js --- .../examples/vite-vanilla-repl-cm6/drawer.js | 91 +++++++++++++++++++ .../vite-vanilla-repl-cm6/highlighter.js | 44 --------- .../examples/vite-vanilla-repl-cm6/index.html | 1 + .../examples/vite-vanilla-repl-cm6/main.js | 22 +++-- .../examples/vite-vanilla-repl-cm6/style.css | 4 + packages/core/pianoroll.mjs | 8 +- 6 files changed, 117 insertions(+), 53 deletions(-) create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/drawer.js delete mode 100644 packages/core/examples/vite-vanilla-repl-cm6/highlighter.js diff --git a/packages/core/examples/vite-vanilla-repl-cm6/drawer.js b/packages/core/examples/vite-vanilla-repl-cm6/drawer.js new file mode 100644 index 00000000..3bd6fbc9 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/drawer.js @@ -0,0 +1,91 @@ +const round = (x) => Math.round(x * 1000) / 1000; + +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(); + } + } +} + +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); + }, + ); + } + check() { + if (!this.scheduler) { + throw new Error('no scheduler set..'); + } + } + invalidate() { + this.check(); + const t = this.scheduler.now(); + let [_, lookahead] = this.drawTime; + // remove all future haps + this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); + // query future haps + const futureHaps = this.scheduler.pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +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(); + } + } +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js b/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js deleted file mode 100644 index 0a306e70..00000000 --- a/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js +++ /dev/null @@ -1,44 +0,0 @@ -const round = (x) => Math.round(x * 1000) / 1000; - -// this class can be used to create a code highlighter -// it is encapsulated from the editor via the onUpdate callback -// the scheduler is expected to be an instance of Cyclist -export class Highlighter { - constructor(onUpdate) { - this.onUpdate = onUpdate; - } - start(scheduler) { - let highlights = []; - let lastEnd = 0; - this.stop(); - const self = this; - let frame = requestAnimationFrame(function updateHighlights() { - try { - const time = scheduler.now(); - // force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away - // see https://github.com/tidalcycles/strudel/issues/108 - const begin = Math.max(lastEnd ?? time, time - 1 / 10, -0.01); // negative time seems buggy - const span = [round(begin), round(time + 1 / 60)]; - lastEnd = span[1]; - highlights = highlights.filter((hap) => hap.whole.end > time); // keep only highlights that are still active - const haps = scheduler.pattern - .queryArc(...span) - .filter((hap) => hap.hasOnset()); - highlights = highlights.concat(haps); // add potential new onsets - self.onUpdate(highlights); // highlight all still active + new active haps - } catch (err) { - self.onUpdate([]); - } - frame = requestAnimationFrame(updateHighlights); - }); - self.cancel = () => { - cancelAnimationFrame(frame); - }; - } - stop() { - if (this.cancel) { - this.cancel(); - this.onUpdate([]); - } - } -} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/index.html b/packages/core/examples/vite-vanilla-repl-cm6/index.html index 32381f03..1a214021 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/index.html +++ b/packages/core/examples/vite-vanilla-repl-cm6/index.html @@ -15,6 +15,7 @@
+ diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index ff10ad64..6e0ed677 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -2,10 +2,13 @@ import { initEditor, highlightHaps, flash } from './codemirror'; import { initStrudel } from './strudel'; -import { Highlighter } from './highlighter'; +import { Drawer } from './drawer'; import { bumpStreet } from './tunes'; +import { pianoroll, getDrawOptions } from '@strudel.cycles/core'; + let code = bumpStreet; const repl = initStrudel(); +const roll = document.getElementById('roll'); const view = initEditor({ initialCode: code, @@ -22,20 +25,27 @@ async function onEvaluate() { if (!scheduler.started) { scheduler.stop(); await evaluate(code); - highlighter.start(scheduler); + drawer.start(scheduler); } else { await evaluate(code); + drawer.invalidate(); // this is a bit mystic } } async function onStop() { const { scheduler } = await repl; scheduler.stop(); - highlighter.stop(); + drawer.stop(); } - -let highlighter = new Highlighter((haps) => highlightHaps(view, haps)); +const ctx = roll.getContext('2d'); +let drawer = new Drawer( + (haps, time, { drawTime }) => { + const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); + highlightHaps(view, currentFrame); + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 1 }) }); + }, + [-2, 2], +); document.getElementById('play').addEventListener('click', () => onEvaluate()); - document.getElementById('stop').addEventListener('click', async () => onStop()); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/style.css b/packages/core/examples/vite-vanilla-repl-cm6/style.css index 20970778..fed3f86a 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/style.css +++ b/packages/core/examples/vite-vanilla-repl-cm6/style.css @@ -22,3 +22,7 @@ main { .container { flex-grow: 1; } + +#roll { + height: 300px; +} diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 7f5f2fec..b0f4c074 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -156,7 +156,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; @@ -165,11 +165,13 @@ 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, ...getOptions(drawTime, { fold: 0, ...options }) }), + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }), ); }; From d96f3f9dd4f9a53c9847a31876570133b2b880f4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 11:09:44 +0200 Subject: [PATCH 06/86] vanilla-repl-cm6: style + codesplitting --- .../examples/vite-vanilla-repl-cm6/strudel.js | 39 +++++++------------ .../examples/vite-vanilla-repl-cm6/style.css | 19 ++++----- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js index a1e999ee..efd899ea 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js @@ -1,38 +1,29 @@ -import { controls, repl, evalScope } from "@strudel.cycles/core"; -import { transpiler } from "@strudel.cycles/transpiler"; -import { - getAudioContext, - webaudioOutput, - initAudioOnFirstClick, - registerSynthSounds -} from "@strudel.cycles/webaudio"; -import { registerSoundfonts } from "@strudel.cycles/soundfonts"; +import { getAudioContext, webaudioOutput, initAudioOnFirstClick, registerSynthSounds } from '@strudel.cycles/webaudio'; const initAudio = initAudioOnFirstClick(); const ctx = getAudioContext(); -const loadModules = (scope = {}) => - evalScope( +export async function initStrudel(options = {}) { + const [{ controls, repl, evalScope }, { registerSoundfonts }, { transpiler }] = await Promise.all([ + import('@strudel.cycles/core'), + import('@strudel.cycles/soundfonts'), + import('@strudel.cycles/transpiler'), + ]); + + const loadModules = evalScope( controls, - import("@strudel.cycles/core"), - import("@strudel.cycles/mini"), - import("@strudel.cycles/tonal"), - import("@strudel.cycles/webaudio"), - scope + import('@strudel.cycles/core'), + import('@strudel.cycles/mini'), + import('@strudel.cycles/tonal'), + import('@strudel.cycles/webaudio'), ); -export async function initStrudel(options = {}) { - await Promise.all([ - initAudio, - loadModules(), - registerSynthSounds(), - registerSoundfonts() - ]); + await Promise.all([initAudio, loadModules, registerSynthSounds(), registerSoundfonts()]); return repl({ defaultOutput: webaudioOutput, getTime: () => ctx.currentTime, transpiler, - ...options + ...options, }); } diff --git a/packages/core/examples/vite-vanilla-repl-cm6/style.css b/packages/core/examples/vite-vanilla-repl-cm6/style.css index fed3f86a..21f011a0 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/style.css +++ b/packages/core/examples/vite-vanilla-repl-cm6/style.css @@ -4,15 +4,6 @@ html { height: 100%; } -#editor { - overflow: auto; - height: 100%; -} - -.cm-editor { - height: 100%; -} - main { height: 100%; display: flex; @@ -21,6 +12,16 @@ main { .container { flex-grow: 1; + max-height: 100%; + position: relative; +} + +#editor { + overflow: auto; +} + +.cm-editor { + height: 100%; } #roll { From d35bf9591c5b5f8021c8f5505db9701ebba4f81e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 11:54:09 +0200 Subject: [PATCH 07/86] vanilla-repl-cm6: better theme --- .../vite-vanilla-repl-cm6/codemirror.js | 3 +- .../examples/vite-vanilla-repl-cm6/main.js | 13 +- .../vite-vanilla-repl-cm6/one-dark.js | 139 ++++++++++++++++++ .../vite-vanilla-repl-cm6/package.json | 5 +- .../examples/vite-vanilla-repl-cm6/style.css | 2 + .../examples/vite-vanilla-repl-cm6/tunes.mjs | 80 ++++++++++ pnpm-lock.yaml | 3 + 7 files changed, 237 insertions(+), 8 deletions(-) create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/one-dark.js diff --git a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js index a79aaf4f..103edd92 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js @@ -4,13 +4,14 @@ import { defaultKeymap } from '@codemirror/commands'; import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'; import { javascript } from '@codemirror/lang-javascript'; import { StateField, StateEffect } from '@codemirror/state'; -import './style.css'; +import { oneDark } from './one-dark'; // https://codemirror.net/docs/guide/ export function initEditor({ initialCode, onChange, onEvaluate, onStop }) { let state = EditorState.create({ doc: initialCode, extensions: [ + oneDark, javascript(), lineNumbers(), highlightField, diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index 6e0ed677..a73ccff3 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -3,12 +3,15 @@ import { initEditor, highlightHaps, flash } from './codemirror'; import { initStrudel } from './strudel'; import { Drawer } from './drawer'; -import { bumpStreet } from './tunes'; +import { bumpStreet, trafficFlam, funk42 } from './tunes'; import { pianoroll, getDrawOptions } from '@strudel.cycles/core'; +import './style.css'; -let code = bumpStreet; +let code = funk42; const repl = initStrudel(); -const roll = document.getElementById('roll'); +const canvas = document.getElementById('roll'); +canvas.width = canvas.width * 2; +canvas.height = canvas.height * 2; const view = initEditor({ initialCode: code, @@ -37,12 +40,12 @@ async function onStop() { scheduler.stop(); drawer.stop(); } -const ctx = roll.getContext('2d'); +const ctx = canvas.getContext('2d'); let drawer = new Drawer( (haps, time, { drawTime }) => { const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); highlightHaps(view, currentFrame); - pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 1 }) }); + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0 }) }); }, [-2, 2], ); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js b/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js new file mode 100644 index 00000000..cce83699 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js @@ -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)]; diff --git a/packages/core/examples/vite-vanilla-repl-cm6/package.json b/packages/core/examples/vite-vanilla-repl-cm6/package.json index f84b23ed..0251bf96 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/package.json +++ b/packages/core/examples/vite-vanilla-repl-cm6/package.json @@ -17,11 +17,12 @@ "@codemirror/language": "^6.6.0", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.10.0", + "@lezer/highlight": "^1.1.4", "@strudel.cycles/core": "workspace:*", "@strudel.cycles/mini": "workspace:*", + "@strudel.cycles/soundfonts": "workspace:*", "@strudel.cycles/tonal": "workspace:*", "@strudel.cycles/transpiler": "workspace:*", - "@strudel.cycles/webaudio": "workspace:*", - "@strudel.cycles/soundfonts": "workspace:*" + "@strudel.cycles/webaudio": "workspace:*" } } diff --git a/packages/core/examples/vite-vanilla-repl-cm6/style.css b/packages/core/examples/vite-vanilla-repl-cm6/style.css index 21f011a0..5a95388a 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/style.css +++ b/packages/core/examples/vite-vanilla-repl-cm6/style.css @@ -2,6 +2,7 @@ body, html { margin: 0; height: 100%; + background: #282c34; } main { @@ -14,6 +15,7 @@ main { flex-grow: 1; max-height: 100%; position: relative; + overflow:auto; } #editor { diff --git a/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs b/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs index 4b0155e4..b235aa95 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs +++ b/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs @@ -30,3 +30,83 @@ await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'githu // 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( + "/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), + "/2".note().s('gm_acoustic_bass'), + n("<0 2 3>(3,8)".off(1/8, add(4))) + .scale("/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 +)`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40a6442f..a2e65f7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,6 +118,9 @@ importers: '@codemirror/view': specifier: ^6.10.0 version: 6.10.0 + '@lezer/highlight': + specifier: ^1.1.4 + version: 1.1.4 '@strudel.cycles/core': specifier: workspace:* version: link:../.. From a6f57bced87e8946fc3f2ff983481d62f921d410 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 12:00:13 +0200 Subject: [PATCH 08/86] prettier --- packages/core/examples/vite-vanilla-repl-cm6/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/examples/vite-vanilla-repl-cm6/style.css b/packages/core/examples/vite-vanilla-repl-cm6/style.css index 5a95388a..67ab3917 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/style.css +++ b/packages/core/examples/vite-vanilla-repl-cm6/style.css @@ -15,7 +15,7 @@ main { flex-grow: 1; max-height: 100%; position: relative; - overflow:auto; + overflow: auto; } #editor { From f5075906e2166361dc58c9c16b8d4fdb81eaaff1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 15:13:17 +0200 Subject: [PATCH 09/86] add codemirror package + use it in vite-vanilla-repl-cm6 --- packages/codemirror/README.md | 3 + .../codemirror.mjs} | 32 +++- packages/codemirror/package.json | 46 ++++++ packages/codemirror/themes/one-dark.mjs | 139 ++++++++++++++++++ .../examples/vite-vanilla-repl-cm6/main.js | 12 +- .../vite-vanilla-repl-cm6/package.json | 7 +- 6 files changed, 222 insertions(+), 17 deletions(-) create mode 100644 packages/codemirror/README.md rename packages/{core/examples/vite-vanilla-repl-cm6/codemirror.js => codemirror/codemirror.mjs} (84%) create mode 100644 packages/codemirror/package.json create mode 100644 packages/codemirror/themes/one-dark.mjs diff --git a/packages/codemirror/README.md b/packages/codemirror/README.md new file mode 100644 index 00000000..300e30b3 --- /dev/null +++ b/packages/codemirror/README.md @@ -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. diff --git a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js b/packages/codemirror/codemirror.mjs similarity index 84% rename from packages/core/examples/vite-vanilla-repl-cm6/codemirror.js rename to packages/codemirror/codemirror.mjs index 103edd92..1d874aff 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js +++ b/packages/codemirror/codemirror.mjs @@ -4,14 +4,14 @@ 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 './one-dark'; +import { oneDark } from './themes/one-dark'; // https://codemirror.net/docs/guide/ -export function initEditor({ initialCode, onChange, onEvaluate, onStop }) { +export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) { let state = EditorState.create({ doc: initialCode, extensions: [ - oneDark, + theme, javascript(), lineNumbers(), highlightField, @@ -35,7 +35,7 @@ export function initEditor({ initialCode, onChange, onEvaluate, onStop }) { return new EditorView({ state, - parent: document.getElementById('editor'), + parent: root, }); } @@ -119,9 +119,29 @@ const flashField = StateField.define({ provide: (f) => EditorView.decorations.from(f), }); -export const flash = (view) => { +export const flash = (view, ms = 200) => { view.dispatch({ effects: setFlash.of(true) }); setTimeout(() => { view.dispatch({ effects: setFlash.of(false) }); - }, 200); + }, ms); }; + +export class StrudelMirror { + constructor({ root, initialCode = '', onEvaluate, onStop }) { + this.view = initEditor({ + root, + initialCode, + onChange: (v) => { + this.code = v.state.doc.toString(); + }, + onEvaluate, + onStop, + }); + } + flash(ms) { + flash(this.view, ms); + } + highlight(haps) { + highlightHaps(this.view, haps); + } +} diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json new file mode 100644 index 00000000..6df43e80 --- /dev/null +++ b/packages/codemirror/package.json @@ -0,0 +1,46 @@ +{ + "name": "@strudel/codemirror", + "version": "0.8.0", + "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 ", + "contributors": [ + "Alex McLean " + ], + "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" + }, + "devDependencies": { + "vite": "^4.3.3" + } +} diff --git a/packages/codemirror/themes/one-dark.mjs b/packages/codemirror/themes/one-dark.mjs new file mode 100644 index 00000000..cce83699 --- /dev/null +++ b/packages/codemirror/themes/one-dark.mjs @@ -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)]; diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index a73ccff3..7b19596d 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -1,9 +1,9 @@ // moved from sandbox: https://codesandbox.io/s/vanilla-codemirror-strudel-2wb7yw?file=/index.html:114-186 -import { initEditor, highlightHaps, flash } from './codemirror'; +import { StrudelMirror } from '@strudel/codemirror'; import { initStrudel } from './strudel'; import { Drawer } from './drawer'; -import { bumpStreet, trafficFlam, funk42 } from './tunes'; +import { funk42 } from './tunes'; import { pianoroll, getDrawOptions } from '@strudel.cycles/core'; import './style.css'; @@ -13,7 +13,8 @@ const canvas = document.getElementById('roll'); canvas.width = canvas.width * 2; canvas.height = canvas.height * 2; -const view = initEditor({ +const editor = new StrudelMirror({ + root: document.getElementById('editor'), initialCode: code, onChange: (v) => { code = v.state.doc.toString(); @@ -24,7 +25,7 @@ const view = initEditor({ async function onEvaluate() { const { evaluate, scheduler } = await repl; - flash(view); + editor.flash(); if (!scheduler.started) { scheduler.stop(); await evaluate(code); @@ -40,11 +41,12 @@ async function onStop() { scheduler.stop(); drawer.stop(); } + const ctx = canvas.getContext('2d'); let drawer = new Drawer( (haps, time, { drawTime }) => { const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); - highlightHaps(view, currentFrame); + editor.highlight(currentFrame); pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0 }) }); }, [-2, 2], diff --git a/packages/core/examples/vite-vanilla-repl-cm6/package.json b/packages/core/examples/vite-vanilla-repl-cm6/package.json index 0251bf96..4e093d10 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/package.json +++ b/packages/core/examples/vite-vanilla-repl-cm6/package.json @@ -12,12 +12,7 @@ "vite": "^4.3.2" }, "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/codemirror": "workspace:*", "@strudel.cycles/core": "workspace:*", "@strudel.cycles/mini": "workspace:*", "@strudel.cycles/soundfonts": "workspace:*", From e6b66f25250cf1a681277c25717cc9657e576e69 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 15:14:46 +0200 Subject: [PATCH 10/86] lockfile --- pnpm-lock.yaml | 46 ++++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2e65f7a..570983bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,6 +66,31 @@ importers: specifier: ^5.8.1 version: 5.9.0 + packages/codemirror: + dependencies: + '@codemirror/commands': + specifier: ^6.2.4 + version: 6.2.4 + '@codemirror/lang-javascript': + specifier: ^6.1.7 + version: 6.1.7 + '@codemirror/language': + specifier: ^6.6.0 + version: 6.6.0 + '@codemirror/state': + specifier: ^6.2.0 + version: 6.2.0 + '@codemirror/view': + specifier: ^6.10.0 + version: 6.10.0 + '@lezer/highlight': + specifier: ^1.1.4 + version: 1.1.4 + devDependencies: + vite: + specifier: ^4.3.3 + version: 4.3.3(@types/node@18.16.3) + packages/core: dependencies: fraction.js: @@ -103,24 +128,6 @@ importers: packages/core/examples/vite-vanilla-repl-cm6: dependencies: - '@codemirror/commands': - specifier: ^6.2.4 - version: 6.2.4 - '@codemirror/lang-javascript': - specifier: ^6.1.7 - version: 6.1.7 - '@codemirror/language': - specifier: ^6.6.0 - version: 6.6.0 - '@codemirror/state': - specifier: ^6.2.0 - version: 6.2.0 - '@codemirror/view': - specifier: ^6.10.0 - version: 6.10.0 - '@lezer/highlight': - specifier: ^1.1.4 - version: 1.1.4 '@strudel.cycles/core': specifier: workspace:* version: link:../.. @@ -139,6 +146,9 @@ importers: '@strudel.cycles/webaudio': specifier: workspace:* version: link:../../../webaudio + '@strudel/codemirror': + specifier: workspace:* + version: link:../../../codemirror devDependencies: vite: specifier: ^4.3.2 From 352e647d37169fcf1572dba30bbf622ff4ae5d43 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 15:15:45 +0200 Subject: [PATCH 11/86] add build script for codemirror package --- packages/codemirror/vite.config.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 packages/codemirror/vite.config.js diff --git a/packages/codemirror/vite.config.js b/packages/codemirror/vite.config.js new file mode 100644 index 00000000..8562915c --- /dev/null +++ b/packages/codemirror/vite.config.js @@ -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', + }, +}); From b5b0156e080025eb8de0657d2138387e7d062206 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 15:31:04 +0200 Subject: [PATCH 12/86] fix: codemirror reeval --- packages/codemirror/codemirror.mjs | 1 + packages/core/examples/vite-vanilla-repl-cm6/main.js | 10 +++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 1d874aff..b9147226 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -128,6 +128,7 @@ export const flash = (view, ms = 200) => { export class StrudelMirror { constructor({ root, initialCode = '', onEvaluate, onStop }) { + this.code = initialCode; this.view = initEditor({ root, initialCode, diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index 7b19596d..33f529ff 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -7,7 +7,6 @@ import { funk42 } from './tunes'; import { pianoroll, getDrawOptions } from '@strudel.cycles/core'; import './style.css'; -let code = funk42; const repl = initStrudel(); const canvas = document.getElementById('roll'); canvas.width = canvas.width * 2; @@ -15,10 +14,7 @@ canvas.height = canvas.height * 2; const editor = new StrudelMirror({ root: document.getElementById('editor'), - initialCode: code, - onChange: (v) => { - code = v.state.doc.toString(); - }, + initialCode: funk42, onEvaluate, onStop, }); @@ -28,10 +24,10 @@ async function onEvaluate() { editor.flash(); if (!scheduler.started) { scheduler.stop(); - await evaluate(code); + await evaluate(editor.code); drawer.start(scheduler); } else { - await evaluate(code); + await evaluate(editor.code); drawer.invalidate(); // this is a bit mystic } } From 9039600f6dda4d4f8aab2f0440b2576ce0bbd73e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 15:31:19 +0200 Subject: [PATCH 13/86] bump codemirror to 0.8.1 --- packages/codemirror/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 6df43e80..6ea2ac25 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "0.8.0", + "version": "0.8.1", "description": "Codemirror Extensions for Strudel", "main": "codemirror.mjs", "publishConfig": { From 9807c4b7a1dc6adb30a0157dd45570c8935eb5e2 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 16:06:11 +0200 Subject: [PATCH 14/86] move Framer + Drawer to core ... from vite-vanilla-repl-cm6 --- packages/core/draw.mjs | 95 ++++++++++++ .../examples/vite-vanilla-repl-cm6/drawer.js | 91 ------------ .../examples/vite-vanilla-repl-cm6/main.js | 3 +- .../vite-vanilla-repl-cm6/one-dark.js | 139 ------------------ 4 files changed, 96 insertions(+), 232 deletions(-) delete mode 100644 packages/core/examples/vite-vanilla-repl-cm6/drawer.js delete mode 100644 packages/core/examples/vite-vanilla-repl-cm6/one-dark.js diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 81700f6c..bc6d3cd8 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -29,3 +29,98 @@ 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); + }, + ); + } + check() { + if (!this.scheduler) { + throw new Error('no scheduler set..'); + } + } + invalidate() { + this.check(); + const t = this.scheduler.now(); + let [_, lookahead] = this.drawTime; + // remove all future haps + this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); + // query future haps + const futureHaps = this.scheduler.pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +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(); + } + } +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/drawer.js b/packages/core/examples/vite-vanilla-repl-cm6/drawer.js deleted file mode 100644 index 3bd6fbc9..00000000 --- a/packages/core/examples/vite-vanilla-repl-cm6/drawer.js +++ /dev/null @@ -1,91 +0,0 @@ -const round = (x) => Math.round(x * 1000) / 1000; - -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(); - } - } -} - -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); - }, - ); - } - check() { - if (!this.scheduler) { - throw new Error('no scheduler set..'); - } - } - invalidate() { - this.check(); - const t = this.scheduler.now(); - let [_, lookahead] = this.drawTime; - // remove all future haps - this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); - // query future haps - const futureHaps = this.scheduler.pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +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(); - } - } -} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index 33f529ff..bede9161 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -2,9 +2,8 @@ import { StrudelMirror } from '@strudel/codemirror'; import { initStrudel } from './strudel'; -import { Drawer } from './drawer'; import { funk42 } from './tunes'; -import { pianoroll, getDrawOptions } from '@strudel.cycles/core'; +import { pianoroll, getDrawOptions, Drawer } from '@strudel.cycles/core'; import './style.css'; const repl = initStrudel(); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js b/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js deleted file mode 100644 index cce83699..00000000 --- a/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js +++ /dev/null @@ -1,139 +0,0 @@ -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)]; From 5b67fccb1b10fb7fd4edd25569c4b54e3fa156ad Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 16:17:22 +0200 Subject: [PATCH 15/86] vanilla-repl-cm6: draw first frame --- .../examples/vite-vanilla-repl-cm6/main.js | 42 +++++++++++++------ .../examples/vite-vanilla-repl-cm6/strudel.js | 5 +-- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index bede9161..a8a1c754 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -5,11 +5,9 @@ import { initStrudel } from './strudel'; import { funk42 } from './tunes'; import { pianoroll, getDrawOptions, Drawer } from '@strudel.cycles/core'; import './style.css'; +import { initAudioOnFirstClick } from '@strudel.cycles/webaudio'; -const repl = initStrudel(); -const canvas = document.getElementById('roll'); -canvas.width = canvas.width * 2; -canvas.height = canvas.height * 2; +const initAudio = initAudioOnFirstClick(); const editor = new StrudelMirror({ root: document.getElementById('editor'), @@ -18,8 +16,26 @@ const editor = new StrudelMirror({ onStop, }); +async function drawFirstFrame(editor, repl) { + const { evaluate } = repl; + const pattern = await evaluate(editor.code, false); + const initialHaps = pattern.queryArc(0, drawTime[1]); + drawPianoroll(initialHaps, 0); + return repl; +} + +const repl = initStrudel().then(async (repl) => { + await drawFirstFrame(editor, repl); + return repl; +}); + +const canvas = document.getElementById('roll'); +canvas.width = canvas.width * 2; +canvas.height = canvas.height * 2; + async function onEvaluate() { const { evaluate, scheduler } = await repl; + await initAudio; editor.flash(); if (!scheduler.started) { scheduler.stop(); @@ -37,15 +53,17 @@ async function onStop() { drawer.stop(); } +const drawTime = [-2, 2]; +function drawPianoroll(haps, time) { + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0 }) }); +} + const ctx = canvas.getContext('2d'); -let drawer = new Drawer( - (haps, time, { drawTime }) => { - const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); - editor.highlight(currentFrame); - pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0 }) }); - }, - [-2, 2], -); +let drawer = new Drawer((haps, time) => { + const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); + editor.highlight(currentFrame); + drawPianoroll(haps, time); +}, drawTime); document.getElementById('play').addEventListener('click', () => onEvaluate()); document.getElementById('stop').addEventListener('click', async () => onStop()); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js index efd899ea..de9b2cd7 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js @@ -1,6 +1,5 @@ -import { getAudioContext, webaudioOutput, initAudioOnFirstClick, registerSynthSounds } from '@strudel.cycles/webaudio'; +import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio'; -const initAudio = initAudioOnFirstClick(); const ctx = getAudioContext(); export async function initStrudel(options = {}) { @@ -18,7 +17,7 @@ export async function initStrudel(options = {}) { import('@strudel.cycles/webaudio'), ); - await Promise.all([initAudio, loadModules, registerSynthSounds(), registerSoundfonts()]); + await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]); return repl({ defaultOutput: webaudioOutput, From 9ec7109dc8dbb2fa894cfd9a0b01413dbff32962 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 23:43:07 +0200 Subject: [PATCH 16/86] + simplify vanilla setup drastically + move repl + drawer boilerplate inside StrudelMirror --- packages/codemirror/codemirror.mjs | 63 +++++++++++-- packages/codemirror/package.json | 3 +- packages/core/draw.mjs | 15 ++-- .../examples/vite-vanilla-repl-cm6/main.js | 88 ++++++------------- .../examples/vite-vanilla-repl-cm6/strudel.js | 28 ------ .../examples/vite-vanilla-repl-cm6/tunes.mjs | 2 +- packages/core/pianoroll.mjs | 5 ++ 7 files changed, 101 insertions(+), 103 deletions(-) delete mode 100644 packages/core/examples/vite-vanilla-repl-cm6/strudel.js diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index b9147226..39429432 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -5,6 +5,7 @@ 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 }) { @@ -127,22 +128,72 @@ export const flash = (view, ms = 200) => { }; export class StrudelMirror { - constructor({ root, initialCode = '', onEvaluate, onStop }) { + constructor(options) { + const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options; this.code = initialCode; - this.view = initEditor({ + + 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, - onStop, + 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.view, ms); + flash(this.editor, ms); } highlight(haps) { - highlightHaps(this.view, haps); + highlightHaps(this.editor, haps); } } diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 6ea2ac25..a4ebf03a 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -38,7 +38,8 @@ "@codemirror/language": "^6.6.0", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.10.0", - "@lezer/highlight": "^1.1.4" + "@lezer/highlight": "^1.1.4", + "@strudel.cycles/core": "workspace:*" }, "devDependencies": { "vite": "^4.3.3" diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index bc6d3cd8..58b14040 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -97,19 +97,18 @@ export class Drawer { }, ); } - check() { - if (!this.scheduler) { - throw new Error('no scheduler set..'); + invalidate(scheduler = this.scheduler) { + if (!scheduler) { + return; } - } - invalidate() { - this.check(); - const t = this.scheduler.now(); + 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 = this.scheduler.pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +0.1 = workaround for weird holes in query.. + const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query.. // append future haps this.visibleHaps = this.visibleHaps.concat(futureHaps); } diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index a8a1c754..425799be 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -1,69 +1,39 @@ -// moved from sandbox: https://codesandbox.io/s/vanilla-codemirror-strudel-2wb7yw?file=/index.html:114-186 - import { StrudelMirror } from '@strudel/codemirror'; -import { initStrudel } from './strudel'; import { funk42 } from './tunes'; -import { pianoroll, getDrawOptions, Drawer } from '@strudel.cycles/core'; +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'; -const initAudio = initAudioOnFirstClick(); - -const editor = new StrudelMirror({ - root: document.getElementById('editor'), - initialCode: funk42, - onEvaluate, - onStop, -}); - -async function drawFirstFrame(editor, repl) { - const { evaluate } = repl; - const pattern = await evaluate(editor.code, false); - const initialHaps = pattern.queryArc(0, drawTime[1]); - drawPianoroll(initialHaps, 0); - return repl; -} - -const repl = initStrudel().then(async (repl) => { - await drawFirstFrame(editor, repl); - return repl; -}); - +// 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 -async function onEvaluate() { - const { evaluate, scheduler } = await repl; - await initAudio; - editor.flash(); - if (!scheduler.started) { - scheduler.stop(); - await evaluate(editor.code); - drawer.start(scheduler); - } else { - await evaluate(editor.code); - drawer.invalidate(); // this is a bit mystic - } -} +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()]); + }, +}); -async function onStop() { - const { scheduler } = await repl; - scheduler.stop(); - drawer.stop(); -} - -const drawTime = [-2, 2]; -function drawPianoroll(haps, time) { - pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0 }) }); -} - -const ctx = canvas.getContext('2d'); -let drawer = new Drawer((haps, time) => { - const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); - editor.highlight(currentFrame); - drawPianoroll(haps, time); -}, drawTime); - -document.getElementById('play').addEventListener('click', () => onEvaluate()); -document.getElementById('stop').addEventListener('click', async () => onStop()); +document.getElementById('play').addEventListener('click', () => editor.evaluate()); +document.getElementById('stop').addEventListener('click', () => editor.stop()); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js deleted file mode 100644 index de9b2cd7..00000000 --- a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js +++ /dev/null @@ -1,28 +0,0 @@ -import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio'; - -const ctx = getAudioContext(); - -export async function initStrudel(options = {}) { - const [{ controls, repl, evalScope }, { registerSoundfonts }, { transpiler }] = await Promise.all([ - import('@strudel.cycles/core'), - import('@strudel.cycles/soundfonts'), - import('@strudel.cycles/transpiler'), - ]); - - 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()]); - - return repl({ - defaultOutput: webaudioOutput, - getTime: () => ctx.currentTime, - transpiler, - ...options, - }); -} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs b/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs index b235aa95..242a0d4b 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs +++ b/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs @@ -78,7 +78,7 @@ 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) + //s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02) ).fast(2) let wurli = note(\`< diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index b0f4c074..336bd428 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -175,3 +175,8 @@ Pattern.prototype.pianoroll = function (options) { pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }), ); }; + +export function drawPianoroll(options) { + const { drawTime, ...rest } = options; + pianoroll({ ...getDrawOptions(drawTime), ...rest }); +} From bb71c519dbfd4b16af0f47a382633f63cc1e384b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 23:46:42 +0200 Subject: [PATCH 17/86] bump codemirror package --- packages/codemirror/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index a4ebf03a..a6bb1bfe 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "0.8.1", + "version": "0.8.2", "description": "Codemirror Extensions for Strudel", "main": "codemirror.mjs", "publishConfig": { From 299275e36805388e18711b6a91c37e3c52212fa8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 23:47:15 +0200 Subject: [PATCH 18/86] lockfile --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 570983bd..596351fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ importers: '@lezer/highlight': specifier: ^1.1.4 version: 1.1.4 + '@strudel.cycles/core': + specifier: workspace:* + version: link:../core devDependencies: vite: specifier: ^4.3.3 From b52f65c4d7e4508d402aa3201a1f98c85cc76988 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 23:50:06 +0200 Subject: [PATCH 19/86] rename core package + bump to 0.8.0 --- packages/core/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 14237689..f1666260 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { - "name": "@strudel.cycles/core", - "version": "0.7.2", + "name": "@strudel/core", + "version": "0.8.0", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", "type": "module", From 692fc4e11d38c8fbae3163a09d3540292ca1047e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 23:52:04 +0200 Subject: [PATCH 20/86] revert rename core package --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index f1666260..9aebd5f5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,5 +1,5 @@ { - "name": "@strudel/core", + "name": "@strudel.cycles/core", "version": "0.8.0", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", From 308f1391937aea44b552f36e4d82a22a9d0fa9a8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 23:52:55 +0200 Subject: [PATCH 21/86] bump codemirror again to get new core version --- packages/codemirror/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index a6bb1bfe..c3b885db 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "0.8.2", + "version": "0.8.3", "description": "Codemirror Extensions for Strudel", "main": "codemirror.mjs", "publishConfig": { From b27a58df7ea4ba9c1bd685e5974fcbb91594c5a5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 23:58:47 +0200 Subject: [PATCH 22/86] bump more packages to 0.8.0 --- packages/mini/package.json | 2 +- packages/soundfonts/package.json | 2 +- packages/tonal/package.json | 2 +- packages/transpiler/package.json | 2 +- packages/webaudio/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/mini/package.json b/packages/mini/package.json index 1f8ab829..eb525e9f 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/mini", - "version": "0.7.2", + "version": "0.8.0", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index ce1b4446..95bf91b0 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/soundfonts", - "version": "0.7.1", + "version": "0.8.0", "description": "Soundsfont support for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/tonal/package.json b/packages/tonal/package.json index 2309a7d1..f50c7ff7 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/tonal", - "version": "0.7.1", + "version": "0.8.0", "description": "Tonal functions for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 6b08191c..39046dc3 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/transpiler", - "version": "0.7.1", + "version": "0.8.0", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "main": "index.mjs", "publishConfig": { diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index bac45088..bb806543 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/webaudio", - "version": "0.7.1", + "version": "0.8.0", "description": "Web Audio helpers for Strudel", "main": "index.mjs", "type": "module", From 3b631cb6af8b10f2dff6ff9a6c7b1aa5e7bc2814 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 7 May 2023 11:56:35 +0200 Subject: [PATCH 23/86] add @strudel/web umbrella package + example --- packages/web/README.md | 35 ++++++++++++++ packages/web/examples/repl-example/.gitignore | 24 ++++++++++ packages/web/examples/repl-example/index.html | 29 ++++++++++++ .../web/examples/repl-example/package.json | 18 ++++++++ packages/web/package.json | 46 +++++++++++++++++++ packages/web/repl.mjs | 38 +++++++++++++++ pnpm-lock.yaml | 35 ++++++++++++++ pnpm-workspace.yaml | 3 +- 8 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 packages/web/README.md create mode 100644 packages/web/examples/repl-example/.gitignore create mode 100644 packages/web/examples/repl-example/index.html create mode 100644 packages/web/examples/repl-example/package.json create mode 100644 packages/web/package.json create mode 100644 packages/web/repl.mjs diff --git a/packages/web/README.md b/packages/web/README.md new file mode 100644 index 00000000..c70a5df7 --- /dev/null +++ b/packages/web/README.md @@ -0,0 +1,35 @@ +# @strudel/web + +This package provides an easy to use bundle of multiple strudel packages for the web. + +## Usage + +```js +import { repl } from '@strudel/web'; + +const strudel = repl(); + +document.getElementById('play').addEventListener('click', + () => strudel.evaluate('note("c a f e").jux(rev)') +); +``` + +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. + +### Loading samples + +By default, no external samples are loaded, but you can add them like this: + +```js +import { repl, samples } from '@strudel/web'; + +const strudel = repl({ + prebake: () => samples('github:tidalcycles/Dirt-Samples/master'), +}); + +document.getElementById('play').addEventListener('click', + () => strudel.evaluate('s("bd,jvbass(3,8)").jux(rev)') +); +``` + +You can learn [more about the `samples` function here](https://strudel.tidalcycles.org/learn/samples#loading-custom-samples). diff --git a/packages/web/examples/repl-example/.gitignore b/packages/web/examples/repl-example/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/packages/web/examples/repl-example/.gitignore @@ -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? diff --git a/packages/web/examples/repl-example/index.html b/packages/web/examples/repl-example/index.html new file mode 100644 index 00000000..a7954e64 --- /dev/null +++ b/packages/web/examples/repl-example/index.html @@ -0,0 +1,29 @@ + + + + + + + @strudel/web REPL Example + + +
+ + + + + + + diff --git a/packages/web/examples/repl-example/package.json b/packages/web/examples/repl-example/package.json new file mode 100644 index 00000000..42fb8b83 --- /dev/null +++ b/packages/web/examples/repl-example/package.json @@ -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:*" + } +} diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 00000000..11e9153e --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,46 @@ +{ + "name": "@strudel/web", + "version": "0.8.0", + "description": "Easy to setup, opiniated bundle of Strudel for the browser.", + "main": "repl.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 ", + "contributors": [ + "Alex McLean " + ], + "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/soundfonts": "workspace:*", + "@strudel.cycles/mini": "workspace:*", + "@strudel.cycles/tonal": "workspace:*", + "@strudel.cycles/transpiler": "workspace:*" + }, + "devDependencies": { + "vite": "^4.3.3" + } +} diff --git a/packages/web/repl.mjs b/packages/web/repl.mjs new file mode 100644 index 00000000..f86459b1 --- /dev/null +++ b/packages/web/repl.mjs @@ -0,0 +1,38 @@ +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 { repl as _repl, evalScope, controls } from '@strudel.cycles/core'; +import { initAudioOnFirstClick, getAudioContext, registerSynthSounds, webaudioOutput } from '@strudel.cycles/webaudio'; +import { registerSoundfonts } from '@strudel.cycles/soundfonts'; +import { transpiler } from '@strudel.cycles/transpiler'; + +async function prebake(userPrebake) { + const loadModules = evalScope( + evalScope, + controls, + import('@strudel.cycles/core'), + import('@strudel.cycles/mini'), + import('@strudel.cycles/tonal'), + import('@strudel.cycles/webaudio'), + ); + await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts(), userPrebake?.()]); +} + +export function repl(options = {}) { + const prebaked = prebake(options?.prebake); + initAudioOnFirstClick(); + return _repl({ + defaultOutput: webaudioOutput, + getTime: () => getAudioContext().currentTime, + transpiler, + ...options, + beforeEval: async (args) => { + options?.beforeEval?.(args); + await prebaked; + }, + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 596351fa..ee27ce0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -471,6 +471,41 @@ importers: specifier: ^0.28.0 version: 0.28.0(@vitest/ui@0.28.0) + packages/web: + dependencies: + '@strudel.cycles/core': + specifier: workspace:* + version: link:../core + '@strudel.cycles/mini': + specifier: workspace:* + version: link:../mini + '@strudel.cycles/soundfonts': + specifier: workspace:* + version: link:../soundfonts + '@strudel.cycles/tonal': + specifier: workspace:* + version: link:../tonal + '@strudel.cycles/transpiler': + specifier: workspace:* + version: link:../transpiler + '@strudel.cycles/webaudio': + specifier: workspace:* + version: link:../webaudio + devDependencies: + vite: + specifier: ^4.3.3 + version: 4.3.3(@types/node@18.16.3) + + packages/web/examples/repl-example: + dependencies: + '@strudel/web': + specifier: workspace:* + version: link:../.. + devDependencies: + vite: + specifier: ^4.3.2 + version: 4.3.3(@types/node@18.16.3) + packages/webaudio: dependencies: '@strudel.cycles/core': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4b821d33..984f2447 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,4 +4,5 @@ packages: - "website/" - "packages/core/examples/vite-vanilla-repl" - "packages/core/examples/vite-vanilla-repl-cm6" - - "packages/react/examples/nano-repl" \ No newline at end of file + - "packages/react/examples/nano-repl" + - "packages/web/examples/repl-example" From 12228c56d97d004cb2ca536ea8514c02c690882c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 7 May 2023 22:36:26 +0200 Subject: [PATCH 24/86] improve api for web package --- packages/core/index.mjs | 1 + packages/core/repl.mjs | 39 ++++++----- packages/mini/mini.mjs | 5 ++ packages/web/README.md | 39 +++++++---- packages/web/examples/repl-example/index.html | 13 ++-- packages/web/package.json | 2 +- packages/web/repl.mjs | 38 ----------- packages/web/vite.config.js | 19 ++++++ packages/web/web.mjs | 64 +++++++++++++++++++ packages/webaudio/webaudio.mjs | 13 ++++ 10 files changed, 160 insertions(+), 73 deletions(-) delete mode 100644 packages/web/repl.mjs create mode 100644 packages/web/vite.config.js create mode 100644 packages/web/web.mjs diff --git a/packages/core/index.mjs b/packages/core/index.mjs index 30504a5b..78241f74 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -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 './draw.mjs'; export * from './animate.mjs'; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index de979258..abb9ef80 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -17,23 +17,15 @@ export function repl({ }) { const scheduler = new Cyclist({ interval, - onTrigger: async (hap, deadline, duration, cps) => { - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps); - } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps); - } - } catch (err) { - logger(`[cyclist] error: ${err.message}`, 'error'); - } - }, + onTrigger: getTrigger({ defaultOutput, getTime }), onError: onSchedulerError, getTime, onToggle, }); + const setPattern = (pattern, autostart = true) => { + pattern = editPattern?.(pattern) || pattern; + scheduler.setPattern(pattern, autostart); + }; const evaluate = async (code, autostart = true) => { if (!code) { throw new Error('no code to evaluate'); @@ -43,8 +35,7 @@ export function repl({ let { pattern } = await _evaluate(code, transpiler); logger(`[eval] code updated`); - pattern = editPattern?.(pattern) || pattern; - scheduler.setPattern(pattern, autostart); + setPattern(pattern, autostart); afterEval?.({ code, pattern }); return pattern; } catch (err) { @@ -61,5 +52,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'); + } + }; diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 648499e6..3f9228d9 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -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); +} diff --git a/packages/web/README.md b/packages/web/README.md index c70a5df7..53609c3e 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -4,16 +4,20 @@ This package provides an easy to use bundle of multiple strudel packages for the ## Usage -```js -import { repl } from '@strudel/web'; +Minimal example: -const strudel = repl(); +```js +import '@strudel/web'; document.getElementById('play').addEventListener('click', - () => strudel.evaluate('note("c a f e").jux(rev)') -); + () => note("c a f e").play() +) ``` +As soon as you `import '@strudel/web'`, all strudel functions will be available in the global scope. +In this case, we are using `note` 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. ### Loading samples @@ -21,15 +25,28 @@ Note: Due to the [Autoplay policy](https://developer.mozilla.org/en-US/docs/Web/ By default, no external samples are loaded, but you can add them like this: ```js -import { repl, samples } from '@strudel/web'; +import { prebake } from '@strudel/web'; -const strudel = repl({ - prebake: () => samples('github:tidalcycles/Dirt-Samples/master'), -}); +prebake(() => samples('github:tidalcycles/Dirt-Samples/master')) document.getElementById('play').addEventListener('click', - () => strudel.evaluate('s("bd,jvbass(3,8)").jux(rev)') -); + () => 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 +import '@strudel/web'; + +document.getElementById('play').addEventListener('click', + () => evaluate('note("c a f e").jux(rev)') +); +``` diff --git a/packages/web/examples/repl-example/index.html b/packages/web/examples/repl-example/index.html index a7954e64..853e02db 100644 --- a/packages/web/examples/repl-example/index.html +++ b/packages/web/examples/repl-example/index.html @@ -13,17 +13,16 @@ diff --git a/packages/web/package.json b/packages/web/package.json index 11e9153e..be1a4bb9 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@strudel/web", "version": "0.8.0", "description": "Easy to setup, opiniated bundle of Strudel for the browser.", - "main": "repl.mjs", + "main": "web.mjs", "publishConfig": { "main": "dist/index.js", "module": "dist/index.mjs" diff --git a/packages/web/repl.mjs b/packages/web/repl.mjs deleted file mode 100644 index f86459b1..00000000 --- a/packages/web/repl.mjs +++ /dev/null @@ -1,38 +0,0 @@ -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 { repl as _repl, evalScope, controls } from '@strudel.cycles/core'; -import { initAudioOnFirstClick, getAudioContext, registerSynthSounds, webaudioOutput } from '@strudel.cycles/webaudio'; -import { registerSoundfonts } from '@strudel.cycles/soundfonts'; -import { transpiler } from '@strudel.cycles/transpiler'; - -async function prebake(userPrebake) { - const loadModules = evalScope( - evalScope, - controls, - import('@strudel.cycles/core'), - import('@strudel.cycles/mini'), - import('@strudel.cycles/tonal'), - import('@strudel.cycles/webaudio'), - ); - await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts(), userPrebake?.()]); -} - -export function repl(options = {}) { - const prebaked = prebake(options?.prebake); - initAudioOnFirstClick(); - return _repl({ - defaultOutput: webaudioOutput, - getTime: () => getAudioContext().currentTime, - transpiler, - ...options, - beforeEval: async (args) => { - options?.beforeEval?.(args); - await prebaked; - }, - }); -} diff --git a/packages/web/vite.config.js b/packages/web/vite.config.js new file mode 100644 index 00000000..ffa2ad27 --- /dev/null +++ b/packages/web/vite.config.js @@ -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', + }, +}); diff --git a/packages/web/web.mjs b/packages/web/web.mjs new file mode 100644 index 00000000..e546a8f9 --- /dev/null +++ b/packages/web/web.mjs @@ -0,0 +1,64 @@ +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 init(options = {}) { + initAudioOnFirstClick(); + miniAllStrings(); + const { prebake, ...schedulerOptions } = options; + + initDone = (async () => { + await defaultPrebake(); + await prebake?.(); + })(); + scheduler = webaudioScheduler(schedulerOptions); +} + +// 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(); +} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index c75bde0b..6b069a85 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -243,3 +243,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 }), + }); +} From 6e02bf59e94024985d7f9f3dc2bdd3365a8f6a72 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 10 May 2023 11:44:55 +0200 Subject: [PATCH 25/86] Revert "refactor: remove old draw logic" This reverts commit 95719654f3bfc5d3cc98c33816b402e2c0e38bd7. --- packages/core/draw.mjs | 38 +++++++++- packages/core/index.mjs | 1 + packages/core/pianoroll.mjs | 134 +++++++++++++++++++++++++++++++++++- packages/core/repl.mjs | 2 + packages/core/time.mjs | 11 +++ packages/core/ui.mjs | 23 +++++++ website/src/repl/Repl.jsx | 5 +- 7 files changed, 208 insertions(+), 6 deletions(-) create mode 100644 packages/core/time.mjs diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 58b14040..1b5c87e7 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern } from './index.mjs'; +import { Pattern, getTime, State, TimeSpan } from './index.mjs'; export const getDrawContext = (id = 'test-canvas') => { let canvas = document.querySelector('#' + id); @@ -19,9 +19,45 @@ export const getDrawContext = (id = 'test-canvas') => { return canvas.getContext('2d'); }; +Pattern.prototype.draw = function (callback, { from, to, onQuery }) { + if (window.strudelAnimation) { + cancelAnimationFrame(window.strudelAnimation); + } + const ctx = getDrawContext(); + let cycle, + events = []; + const animate = (time) => { + const t = getTime(); + if (from !== undefined && to !== undefined) { + const currentCycle = Math.floor(t); + if (cycle !== currentCycle) { + cycle = currentCycle; + const begin = currentCycle + from; + const end = currentCycle + to; + setTimeout(() => { + events = this.query(new State(new TimeSpan(begin, end))) + .filter(Boolean) + .filter((event) => event.part.begin.equals(event.whole.begin)); + onQuery?.(events); + }, 0); + } + } + callback(ctx, events, t, time); + window.strudelAnimation = requestAnimationFrame(animate); + }; + requestAnimationFrame(animate); + return this; +}; + export const cleanupDraw = (clearScreen = true) => { const ctx = getDrawContext(); clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); + if (window.strudelAnimation) { + cancelAnimationFrame(window.strudelAnimation); + } + if (window.strudelScheduler) { + clearInterval(window.strudelScheduler); + } }; Pattern.prototype.onPaint = function (onPaint) { diff --git a/packages/core/index.mjs b/packages/core/index.mjs index 78241f74..16ef3be4 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -20,6 +20,7 @@ export * from './evaluate.mjs'; export * from './repl.mjs'; export * from './cyclist.mjs'; export * from './logger.mjs'; +export * from './time.mjs'; export * from './draw.mjs'; export * from './animate.mjs'; export * from './pianoroll.mjs'; diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 336bd428..59476015 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, noteToMidi, freqToMidi } from './index.mjs'; +import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs'; const scale = (normalized, min, max) => normalized * (max - min) + min; const getValue = (e) => { @@ -29,6 +29,134 @@ const getValue = (e) => { return value; }; +Pattern.prototype.pianoroll = function ({ + cycles = 4, + playhead = 0.5, + overscan = 1, + flipTime = 0, + flipValues = 0, + hideNegative = false, + // inactive = '#C9E597', + // inactive = '#FFCA28', + inactive = '#7491D2', + active = '#FFCA28', + // background = '#2A3236', + background = 'transparent', + smear = 0, + playheadColor = 'white', + minMidi = 10, + maxMidi = 90, + autorange = 0, + timeframe: timeframeProp, + fold = 0, + vertical = 0, +} = {}) { + const ctx = getDrawContext(); + const w = ctx.canvas.width; + const h = ctx.canvas.height; + let from = -cycles * playhead; + let to = cycles * (1 - playhead); + + if (timeframeProp) { + console.warn('timeframe is deprecated! use from/to instead'); + from = 0; + to = timeframeProp; + } + const timeAxis = vertical ? h : w; + const valueAxis = vertical ? w : h; + let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time + const timeExtent = to - from; // number of seconds that fit inside the canvas frame + const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values + let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true + let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true + let foldValues = []; + flipTime && timeRange.reverse(); + flipValues && valueRange.reverse(); + + this.draw( + (ctx, events, t) => { + ctx.fillStyle = background; + ctx.globalAlpha = 1; // reset! + if (!smear) { + ctx.clearRect(0, 0, w, h); + ctx.fillRect(0, 0, w, h); + } + const inFrame = (event) => + (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from; + events.filter(inFrame).forEach((event) => { + 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; + const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange); + let durationPx = scale(event.duration / timeExtent, 0, timeAxis); + const value = getValue(event); + const valuePx = scale( + fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent, + ...valueRange, + ); + let margin = 0; + const offset = scale(t / timeExtent, ...timeRange); + let coords; + if (vertical) { + coords = [ + valuePx + 1 - (flipValues ? barThickness : 0), // x + timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y + barThickness - 2, // width + durationPx - 2, // height + ]; + } else { + coords = [ + timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x + valuePx + 1 - (flipValues ? 0 : barThickness), // y + durationPx - 2, // widith + barThickness - 2, // height + ]; + } + isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); + }); + ctx.globalAlpha = 1; // reset! + const playheadPosition = scale(-from / timeExtent, ...timeRange); + // draw playhead + ctx.strokeStyle = playheadColor; + ctx.beginPath(); + if (vertical) { + ctx.moveTo(0, playheadPosition); + ctx.lineTo(valueAxis, playheadPosition); + } else { + ctx.moveTo(playheadPosition, 0); + ctx.lineTo(playheadPosition, valueAxis); + } + ctx.stroke(); + }, + { + from: from - overscan, + to: to + overscan, + onQuery: (events) => { + const { min, max, values } = events.reduce( + ({ min, max, values }, e) => { + const v = getValue(e); + return { + min: v < min ? v : min, + max: v > max ? v : max, + values: values.includes(v) ? values : [...values, v], + }; + }, + { min: Infinity, max: -Infinity, values: [] }, + ); + if (autorange) { + minMidi = min; + maxMidi = max; + valueExtent = maxMidi - minMidi + 1; + } + foldValues = values.sort((a, b) => String(a).localeCompare(String(b))); + barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent; + }, + }, + ); + return this; +}; + // this function allows drawing a pianoroll without ties to Pattern.prototype // it will probably replace the above in the future export function pianoroll({ @@ -170,11 +298,11 @@ Pattern.prototype.punchcard = function (options) { ); }; -Pattern.prototype.pianoroll = function (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; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index abb9ef80..c88145d1 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -1,6 +1,7 @@ import { Cyclist } from './cyclist.mjs'; import { evaluate as _evaluate } from './evaluate.mjs'; import { logger } from './logger.mjs'; +import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; export function repl({ @@ -26,6 +27,7 @@ export function repl({ 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'); diff --git a/packages/core/time.mjs b/packages/core/time.mjs new file mode 100644 index 00000000..80daaf53 --- /dev/null +++ b/packages/core/time.mjs @@ -0,0 +1,11 @@ +let time; +export function getTime() { + if (!time) { + throw new Error('no time set! use setTime to define a time source'); + } + return time(); +} + +export function setTime(func) { + time = func; +} diff --git a/packages/core/ui.mjs b/packages/core/ui.mjs index cc148553..df8230ec 100644 --- a/packages/core/ui.mjs +++ b/packages/core/ui.mjs @@ -4,6 +4,19 @@ Copyright (C) 2022 Strudel contributors - see . */ +import { getTime } from './time.mjs'; + +function frame(callback) { + if (window.strudelAnimation) { + cancelAnimationFrame(window.strudelAnimation); + } + const animate = (animationTime) => { + callback(animationTime, getTime()); + window.strudelAnimation = requestAnimationFrame(animate); + }; + requestAnimationFrame(animate); +} + export const backgroundImage = function (src, animateOptions = {}) { const container = document.getElementById('code'); const bg = 'background-image:url(' + src + ');background-size:contain;'; @@ -15,8 +28,18 @@ export const backgroundImage = function (src, animateOptions = {}) { className: () => (container.className = value + ' ' + initialClassName), })[option](); }; + const funcOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'function'); const stringOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'string'); stringOptions.forEach(([option, value]) => handleOption(option, value)); + + if (funcOptions.length === 0) { + return; + } + frame((_, t) => + funcOptions.forEach(([option, value]) => { + handleOption(option, value(t)); + }), + ); }; export const cleanupUi = () => { diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index d8270b16..4ad387fe 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -50,9 +50,10 @@ const modulesLoading = evalScope( const presets = prebake(); -let drawContext; +let drawContext, clearCanvas; if (typeof window !== 'undefined') { drawContext = getDrawContext(); + clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width); } const getTime = () => getAudioContext().currentTime; @@ -207,7 +208,7 @@ export function Repl({ embedded = false }) { const handleShuffle = async () => { const { code, name } = getRandomTune(); logger(`[repl] ✨ loading random tune "${name}"`); - cleanupDraw(); + clearCanvas(); resetLoadedSounds(); scheduler.setCps(1); await prebake(); // declare default samples From 6089849d351aba12f72b94ff93fe660ce8c0e19b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 10 May 2023 11:53:26 +0200 Subject: [PATCH 26/86] rename init to initStrudel + add to window --- packages/web/examples/repl-example/index.html | 4 ++-- packages/web/web.mjs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/web/examples/repl-example/index.html b/packages/web/examples/repl-example/index.html index 853e02db..12003e4b 100644 --- a/packages/web/examples/repl-example/index.html +++ b/packages/web/examples/repl-example/index.html @@ -13,8 +13,8 @@ From 087f57435542f3194ab2073b6e9de07c8ee050e4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 10 May 2023 12:49:45 +0200 Subject: [PATCH 31/86] update web readme + add more html examples --- packages/web/README.md | 56 ++++++++++++++++++++++------- packages/web/examples/evaluate.html | 10 ++++++ packages/web/examples/minimal.html | 10 ++++++ packages/web/examples/samples.html | 12 +++++++ pnpm-lock.yaml | 3 -- 5 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 packages/web/examples/evaluate.html create mode 100644 packages/web/examples/minimal.html create mode 100644 packages/web/examples/samples.html diff --git a/packages/web/README.md b/packages/web/README.md index 53609c3e..f1ed2bbc 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -4,30 +4,47 @@ This package provides an easy to use bundle of multiple strudel packages for the ## Usage -Minimal example: +Save this code as a `.html` file and double click it: -```js -import '@strudel/web'; +```html + + + + ``` -As soon as you `import '@strudel/web'`, all strudel functions will be available in the global scope. -In this case, we are using `note` to create a pattern. +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 -import { prebake } from '@strudel/web'; - -prebake(() => samples('github:tidalcycles/Dirt-Samples/master')) +initStrudel({ + prebake: () => samples('github:tidalcycles/Dirt-Samples/master'), +}); document.getElementById('play').addEventListener('click', () => s("bd sd").play() @@ -44,9 +61,22 @@ 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 -import '@strudel/web'; - +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! \ No newline at end of file diff --git a/packages/web/examples/evaluate.html b/packages/web/examples/evaluate.html new file mode 100644 index 00000000..77e6431f --- /dev/null +++ b/packages/web/examples/evaluate.html @@ -0,0 +1,10 @@ + + + + diff --git a/packages/web/examples/minimal.html b/packages/web/examples/minimal.html new file mode 100644 index 00000000..58487009 --- /dev/null +++ b/packages/web/examples/minimal.html @@ -0,0 +1,10 @@ + + + + diff --git a/packages/web/examples/samples.html b/packages/web/examples/samples.html new file mode 100644 index 00000000..4a839581 --- /dev/null +++ b/packages/web/examples/samples.html @@ -0,0 +1,12 @@ + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee27ce0d..9710374f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -479,9 +479,6 @@ importers: '@strudel.cycles/mini': specifier: workspace:* version: link:../mini - '@strudel.cycles/soundfonts': - specifier: workspace:* - version: link:../soundfonts '@strudel.cycles/tonal': specifier: workspace:* version: link:../tonal From a1ec8545fbc71a934716052c3f3f3d8524363874 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 10 May 2023 13:03:06 +0200 Subject: [PATCH 32/86] format --- packages/web/examples/samples.html | 2 +- packages/web/web.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/examples/samples.html b/packages/web/examples/samples.html index 4a839581..8b967ec2 100644 --- a/packages/web/examples/samples.html +++ b/packages/web/examples/samples.html @@ -3,7 +3,7 @@ diff --git a/website/src/pages/workshop/intro.mdx b/website/src/pages/workshop/intro.mdx new file mode 100644 index 00000000..e7b075b4 --- /dev/null +++ b/website/src/pages/workshop/intro.mdx @@ -0,0 +1,6 @@ +--- +title: Introduction +layout: ../../layouts/MainLayout.astro +--- + +# Introduction diff --git a/website/src/pages/workshop/langebank.mdx b/website/src/pages/workshop/langebank.mdx new file mode 100644 index 00000000..55fbdf4b --- /dev/null +++ b/website/src/pages/workshop/langebank.mdx @@ -0,0 +1,37 @@ +Everythings repeats once per second => 1 **c**ycle **p**er **s**econd (cps) + +**Change tempo** + + + +adding your own samples + + + +").slow(3)`} + punchcard +/> + + +n(run(8)).sound("east") \ No newline at end of file diff --git a/website/src/pages/workshop/mini-notation.mdx b/website/src/pages/workshop/mini-notation.mdx new file mode 100644 index 00000000..df1c8cbe --- /dev/null +++ b/website/src/pages/workshop/mini-notation.mdx @@ -0,0 +1,69 @@ +--- +title: First Sounds +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; + +# Mini Notation + +Mini Notation is everything between the quotes. It the short rhythm language of Tidal. + +## Cycles + +**The longer the sequence, the faster it runs** + + + +**Play less sounds per cycle with \{curly braces\}** + + + +**Use \`backticks\` for multiple lines** + + + +**Play one sounds per cycle with \** + +")`} punchcard /> + +This is the same as `{...}%1` + +## Operators + +**Multiplication: Speed things up** + + + +**Division: Slow things down** + + + +`bd` will play only every second time + +## Combining it all + +**Speed up Sub-Sequences** + + + +**Slow down Sequences** + + + +**Parallel Sub-Sequences** + + + +**Sample Numbers on groups** + + diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index edf63f54..f8a4e477 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -22,8 +22,8 @@ export async function prebake() { tag: 'drum-machines', }), samples(`./EmuSP12.json`, `./EmuSP12/`, { prebake: true, tag: 'drum-machines' }), - // samples('github:tidalcycles/Dirt-Samples/master'), ]); + await samples('github:tidalcycles/Dirt-Samples/master'); } const maxPan = noteToMidi('C8'); diff --git a/website/tsconfig.json b/website/tsconfig.json index 78017eaf..90aa524f 100644 --- a/website/tsconfig.json +++ b/website/tsconfig.json @@ -7,6 +7,11 @@ "noImplicitAny": false, "types": [ "vite-plugin-pwa/client" - ] + ], + "baseUrl": ".", + "paths": { + "@components/*": ["src/components/*"], + "@src/*": ["src/*"], + } } } \ No newline at end of file From 0d6fcf78d83691ca324fb69acd63e14322924688 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 26 May 2023 16:05:53 +0200 Subject: [PATCH 38/86] hide mini repl headers + improve workshop --- packages/react/src/components/MiniRepl.jsx | 62 ++++--- website/src/docs/MiniRepl.jsx | 3 +- website/src/pages/workshop/first-sounds.mdx | 182 +++++++++++++------- website/src/pages/workshop/langebank.mdx | 23 ++- 4 files changed, 179 insertions(+), 91 deletions(-) diff --git a/packages/react/src/components/MiniRepl.jsx b/packages/react/src/components/MiniRepl.jsx index 8c45f1ed..9c30dc29 100644 --- a/packages/react/src/components/MiniRepl.jsx +++ b/packages/react/src/components/MiniRepl.jsx @@ -20,11 +20,12 @@ export function MiniRepl({ enableKeyboard, drawTime, punchcard, - span, canvasHeight = 200, + fontSize = 18, + hideHeader = false, theme, }) { - drawTime = drawTime || (punchcard ? span || [0, 4] : undefined); + drawTime = drawTime || (punchcard ? [0, 4] : undefined); const evalOnMount = !!drawTime; const drawContext = useCallback( !!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null, @@ -48,7 +49,10 @@ export function MiniRepl({ } = useStrudel({ initialCode: tune, defaultOutput: webaudioOutput, - editPattern: (pat) => (punchcard ? pat.punchcard() : pat), + editPattern: (pat, id) => { + //pat = pat.withContext((ctx) => ({ ...ctx, id })); + return punchcard ? pat.punchcard() : pat; + }, getTime, evalOnMount, drawContext, @@ -102,7 +106,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); }); } }, []), @@ -110,31 +114,35 @@ export function MiniRepl({ return (
-
-
- - + {!hideHeader && ( +
+
+ + +
- {error &&
{error.message}
} -
+ )}
- {show && } + {show && ( + + )} + {error &&
{error.message}
}
{drawTime && ( { @@ -47,6 +47,7 @@ export function MiniRepl({ tune, drawTime, punchcard, span = [0, 4], canvasHeigh span={span} canvasHeight={canvasHeight} theme={themes[theme]} + hideHeader={hideHeader} />
) : ( diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx index 1a9df5f6..65a79093 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -13,50 +13,51 @@ import QA from '@components/QA'; Let's start by making some noise: - + -1. press play button to start -2. change `house` to `casio` -3. press refresh button to update -4. press stop button to stop - - - -Congratulations, you've played your first pattern! - -Instead of clicking update all the time, you can use keyboard shortcuts: - - - -1. click into the text field +1. ⬆️ click into the text field above ⬆️ 2. press `ctrl`+`enter` to play -3. change `casio` to `crow` +3. change `house` to `casio` 4. press `ctrl`+`enter` to update 5. press `ctrl`+`.` to stop -To play code like an instrument, these shortcuts should become second nature to you. +Congratulations, you are now live coding! **Try more Sounds** You can pick a different sample from the same set, with ':' - + -Try changing `east:1` to `east:2` + -Here are some more sound sets to try +Try changing `east:1` to `east:2` to hear a different sound in the `east` set. + +You can try other numbers too! You might hear a little pause while the sound is loading + + + +Here are some more sound sets to try: ``` casio control crow techno house jazz metal east jvbass juno insect space wind -bd sd rim hh oh ``` - +Now you know how to use different sounds. +For now we'll stick to this little selection of sounds, but we'll find out how to load your own sounds later. + +## Drum Sounds + +By default, Strudel comes with a wide selection of drum sounds: + + + +These letter combinations stand for different parts of a drum set: - `bd` = **b**ass **d**rum - `sd` = **s**nare **d**rum @@ -65,13 +66,30 @@ bd sd rim hh oh - `hh` = **h**i**h**at - `oh` = **o**pen **h**ihat - +To change the sound character of our drums, we can use `bank` to change the drum machine: + + + +In this example `RolandTR909` is the name of the drum machine that we're using. +It is a famous drum machine for house and techno beats. + + + +Try changing `RolandTR909` to one of + +- `AkaiLinn` +- `RhythmAce` +- `RolandTR808` +- `RolandTR707` +- `ViscoSpaceDrum` + + ## Sequences -**Make a Sequence** +In the last example, we already saw that you can play multiple sounds in a sequence by separating them with a space: - + Notice how the currently playing sound is highlighted in the code and also visualized below. @@ -83,13 +101,13 @@ Try adding more sounds to the sequence! **The longer the sequence, the faster it runs** - + -The content of the sequence will be squished into one second, called a cycle. +The content of a sequence will be squished into what's called a cycle. **One way to change the tempo is using `cpm`** - + @@ -103,11 +121,11 @@ We will look at other ways to change the tempo later! **Add a rests in a sequence with '~'** - + **Sub-Sequences with [brackets]** - + @@ -119,31 +137,44 @@ Similar to the whole sequence, the content of a sub-sequence will be squished to **Multiplication: Speed things up** - + + +**Multiplication: Speed up sequences** + + **Multiplication: Speeeeeeeeed things up** - + -Pitch = Really fast Rhythm +Pitch = really fast rhythm **Sub-Sub-Sequences with [[brackets]]** - + -**Play Sounds in parallel with comma** + - +You can go as deep as you want! - + + +**Play sequences in parallel with comma** + + + +You can use as many commas as you want: + + **Multiple Lines with backticks** | -| Sample Number | :x | | -| Rests | ~ | | -| Sub-Sequences | \[ \] | | -| Sub-Sub-Sequences | \[ \[ \]\] | | -| Speed up | \* | | -| Parallel | , | | +| Concept | Syntax | Example | +| ----------------- | ---------- | -------------------------------------------------------------------------------- | +| Sequence | space | | +| Sample Number | :x | | +| Rests | ~ | | +| Sub-Sequences | \[ \] | | +| Sub-Sub-Sequences | \[ \[ \]\] | | +| Speed up | \* | | +| Parallel | , | | ## Examples -Imitation of a step sequencer: +**Basic rock beat** + + + +**Classic house** + + + +Notice that the house and rock beats are extremely similar. Besides their different tempos and minor differences in the hihat and kick drum lines, these patterns are the same. You'll find certain drum patterns reused in many styles. + +We Will Rock you + + + +**Yellow Magic Orchestra - Firecracker** + +**Imitation of a 16 step sequencer** + + -Shorter variant: - - - -Another beat: +**Another one** + +**Not your average drums** + + + +This was just the tip of the iceberg! diff --git a/website/src/pages/workshop/langebank.mdx b/website/src/pages/workshop/langebank.mdx index 55fbdf4b..b41d9658 100644 --- a/website/src/pages/workshop/langebank.mdx +++ b/website/src/pages/workshop/langebank.mdx @@ -1,4 +1,11 @@ -Everythings repeats once per second => 1 **c**ycle **p**er **s**econd (cps) + + +1. press play button to start +2. change `house` to `casio` +3. press refresh button to update +4. press stop button to stop + + **Change tempo** @@ -33,5 +40,17 @@ adding your own samples punchcard /> +n(run(8)).sound("east") -n(run(8)).sound("east") \ No newline at end of file +Shorter variant: + + From fc0618121701b39c46d6bd4e3ab0c8fbcaff837f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 27 May 2023 13:30:57 +0200 Subject: [PATCH 39/86] - add claviature flag to minirepl - bring back option+dot on macos - consume more editor settings in minirepl --- packages/core/util.mjs | 3 +- packages/react/src/components/MiniRepl.jsx | 30 ++++++++++--- packages/react/src/hooks/useStrudel.mjs | 3 +- pnpm-lock.yaml | 51 ++++++++++++---------- website/package.json | 3 +- website/src/components/Claviature.jsx | 24 ++++++++++ website/src/docs/MiniRepl.jsx | 41 +++++++++++++++-- website/src/repl/Repl.jsx | 2 +- 8 files changed, 121 insertions(+), 36 deletions(-) create mode 100644 website/src/components/Claviature.jsx diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 2b43cf0b..37fe6b6c 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -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; }; diff --git a/packages/react/src/components/MiniRepl.jsx b/packages/react/src/components/MiniRepl.jsx index 9c30dc29..8ff738e6 100644 --- a/packages/react/src/components/MiniRepl.jsx +++ b/packages/react/src/components/MiniRepl.jsx @@ -18,18 +18,21 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, + onTrigger, drawTime, punchcard, + onPaint, canvasHeight = 200, fontSize = 18, hideHeader = false, theme, + keybindings, }) { 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, @@ -51,7 +54,15 @@ export function MiniRepl({ defaultOutput: webaudioOutput, editPattern: (pat, id) => { //pat = pat.withContext((ctx) => ({ ...ctx, id })); - return punchcard ? pat.punchcard() : pat; + if (onTrigger) { + pat = pat.onTrigger(onTrigger, false); + } + if (onPaint) { + pat = pat.onPaint(onPaint); + } else if (punchcard) { + pat = pat.punchcard(); + } + return pat; }, getTime, evalOnMount, @@ -87,7 +98,7 @@ export function MiniRepl({ e.preventDefault(); flash(view); await activateCode(); - } else if (e.key === '.') { + } else if (e.key === '.' || e.code === 'Period') { stop(); e.preventDefault(); } @@ -140,11 +151,18 @@ export function MiniRepl({ )}
{show && ( - + )} {error &&
{error.message}
}
- {drawTime && ( + {punchcard && ( !!(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( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a135d89..6542a707 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,7 +92,7 @@ importers: devDependencies: vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) packages/core: dependencies: @@ -102,7 +102,7 @@ importers: devDependencies: vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) vitest: specifier: ^0.28.0 version: 0.28.0(@vitest/ui@0.28.0) @@ -127,7 +127,7 @@ importers: devDependencies: vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) packages/core/examples/vite-vanilla-repl-cm6: dependencies: @@ -155,7 +155,7 @@ importers: devDependencies: vite: specifier: ^4.3.2 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) packages/csound: dependencies: @@ -171,7 +171,7 @@ importers: devDependencies: vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) packages/embed: {} @@ -204,7 +204,7 @@ importers: version: link:../mini vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) vitest: specifier: ^0.28.0 version: 0.28.0(@vitest/ui@0.28.0) @@ -223,7 +223,7 @@ importers: devDependencies: vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) packages/mini: dependencies: @@ -236,7 +236,7 @@ importers: version: 3.0.2 vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) vitest: specifier: ^0.28.0 version: 0.28.0(@vitest/ui@0.28.0) @@ -255,7 +255,7 @@ importers: version: 5.8.1 vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) packages/react: dependencies: @@ -325,7 +325,7 @@ importers: version: 3.3.2 vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) packages/react/examples/nano-repl: dependencies: @@ -380,7 +380,7 @@ importers: version: 3.3.2 vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) packages/serial: dependencies: @@ -390,7 +390,7 @@ importers: devDependencies: vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) packages/soundfonts: dependencies: @@ -412,7 +412,7 @@ importers: version: 3.3.1 vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) packages/tonal: dependencies: @@ -431,7 +431,7 @@ importers: devDependencies: vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) vitest: specifier: ^0.28.0 version: 0.28.0(@vitest/ui@0.28.0) @@ -447,7 +447,7 @@ importers: devDependencies: vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) vitest: specifier: ^0.28.0 version: 0.28.0(@vitest/ui@0.28.0) @@ -469,7 +469,7 @@ importers: devDependencies: vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) vitest: specifier: ^0.28.0 version: 0.28.0(@vitest/ui@0.28.0) @@ -494,7 +494,7 @@ importers: devDependencies: vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) packages/web/examples/repl-example: dependencies: @@ -504,7 +504,7 @@ importers: devDependencies: vite: specifier: ^4.3.2 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) packages/webaudio: dependencies: @@ -517,7 +517,7 @@ importers: devDependencies: vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) packages/webdirt: dependencies: @@ -533,7 +533,7 @@ importers: devDependencies: vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) vitest: specifier: ^0.28.0 version: 0.28.0(@vitest/ui@0.28.0) @@ -546,7 +546,7 @@ importers: devDependencies: vite: specifier: ^4.3.3 - version: 4.3.3(@types/node@18.16.3) + version: 4.3.3(@types/node@18.11.18) vitest: specifier: ^0.28.0 version: 0.28.0(@vitest/ui@0.28.0) @@ -646,6 +646,9 @@ importers: canvas: specifier: ^2.11.2 version: 2.11.2 + claviature: + specifier: ^0.1.0 + version: 0.1.0 fraction.js: specifier: ^4.2.0 version: 4.2.0 @@ -4502,7 +4505,7 @@ packages: '@babel/plugin-transform-react-jsx-self': 7.21.0(@babel/core@7.21.5) '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.21.5) react-refresh: 0.14.0 - vite: 4.3.3(@types/node@18.16.3) + vite: 4.3.3(@types/node@18.11.18) transitivePeerDependencies: - supports-color dev: true @@ -5413,6 +5416,10 @@ packages: resolution: {integrity: sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==} engines: {node: '>=8'} + /claviature@0.1.0: + resolution: {integrity: sha512-Ai12axNwQ7x/F9QAj64RYKsgvi5Y33+X3GUSKAC/9s/adEws8TSSc0efeiqhKNGKBo6rT/c+CSCwSXzXxwxZzQ==} + dev: false + /clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} diff --git a/website/package.json b/website/package.json index 6b8b7a6c..d6100625 100644 --- a/website/package.json +++ b/website/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@algolia/client-search": "^4.17.0", - "@astrojs/mdx": "^0.19.0", + "@astrojs/mdx": "^0.19.0", "@astrojs/react": "^2.1.1", "@astrojs/tailwind": "^3.1.1", "@docsearch/css": "^3.3.4", @@ -43,6 +43,7 @@ "@uiw/codemirror-themes-all": "^4.19.16", "astro": "^2.3.2", "canvas": "^2.11.2", + "claviature": "^0.1.0", "fraction.js": "^4.2.0", "nanoid": "^4.0.2", "nanostores": "^0.8.1", diff --git a/website/src/components/Claviature.jsx b/website/src/components/Claviature.jsx new file mode 100644 index 00000000..e97facbc --- /dev/null +++ b/website/src/components/Claviature.jsx @@ -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.children.map((el, i) => { + const TagName = el.name; + return ( + + {el.value} + + ); + })} + + ); +} diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index 6b434353..651241a9 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -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,19 @@ if (typeof window !== 'undefined') { prebake(); } -export function MiniRepl({ tune, drawTime, punchcard, span = [0, 4], canvasHeight = 100, hideHeader }) { +export function MiniRepl({ + tune, + drawTime, + punchcard, + span = [0, 4], + canvasHeight = 100, + hideHeader, + claviature, + claviatureLabels, +}) { const [Repl, setRepl] = useState(); - const { theme } = useSettings(); + const { theme, keybindings, fontSize, fontFamily } = useSettings(); + const [activeNotes, setActiveNotes] = useState([]); useEffect(() => { // we have to load this package on the client // because codemirror throws an error on the server @@ -42,13 +53,35 @@ export function MiniRepl({ tune, drawTime, punchcard, span = [0, 4], canvasHeigh { + const active = haps + .map((hap) => hap.value.note) + .filter(Boolean) + .map((n) => (typeof n === 'string' ? noteToMidi(n) : n)); + setActiveNotes(active); + } + : undefined + } /> + {claviature && ( + + )}
) : (
{tune}
diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 4ad387fe..1db8f8dd 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -157,7 +157,7 @@ export function Repl({ embedded = false }) { e.preventDefault(); flash(view); await activateCode(); - } else if (e.key === '.') { + } else if (e.key === '.' || e.keyCode === 'Period') { stop(); e.preventDefault(); } From 4e575c44b3f2821d745303f90034a99fec0299c8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 27 May 2023 13:31:18 +0200 Subject: [PATCH 40/86] begin first notes page --- website/src/components/QA.tsx | 1 + website/src/config.ts | 1 + website/src/pages/workshop/first-notes.mdx | 227 ++++++++++++++++++++ website/src/pages/workshop/first-sounds.mdx | 4 +- website/src/pages/workshop/langebank.mdx | 35 +++ 5 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 website/src/pages/workshop/first-notes.mdx diff --git a/website/src/components/QA.tsx b/website/src/components/QA.tsx index ef743bed..7d2ac53d 100644 --- a/website/src/components/QA.tsx +++ b/website/src/components/QA.tsx @@ -1,5 +1,6 @@ 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 }) { diff --git a/website/src/config.ts b/website/src/config.ts index 54170378..6fc7bb77 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -45,6 +45,7 @@ export const SIDEBAR: Sidebar = { Workshop: [ { text: 'Intro', link: 'workshop/intro' }, { text: 'First Sounds', link: 'workshop/first-sounds' }, + { text: 'First Notes', link: 'workshop/first-notes' }, { text: 'First Effects', link: 'workshop/first-effects' }, { text: 'Mini Notation', link: 'workshop/mini-notation' }, ], diff --git a/website/src/pages/workshop/first-notes.mdx b/website/src/pages/workshop/first-notes.mdx new file mode 100644 index 00000000..6e4f9110 --- /dev/null +++ b/website/src/pages/workshop/first-notes.mdx @@ -0,0 +1,227 @@ +--- +title: First Notes +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '@src/docs/MiniRepl'; +import { midi2note } from '@strudel.cycles/core/'; +import Box from '@components/Box.astro'; +import QA from '@components/QA'; + +# First Notes + +Let's look at how we can play notes + +## numbers and notes + +**play notes with numbers** + + [midi2note(i + 36), i + 36]), + )} +/> + + + +Try out different numbers! + +Try decimal numbers, like 55.5 + + + +**play notes with letters** + + [n, n.split('')[0]]))} +/> + + + +Try out different letters (a - g). + +Can you find melodies that are actual words? Hint: ☕ 😉 ⚪ + + + +**add flats or sharps to play the black keys** + + [n, n.split('').slice(0, 2).join('')]), + )} +/> + + [n, n.split('').slice(0, 2).join('')]), + )} +/> + +**play notes with letters in different octaves** + + [n, n]))} + claviatureLabels={Object.fromEntries( + Array(49) + .fill() + .map((_, i) => [midi2note(i + 36), midi2note(i + 36)]), + )} +/> + + + +Try out different octaves (1-8) + + + +## changing the sound + +Just like with unpitched sounds, we can change the sound of our notes with `sound`: + + + + + +Try out different sounds: + +- gm_electric_guitar_muted +- gm_acoustic_bass +- gm_voice_oohs +- gm_blown_bottle +- sawtooth +- square +- triangle +- how about bd, sd or hh? +- remove `.sound('...')` completely + + + +**switch between sounds** + + + +**stack multiple sounds** + + + + + +The `note` and `sound` patterns are combined! + +We will see more ways to combine patterns later.. + + + +## Longer Sequences + +**Divide sequences with `/` to slow them down** + +{/* [c2 bb1 f2 eb2] */} + + + + + +The `/4` plays the sequence in brackets over 4 cycles (=4s). + +Try adding more notes inside the brackets and notice how it gets faster. + + + +Because it is so common to just play one thing per cycle, you can.. + +**Play one per cycle with \< \>** + +").sound("gm_acoustic_bass")`} punchcard /> + + + +Try adding more notes inside the brackets and notice how it does **not** get faster. + + + +**Play one sequence per cycle** + +{/* <[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2 */} + +/2") + .sound("gm_acoustic_bass")`} +/> + +**Play X per cycle with \{ \}** + + + + + +Try different numbers after `%` + +`{ ... }%1` is the same as `< ... >` + + + +## Examples + +Small Town Boy + +/2") +.sound("gm_synth_bass_1").lpf(1000)`} +/> + +/2" +.add.squeeze("[0 12]\*4") +.note() +.sound("gm_synth_bass_1")`} +/> diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx index 65a79093..b0aeb1fb 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -83,6 +83,8 @@ Try changing `RolandTR909` to one of - `RolandTR707` - `ViscoSpaceDrum` +There are a lot more, but let's keep it simple for now + ## Sequences @@ -264,4 +266,4 @@ insect [crow metal] ~ ~, punchcard /> -This was just the tip of the iceberg! +Now that we know the basics of how to make beats, let's look at how we can play [notes](/workshop/first-notes) diff --git a/website/src/pages/workshop/langebank.mdx b/website/src/pages/workshop/langebank.mdx index b41d9658..44e5d600 100644 --- a/website/src/pages/workshop/langebank.mdx +++ b/website/src/pages/workshop/langebank.mdx @@ -54,3 +54,38 @@ Shorter variant: bd [~ ~ ~ bd] [~ bd] [~ ~ ~ bd] \`).cpm(90/4)`} /> + +polyrythms & polymeters + +-- This can make for flexible time signatures: + +d1 $ sound "[bd bd sn:5] [bd sn:3]" + +-- You can put subsequences inside subsequences: +d1 $ sound "[[bd bd] bd sn:5] [bd sn:3]" + +-- Keep going.. +d1 $ sound "[[bd [bd bd bd bd]] bd sn:5] [bd sn:3]" + +-- * Polymetric / polyrhythmic sequences + +-- Play two subsequences at once by separating with a comma: + +d1 $ sound "[voodoo voodoo:3, arpy arpy:4 arpy:2]" + +-- compare how [,] and {,} work: + +d1 $ sound "[voodoo voodoo:3, arpy arpy:4 arpy:2]" + +d1 $ sound "{voodoo voodoo:3, arpy arpy:4 arpy:2}" + +d1 $ sound "[drum bd hh bd, can can:2 can:3 can:4 can:2]" + +d1 $ sound "{drum bd hh bd, can can:2 can:3 can:4 can:2}" + +d1 $ sound "[bd sn, can:2 can:3 can:1, arpy arpy:1 arpy:2 arpy:3 arpy:5]" + +d1 $ sound "{bd sn, can:2 can:3 can:1, arpy arpy:1 arpy:2 arpy:3 arpy:5}" + + + From ed792fc0d46a75b6836a5d74ca79079f004b2111 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 27 May 2023 16:29:05 +0200 Subject: [PATCH 41/86] continue notes chapter --- website/src/pages/workshop/first-notes.mdx | 74 ++++++++++++++++------ website/src/pages/workshop/langebank.mdx | 30 +++++++++ 2 files changed, 86 insertions(+), 18 deletions(-) diff --git a/website/src/pages/workshop/first-notes.mdx b/website/src/pages/workshop/first-notes.mdx index 6e4f9110..cb40713f 100644 --- a/website/src/pages/workshop/first-notes.mdx +++ b/website/src/pages/workshop/first-notes.mdx @@ -97,11 +97,17 @@ Try out different octaves (1-8) +If you are not comfortable with the note letter system, it should be easier to use numbers instead. +Most of the examples below will use numbers for that reason. +We will also look at ways to make it easier to play the right notes later. + ## changing the sound Just like with unpitched sounds, we can change the sound of our notes with `sound`: - + + +{/* c2 g2, e3 b3 d4 e4 */} @@ -157,6 +163,8 @@ We will see more ways to combine patterns later.. The `/4` plays the sequence in brackets over 4 cycles (=4s). +So each of the 4 notes is 1s long. + Try adding more notes inside the brackets and notice how it gets faster. @@ -181,47 +189,77 @@ Try adding more notes inside the brackets and notice how it does **not** get fas hideHeader client:visible tune={`note("<[36 48]*4 [34 46]*4 [41 53]*4 [39 51]*4>/2") - .sound("gm_acoustic_bass")`} +.sound("gm_acoustic_bass")`} /> -**Play X per cycle with \{ \}** +**Alternate between multiple things** ") +.sound("gm_xylophone")`} +/> + +This is also useful for unpitched sounds: + +, [~ hh]*2") +.bank("RolandTR909")`} +/> + +## Scales + +Finding the right notes can be difficult.. Scales are here to help: + +") +.scale("C:minor").sound("piano")`} /> -Try different numbers after `%` +Try out different numbers. Any number should sound good! -`{ ... }%1` is the same as `< ... >` +Try out different scales: + +- C:major +- A2:minor +- D:dorian +- G:mixolydian +- A2:minor:pentatonic +- F:major:pentatonic -## Examples +**automate scales** -Small Town Boy +Just like anything, we can automate the scale with a pattern: + +") +.scale("/2") +.sound("piano")`} +/> + +## Examples /2") -.sound("gm_synth_bass_1").lpf(1000)`} +.sound("gm_synth_bass_1")`} /> /2" -.add.squeeze("[0 12]\*4") -.note() + tune={`note("[0 12]*2".add("<36 34 41 39>/2")) .sound("gm_synth_bass_1")`} /> diff --git a/website/src/pages/workshop/langebank.mdx b/website/src/pages/workshop/langebank.mdx index 44e5d600..9190afa6 100644 --- a/website/src/pages/workshop/langebank.mdx +++ b/website/src/pages/workshop/langebank.mdx @@ -89,3 +89,33 @@ d1 $ sound "{bd sn, can:2 can:3 can:1, arpy arpy:1 arpy:2 arpy:3 arpy:5}" + + + +**Play X per cycle with \{ \}** + + + + + +Try different numbers after `%` + +`{ ... }%1` is the same as `< ... >` + + + +## Bracket Recap + +- `[]` squeezes contents to 1 cycle +- `<>` plays one item per cycle +- `{}%x` plays x items per cycle From 1ba5d2e1ca09c5aa086130c57c87746e021e3081 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 27 May 2023 21:52:44 +0200 Subject: [PATCH 42/86] finish notes chapter --- website/src/pages/workshop/first-effects.mdx | 22 ++-- website/src/pages/workshop/first-notes.mdx | 127 ++++++++++++++++++- website/src/pages/workshop/first-sounds.mdx | 23 +++- website/src/pages/workshop/langebank.mdx | 45 ++++++- 4 files changed, 191 insertions(+), 26 deletions(-) diff --git a/website/src/pages/workshop/first-effects.mdx b/website/src/pages/workshop/first-effects.mdx index 56914dbc..1b718b16 100644 --- a/website/src/pages/workshop/first-effects.mdx +++ b/website/src/pages/workshop/first-effects.mdx @@ -9,54 +9,52 @@ import { MiniRepl } from '../../docs/MiniRepl'; **vowel** - + You can probably think of more vowels :) **gain** - + **control the gain with a sine wave** - + Try also `saw`, `square`, `tri` **The 'structure' comes from the left - try swapping:** - + **speed of playback, e.g. 2 = double speed (up 1 octave)** - + -**set note** - - - **pan** - + **delay** - + **room** - + diff --git a/website/src/pages/workshop/first-notes.mdx b/website/src/pages/workshop/first-notes.mdx index cb40713f..b986ba79 100644 --- a/website/src/pages/workshop/first-notes.mdx +++ b/website/src/pages/workshop/first-notes.mdx @@ -190,6 +190,7 @@ Try adding more notes inside the brackets and notice how it does **not** get fas client:visible tune={`note("<[36 48]*4 [34 46]*4 [41 53]*4 [39 51]*4>/2") .sound("gm_acoustic_bass")`} + punchcard /> **Alternate between multiple things** @@ -199,6 +200,7 @@ Try adding more notes inside the brackets and notice how it does **not** get fas client:visible tune={`note("60 <63 62 65 63>") .sound("gm_xylophone")`} + punchcard /> This is also useful for unpitched sounds: @@ -208,6 +210,7 @@ This is also useful for unpitched sounds: client:visible tune={`sound("bd*2, ~ , [~ hh]*2") .bank("RolandTR909")`} + punchcard /> ## Scales @@ -219,6 +222,7 @@ Finding the right notes can be difficult.. Scales are here to help: client:visible tune={`n("0 2 4 <[6,8] [7,9]>") .scale("C:minor").sound("piano")`} + punchcard /> @@ -243,23 +247,136 @@ Just like anything, we can automate the scale with a pattern: ") -.scale("/2") + tune={`n("<0 -3>, 2 4 <[6,8] [7,9]>") +.scale("/4") .sound("piano")`} + punchcard /> + + +If you have no idea what these scale mean, don't worry. +These are just labels for different sets of notes that go well together. + +Take your time and you'll find scales you like! + + + +## Repeat & Elongate + +**Elongate with @** + + + + + +Not using `@` is like using `@1`. In the above example, c is 3 units long and eb is 1 unit long. + +Try changing that number! + + + +**Elongate within sub-sequences** + +*2") +.scale("/4") +.sound("gm_acoustic_bass")`} + punchcard +/> + + + +This groove is called a `shuffle`. +Each beat has two notes, where the first is twice as long as the second. +This is also sometimes called triplet swing. You'll often find it in blues and jazz. + + + +**Replicate** + +]").sound("piano")`} punchcard /> + + + +Try switching between `!`, `*` and `@` + +What's the difference? + + + +## Recap + +Let's recap what we've learned in this chapter: + +| Concept | Syntax | Example | +| --------- | ------ | ------------------------------------------------------------------- | +| Slow down | \/ | | +| Alternate | \<\> | ")`} /> | +| Elongate | @ | | +| Replicate | ! | | + ## Examples +**Classy Bassline** + /2") -.sound("gm_synth_bass_1")`} +.sound("gm_synth_bass_1") +.lpf(800) // <-- we'll learn about this soon`} /> +**Classy Melody** + /2")) -.sound("gm_synth_bass_1")`} + tune={`n(\`< +[~ 0] 2 [0 2] [~ 2] +[~ 0] 1 [0 1] [~ 1] +[~ 0] 3 [0 3] [~ 3] +[~ 0] 2 [0 2] [~ 2] +>*2\`).scale("C4:minor") +.sound("gm_synth_strings_1")`} /> + +**Classy Drums** + +, [~ hh]*2") +.bank("RolandTR909")`} +/> + +**If there just was a way to play all the above at the same time.......** + + + +It's called `stack` 😙 + + + +/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, ~ , [~ hh]*2") + .bank("RolandTR909") +)`} +/> + +This is starting to sound like actual music! We have sounds, we have notes, now the last piece of the puzzle is missing: [effects](/workshop/first-effects) diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx index b0aeb1fb..852977a3 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -91,7 +91,7 @@ There are a lot more, but let's keep it simple for now In the last example, we already saw that you can play multiple sounds in a sequence by separating them with a space: - + Notice how the currently playing sound is highlighted in the code and also visualized below. @@ -173,6 +173,18 @@ You can use as many commas as you want: +Commas can also be used inside sub-sequences: + + + + + +Notice how the 2 above are the same? + +It is quite common that there are many ways to express the same idea. + + + **Multiple Lines with backticks** + **Classic house** -Notice that the house and rock beats are extremely similar. Besides their different tempos and minor differences in the hihat and kick drum lines, these patterns are the same. You'll find certain drum patterns reused in many styles. + + +Notice that the two patterns are extremely similar. +Certain drum patterns are reused across genres. + + We Will Rock you diff --git a/website/src/pages/workshop/langebank.mdx b/website/src/pages/workshop/langebank.mdx index 9190afa6..7a1ca697 100644 --- a/website/src/pages/workshop/langebank.mdx +++ b/website/src/pages/workshop/langebank.mdx @@ -67,7 +67,7 @@ d1 $ sound "[[bd bd] bd sn:5] [bd sn:3]" -- Keep going.. d1 $ sound "[[bd [bd bd bd bd]] bd sn:5] [bd sn:3]" --- * Polymetric / polyrhythmic sequences +-- \* Polymetric / polyrhythmic sequences -- Play two subsequences at once by separating with a comma: @@ -87,11 +87,6 @@ d1 $ sound "[bd sn, can:2 can:3 can:1, arpy arpy:1 arpy:2 arpy:3 arpy:5]" d1 $ sound "{bd sn, can:2 can:3 can:1, arpy arpy:1 arpy:2 arpy:3 arpy:5}" - - - - - **Play X per cycle with \{ \}** ` plays one item per cycle - `{}%x` plays x items per cycle + +/2")) +.sound("gm_synth_bass_1")`} +/> + +vertical + + +< 4 4 4 3> +<[2,7] [2,6] [1,6] [1,6]> +< 4 4 4 3> +>*2\`) +.scale("/4") +.sound("piano")`} +/> + +horizontal + +*2\`) +.scale("/4") +.sound("piano")`} +/> From 8c93e578a062ee24fdc0edc2c356caa118bf5964 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 28 May 2023 12:41:46 +0200 Subject: [PATCH 43/86] clamp delayfeedback --- packages/webaudio/webaudio.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 6b069a85..c5922303 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -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); From 9971867e2f2adfa9b93f244bccdf71875ab65913 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 28 May 2023 12:41:53 +0200 Subject: [PATCH 44/86] clamp function --- packages/core/util.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 37fe6b6c..5dbf65fc 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -213,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); From d2dffe318685ec7d196d654ccb1a60b4cecc6acc Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 28 May 2023 12:42:15 +0200 Subject: [PATCH 45/86] MiniRepl: consume font settings --- packages/react/src/components/MiniRepl.jsx | 2 ++ website/src/docs/MiniRepl.css | 4 ++++ website/src/docs/MiniRepl.jsx | 2 ++ 3 files changed, 8 insertions(+) diff --git a/packages/react/src/components/MiniRepl.jsx b/packages/react/src/components/MiniRepl.jsx index 8ff738e6..c13ff28e 100644 --- a/packages/react/src/components/MiniRepl.jsx +++ b/packages/react/src/components/MiniRepl.jsx @@ -24,6 +24,7 @@ export function MiniRepl({ onPaint, canvasHeight = 200, fontSize = 18, + fontFamily, hideHeader = false, theme, keybindings, @@ -156,6 +157,7 @@ export function MiniRepl({ onChange={setCode} onViewChanged={setView} theme={theme} + fontFamily={fontFamily} fontSize={fontSize} keybindings={keybindings} /> diff --git a/website/src/docs/MiniRepl.css b/website/src/docs/MiniRepl.css index e9b49af8..5e520671 100644 --- a/website/src/docs/MiniRepl.css +++ b/website/src/docs/MiniRepl.css @@ -7,3 +7,7 @@ border: 1px solid var(--lineHighlight); padding: 2px; } + +.cm-scroller { + font-family: inherit !important; +} diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index 651241a9..6dfaf28e 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -60,6 +60,8 @@ export function MiniRepl({ theme={themes[theme]} hideHeader={hideHeader} keybindings={keybindings} + fontFamily={fontFamily} + fontSize={fontSize} onPaint={ claviature ? (ctx, time, haps, drawTime) => { From 1e9979ae186b84d3c1f97f36b9167b7a7d5f5094 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 28 May 2023 12:42:24 +0200 Subject: [PATCH 46/86] start fleshing out effects chapter --- website/src/pages/workshop/first-effects.mdx | 166 ++++++++++++++++++- website/src/pages/workshop/first-sounds.mdx | 35 ++-- 2 files changed, 181 insertions(+), 20 deletions(-) diff --git a/website/src/pages/workshop/first-effects.mdx b/website/src/pages/workshop/first-effects.mdx index 1b718b16..3c0fc0fd 100644 --- a/website/src/pages/workshop/first-effects.mdx +++ b/website/src/pages/workshop/first-effects.mdx @@ -7,15 +7,175 @@ import { MiniRepl } from '../../docs/MiniRepl'; # First Effects +import Box from '@components/Box.astro'; + +We have sounds, we have notes, now let's look at effects! + +**low-pass filter with lpf** + +/2") +.sound("sawtooth").lpf(800)`} +/> + + + +- Change lpf to 200. Notice how it gets muffled. Think of it as standing in front of the club with the door closed 🚪. +- Now let's open the door... change it to 8000. Notice how it gets sharper 🪩 + + + +**automate the filter** + +/2") +.sound("sawtooth").lpf("200 1000")`} +/> + + + +- Try adding more values +- Notice how the pattern in lpf does not change the overall rhythm + + + **vowel** - +/2") +.sound("sawtooth").vowel("/2")`} +/> -You can probably think of more vowels :) + + +- Try adding more values +- Notice how the pattern in lpf does not change the overall rhythm + + **gain** - + + + + +Rhythm is all about dynamics! + +Remove the gain and notice how flat it sounds. + + + +**stacks within stacks** + +Let's combine all of the above into a little tune: + +/2") + .sound("sawtooth").lpf("200 1000"), + note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>/2") + .sound("sawtooth").vowel("/2") +) `} +/> + + + +Pay attention to where the commas are and identify the individual parts of the stacks. +The 3 parts (drums, bassline, chords) are exactly as earlier, just stacked together. + + + +**fast and slow** + +We can use `fast` and `slow` to change the tempo of a pattern outside of Mini-Notation: + + + + + +Change the `slow` value. Try replacing it with `fast`. + +What happens if you use a pattern like `"<1 [2 4]>"`? + + + +By the way, inside Mini-Notation, `fast` is `*` and slow is `/`. + +")`} /> + +**delay** + + ~]") + .sound("gm_electric_guitar_muted"), + sound("").bank("RolandTR707") +).delay(".5")`} +/> + + + +Try some `delay` values between 0 and 1. Btw, `.5` is short for `0.5` + +What happens if you use `.delay(".8:.125")` ? Can you guess what the second number does? + +What happens if you use `.delay(".8:.06:.8")` ? Can you guess what the third number does? + + + +**room** + + ~@16] ~>/2") +.scale("D4:minor").sound("gm_accordion:2") +.room(2)`} +/> + + + +Try different values! + +Add a delay too! + + + +**little dub tune** + + ~]") + .sound("gm_electric_guitar_muted"), + sound("").bank("RolandTR707"), + n("<4 [3@3 4] [<2 0> ~@16] ~>/2") + .scale("D4:minor").sound("gm_accordion:2") + .room(2).gain(.5) +).delay(.5)`} +/> **control the gain with a sine wave** diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx index 852977a3..5b87d10f 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -190,9 +190,10 @@ It is quite common that there are many ways to express the same idea. @@ -201,25 +202,25 @@ bd*2, [~ casio], Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal. This is what we've leared so far: -| Concept | Syntax | Example | -| ----------------- | ---------- | -------------------------------------------------------------------------------- | -| Sequence | space | | -| Sample Number | :x | | -| Rests | ~ | | -| Sub-Sequences | \[ \] | | -| Sub-Sub-Sequences | \[ \[ \]\] | | -| Speed up | \* | | -| Parallel | , | | +| Concept | Syntax | Example | +| ----------------- | -------- | -------------------------------------------------------------------------------- | +| Sequence | space | | +| Sample Number | :x | | +| Rests | ~ | | +| Sub-Sequences | \[\] | | +| Sub-Sub-Sequences | \[\[\]\] | | +| Speed up | \* | | +| Parallel | , | | ## Examples **Basic rock beat** - + **Classic house** - + @@ -230,15 +231,15 @@ Certain drum patterns are reused across genres. We Will Rock you - + **Yellow Magic Orchestra - Firecracker** From 8679dc63beee41032e0e228994b2c5b3785eefb3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 29 May 2023 02:18:27 +0200 Subject: [PATCH 47/86] pianoroll: also reflect gain in transparency --- packages/core/pianoroll.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 59476015..2b5af1a7 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -87,7 +87,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); @@ -240,7 +240,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); From 536327f403dd3d79b3b0b00a088a4c8dcf9adf17 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 29 May 2023 02:18:36 +0200 Subject: [PATCH 48/86] effects chapter mostly finished --- website/src/pages/workshop/first-effects.mdx | 170 ++++++++++++------- 1 file changed, 112 insertions(+), 58 deletions(-) diff --git a/website/src/pages/workshop/first-effects.mdx b/website/src/pages/workshop/first-effects.mdx index 3c0fc0fd..f1162ef0 100644 --- a/website/src/pages/workshop/first-effects.mdx +++ b/website/src/pages/workshop/first-effects.mdx @@ -11,7 +11,9 @@ import Box from '@components/Box.astro'; We have sounds, we have notes, now let's look at effects! -**low-pass filter with lpf** +## Some basic effects + +**low-pass filter** +lpf = **l**ow **p**ass **f**ilter + - Change lpf to 200. Notice how it gets muffled. Think of it as standing in front of the club with the door closed 🚪. -- Now let's open the door... change it to 8000. Notice how it gets sharper 🪩 +- Now let's open the door... change it to 5000. Notice how it gets brighter ✨🪩 -**automate the filter** +**pattern the filter** **vowel** @@ -52,13 +58,6 @@ We have sounds, we have notes, now let's look at effects! .sound("sawtooth").vowel("/2")`} /> - - -- Try adding more values -- Notice how the pattern in lpf does not change the overall rhythm - - - **gain** Rhythm is all about dynamics! -Remove the gain and notice how flat it sounds. +- Remove `.gain(...)` and notice how flat it sounds. +- Bring it back by undoing (ctrl+z) @@ -88,7 +89,7 @@ Let's combine all of the above into a little tune: tune={`stack( stack( sound("hh*8").gain("[.25 1]*2"), - sound("bd*2,~ rim") + sound("bd*2,~ sd:1") ), note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2") .sound("sawtooth").lpf("200 1000"), @@ -99,29 +100,11 @@ Let's combine all of the above into a little tune: -Pay attention to where the commas are and identify the individual parts of the stacks. -The 3 parts (drums, bassline, chords) are exactly as earlier, just stacked together. +Try to identify the individual parts of the stacks, pay attention to where the commas are. +The 3 parts (drums, bassline, chords) are exactly as earlier, just stacked together, separated by comma. -**fast and slow** - -We can use `fast` and `slow` to change the tempo of a pattern outside of Mini-Notation: - - - - - -Change the `slow` value. Try replacing it with `fast`. - -What happens if you use a pattern like `"<1 [2 4]>"`? - - - -By the way, inside Mini-Notation, `fast` is `*` and slow is `/`. - -")`} /> - **delay** -**room** +**room aka reverb** ~]") - .sound("gm_electric_guitar_muted"), - sound("").bank("RolandTR707"), + .sound("gm_electric_guitar_muted").delay(.5), + sound("").bank("RolandTR707").delay(.5), n("<4 [3@3 4] [<2 0> ~@16] ~>/2") .scale("D4:minor").sound("gm_accordion:2") .room(2).gain(.5) -).delay(.5)`} +)`} /> -**control the gain with a sine wave** - - - -Try also `saw`, `square`, `tri` - -**The 'structure' comes from the left - try swapping:** - - - -**speed of playback, e.g. 2 = double speed (up 1 octave)** - - +Let's add a bass to make this complete: ~]") + .sound("gm_electric_guitar_muted").delay(.5), + sound("").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) +)`} /> + + +Try adding `.hush()` at the end of one of the patterns in the stack... + + + **pan** - +**speed** -**delay** +").room(.2)`} /> - +**fast and slow** -**room** +We can use `fast` and `slow` to change the tempo of a pattern outside of Mini-Notation: - + + + + +Change the `slow` value. Try replacing it with `fast`. + +What happens if you use a pattern like `.fast("<1 [2 4]>")`? + + + +By the way, inside Mini-Notation, `fast` is `*` and slow is `/`. + +")`} /> + +## automation with signals + +Instead of changing values stepwise, we can also control them with signals: + + + + + +The basic waveforms for signals are `sine`, `saw`, `square`, `tri` 🌊 + +Try also random signals `rand` and `perlin`! + +The gain is visualized as transparency in the pianoroll. + + + +**setting a range** + + + + + +What happens if you flip the range values? + + + +**setting a range** + +By default, waves oscillate between 0 to 1. We can change that with `range`: + +/2") +.sound("sawtooth") +.lpf(sine.range(100, 2000).slow(8))`} +/> + + + +Notice how the wave is slowed down. The whole automation will take 8 cycles to repeat. + + + +## Recap + +| name | example | +| ----- | ----------------------------------------------------------------------------------------------- | +| lpf | ")`} /> | +| vowel | ")`} /> | +| gain | | +| delay | | +| room | | +| pan | | +| speed | ")`} /> | +| range | | From 39d5955e587f70e136a09267540755dfcef66aa6 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 29 May 2023 12:39:05 +0200 Subject: [PATCH 49/86] add function recap to sounds chapter --- website/src/pages/workshop/first-sounds.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx index 5b87d10f..b651c0ba 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -212,6 +212,14 @@ This is what we've leared so far: | Speed up | \* | | | Parallel | , | | +The Mini-Notation is usually used inside some function. These are the functions we've seen so far: + +| Name | Description | Example | +| ----- | ----------------------------------- | ---------------------------------------------------------------------------------- | +| sound | plays the sound of the given name | | +| bank | selects the sound bank | | +| cpm | sets the tempo in cycles per minute | | + ## Examples **Basic rock beat** From 8b7bb7b6ae482458872971ac9d63d0d473b72d1b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 29 May 2023 12:39:17 +0200 Subject: [PATCH 50/86] add adsr section to effects chapter --- website/src/pages/workshop/first-effects.mdx | 49 ++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/website/src/pages/workshop/first-effects.mdx b/website/src/pages/workshop/first-effects.mdx index f1162ef0..c9b6dc56 100644 --- a/website/src/pages/workshop/first-effects.mdx +++ b/website/src/pages/workshop/first-effects.mdx @@ -4,6 +4,7 @@ layout: ../../layouts/MainLayout.astro --- import { MiniRepl } from '../../docs/MiniRepl'; +import QA from '@components/QA'; # First Effects @@ -105,6 +106,54 @@ The 3 parts (drums, bassline, chords) are exactly as earlier, just stacked toget +**adsr envelope** + +") +.sound("sawtooth").lpf(600) +.attack(.1) +.decay(.1) +.sustain(.25) +.release(.2)`} +/> + + + +Try to find out what the numbers do.. Compare the following + +- attack: `.5` vs `0` +- decay: `.5` vs `0` +- sustain: `1` vs `.25` vs `0` +- release: `0` vs `.5` vs `1` + +Can you guess what they do? + + + + + +- attack: time it takes to fade in +- decay: time it takes to fade to sustain +- sustain: level after decay +- release: time it takes to fade out after note is finished + +![ADSR](https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/ADSR_parameter.svg/1920px-ADSR_parameter.svg.png) + + + +**adsr short notation** + +") +.sound("sawtooth").lpf(600) +.adsr(".1:.1:.5:.2") +`} +/> + **delay** Date: Mon, 29 May 2023 12:39:36 +0200 Subject: [PATCH 51/86] add compound adsr + ds controls --- packages/core/controls.mjs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index ef5bd33f..c4044d4a 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -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; From c4a38d9008fd1cc81de288c171286aafa0716bcc Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 30 May 2023 06:16:02 +0200 Subject: [PATCH 52/86] + pattern effects chapter + recap page + only load mini repls when visible --- website/src/config.ts | 3 +- website/src/pages/workshop/first-effects.mdx | 66 +++--- website/src/pages/workshop/first-notes.mdx | 8 + website/src/pages/workshop/first-sounds.mdx | 11 + website/src/pages/workshop/intro.mdx | 16 ++ .../src/pages/workshop/pattern-effects.mdx | 194 ++++++++++++++++++ website/src/pages/workshop/recap.mdx | 98 +++++++++ 7 files changed, 362 insertions(+), 34 deletions(-) create mode 100644 website/src/pages/workshop/pattern-effects.mdx create mode 100644 website/src/pages/workshop/recap.mdx diff --git a/website/src/config.ts b/website/src/config.ts index 6fc7bb77..a3629fe3 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -47,7 +47,8 @@ export const SIDEBAR: Sidebar = { { text: 'First Sounds', link: 'workshop/first-sounds' }, { text: 'First Notes', link: 'workshop/first-notes' }, { text: 'First Effects', link: 'workshop/first-effects' }, - { text: 'Mini Notation', link: 'workshop/mini-notation' }, + { text: 'Pattern Effects', link: 'workshop/pattern-effects' }, + { text: 'Recap', link: 'workshop/recap' }, ], Tutorial: [ { text: 'Getting Started', link: 'learn/getting-started' }, diff --git a/website/src/pages/workshop/first-effects.mdx b/website/src/pages/workshop/first-effects.mdx index c9b6dc56..1428e2bd 100644 --- a/website/src/pages/workshop/first-effects.mdx +++ b/website/src/pages/workshop/first-effects.mdx @@ -18,7 +18,7 @@ We have sounds, we have notes, now let's look at effects! /2") .sound("sawtooth").lpf(800)`} /> @@ -36,7 +36,7 @@ lpf = **l**ow **p**ass **f**ilter /2") .sound("sawtooth").lpf("200 1000")`} /> @@ -54,7 +54,7 @@ We will learn how to automate with waves later... /2") .sound("sawtooth").vowel("/2")`} /> @@ -63,7 +63,7 @@ We will learn how to automate with waves later... -**adsr envelope** +**shape the sound with an adsr envelope** ") .sound("sawtooth").lpf(600) .attack(.1) @@ -147,7 +147,7 @@ Can you guess what they do? ") .sound("sawtooth").lpf(600) .adsr(".1:.1:.5:.2") @@ -158,7 +158,7 @@ Can you guess what they do? ~]") .sound("gm_electric_guitar_muted"), @@ -180,7 +180,7 @@ What happens if you use `.delay(".8:.06:.8")` ? Can you guess what the third num ~@16] ~>/2") .scale("D4:minor").sound("gm_accordion:2") .room(2)`} @@ -198,7 +198,7 @@ Add a delay too! ~]") .sound("gm_electric_guitar_muted").delay(.5), @@ -213,7 +213,7 @@ Let's add a bass to make this complete: ~]") .sound("gm_electric_guitar_muted").delay(.5), @@ -237,7 +237,7 @@ Try adding `.hush()` at the end of one of the patterns in the stack... ").room(.2)`} /> +").room(.2)`} /> **fast and slow** We can use `fast` and `slow` to change the tempo of a pattern outside of Mini-Notation: - + @@ -263,13 +263,13 @@ What happens if you use a pattern like `.fast("<1 [2 4]>")`? By the way, inside Mini-Notation, `fast` is `*` and slow is `/`. -")`} /> +")`} /> ## automation with signals Instead of changing values stepwise, we can also control them with signals: - + @@ -283,7 +283,9 @@ The gain is visualized as transparency in the pianoroll. **setting a range** - +By default, waves oscillate between 0 to 1. We can change that with `range`: + + @@ -291,13 +293,11 @@ What happens if you flip the range values? -**setting a range** - -By default, waves oscillate between 0 to 1. We can change that with `range`: +We can change the automation speed with slow / fast: /2") .sound("sawtooth") .lpf(sine.range(100, 2000).slow(8))`} @@ -305,19 +305,19 @@ By default, waves oscillate between 0 to 1. We can change that with `range`: -Notice how the wave is slowed down. The whole automation will take 8 cycles to repeat. +The whole automation will now take 8 cycles to repeat. ## Recap -| name | example | -| ----- | ----------------------------------------------------------------------------------------------- | -| lpf | ")`} /> | -| vowel | ")`} /> | -| gain | | -| delay | | -| room | | -| pan | | -| speed | ")`} /> | -| range | | +| name | example | +| ----- | -------------------------------------------------------------------------------------------------- | +| lpf | ")`} /> | +| vowel | ")`} /> | +| gain | | +| delay | | +| room | | +| pan | | +| speed | ")`} /> | +| range | | diff --git a/website/src/pages/workshop/first-notes.mdx b/website/src/pages/workshop/first-notes.mdx index b986ba79..330dd072 100644 --- a/website/src/pages/workshop/first-notes.mdx +++ b/website/src/pages/workshop/first-notes.mdx @@ -318,6 +318,14 @@ Let's recap what we've learned in this chapter: | Elongate | @ | | | Replicate | ! | | +New functions: + +| Name | Description | Example | +| ----- | ----------------------------------- | -------------------------------------------------------------------------------------------- | +| note | set pitch as number or letter | | +| scale | interpret `n` as scale degree | | +| stack | play patterns in parallel (read on) | | + ## Examples **Classy Bassline** diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx index b651c0ba..61894333 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -197,6 +197,16 @@ It is quite common that there are many ways to express the same idea. punchcard /> +**selecting sample numbers separately** + +Instead of using ":", we can also use the `n` function to select sample numbers: + + + +This is shorter and more readable than: + + + ## Recap Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal. @@ -219,6 +229,7 @@ The Mini-Notation is usually used inside some function. These are the functions | sound | plays the sound of the given name | | | bank | selects the sound bank | | | cpm | sets the tempo in cycles per minute | | +| n | select sample number | | ## Examples diff --git a/website/src/pages/workshop/intro.mdx b/website/src/pages/workshop/intro.mdx index e7b075b4..9e18bbfe 100644 --- a/website/src/pages/workshop/intro.mdx +++ b/website/src/pages/workshop/intro.mdx @@ -4,3 +4,19 @@ layout: ../../layouts/MainLayout.astro --- # Introduction + +## goals + +- be beginner friendly +- teach a representative subset of strudel / tidal +- introduce one new thing at a time +- give practical / musical examples +- encourage self-experimentation +- hands on learning > walls of text +- maintain flow state +- no setup required + +## inspired by + +- https://github.com/tidalcycles/tidal-workshop/blob/master/workshop.tidal +- https://learningmusic.ableton.com diff --git a/website/src/pages/workshop/pattern-effects.mdx b/website/src/pages/workshop/pattern-effects.mdx new file mode 100644 index 00000000..7e028b6f --- /dev/null +++ b/website/src/pages/workshop/pattern-effects.mdx @@ -0,0 +1,194 @@ +--- +title: Pattern Effects +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '@src/docs/MiniRepl'; +import Box from '@components/Box.astro'; +import QA from '@components/QA'; + +# Pattern Effects + +Up until now, most of the functions we've seen are what other music programs are typically capable of: sequencing sounds, playing notes, controlling effects. + +In this chapter, we are going to look at functions that are more unique to tidal. + +**reverse patterns with rev** + + + +**play pattern left and modify it right with jux** + + + +This is the same as: + + + +Let's visualize what happens here: + + + + + +Try commenting out one of the two by adding `//` before a line + + + +**multiple tempos** + + + +This is like doing + + + + + +Try commenting out one or more by adding `//` before a line + + + +**add** + +>")) +.color(">").adsr("[.1 0]:.2:[1 0]") +.sound("gm_acoustic_bass").room(.5)`} + punchcard +/> + + + +If you add a number to a note, the note will be treated as if it was a number + + + +We can add as often as we like: + +>").add("0,7")) +.color(">").adsr("[.1 0]:.2:[1 0]") +.sound("gm_acoustic_bass").room(.5)`} + punchcard +/> + +**add with scale** + + [~ <4 1>]>*2".add("<0 [0,2,4]>/4")) +.scale("C5:minor").release(.5) +.sound("gm_xylophone").room(.5)`} + punchcard +/> + +**time to stack** + + [~ <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** + + + +this is like writing: + + + + + +Try patterning the `ply` function, for example using `"<1 2 1 3>"` + + + +**off** + +] <2 3> [~ 1]>" + .off(1/8, x=>x.add(4)) + //.off(1/4, x=>x.add(7)) +).scale("/4") +.s("triangle").room(.5).ds(".1:0").delay(.5)`} + punchcard +/> + + + +In the notation `x=>x.`, the `x` is the shifted pattern, which where modifying. + + + +The above is like writing: + +] <2 3> [~ 1]>*2").color("cyan"), + n("<0 [4 <3 2>] <2 3> [~ 1]>*2".add(7).late(1/8)).color("magenta") +).scale("/2") +.s("triangle").adsr(".01:.1:0").room(.5)`} + punchcard +/> + +off is also useful for sounds: + +x.speed(1.5).gain(.25))`} +/> + +| name | description | example | +| ---- | ------------------------------ | ---------------------------------------------------------------------------------------------- | +| rev | reverse | | +| jux | split left/right, modify right | | +| add | add numbers / notes | ")).scale("C:minor")`} /> | +| ply | speed up each event n times | ")`} /> | +| off | copy, shift time & modify | x.speed(2))`} /> | diff --git a/website/src/pages/workshop/recap.mdx b/website/src/pages/workshop/recap.mdx new file mode 100644 index 00000000..bc9c4648 --- /dev/null +++ b/website/src/pages/workshop/recap.mdx @@ -0,0 +1,98 @@ +--- +title: Recap +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; + +# Workshop Recap + +This page is just a listing of all functions covered in the workshop! + +## Mini Notation + +| Concept | Syntax | Example | +| ----------------- | -------- | -------------------------------------------------------------------------------- | +| Sequence | space | | +| Sample Number | :x | | +| Rests | ~ | | +| Sub-Sequences | \[\] | | +| Sub-Sub-Sequences | \[\[\]\] | | +| Speed up | \* | | +| Parallel | , | | +| Slow down | \/ | | +| Alternate | \<\> | ")`} /> | +| Elongate | @ | | +| Replicate | ! | | + +## Sounds + +| Name | Description | Example | +| ----- | --------------------------------- | ---------------------------------------------------------------------------------- | +| sound | plays the sound of the given name | | +| bank | selects the sound bank | | +| n | select sample number | | + +## Notes + +| Name | Description | Example | +| --------- | ----------------------------------- | -------------------------------------------------------------------------------------------- | +| note | set pitch as number or letter | | +| n + scale | set note in scale | | +| stack | play patterns in parallel (read on) | | + +## Audio Effects + +| name | example | +| ----- | -------------------------------------------------------------------------------------------------- | +| lpf | ")`} /> | +| vowel | ")`} /> | +| gain | | +| delay | | +| room | | +| pan | | +| speed | ")`} /> | +| range | | + +## Pattern Effects + +| name | description | example | +| ---- | ----------------------------------- | ---------------------------------------------------------------------------------------------- | +| cpm | sets the tempo in cycles per minute | | +| fast | speed up | | +| slow | slow down | | +| rev | reverse | | +| jux | split left/right, modify right | | +| add | add numbers / notes | ")).scale("C:minor")`} /> | +| ply | speed up each event n times | ")`} /> | +| off | copy, shift time & modify | x.speed(2))`} /> | + +## Samples + +``` +casio control crow techno house jazz +metal east jvbass juno insect space wind +bd sd sn cp hh +piano +``` + +## Synths + +``` +gm_electric_guitar_muted gm_acoustic_bass +gm_voice_oohs gm_blown_bottle sawtooth square triangle +gm_xylophone gm_synth_bass_1 gm_synth_strings_1 +``` + +## Banks + +``` +RolandTR909 +``` + +## Scales + +``` +major minor dorian mixolydian +minor:pentatonic major:pentatonic +``` From 61a9b01062f9952eabc8b178c27f8d8d1795ccd8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 5 Jun 2023 23:48:04 +0200 Subject: [PATCH 53/86] started german translation of workshop --- .../components/LeftSidebar/LeftSidebar.astro | 4 +- website/src/config.ts | 11 + .../src/pages/de/workshop/first-effects.mdx | 323 +++++++++++++++ website/src/pages/de/workshop/first-notes.mdx | 388 ++++++++++++++++++ .../src/pages/de/workshop/first-sounds.mdx | 310 ++++++++++++++ website/src/pages/de/workshop/index.astro | 3 + website/src/pages/de/workshop/intro.mdx | 22 + website/src/pages/de/workshop/langebank.mdx | 154 +++++++ .../src/pages/de/workshop/mini-notation.mdx | 69 ++++ .../src/pages/de/workshop/pattern-effects.mdx | 194 +++++++++ website/src/pages/de/workshop/recap.mdx | 98 +++++ 11 files changed, 1574 insertions(+), 2 deletions(-) create mode 100644 website/src/pages/de/workshop/first-effects.mdx create mode 100644 website/src/pages/de/workshop/first-notes.mdx create mode 100644 website/src/pages/de/workshop/first-sounds.mdx create mode 100644 website/src/pages/de/workshop/index.astro create mode 100644 website/src/pages/de/workshop/intro.mdx create mode 100644 website/src/pages/de/workshop/langebank.mdx create mode 100644 website/src/pages/de/workshop/mini-notation.mdx create mode 100644 website/src/pages/de/workshop/pattern-effects.mdx create mode 100644 website/src/pages/de/workshop/recap.mdx diff --git a/website/src/components/LeftSidebar/LeftSidebar.astro b/website/src/components/LeftSidebar/LeftSidebar.astro index 18327ebe..ce4dbb80 100644 --- a/website/src/components/LeftSidebar/LeftSidebar.astro +++ b/website/src/components/LeftSidebar/LeftSidebar.astro @@ -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]; --- diff --git a/website/src/config.ts b/website/src/config.ts index a3629fe3..e824f35b 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -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); @@ -41,6 +42,16 @@ export const ALGOLIA = { export type SidebarLang = Record; export type Sidebar = Record<(typeof KNOWN_LANGUAGE_CODES)[number], SidebarLang>; export const SIDEBAR: Sidebar = { + de: { + Workshop: [ + { text: 'Intro', link: 'de/workshop/intro' }, + { 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' }, + ], + }, en: { Workshop: [ { text: 'Intro', link: 'workshop/intro' }, diff --git a/website/src/pages/de/workshop/first-effects.mdx b/website/src/pages/de/workshop/first-effects.mdx new file mode 100644 index 00000000..d547548d --- /dev/null +++ b/website/src/pages/de/workshop/first-effects.mdx @@ -0,0 +1,323 @@ +--- +title: First Effects +layout: ../../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../../docs/MiniRepl'; +import QA from '@components/QA'; + +# First Effects + +import Box from '@components/Box.astro'; + +We have sounds, we have notes, now let's look at effects! + +## Some basic effects + +**low-pass filter** + +/2") +.sound("sawtooth").lpf(800)`} +/> + + + +lpf = **l**ow **p**ass **f**ilter + +- Change lpf to 200. Notice how it gets muffled. Think of it as standing in front of the club with the door closed 🚪. +- Now let's open the door... change it to 5000. Notice how it gets brighter ✨🪩 + + + +**pattern the filter** + +/2") +.sound("sawtooth").lpf("200 1000")`} +/> + + + +- Try adding more values +- Notice how the pattern in lpf does not change the overall rhythm + +We will learn how to automate with waves later... + + + +**vowel** + +/2") +.sound("sawtooth").vowel("/2")`} +/> + +**gain** + + + + + +Rhythm is all about dynamics! + +- Remove `.gain(...)` and notice how flat it sounds. +- Bring it back by undoing (ctrl+z) + + + +**stacks within stacks** + +Let's combine all of the above into a little tune: + +/2") + .sound("sawtooth").lpf("200 1000"), + note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>/2") + .sound("sawtooth").vowel("/2") +) `} +/> + + + +Try to identify the individual parts of the stacks, pay attention to where the commas are. +The 3 parts (drums, bassline, chords) are exactly as earlier, just stacked together, separated by comma. + + + +**shape the sound with an adsr envelope** + +") +.sound("sawtooth").lpf(600) +.attack(.1) +.decay(.1) +.sustain(.25) +.release(.2)`} +/> + + + +Try to find out what the numbers do.. Compare the following + +- attack: `.5` vs `0` +- decay: `.5` vs `0` +- sustain: `1` vs `.25` vs `0` +- release: `0` vs `.5` vs `1` + +Can you guess what they do? + + + + + +- attack: time it takes to fade in +- decay: time it takes to fade to sustain +- sustain: level after decay +- release: time it takes to fade out after note is finished + +![ADSR](https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/ADSR_parameter.svg/1920px-ADSR_parameter.svg.png) + + + +**adsr short notation** + +") +.sound("sawtooth").lpf(600) +.adsr(".1:.1:.5:.2") +`} +/> + +**delay** + + ~]") + .sound("gm_electric_guitar_muted"), + sound("").bank("RolandTR707") +).delay(".5")`} +/> + + + +Try some `delay` values between 0 and 1. Btw, `.5` is short for `0.5` + +What happens if you use `.delay(".8:.125")` ? Can you guess what the second number does? + +What happens if you use `.delay(".8:.06:.8")` ? Can you guess what the third number does? + + + +**room aka reverb** + + ~@16] ~>/2") +.scale("D4:minor").sound("gm_accordion:2") +.room(2)`} +/> + + + +Try different values! + +Add a delay too! + + + +**little dub tune** + + ~]") + .sound("gm_electric_guitar_muted").delay(.5), + sound("").bank("RolandTR707").delay(.5), + n("<4 [3@3 4] [<2 0> ~@16] ~>/2") + .scale("D4:minor").sound("gm_accordion:2") + .room(2).gain(.5) +)`} +/> + +Let's add a bass to make this complete: + + ~]") + .sound("gm_electric_guitar_muted").delay(.5), + sound("").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) +)`} +/> + + + +Try adding `.hush()` at the end of one of the patterns in the stack... + + + +**pan** + + + +**speed** + +").room(.2)`} /> + +**fast and slow** + +We can use `fast` and `slow` to change the tempo of a pattern outside of Mini-Notation: + + + + + +Change the `slow` value. Try replacing it with `fast`. + +What happens if you use a pattern like `.fast("<1 [2 4]>")`? + + + +By the way, inside Mini-Notation, `fast` is `*` and slow is `/`. + +")`} /> + +## automation with signals + +Instead of changing values stepwise, we can also control them with signals: + + + + + +The basic waveforms for signals are `sine`, `saw`, `square`, `tri` 🌊 + +Try also random signals `rand` and `perlin`! + +The gain is visualized as transparency in the pianoroll. + + + +**setting a range** + +By default, waves oscillate between 0 to 1. We can change that with `range`: + + + + + +What happens if you flip the range values? + + + +We can change the automation speed with slow / fast: + +/2") +.sound("sawtooth") +.lpf(sine.range(100, 2000).slow(8))`} +/> + + + +The whole automation will now take 8 cycles to repeat. + + + +## Recap + +| name | example | +| ----- | -------------------------------------------------------------------------------------------------- | +| lpf | ")`} /> | +| vowel | ")`} /> | +| gain | | +| delay | | +| room | | +| pan | | +| speed | ")`} /> | +| range | | diff --git a/website/src/pages/de/workshop/first-notes.mdx b/website/src/pages/de/workshop/first-notes.mdx new file mode 100644 index 00000000..c33fbb1a --- /dev/null +++ b/website/src/pages/de/workshop/first-notes.mdx @@ -0,0 +1,388 @@ +--- +title: First Notes +layout: ../../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '@src/docs/MiniRepl'; +import { midi2note } from '@strudel.cycles/core/'; +import Box from '@components/Box.astro'; +import QA from '@components/QA'; + +# 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** + + [midi2note(i + 36), i + 36]), + )} +/> + + + +Probiere verschiedene Zahlen aus! + +Versuch auch mal Kommazahlen, z.B. 55.5 (beachte die englische Schreibweise von Kommazahlen mit "." anstatt ",") + + + +**Töne mit Buchstaben** + + [n, n.split('')[0]]))} +/> + + + +Versuch verschiedene Buchstaben aus (a - g). + +Findest du Melodien die auch gleichzeitig ein Wort sind? Tipp: ☕ 🙈 🧚 + + + +**Vorzeichen** + + [n, n.split('').slice(0, 2).join('')]), + )} +/> + + [n, n.split('').slice(0, 2).join('')]), + )} +/> + +**Andere Oktaven** + + [n, n]))} + claviatureLabels={Object.fromEntries( + Array(49) + .fill() + .map((_, i) => [midi2note(i + 36), midi2note(i + 36)]), + )} +/> + + + +Probiere verschiedene Oktaven aus (1-8) + + + +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: + + + + + +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 + + + +**Zwischen Sounds hin und her wechseln** + + + +**Gleichzeitige Sounds** + + + + + +Die patterns in `note` und `sound` werden kombiniert! + +Wir schauen uns später noch mehr Möglichkeiten an wie man patterns kombiniert. + + + +## Längere Sequenzen + +**Divide sequences with `/` to slow them down** + +{/* [c2 bb1 f2 eb2] */} + + + + + +The `/4` plays the sequence in brackets over 4 cycles (=4s). + +So each of the 4 notes is 1s long. + +Try adding more notes inside the brackets and notice how it gets faster. + + + +Because it is so common to just play one thing per cycle, you can.. + +**Play one per cycle with \< \>** + +").sound("gm_acoustic_bass")`} punchcard /> + + + +Try adding more notes inside the brackets and notice how it does **not** get faster. + + + +**Play one sequence per cycle** + +{/* <[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2 */} + +/2") +.sound("gm_acoustic_bass")`} + punchcard +/> + +**Alternate between multiple things** + +") +.sound("gm_xylophone")`} + punchcard +/> + +This is also useful for unpitched sounds: + +, [~ hh]*2") +.bank("RolandTR909")`} + punchcard +/> + +## Scales + +Finding the right notes can be difficult.. Scales are here to help: + +") +.scale("C:minor").sound("piano")`} + punchcard +/> + + + +Try out different numbers. Any number should sound good! + +Try out different scales: + +- C:major +- A2:minor +- D:dorian +- G:mixolydian +- A2:minor:pentatonic +- F:major:pentatonic + + + +**automate scales** + +Just like anything, we can automate the scale with a pattern: + +, 2 4 <[6,8] [7,9]>") +.scale("/4") +.sound("piano")`} + punchcard +/> + + + +If you have no idea what these scale mean, don't worry. +These are just labels for different sets of notes that go well together. + +Take your time and you'll find scales you like! + + + +## Repeat & Elongate + +**Elongate with @** + + + + + +Not using `@` is like using `@1`. In the above example, c is 3 units long and eb is 1 unit long. + +Try changing that number! + + + +**Elongate within sub-sequences** + +*2") +.scale("/4") +.sound("gm_acoustic_bass")`} + punchcard +/> + + + +This groove is called a `shuffle`. +Each beat has two notes, where the first is twice as long as the second. +This is also sometimes called triplet swing. You'll often find it in blues and jazz. + + + +**Replicate** + +]").sound("piano")`} punchcard /> + + + +Try switching between `!`, `*` and `@` + +What's the difference? + + + +## Recap + +Let's recap what we've learned in this chapter: + +| Concept | Syntax | Example | +| --------- | ------ | ------------------------------------------------------------------- | +| Slow down | \/ | | +| Alternate | \<\> | ")`} /> | +| Elongate | @ | | +| Replicate | ! | | + +New functions: + +| Name | Description | Example | +| ----- | ----------------------------------- | -------------------------------------------------------------------------------------------- | +| note | set pitch as number or letter | | +| scale | interpret `n` as scale degree | | +| stack | play patterns in parallel (read on) | | + +## Examples + +**Classy Bassline** + +/2") +.sound("gm_synth_bass_1") +.lpf(800) // <-- we'll learn about this soon`} +/> + +**Classy Melody** + +*2\`).scale("C4:minor") +.sound("gm_synth_strings_1")`} +/> + +**Classy Drums** + +, [~ hh]*2") +.bank("RolandTR909")`} +/> + +**If there just was a way to play all the above at the same time.......** + + + +It's called `stack` 😙 + + + +/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, ~ , [~ hh]*2") + .bank("RolandTR909") +)`} +/> + +This is starting to sound like actual music! We have sounds, we have notes, now the last piece of the puzzle is missing: [effects](/workshop/first-effects) diff --git a/website/src/pages/de/workshop/first-sounds.mdx b/website/src/pages/de/workshop/first-sounds.mdx new file mode 100644 index 00000000..0f132bec --- /dev/null +++ b/website/src/pages/de/workshop/first-sounds.mdx @@ -0,0 +1,310 @@ +--- +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 + +{/* Let's start by making some noise: */} + +Los geht's mit ein paar Sounds: + + + + + +1. ⬆️ Klicke in das obige Textfeld ⬆️ +2. Drücke `Strg`+`Enter` zum Abspielen +3. Ändere `house` in `casio` +4. Drücke `Strg`+`Enter` zum Aktualisieren +5. Drücke `Strg`+`.` zum Stoppen + + + +Glückwunsch, du bist nun am live coden! + +**Probiere mehr Sounds aus** + +Mit ":" kannst du einen anderen Sound aus dem Set wählen: + + + + + +Ändere `east:1` in `east:2` um einen anderen Sound aus dem Set `east` zu hören. + +Du kannst auch andere Zahlen ausprobieren! Es kann sein dass du kurz nichts hörst während ein neuer Sound lädt. + + + +Hier sind ein paar mehr Sounds zum ausprobieren: + +``` +casio control crow techno house jazz +metal east jvbass juno insect space wind +``` + +Jetzt weißt du wie man verschiedene Sounds benutzt. +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: + + + +Diese 2-Buchstaben Kombinationen stehen für verschiedene Teile eines Drumsets: + +- `bd` = **b**ass **d**rum - Basstrommel +- `sd` = **s**nare **d**rum - Schnarrtrommel +- `sn` = **sn**are +- `rim` = **rim**shot - Rahmenschlag +- `hh` = **h**i**h**at +- `oh` = **o**pen **h**ihat - Offenes Hi-Hat + +Wir können den Charakter des Drum Sounds verändern, indem wir mit `bank` die Drum Machine auswählen: + +{/* To change the sound character of our drums, we can use `bank` to change the drum machine: */} + + + +In diesem Beispiel ist `RolandTR909` der Name der Drum Machine, welche eine prägende Rolle für House und Techno Musik spielte. + + + +Ändere `RolandTR909` in + +- `AkaiLinn` +- `RhythmAce` +- `RolandTR808` +- `RolandTR707` +- `ViscoSpaceDrum` + +Es gibt noch viel mehr, aber das sollte fürs Erste reichen.. + + + +## Sequenzen / Sequences + +Im letzten Beispiel haben wir schon gesehen dass man mehrere Sounds hintereinander abspielen kann wenn man sie durch Leerzeichen trennt: + + + +Beachte wie der aktuell gespielte Sound im Code markiert und auch darunter visualisiert wird. + + + +Versuch noch mehr Sounds hinzuzfügen! + + + +**Je länger die Sequence, desto schneller** + + + +Der Inhalt einer Sequence wird in einen sogenannten Cycle (=Zyklus) zusammengequetscht. + +**Tempo ändern mit `cpm`** + + + + + +cpm = **c**ycles per **m**inute = Cycles pro Minute + +Das Tempo ist standardmäßig auf 60cpm eingestellt, also 1 Cycle pro Sekunde. + + + +Wir werden später noch mehr Möglichkeiten kennen lernen wie man das Tempo verändert. + +**Pausen mit '~'** + + + +**Unter-Sequenzen mit [Klammern]** + + + + + +Füge noch mehr Sounds in die Klammern ein! + + + +Genau wie bei der ganzen Sequence wird der eine Unter-Sequence schneller je mehr drin ist. + +**Multiplikation: Dinge schneller machen** + + + +**Multiplikation: Unter-Sequences schneller machen** + + + +**Multiplikation: Vieeeeeeel schneller** + + + + + +Tonhöhe = sehr schneller Rhythmus + + + +**Unter-Unter-Sequenzen mit [[Klammern]]** + + + + + +Du kannst so tief verschachteln wie du willst! + + + +**Parallele Sequenzen mit Komma** + + + +Du kannst soviele Kommas benutzen wie du magst: + + + +Kommas können auch in Unter-Sequenzen verwendet werden: + + + + + +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. + + + +**Mehrere Zeilen mit \`** + + + +**Sound Nummer separat auswählen** + +Anstatt mit ":" kann man die Sound Nummer auch separat mir der `n` Funktion steuern: + + + +Das ist kürzer und lesbarer als: + + + +## 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 | | +| Sound Nummer | :x | | +| Pausen | ~ | | +| Unter-Sequenzen | \[\] | | +| Unter-Unter-Sequenzen | \[\[\]\] | | +| Schneller | \* | | +| Parallel | , | | + +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 | | +| bank | Wählt die Soundbank / Drum Machine | | +| cpm | Tempo in **C**ycles **p**ro **M**inute | | +| n | Sample Nummer | | + +## Beispiele + +**Einfacher Rock Beat** + + + +**Klassischer House** + + + + + +Ist die aufgefallen dass die letzten 2 Patterns extrem ähnlich sind? +Bestimmte Drum Patterns werden oft genreübergreifend wiederverwendet. + + + +We Will Rock you + + + +**Yellow Magic Orchestra - Firecracker** + + + +**Nachahmung eines 16 step sequencers** + + + +**Noch eins** + + + +**Nicht so typische Drums** + + + +Jetzt haben wir eine grundlegende Ahnung davon wie man mit Strudel Beats baut! +Im nächsten Kapitel werden wir ein paar [Töne spielen](/workshop/first-notes). diff --git a/website/src/pages/de/workshop/index.astro b/website/src/pages/de/workshop/index.astro new file mode 100644 index 00000000..9f79e4c2 --- /dev/null +++ b/website/src/pages/de/workshop/index.astro @@ -0,0 +1,3 @@ + diff --git a/website/src/pages/de/workshop/intro.mdx b/website/src/pages/de/workshop/intro.mdx new file mode 100644 index 00000000..5688476b --- /dev/null +++ b/website/src/pages/de/workshop/intro.mdx @@ -0,0 +1,22 @@ +--- +title: Introduction +layout: ../../../layouts/MainLayout.astro +--- + +# Introduction + +## goals + +- be beginner friendly +- teach a representative subset of strudel / tidal +- introduce one new thing at a time +- give practical / musical examples +- encourage self-experimentation +- hands on learning > walls of text +- maintain flow state +- no setup required + +## inspired by + +- https://github.com/tidalcycles/tidal-workshop/blob/master/workshop.tidal +- https://learningmusic.ableton.com diff --git a/website/src/pages/de/workshop/langebank.mdx b/website/src/pages/de/workshop/langebank.mdx new file mode 100644 index 00000000..7a1ca697 --- /dev/null +++ b/website/src/pages/de/workshop/langebank.mdx @@ -0,0 +1,154 @@ + + +1. press play button to start +2. change `house` to `casio` +3. press refresh button to update +4. press stop button to stop + + + +**Change tempo** + + + +adding your own samples + + + +").slow(3)`} + punchcard +/> + +n(run(8)).sound("east") + +Shorter variant: + + + +polyrythms & polymeters + +-- This can make for flexible time signatures: + +d1 $ sound "[bd bd sn:5] [bd sn:3]" + +-- You can put subsequences inside subsequences: +d1 $ sound "[[bd bd] bd sn:5] [bd sn:3]" + +-- Keep going.. +d1 $ sound "[[bd [bd bd bd bd]] bd sn:5] [bd sn:3]" + +-- \* Polymetric / polyrhythmic sequences + +-- Play two subsequences at once by separating with a comma: + +d1 $ sound "[voodoo voodoo:3, arpy arpy:4 arpy:2]" + +-- compare how [,] and {,} work: + +d1 $ sound "[voodoo voodoo:3, arpy arpy:4 arpy:2]" + +d1 $ sound "{voodoo voodoo:3, arpy arpy:4 arpy:2}" + +d1 $ sound "[drum bd hh bd, can can:2 can:3 can:4 can:2]" + +d1 $ sound "{drum bd hh bd, can can:2 can:3 can:4 can:2}" + +d1 $ sound "[bd sn, can:2 can:3 can:1, arpy arpy:1 arpy:2 arpy:3 arpy:5]" + +d1 $ sound "{bd sn, can:2 can:3 can:1, arpy arpy:1 arpy:2 arpy:3 arpy:5}" + +**Play X per cycle with \{ \}** + + + + + +Try different numbers after `%` + +`{ ... }%1` is the same as `< ... >` + + + +## Bracket Recap + +- `[]` squeezes contents to 1 cycle +- `<>` plays one item per cycle +- `{}%x` plays x items per cycle + +/2")) +.sound("gm_synth_bass_1")`} +/> + +vertical + + +< 4 4 4 3> +<[2,7] [2,6] [1,6] [1,6]> +< 4 4 4 3> +>*2\`) +.scale("/4") +.sound("piano")`} +/> + +horizontal + +*2\`) +.scale("/4") +.sound("piano")`} +/> diff --git a/website/src/pages/de/workshop/mini-notation.mdx b/website/src/pages/de/workshop/mini-notation.mdx new file mode 100644 index 00000000..86bef096 --- /dev/null +++ b/website/src/pages/de/workshop/mini-notation.mdx @@ -0,0 +1,69 @@ +--- +title: First Sounds +layout: ../../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; + +# Mini Notation + +Mini Notation is everything between the quotes. It the short rhythm language of Tidal. + +## Cycles + +**The longer the sequence, the faster it runs** + + + +**Play less sounds per cycle with \{curly braces\}** + + + +**Use \`backticks\` for multiple lines** + + + +**Play one sounds per cycle with \** + +")`} punchcard /> + +This is the same as `{...}%1` + +## Operators + +**Multiplication: Speed things up** + + + +**Division: Slow things down** + + + +`bd` will play only every second time + +## Combining it all + +**Speed up Sub-Sequences** + + + +**Slow down Sequences** + + + +**Parallel Sub-Sequences** + + + +**Sample Numbers on groups** + + diff --git a/website/src/pages/de/workshop/pattern-effects.mdx b/website/src/pages/de/workshop/pattern-effects.mdx new file mode 100644 index 00000000..4be0a77a --- /dev/null +++ b/website/src/pages/de/workshop/pattern-effects.mdx @@ -0,0 +1,194 @@ +--- +title: Pattern Effects +layout: ../../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '@src/docs/MiniRepl'; +import Box from '@components/Box.astro'; +import QA from '@components/QA'; + +# Pattern Effects + +Up until now, most of the functions we've seen are what other music programs are typically capable of: sequencing sounds, playing notes, controlling effects. + +In this chapter, we are going to look at functions that are more unique to tidal. + +**reverse patterns with rev** + + + +**play pattern left and modify it right with jux** + + + +This is the same as: + + + +Let's visualize what happens here: + + + + + +Try commenting out one of the two by adding `//` before a line + + + +**multiple tempos** + + + +This is like doing + + + + + +Try commenting out one or more by adding `//` before a line + + + +**add** + +>")) +.color(">").adsr("[.1 0]:.2:[1 0]") +.sound("gm_acoustic_bass").room(.5)`} + punchcard +/> + + + +If you add a number to a note, the note will be treated as if it was a number + + + +We can add as often as we like: + +>").add("0,7")) +.color(">").adsr("[.1 0]:.2:[1 0]") +.sound("gm_acoustic_bass").room(.5)`} + punchcard +/> + +**add with scale** + + [~ <4 1>]>*2".add("<0 [0,2,4]>/4")) +.scale("C5:minor").release(.5) +.sound("gm_xylophone").room(.5)`} + punchcard +/> + +**time to stack** + + [~ <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** + + + +this is like writing: + + + + + +Try patterning the `ply` function, for example using `"<1 2 1 3>"` + + + +**off** + +] <2 3> [~ 1]>" + .off(1/8, x=>x.add(4)) + //.off(1/4, x=>x.add(7)) +).scale("/4") +.s("triangle").room(.5).ds(".1:0").delay(.5)`} + punchcard +/> + + + +In the notation `x=>x.`, the `x` is the shifted pattern, which where modifying. + + + +The above is like writing: + +] <2 3> [~ 1]>*2").color("cyan"), + n("<0 [4 <3 2>] <2 3> [~ 1]>*2".add(7).late(1/8)).color("magenta") +).scale("/2") +.s("triangle").adsr(".01:.1:0").room(.5)`} + punchcard +/> + +off is also useful for sounds: + +x.speed(1.5).gain(.25))`} +/> + +| name | description | example | +| ---- | ------------------------------ | ---------------------------------------------------------------------------------------------- | +| rev | reverse | | +| jux | split left/right, modify right | | +| add | add numbers / notes | ")).scale("C:minor")`} /> | +| ply | speed up each event n times | ")`} /> | +| off | copy, shift time & modify | x.speed(2))`} /> | diff --git a/website/src/pages/de/workshop/recap.mdx b/website/src/pages/de/workshop/recap.mdx new file mode 100644 index 00000000..85f3e015 --- /dev/null +++ b/website/src/pages/de/workshop/recap.mdx @@ -0,0 +1,98 @@ +--- +title: Recap +layout: ../../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../../docs/MiniRepl'; + +# Workshop Recap + +This page is just a listing of all functions covered in the workshop! + +## Mini Notation + +| Concept | Syntax | Example | +| ----------------- | -------- | -------------------------------------------------------------------------------- | +| Sequence | space | | +| Sample Number | :x | | +| Rests | ~ | | +| Sub-Sequences | \[\] | | +| Sub-Sub-Sequences | \[\[\]\] | | +| Speed up | \* | | +| Parallel | , | | +| Slow down | \/ | | +| Alternate | \<\> | ")`} /> | +| Elongate | @ | | +| Replicate | ! | | + +## Sounds + +| Name | Description | Example | +| ----- | --------------------------------- | ---------------------------------------------------------------------------------- | +| sound | plays the sound of the given name | | +| bank | selects the sound bank | | +| n | select sample number | | + +## Notes + +| Name | Description | Example | +| --------- | ----------------------------------- | -------------------------------------------------------------------------------------------- | +| note | set pitch as number or letter | | +| n + scale | set note in scale | | +| stack | play patterns in parallel (read on) | | + +## Audio Effects + +| name | example | +| ----- | -------------------------------------------------------------------------------------------------- | +| lpf | ")`} /> | +| vowel | ")`} /> | +| gain | | +| delay | | +| room | | +| pan | | +| speed | ")`} /> | +| range | | + +## Pattern Effects + +| name | description | example | +| ---- | ----------------------------------- | ---------------------------------------------------------------------------------------------- | +| cpm | sets the tempo in cycles per minute | | +| fast | speed up | | +| slow | slow down | | +| rev | reverse | | +| jux | split left/right, modify right | | +| add | add numbers / notes | ")).scale("C:minor")`} /> | +| ply | speed up each event n times | ")`} /> | +| off | copy, shift time & modify | x.speed(2))`} /> | + +## Samples + +``` +casio control crow techno house jazz +metal east jvbass juno insect space wind +bd sd sn cp hh +piano +``` + +## Synths + +``` +gm_electric_guitar_muted gm_acoustic_bass +gm_voice_oohs gm_blown_bottle sawtooth square triangle +gm_xylophone gm_synth_bass_1 gm_synth_strings_1 +``` + +## Banks + +``` +RolandTR909 +``` + +## Scales + +``` +major minor dorian mixolydian +minor:pentatonic major:pentatonic +``` From 3db1c7a52122cc0eb180c764ea87447b5b88a3bc Mon Sep 17 00:00:00 2001 From: Roipoussiere Date: Mon, 5 Jun 2023 19:04:56 +0200 Subject: [PATCH 54/86] swatch: look for song titles in code metadata --- website/src/pages/swatch/list.json.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/website/src/pages/swatch/list.json.js b/website/src/pages/swatch/list.json.js index 6d86e384..ecc1d41a 100644 --- a/website/src/pages/swatch/list.json.js +++ b/website/src/pages/swatch/list.json.js @@ -1,9 +1,26 @@ +export function getMetadata(raw_code) { + // https://stackoverflow.com/a/15123777 + const comment_regexp = /\/\*([\s\S]*?)\*\/|([^\\:]|^)\/\/(.*)$/gm; + + const tag_regexp = /@([a-z]*):? (.*)/gm + const tags = {}; + + for (const match of raw_code.matchAll(comment_regexp)) { + const comment = match[1] ? match[1] : '' + match[3] ? match[3] : ''; + for (const tag_match of comment.trim().matchAll(tag_regexp)) { + tags[tag_match[1]] = tag_match[2].trim() + } + } + + return tags; +} + export function getMyPatterns() { const my = import.meta.glob('../../../../my-patterns/**', { as: 'raw', eager: true }); return Object.fromEntries( - Object.entries(my) // - .filter(([name]) => name.endsWith('.txt')) // - .map(([name, raw]) => [name.split('/').slice(-1), raw]), // + Object.entries(my) + .filter(([name]) => name.endsWith('.txt')) + .map(([name, raw]) => [ getMetadata(raw)['title'] || name.split('/').slice(-1), raw ]), ); } From f9ad5f0d01af70d3d633f08b4b20412d86c61b94 Mon Sep 17 00:00:00 2001 From: Roipoussiere Date: Mon, 5 Jun 2023 19:35:41 +0200 Subject: [PATCH 55/86] add tunes metadata --- website/src/repl/tunes.mjs | 170 ++++++++++++++++++++++++------------- 1 file changed, 112 insertions(+), 58 deletions(-) diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index 9a564609..0e60ab26 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -121,8 +121,10 @@ stack( .room(1) //.pianoroll({fold:1})`; -export const caverave = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const caverave = `// @title Caverave +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + const keys = x => x.s('sawtooth').cutoff(1200).gain(.5) .attack(0).decay(.16).sustain(.3).release(.1); @@ -184,8 +186,10 @@ export const barryHarris = `// adapted from a Barry Harris excercise .color('#00B8D4') `; -export const blippyRhodes = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const blippyRhodes = `// @title Blippy Rhodes +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + samples({ bd: 'samples/tidal/bd/BT0A0D0.wav', sn: 'samples/tidal/sn/ST0T0S3.wav', @@ -226,8 +230,10 @@ stack( ).fast(3/2) //.pianoroll({fold:1})`; -export const wavyKalimba = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const wavyKalimba = `// @title Wavy kalimba +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + samples({ 'kalimba': { c5:'https://freesound.org/data/previews/536/536549_11935698-lq.mp3' } }) @@ -256,8 +262,10 @@ stack( .delay(.2) `; -export const festivalOfFingers = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const festivalOfFingers = `// @title Festival of fingers +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + const chords = ""; stack( chords.voicings('lefthand').struct("x(3,8,-1)").velocity(.5).off(1/7,x=>x.transpose(12).velocity(.2)), @@ -271,8 +279,10 @@ stack( .note().piano()`; // iter, echo, echoWith -export const undergroundPlumber = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos, inspired by Friendship - Let's not talk about it (1979) :) +export const undergroundPlumber = `// @title Underground plumber +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos +// @details inspired by Friendship - Let's not talk about it (1979) :) samples({ bd: 'bd/BT0A0D0.wav', sn: 'sn/ST0T0S3.wav', hh: 'hh/000_hh3closedhh.wav', cp: 'cp/HANDCLP0.wav', }, 'https://loophole-letters.vercel.app/samples/tidal/') @@ -297,8 +307,10 @@ stack( .fast(2/3) .pianoroll({})`; -export const bridgeIsOver = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos, bassline by BDP - The Bridge Is Over +export const bridgeIsOver = `// @title Bridge is over +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos, bassline by BDP - The Bridge Is Over + samples({mad:'https://freesound.org/data/previews/22/22274_109943-lq.mp3'}) stack( stack( @@ -318,8 +330,10 @@ stack( .pianoroll() `; -export const goodTimes = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const goodTimes = `// @title Good times +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + const scale = cat('C3 dorian','Bb2 major').slow(4); stack( "2*4".add(12).scale(scale) @@ -361,8 +375,10 @@ stack( .pianoroll({maxMidi:100,minMidi:20})`; */ -export const echoPiano = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const echoPiano = `// @title Echo piano +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + "<0 2 [4 6](3,4,2) 3*2>" .scale('D minor') .color('salmon') @@ -408,8 +424,10 @@ stack( .legato(.5) ).fast(2).note()`; -export const randomBells = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const randomBells = `// @title Random bells +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + samples({ bell: { c6: 'https://freesound.org/data/previews/411/411089_5121236-lq.mp3' }, bass: { d2: 'https://freesound.org/data/previews/608/608286_13074022-lq.mp3' } @@ -431,8 +449,10 @@ stack( .pianoroll({minMidi:20,maxMidi:120,background:'transparent'}) `; -export const waa2 = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const waa2 = `// @title Waa2 +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + n( "a4 [a3 c3] a3 c3" .sub("<7 12 5 12>".slow(2)) @@ -447,8 +467,10 @@ n( .room(.5) `; -export const hyperpop = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const hyperpop = `// @title Hyperpop +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + const lfo = cosine.slow(15); const lfo2 = sine.slow(16); const filter1 = x=>x.cutoff(lfo2.range(300,3000)); @@ -498,8 +520,10 @@ stack( ).slow(2) // strudel disable-highlighting`; -export const festivalOfFingers3 = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const festivalOfFingers3 = `// @title Festival of fingers 3 +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + "[-7*3],0,2,6,[8 7]" .echoWith(4,1/4, (x,n)=>x .add(n*7) @@ -516,8 +540,10 @@ export const festivalOfFingers3 = `// licensed with CC BY-NC-SA 4.0 https://crea .note().piano() //.pianoroll({maxMidi:160})`; -export const meltingsubmarine = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const meltingsubmarine = `// @title Melting submarine +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + await samples('github:tidalcycles/Dirt-Samples/master/') stack( s("bd:5,[~ ],hh27(3,4,1)") // drums @@ -554,8 +580,10 @@ stack( ) .slow(3/2)`; -export const outroMusic = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const outroMusic = `// @title Outro music +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + samples({ bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'], sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'], @@ -583,8 +611,10 @@ samples({ //.pianoroll({autorange:1,vertical:1,fold:0}) `; -export const bassFuge = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const bassFuge = `// @title Bass fuge +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + samples({ flbass: ['00_c2_finger_long_neck.wav','01_c2_finger_short_neck.wav','02_c2_finger_long_bridge.wav','03_c2_finger_short_bridge.wav','04_c2_pick_long.wav','05_c2_pick_short.wav','06_c2_palm_mute.wav'] }, 'github:cleary/samples-flbass/main/') samples({ @@ -609,8 +639,10 @@ x=>x.add(7).color('steelblue') .stack(s("bd:1*2,~ sd:0,[~ hh:0]*2")) .pianoroll({vertical:1})`; -export const chop = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const chop = `// @title Chop +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + samples({ p: 'https://cdn.freesound.org/previews/648/648433_11943129-lq.mp3' }) s("p") @@ -622,8 +654,10 @@ s("p") .sustain(.6) `; -export const delay = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const delay = `// @title Delay +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + stack( s("bd ") .delay("<0 .5>") @@ -631,8 +665,10 @@ stack( .delayfeedback(".6 | .8") ).sometimes(x=>x.speed("-1"))`; -export const orbit = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const orbit = `// @title Orbit +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + stack( s("bd ") .delay(.5) @@ -645,8 +681,10 @@ stack( .orbit(2) ).sometimes(x=>x.speed("-1"))`; -export const belldub = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const belldub = `// @title Belldub +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + samples({ bell: {b4:'https://cdn.freesound.org/previews/339/339809_5121236-lq.mp3'}}) // "Hand Bells, B, Single.wav" by InspectorJ (www.jshaw.co.uk) of Freesound.org stack( @@ -678,8 +716,10 @@ stack( .mask("<1 0>/8") ).slow(5)`; -export const dinofunk = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const dinofunk = `// @title Dinofunk +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + samples({bass:'https://cdn.freesound.org/previews/614/614637_2434927-hq.mp3', dino:{b4:'https://cdn.freesound.org/previews/316/316403_5123851-hq.mp3'}}) setVoicingRange('lefthand', ['c3','a4']) @@ -699,8 +739,10 @@ note("Abm7".voicings('lefthand').struct("x(3,8,1)".slow(2))), note("").s('dino').delay(.8).slow(8).room(.5) )`; -export const sampleDemo = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const sampleDemo = `// @title Sample demo +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + stack( // percussion s("[woodblock:1 woodblock:2*2] snare_rim:0,gong/8,brakedrum:1(3,8),~@3 cowbell:3") @@ -715,8 +757,10 @@ stack( .release(.1).room(.5) )`; -export const holyflute = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const holyflute = `// @title Holy flute +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + "c3 eb3(3,8) c4/2 g3*2" .superimpose( x=>x.slow(2).add(12), @@ -728,8 +772,10 @@ export const holyflute = `// licensed with CC BY-NC-SA 4.0 https://creativecommo .pianoroll({fold:0,autorange:0,vertical:0,cycles:12,smear:0,minMidi:40}) `; -export const flatrave = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const flatrave = `// @title Flatrave +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + stack( s("bd*2,~ [cp,sd]").bank('RolandTR909'), @@ -751,8 +797,10 @@ stack( .decay(.05).sustain(0).delay(.2).degradeBy(.5).mask("<0 1>/16") )`; -export const amensister = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const amensister = `// @title Amensister +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + await samples('github:tidalcycles/Dirt-Samples/master') stack( @@ -785,8 +833,10 @@ stack( n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5)) ).reset("")`; -export const juxUndTollerei = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const juxUndTollerei = `// @title Jux und tollerei +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + note("c3 eb3 g3 bb3").palindrome() .s('sawtooth') .jux(x=>x.rev().color('green').s('sawtooth')) @@ -798,8 +848,10 @@ note("c3 eb3 g3 bb3").palindrome() .delay(.5).delaytime(.1).delayfeedback(.4) .pianoroll()`; -export const csoundDemo = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos +export const csoundDemo = `// @title CSound demo +// @license with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + await loadCsound\` instr CoolSynth iduration = p3 @@ -832,10 +884,10 @@ endin\` //.pianoroll() .csound('CoolSynth')`; -export const loungeSponge = ` -// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos -// livecode.orc by Steven Yi +export const loungeSponge = `// @title Lounge sponge +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos, livecode.orc by Steven Yi + await loadOrc('github:kunstmusik/csound-live-code/master/livecode.orc') stack( @@ -852,8 +904,10 @@ stack( s("bd*2,[~ hh]*2,~ cp").bank('RolandTR909') )`; -export const arpoon = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// "Arpoon" by Felix Roos +export const arpoon = `// @title Arpoon +// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @by Felix Roos + await samples('github:tidalcycles/Dirt-Samples/master') "< C7 F^7 [Fm7 E7b9]>".voicings('lefthand') From 52641db590fd5652e89d958b0057695f5b4c6b04 Mon Sep 17 00:00:00 2001 From: Roipoussiere Date: Mon, 5 Jun 2023 20:42:41 +0200 Subject: [PATCH 56/86] load titles from metadata in examples page --- website/src/pages/examples/index.astro | 4 +++- website/src/pages/metadata_parser.js | 16 ++++++++++++++++ website/src/pages/swatch/list.json.js | 17 +---------------- 3 files changed, 20 insertions(+), 17 deletions(-) create mode 100644 website/src/pages/metadata_parser.js diff --git a/website/src/pages/examples/index.astro b/website/src/pages/examples/index.astro index b1ceae11..c973f2eb 100644 --- a/website/src/pages/examples/index.astro +++ b/website/src/pages/examples/index.astro @@ -1,6 +1,8 @@ --- import * as tunes from '../../../src/repl/tunes.mjs'; import HeadCommon from '../../components/HeadCommon.astro'; + +import { getMetadata } from '../metadata_parser'; --- @@ -12,7 +14,7 @@ import HeadCommon from '../../components/HeadCommon.astro'; Object.entries(tunes).map(([name, tune]) => (
- {name} + {getMetadata(tune)['title'] || name}
diff --git a/website/src/pages/metadata_parser.js b/website/src/pages/metadata_parser.js new file mode 100644 index 00000000..f6b8f628 --- /dev/null +++ b/website/src/pages/metadata_parser.js @@ -0,0 +1,16 @@ +export function getMetadata(raw_code) { + // https://stackoverflow.com/a/15123777 + const comment_regexp = /\/\*([\s\S]*?)\*\/|([^\\:]|^)\/\/(.*)$/gm; + + const tag_regexp = /@([a-z]*):? (.*)/gm + const tags = {}; + + for (const match of raw_code.matchAll(comment_regexp)) { + const comment = match[1] ? match[1] : '' + match[3] ? match[3] : ''; + for (const tag_match of comment.trim().matchAll(tag_regexp)) { + tags[tag_match[1]] = tag_match[2].trim() + } + } + + return tags; +} diff --git a/website/src/pages/swatch/list.json.js b/website/src/pages/swatch/list.json.js index ecc1d41a..febc52aa 100644 --- a/website/src/pages/swatch/list.json.js +++ b/website/src/pages/swatch/list.json.js @@ -1,19 +1,4 @@ -export function getMetadata(raw_code) { - // https://stackoverflow.com/a/15123777 - const comment_regexp = /\/\*([\s\S]*?)\*\/|([^\\:]|^)\/\/(.*)$/gm; - - const tag_regexp = /@([a-z]*):? (.*)/gm - const tags = {}; - - for (const match of raw_code.matchAll(comment_regexp)) { - const comment = match[1] ? match[1] : '' + match[3] ? match[3] : ''; - for (const tag_match of comment.trim().matchAll(tag_regexp)) { - tags[tag_match[1]] = tag_match[2].trim() - } - } - - return tags; -} +import { getMetadata } from '../metadata_parser'; export function getMyPatterns() { const my = import.meta.glob('../../../../my-patterns/**', { as: 'raw', eager: true }); From 372e33c06691476ce3e9cc451354979ae4865837 Mon Sep 17 00:00:00 2001 From: Roipoussiere Date: Mon, 5 Jun 2023 20:43:53 +0200 Subject: [PATCH 57/86] doc: add section about metadata --- website/src/config.ts | 1 + website/src/pages/learn/code.mdx | 3 +++ website/src/pages/learn/metadata.mdx | 30 ++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 website/src/pages/learn/metadata.mdx diff --git a/website/src/config.ts b/website/src/config.ts index bf26fff1..f4dba71b 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -70,6 +70,7 @@ export const SIDEBAR: Sidebar = { { 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' }, ], Development: [ { text: 'REPL', link: 'technical-manual/repl' }, diff --git a/website/src/pages/learn/code.mdx b/website/src/pages/learn/code.mdx index 678d4ccc..809e04ab 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -67,6 +67,9 @@ 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 `@title` or `@license`. +Those are just a convention to define some information about the music. We will talk about it in the *metadata* section. + # Strings Ok, so what about the content inside the quotes (e.g. `"a3 c#4 e4 a4"`)? diff --git a/website/src/pages/learn/metadata.mdx b/website/src/pages/learn/metadata.mdx new file mode 100644 index 00000000..663e82c0 --- /dev/null +++ b/website/src/pages/learn/metadata.mdx @@ -0,0 +1,30 @@ +--- +title: Music metadata +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Music metadata + +You can optionally add some music metadata in your Strudel code, by using tags in code comments like this: + +``` +// @license: CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ +// @author: Felix Roos +// @details: Inspired by Friendship - Let's not talk about it (1979) :) +``` + +Like other comments, those are ignored by Strudel, but it can be used by other tools to retrieve some information about the music. + +It is for instance used by the [swatch tool](https://github.com/tidalcycles/strudel/tree/main/my-patterns) to display pattern titles in the [examples page](https://strudel.tidalcycles.org/examples/). + +Available tags are: +- `@title`: music title +- `@by`: music author(s), separated with comma, eventually followed with a link in `<>` (ex: `@author john doe `) +- `@license`: music license +- `@details`: some additional information about the music +- `@url`: web page related to the music (git repo, soundcloud link, etc.) +- `@genre`: music genre (pop, jazz, etc) +- `@album`: music album name From 98a89aea11d9cf39a666867c40d111a83dc4bc5a Mon Sep 17 00:00:00 2001 From: Roipoussiere Date: Mon, 5 Jun 2023 21:15:59 +0200 Subject: [PATCH 58/86] lint: metadata parsing --- website/src/pages/learn/code.mdx | 2 +- website/src/pages/learn/metadata.mdx | 1 + website/src/pages/metadata_parser.js | 4 ++-- website/src/pages/swatch/list.json.js | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/website/src/pages/learn/code.mdx b/website/src/pages/learn/code.mdx index 809e04ab..8551a8a2 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -68,7 +68,7 @@ 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 `@title` or `@license`. -Those are just a convention to define some information about the music. We will talk about it in the *metadata* section. +Those are just a convention to define some information about the music. We will talk about it in the _metadata_ section. # Strings diff --git a/website/src/pages/learn/metadata.mdx b/website/src/pages/learn/metadata.mdx index 663e82c0..47a19c1e 100644 --- a/website/src/pages/learn/metadata.mdx +++ b/website/src/pages/learn/metadata.mdx @@ -21,6 +21,7 @@ Like other comments, those are ignored by Strudel, but it can be used by other t It is for instance used by the [swatch tool](https://github.com/tidalcycles/strudel/tree/main/my-patterns) to display pattern titles in the [examples page](https://strudel.tidalcycles.org/examples/). Available tags are: + - `@title`: music title - `@by`: music author(s), separated with comma, eventually followed with a link in `<>` (ex: `@author john doe `) - `@license`: music license diff --git a/website/src/pages/metadata_parser.js b/website/src/pages/metadata_parser.js index f6b8f628..9fadc30d 100644 --- a/website/src/pages/metadata_parser.js +++ b/website/src/pages/metadata_parser.js @@ -2,13 +2,13 @@ export function getMetadata(raw_code) { // https://stackoverflow.com/a/15123777 const comment_regexp = /\/\*([\s\S]*?)\*\/|([^\\:]|^)\/\/(.*)$/gm; - const tag_regexp = /@([a-z]*):? (.*)/gm + const tag_regexp = /@([a-z]*):? (.*)/gm; const tags = {}; for (const match of raw_code.matchAll(comment_regexp)) { const comment = match[1] ? match[1] : '' + match[3] ? match[3] : ''; for (const tag_match of comment.trim().matchAll(tag_regexp)) { - tags[tag_match[1]] = tag_match[2].trim() + tags[tag_match[1]] = tag_match[2].trim(); } } diff --git a/website/src/pages/swatch/list.json.js b/website/src/pages/swatch/list.json.js index febc52aa..4bf6bb4a 100644 --- a/website/src/pages/swatch/list.json.js +++ b/website/src/pages/swatch/list.json.js @@ -5,7 +5,7 @@ export function getMyPatterns() { return Object.fromEntries( Object.entries(my) .filter(([name]) => name.endsWith('.txt')) - .map(([name, raw]) => [ getMetadata(raw)['title'] || name.split('/').slice(-1), raw ]), + .map(([name, raw]) => [getMetadata(raw)['title'] || name.split('/').slice(-1), raw]), ); } From 2e221520895e8eda9ae1063c7f2e0c385e91ed20 Mon Sep 17 00:00:00 2001 From: Roipoussiere Date: Tue, 6 Jun 2023 19:15:46 +0200 Subject: [PATCH 59/86] metadata: allow several values and one-liners --- website/src/pages/metadata_parser.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/website/src/pages/metadata_parser.js b/website/src/pages/metadata_parser.js index 9fadc30d..b4448c2a 100644 --- a/website/src/pages/metadata_parser.js +++ b/website/src/pages/metadata_parser.js @@ -1,14 +1,26 @@ -export function getMetadata(raw_code) { - // https://stackoverflow.com/a/15123777 - const comment_regexp = /\/\*([\s\S]*?)\*\/|([^\\:]|^)\/\/(.*)$/gm; +const ALLOW_MANY = ['by', 'url', 'genre', 'license']; - const tag_regexp = /@([a-z]*):? (.*)/gm; +export function getMetadata(raw_code) { + const comment_regexp = /\/\*([\s\S]*?)\*\/|\/\/(.*)$/gm; const tags = {}; for (const match of raw_code.matchAll(comment_regexp)) { - const comment = match[1] ? match[1] : '' + match[3] ? match[3] : ''; - for (const tag_match of comment.trim().matchAll(tag_regexp)) { - tags[tag_match[1]] = tag_match[2].trim(); + const tag_matches = (match[1] || match[2] || '').trim().split('@').slice(1); + for (const tag_match of tag_matches) { + let [tag, tag_value] = tag_match.split(/ (.*)/s); + tag = tag.trim(); + tag_value = (tag_value || '').replaceAll(/ +/g, ' ').trim(); + + if (ALLOW_MANY.includes(tag)) { + const tag_list = tag_value + .split(/[,\n]/) + .map((t) => t.trim()) + .filter((t) => t !== ''); + tags[tag] = tag in tags ? tags[tag].concat(tag_list) : tag_list; + } else { + tag_value = tag_value.replaceAll(/\s+/g, ' '); + tags[tag] = tag in tags ? tags[tag] + ' ' + tag_value : tag_value; + } } } From bd7f4dd73f51fcee93d6d86555bfb1c947b5c390 Mon Sep 17 00:00:00 2001 From: Roipoussiere Date: Tue, 6 Jun 2023 21:01:09 +0200 Subject: [PATCH 60/86] metadata: add tests --- test/metadata.test.mjs | 196 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 test/metadata.test.mjs diff --git a/test/metadata.test.mjs b/test/metadata.test.mjs new file mode 100644 index 00000000..21646d8f --- /dev/null +++ b/test/metadata.test.mjs @@ -0,0 +1,196 @@ +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 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 empty tags from block comment', 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('does not load code that looks like a metadata tag', async () => { + const tune = ` +const str1 = '@title Awesome song' +`; + expect(getMetadata(tune)).toStrictEqual({}); + }); + +}); From 96ac054392bca821f0d177d6ad3a5aef07a1e45b Mon Sep 17 00:00:00 2001 From: Roipoussiere Date: Tue, 6 Jun 2023 21:33:32 +0200 Subject: [PATCH 61/86] metadata: improve doc --- website/src/pages/learn/code.mdx | 2 +- website/src/pages/learn/metadata.mdx | 75 ++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/website/src/pages/learn/code.mdx b/website/src/pages/learn/code.mdx index 8551a8a2..5e2b37b4 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -68,7 +68,7 @@ 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 `@title` or `@license`. -Those are just a convention to define some information about the music. We will talk about it in the _metadata_ section. +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 diff --git a/website/src/pages/learn/metadata.mdx b/website/src/pages/learn/metadata.mdx index 47a19c1e..2f3be348 100644 --- a/website/src/pages/learn/metadata.mdx +++ b/website/src/pages/learn/metadata.mdx @@ -8,24 +8,81 @@ import { JsDoc } from '../../docs/JsDoc'; # Music metadata -You can optionally add some music metadata in your Strudel code, by using tags in code comments like this: +You can optionally add some music metadata in your Strudel code, by using tags in code comments: -``` -// @license: CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// @author: Felix Roos -// @details: Inspired by Friendship - Let's not talk about it (1979) :) +```js +// @title Hey Hoo +// @by Sam Tagada +// @license CC BY-NC-SA ``` Like other comments, those are ignored by Strudel, but it can be used by other tools to retrieve some information about the music. It is for instance used by the [swatch tool](https://github.com/tidalcycles/strudel/tree/main/my-patterns) to display pattern titles in the [examples page](https://strudel.tidalcycles.org/examples/). +## Alternative syntax + +You can also use comment blocks: + +```js +/* +@title Hey Hoo +@by Sam Tagada +@license CC BY-NC-SA +*/ +``` + +Or define multiple tags in one line: + +```js +// @title Hey Hoo @by Sam Tagada @license CC BY-NC-SA +``` + +## Tags list + Available tags are: - `@title`: music title -- `@by`: music author(s), separated with comma, eventually followed with a link in `<>` (ex: `@author john doe `) -- `@license`: music license +- `@by`: music author(s), separated by comma, eventually followed with a link in `<>` (ex: `@by John Doe `) +- `@license`: music license(s) - `@details`: some additional information about the music -- `@url`: web page related to the music (git repo, soundcloud link, etc.) -- `@genre`: music genre (pop, jazz, etc) +- `@url`: web page(s) related to the music (git repo, soundcloud link, etc.) +- `@genre`: music genre(s) (pop, jazz, etc) - `@album`: music album name + +## Multiple values + +Some of them accepts several values, using the comma or new line separator, or duplicating the tag: + +```js +/* +@by Sam Tagada + Jimmy +@genre pop, jazz +@url https://example.com +@url https://example.org +*/ +``` + +You can also add optinal prefixes and use tags where you want: + +```js +/* +song @by Sam Tagada +samples @by Jimmy +*/ +... +note("a3 c#4 e4 a4") // @by Sandy +``` + +## Multiline + +If a tag doesn't accept a list, it can take multi-line values: + +```js +/* +@details I wrote this song in February 19th, 2023. + It was around midnight and I was lying on + the sofa in the living room. +*/ +``` From a88759ba9462d20e1d7ef8248858e512a09718ef Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 6 Jun 2023 22:43:51 +0200 Subject: [PATCH 62/86] translate notes chapter --- website/src/pages/de/workshop/first-notes.mdx | 104 +++++++++--------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/website/src/pages/de/workshop/first-notes.mdx b/website/src/pages/de/workshop/first-notes.mdx index c33fbb1a..7cf16205 100644 --- a/website/src/pages/de/workshop/first-notes.mdx +++ b/website/src/pages/de/workshop/first-notes.mdx @@ -151,7 +151,7 @@ Wir schauen uns später noch mehr Möglichkeiten an wie man patterns kombiniert. ## Längere Sequenzen -**Divide sequences with `/` to slow them down** +**Sequenzen verlangsamen mit `/`** {/* [c2 bb1 f2 eb2] */} @@ -159,27 +159,27 @@ Wir schauen uns später noch mehr Möglichkeiten an wie man patterns kombiniert. -The `/4` plays the sequence in brackets over 4 cycles (=4s). +Das `/4` spielt die Sequenz 4 mal so langsam, also insgesamt 4 cycles = 4s. -So each of the 4 notes is 1s long. +Jede Note ist nun also 1s lang. -Try adding more notes inside the brackets and notice how it gets faster. +Schreib noch mehr Töne in die Klammern und achte darauf dass es schneller wird. -Because it is so common to just play one thing per cycle, you can.. +Wenn eine Sequenz unabhängig von ihrem Inhalt immer gleich schnell bleiben soll, gibt es noch eine andere Art Klammern: -**Play one per cycle with \< \>** +**Eins pro Cycle per \< \>** ").sound("gm_acoustic_bass")`} punchcard /> -Try adding more notes inside the brackets and notice how it does **not** get faster. +Füg noch mehr Töne hinzu und achte darauf wie das tempo gleich bleibt! -**Play one sequence per cycle** +**Eine Sequenz pro Cycle** {/* <[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>/2 */} @@ -191,7 +191,7 @@ Try adding more notes inside the brackets and notice how it does **not** get fas punchcard /> -**Alternate between multiple things** +**Alternativen** -This is also useful for unpitched sounds: +Das ist auch praktisch für atonale Sounds: -## Scales +## Skalen -Finding the right notes can be difficult.. Scales are here to help: +Es kann mühsam sein die richtigen Noten zu finden wenn man alle zur Verfügung hat. +Mit Skalen ist das einfacher: -Try out different numbers. Any number should sound good! +Probier verschiedene Zahlen aus. Jede klingt gut! -Try out different scales: +Probier verschiedene Skalen: - C:major - A2:minor @@ -238,9 +239,9 @@ Try out different scales: -**automate scales** +**Automatisierte Skalen** -Just like anything, we can automate the scale with a pattern: +Wie alle Funktionen können auch Skalen mit einem Pattern automatisiert werden: -If you have no idea what these scale mean, don't worry. -These are just labels for different sets of notes that go well together. +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. -Take your time and you'll find scales you like! +Nimm dir Zeit um herauszufinden welche Skalen du magst. -## Repeat & Elongate +## Wiederholen und Verlängern -**Elongate with @** +**Verlängern mit @** -Not using `@` is like using `@1`. In the above example, c is 3 units long and eb is 1 unit long. +Ein Element ohne `@` ist gleichbedeutend mit `@1`. Im Beispiel ist `c` drei Einheiten lang, `eb` nur eine. -Try changing that number! +Spiel mit der Länge! -**Elongate within sub-sequences** +**Unter-Sequenzen verlängern** -This groove is called a `shuffle`. -Each beat has two notes, where the first is twice as long as the second. -This is also sometimes called triplet swing. You'll often find it in blues and jazz. +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. -**Replicate** +**Wiederholen** ]").sound("piano")`} punchcard /> -Try switching between `!`, `*` and `@` +Wechsel zwischen `!`, `*` und `@` hin und her. -What's the difference? +Was ist der Unterschied? -## Recap +## Rückblick -Let's recap what we've learned in this chapter: +Das haben wir in diesem Kapitel gelernt: -| Concept | Syntax | Example | -| --------- | ------ | ------------------------------------------------------------------- | -| Slow down | \/ | | -| Alternate | \<\> | ")`} /> | -| Elongate | @ | | -| Replicate | ! | | +| Concept | Syntax | Example | +| ------------ | ------ | ------------------------------------------------------------------- | +| Verlangsamen | \/ | | +| Alternativen | \<\> | ")`} /> | +| Verlängern | @ | | +| Wiederholen | ! | | -New functions: +Neue Funktionen: -| Name | Description | Example | -| ----- | ----------------------------------- | -------------------------------------------------------------------------------------------- | -| note | set pitch as number or letter | | -| scale | interpret `n` as scale degree | | -| stack | play patterns in parallel (read on) | | +| Name | Description | Example | +| ----- | --------------------------------------- | -------------------------------------------------------------------------------------------- | +| note | Tonhöhe als Buchstabe oder Zahl | | +| scale | Interpretiert `n` als Skalenstufe | | +| stack | Spiele mehrere Patterns parallel (s.u.) | | -## Examples +## Beispiele -**Classy Bassline** +**Bassline** -**Classy Melody** +**Melodie** -**Classy Drums** +**Drums** -**If there just was a way to play all the above at the same time.......** +**Wenn es doch nur einen Weg gäbe das alles gleichzeitig zu spielen.......** -It's called `stack` 😙 +Das geht mit `stack` 😙 @@ -385,4 +386,5 @@ It's called `stack` 😙 )`} /> -This is starting to sound like actual music! We have sounds, we have notes, now the last piece of the puzzle is missing: [effects](/workshop/first-effects) +Das hört sich doch langsam nach echter Musik an! +Wir haben Sounds, wir haben Töne.. noch ein Puzzleteil fehlt: [Effekte](/workshop/first-effects) From 185318a70c75e33ad30e908001af442a16ee74d0 Mon Sep 17 00:00:00 2001 From: Roipoussiere Date: Wed, 7 Jun 2023 11:02:38 +0200 Subject: [PATCH 63/86] metadata: fix typos --- test/metadata.test.mjs | 8 ++++---- website/src/pages/learn/metadata.mdx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/metadata.test.mjs b/test/metadata.test.mjs index 21646d8f..9487ebe2 100644 --- a/test/metadata.test.mjs +++ b/test/metadata.test.mjs @@ -187,10 +187,10 @@ describe.concurrent('Metadata parser', () => { }); it('does not load code that looks like a metadata tag', async () => { - const tune = ` -const str1 = '@title Awesome song' -`; + 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({}); }); - }); diff --git a/website/src/pages/learn/metadata.mdx b/website/src/pages/learn/metadata.mdx index 2f3be348..7038644d 100644 --- a/website/src/pages/learn/metadata.mdx +++ b/website/src/pages/learn/metadata.mdx @@ -64,7 +64,7 @@ Some of them accepts several values, using the comma or new line separator, or d */ ``` -You can also add optinal prefixes and use tags where you want: +You can also add optional prefixes and use tags where you want: ```js /* From 51e817468919045b10da51febaa6484f736295a1 Mon Sep 17 00:00:00 2001 From: Roipoussiere Date: Wed, 7 Jun 2023 12:26:23 +0200 Subject: [PATCH 64/86] metadata: add quotes syntax for titles --- test/metadata.test.mjs | 68 ++++++++++++++++++++++++---- website/src/pages/learn/code.mdx | 2 +- website/src/pages/learn/metadata.mdx | 6 +++ website/src/pages/metadata_parser.js | 10 +++- website/src/repl/tunes.mjs | 56 +++++++++++------------ 5 files changed, 102 insertions(+), 40 deletions(-) diff --git a/test/metadata.test.mjs b/test/metadata.test.mjs index 9487ebe2..cbd0f8a3 100644 --- a/test/metadata.test.mjs +++ b/test/metadata.test.mjs @@ -63,6 +63,51 @@ describe.concurrent('Metadata parser', () => { }); }); + 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({ @@ -159,15 +204,6 @@ describe.concurrent('Metadata parser', () => { }); }); - it('loads empty tags from block comment', 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 `; @@ -186,6 +222,20 @@ describe.concurrent('Metadata parser', () => { }); }); + 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: diff --git a/website/src/pages/learn/code.mdx b/website/src/pages/learn/code.mdx index 5e2b37b4..b74002aa 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -67,7 +67,7 @@ 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 `@title` or `@license`. +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 diff --git a/website/src/pages/learn/metadata.mdx b/website/src/pages/learn/metadata.mdx index 7038644d..27f46921 100644 --- a/website/src/pages/learn/metadata.mdx +++ b/website/src/pages/learn/metadata.mdx @@ -38,6 +38,12 @@ Or define multiple tags in one line: // @title Hey Hoo @by Sam Tagada @license CC BY-NC-SA ``` +The `title` tag has an alternative syntax using quotes (must be defined at the very begining): + +```js +// "Hey Hoo" @by Sam Tagada +``` + ## Tags list Available tags are: diff --git a/website/src/pages/metadata_parser.js b/website/src/pages/metadata_parser.js index b4448c2a..6a92a8a6 100644 --- a/website/src/pages/metadata_parser.js +++ b/website/src/pages/metadata_parser.js @@ -2,10 +2,16 @@ const ALLOW_MANY = ['by', 'url', 'genre', 'license']; export function getMetadata(raw_code) { const comment_regexp = /\/\*([\s\S]*?)\*\/|\/\/(.*)$/gm; + const comments = [...raw_code.matchAll(comment_regexp)].map((c) => (c[1] || c[2] || '').trim()); const tags = {}; - for (const match of raw_code.matchAll(comment_regexp)) { - const tag_matches = (match[1] || match[2] || '').trim().split('@').slice(1); + const [prefix, title] = (comments[0] || '').split('"'); + if (prefix.trim() === '' && title !== undefined) { + tags['title'] = title; + } + + for (const comment of comments) { + const tag_matches = comment.split('@').slice(1); for (const tag_match of tag_matches) { let [tag, tag_value] = tag_match.split(/ (.*)/s); tag = tag.trim(); diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index 0e60ab26..f2d492e9 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -121,7 +121,7 @@ stack( .room(1) //.pianoroll({fold:1})`; -export const caverave = `// @title Caverave +export const caverave = `// "Caverave" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -186,7 +186,7 @@ export const barryHarris = `// adapted from a Barry Harris excercise .color('#00B8D4') `; -export const blippyRhodes = `// @title Blippy Rhodes +export const blippyRhodes = `// "Blippy Rhodes" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -230,7 +230,7 @@ stack( ).fast(3/2) //.pianoroll({fold:1})`; -export const wavyKalimba = `// @title Wavy kalimba +export const wavyKalimba = `// "Wavy kalimba" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -262,7 +262,7 @@ stack( .delay(.2) `; -export const festivalOfFingers = `// @title Festival of fingers +export const festivalOfFingers = `// "Festival of fingers" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -279,7 +279,7 @@ stack( .note().piano()`; // iter, echo, echoWith -export const undergroundPlumber = `// @title Underground plumber +export const undergroundPlumber = `// "Underground plumber" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos // @details inspired by Friendship - Let's not talk about it (1979) :) @@ -307,7 +307,7 @@ stack( .fast(2/3) .pianoroll({})`; -export const bridgeIsOver = `// @title Bridge is over +export const bridgeIsOver = `// "Bridge is over" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos, bassline by BDP - The Bridge Is Over @@ -330,7 +330,7 @@ stack( .pianoroll() `; -export const goodTimes = `// @title Good times +export const goodTimes = `// "Good times" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -375,7 +375,7 @@ stack( .pianoroll({maxMidi:100,minMidi:20})`; */ -export const echoPiano = `// @title Echo piano +export const echoPiano = `// "Echo piano" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -424,7 +424,7 @@ stack( .legato(.5) ).fast(2).note()`; -export const randomBells = `// @title Random bells +export const randomBells = `// "Random bells" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -449,7 +449,7 @@ stack( .pianoroll({minMidi:20,maxMidi:120,background:'transparent'}) `; -export const waa2 = `// @title Waa2 +export const waa2 = `// "Waa2" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -467,7 +467,7 @@ n( .room(.5) `; -export const hyperpop = `// @title Hyperpop +export const hyperpop = `// "Hyperpop" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -520,7 +520,7 @@ stack( ).slow(2) // strudel disable-highlighting`; -export const festivalOfFingers3 = `// @title Festival of fingers 3 +export const festivalOfFingers3 = `// "Festival of fingers 3" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -540,7 +540,7 @@ export const festivalOfFingers3 = `// @title Festival of fingers 3 .note().piano() //.pianoroll({maxMidi:160})`; -export const meltingsubmarine = `// @title Melting submarine +export const meltingsubmarine = `// "Melting submarine" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -580,7 +580,7 @@ stack( ) .slow(3/2)`; -export const outroMusic = `// @title Outro music +export const outroMusic = `// "Outro music" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -611,7 +611,7 @@ samples({ //.pianoroll({autorange:1,vertical:1,fold:0}) `; -export const bassFuge = `// @title Bass fuge +export const bassFuge = `// "Bass fuge" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -639,7 +639,7 @@ x=>x.add(7).color('steelblue') .stack(s("bd:1*2,~ sd:0,[~ hh:0]*2")) .pianoroll({vertical:1})`; -export const chop = `// @title Chop +export const chop = `// "Chop" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -654,7 +654,7 @@ s("p") .sustain(.6) `; -export const delay = `// @title Delay +export const delay = `// "Delay" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -665,7 +665,7 @@ stack( .delayfeedback(".6 | .8") ).sometimes(x=>x.speed("-1"))`; -export const orbit = `// @title Orbit +export const orbit = `// "Orbit" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -681,7 +681,7 @@ stack( .orbit(2) ).sometimes(x=>x.speed("-1"))`; -export const belldub = `// @title Belldub +export const belldub = `// "Belldub" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -716,7 +716,7 @@ stack( .mask("<1 0>/8") ).slow(5)`; -export const dinofunk = `// @title Dinofunk +export const dinofunk = `// "Dinofunk" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -739,7 +739,7 @@ note("Abm7".voicings('lefthand').struct("x(3,8,1)".slow(2))), note("").s('dino').delay(.8).slow(8).room(.5) )`; -export const sampleDemo = `// @title Sample demo +export const sampleDemo = `// "Sample demo" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -757,7 +757,7 @@ stack( .release(.1).room(.5) )`; -export const holyflute = `// @title Holy flute +export const holyflute = `// "Holy flute" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -772,7 +772,7 @@ export const holyflute = `// @title Holy flute .pianoroll({fold:0,autorange:0,vertical:0,cycles:12,smear:0,minMidi:40}) `; -export const flatrave = `// @title Flatrave +export const flatrave = `// "Flatrave" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -797,7 +797,7 @@ stack( .decay(.05).sustain(0).delay(.2).degradeBy(.5).mask("<0 1>/16") )`; -export const amensister = `// @title Amensister +export const amensister = `// "Amensister" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -833,7 +833,7 @@ stack( n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5)) ).reset("")`; -export const juxUndTollerei = `// @title Jux und tollerei +export const juxUndTollerei = `// "Jux und tollerei" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -848,7 +848,7 @@ note("c3 eb3 g3 bb3").palindrome() .delay(.5).delaytime(.1).delayfeedback(.4) .pianoroll()`; -export const csoundDemo = `// @title CSound demo +export const csoundDemo = `// "CSound demo" // @license with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos @@ -884,7 +884,7 @@ endin\` //.pianoroll() .csound('CoolSynth')`; -export const loungeSponge = `// @title Lounge sponge +export const loungeSponge = `// "Lounge sponge" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos, livecode.orc by Steven Yi @@ -904,7 +904,7 @@ stack( s("bd*2,[~ hh]*2,~ cp").bank('RolandTR909') )`; -export const arpoon = `// @title Arpoon +export const arpoon = `// "Arpoon" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos From 0c2147d9c55dcc9b0e09825e30ae60780d45e0bc Mon Sep 17 00:00:00 2001 From: Roipoussiere Date: Wed, 7 Jun 2023 15:34:57 +0200 Subject: [PATCH 65/86] repl: add option to display line numbers --- packages/react/src/components/CodeMirror6.jsx | 2 ++ website/src/repl/Footer.jsx | 32 ++++++++++++++----- website/src/repl/Repl.jsx | 3 +- website/src/settings.mjs | 2 ++ website/src/styles/index.css | 4 --- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/packages/react/src/components/CodeMirror6.jsx b/packages/react/src/components/CodeMirror6.jsx index d76ac3d3..ebe3dd11 100644 --- a/packages/react/src/components/CodeMirror6.jsx +++ b/packages/react/src/components/CodeMirror6.jsx @@ -104,6 +104,7 @@ export default function CodeMirror({ onSelectionChange, theme, keybindings, + isLineNumbersDisplayed, fontSize = 18, fontFamily = 'monospace', options, @@ -148,6 +149,7 @@ export default function CodeMirror({ onCreateEditor={handleOnCreateEditor} onUpdate={handleOnUpdate} extensions={extensions} + basicSetup={{ lineNumbers: isLineNumbersDisplayed }} /> ); diff --git a/website/src/repl/Footer.jsx b/website/src/repl/Footer.jsx index 4b3399ad..c31e167a 100644 --- a/website/src/repl/Footer.jsx +++ b/website/src/repl/Footer.jsx @@ -273,6 +273,15 @@ function SoundsTab() { ); } +function Checkbox({ label, value, onChange }) { + return ( + + ); +} + function ButtonGroup({ value, onChange, items }) { return (
@@ -355,7 +364,7 @@ const fontFamilyOptions = { }; function SettingsTab({ scheduler }) { - const { theme, keybindings, fontSize, fontFamily } = useSettings(); + const { theme, keybindings, isLineNumbersDisplayed, fontSize, fontFamily } = useSettings(); return (
{/* @@ -397,13 +406,20 @@ function SettingsTab({ scheduler }) { />
- - settingsMap.setKey('keybindings', keybindings)} - items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs' }} - > - +
+ + settingsMap.setKey('keybindings', keybindings)} + items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs' }} + > + + settingsMap.setKey('isLineNumbersDisplayed', cbEvent.target.checked)} + value={isLineNumbersDisplayed} + /> +