diff --git a/package.json b/package.json index 76d60cb3..ee26531c 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,7 @@ "@strudel.cycles/webaudio": "workspace:*", "@strudel.cycles/xen": "workspace:*", "acorn": "^8.8.1", - "dependency-tree": "^9.0.0", - "vitest": "^0.28.0" + "dependency-tree": "^9.0.0" }, "devDependencies": { "@vitest/ui": "^0.28.0", @@ -66,6 +65,7 @@ "jsdoc-to-markdown": "^8.0.0", "lerna": "^6.6.1", "prettier": "^2.8.8", - "rollup-plugin-visualizer": "^5.8.1" + "rollup-plugin-visualizer": "^5.8.1", + "vitest": "^0.28.0" } } 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; diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 057fb5c7..2e5c1bca 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1582,6 +1582,24 @@ export const range2 = register('range2', function (min, max, pat) { return pat.fromBipolar()._range(min, max); }); +/** + * Allows dividing numbers via list notation using ":". + * Returns a new pattern with just numbers. + * @name ratio + * @memberof Pattern + * @returns Pattern + * @example + * ratio("1, 5:4, 3:2").mul(110).freq().s("piano").slow(2) + */ +export const ratio = register('ratio', (pat) => + pat.fmap((v) => { + if (!Array.isArray(v)) { + return v; + } + return v.slice(1).reduce((acc, n) => acc / n, v[0]); + }), +); + ////////////////////////////////////////////////////////////////////// // Structural and temporal transformations @@ -1677,6 +1695,9 @@ export const ply = register('ply', function (factor, pat) { * s(" hh").fast(2) // s("[ hh]*2") */ export const { fast, density } = register(['fast', 'density'], function (factor, pat) { + if (factor === 0) { + return silence; + } factor = Fraction(factor); const fastQuery = pat.withQueryTime((t) => t.mul(factor)); return fastQuery.withHapTime((t) => t.div(factor)); @@ -1703,6 +1724,9 @@ export const hurry = register('hurry', function (r, pat) { * s(" hh").slow(2) // s("[ hh]/2") */ export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) { + if (factor === 0) { + return silence; + } return pat._fast(Fraction(1).div(factor)); }); diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 59476015..66b56dc8 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -50,6 +50,7 @@ Pattern.prototype.pianoroll = function ({ timeframe: timeframeProp, fold = 0, vertical = 0, + labels = 0, } = {}) { const ctx = getDrawContext(); const w = ctx.canvas.width; @@ -87,7 +88,7 @@ Pattern.prototype.pianoroll = function ({ const isActive = event.whole.begin <= t && event.whole.end > t; ctx.fillStyle = event.context?.color || inactive; ctx.strokeStyle = event.context?.color || active; - ctx.globalAlpha = event.context.velocity ?? 1; + ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1; const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange); let durationPx = scale(event.duration / timeExtent, 0, timeAxis); const value = getValue(event); @@ -114,6 +115,14 @@ Pattern.prototype.pianoroll = function ({ ]; } isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); + if (labels) { + const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : ''); + ctx.font = `${barThickness * 0.75}px monospace`; + ctx.strokeStyle = 'black'; + ctx.fillStyle = isActive ? 'white' : 'black'; + ctx.textBaseline = 'top'; + ctx.fillText(label, ...coords); + } }); ctx.globalAlpha = 1; // reset! const playheadPosition = scale(-from / timeExtent, ...timeRange); @@ -181,6 +190,7 @@ export function pianoroll({ timeframe: timeframeProp, fold = 0, vertical = 0, + labels = false, ctx, } = {}) { const w = ctx.canvas.width; @@ -240,7 +250,7 @@ export function pianoroll({ const color = event.value?.color || event.context?.color; ctx.fillStyle = color || inactive; ctx.strokeStyle = color || active; - ctx.globalAlpha = event.context.velocity ?? 1; + ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1; const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange); let durationPx = scale(event.duration / timeExtent, 0, timeAxis); const value = getValue(event); @@ -267,6 +277,14 @@ export function pianoroll({ ]; } isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); + if (labels) { + const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : ''); + ctx.font = `${barThickness * 0.75}px monospace`; + ctx.strokeStyle = 'black'; + ctx.fillStyle = isActive ? 'white' : 'black'; + ctx.textBaseline = 'top'; + ctx.fillText(label, ...coords); + } }); ctx.globalAlpha = 1; // reset! const playheadPosition = scale(-from / timeExtent, ...timeRange); diff --git a/packages/core/test/solmization.test.js b/packages/core/test/solmization.test.js new file mode 100644 index 00000000..17352951 --- /dev/null +++ b/packages/core/test/solmization.test.js @@ -0,0 +1,40 @@ +/*test for issue 302 support alternative solmization types */ +import { sol2note } from '../util.mjs'; +import { test } from 'vitest'; +import assert from 'assert'; + +test('solmization - letters', () => { + const result = sol2note(60, 'letters'); + const expected = 'C4'; + assert.equal(result, expected); +}); + +test('solmization - solfeggio', () => { + const result = sol2note(60, 'solfeggio'); + const expected = 'Do4'; + assert.equal(result, expected); +}); + +test('solmization - indian', () => { + const result = sol2note(60, 'indian'); + const expected = 'Sa4'; + assert.equal(result, expected); +}); + +test('solmization - german', () => { + const result = sol2note(60, 'german'); + const expected = 'C4'; + assert.equal(result, expected); +}); + +test('solmization - byzantine', () => { + const result = sol2note(60, 'byzantine'); + const expected = 'Ni4'; + assert.equal(result, expected); +}); + +test('solmization - japanese', () => { + const result = sol2note(60, 'japanese'); + const expected = 'I4'; + assert.equal(result, expected); +}); diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 2b43cf0b..3bad9199 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -6,12 +6,12 @@ This program is free software: you can redistribute it and/or modify it under th // returns true if the given string is a note export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name); -export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]?$/.test(name); +export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name); export const tokenizeNote = (note) => { if (typeof note !== 'string') { return []; } - const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || []; + const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9])?$/)?.slice(1) || []; if (!pc) { return []; } @@ -25,7 +25,7 @@ export const noteToMidi = (note) => { throw new Error('not a note: "' + note + '"'); } const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()]; - const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0; + const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1, f: -1 }[char], 0) || 0; return (Number(oct) + 1) * 12 + chroma + offset; }; export const midiToFreq = (n) => { @@ -67,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 + * @deprecated only used in workshop (first-notes) * @noAutocomplete */ export const midi2note = (n) => { const oct = Math.floor(n / 12) - 1; - const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12]; + const pc = pcs[n % 12]; return pc + oct; }; @@ -212,3 +213,61 @@ 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); + +/* solmization, not used yet */ +const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/ +const indian = [ + 'Sa', + 'Re', + 'Ga', + 'Ma', + 'Pa', + 'Dha', + 'Ni', +]; /*indian musical notes, seems like they do not use flats or sharps*/ +const german = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Hb', 'H']; /*german & dutch musical notes*/ +const byzantine = [ + 'Ni', + 'Pab', + 'Pa', + 'Voub', + 'Vou', + 'Ga', + 'Dib', + 'Di', + 'Keb', + 'Ke', + 'Zob', + 'Zo', +]; /*byzantine musical notes*/ +const japanese = [ + 'I', + 'Ro', + 'Ha', + 'Ni', + 'Ho', + 'He', + 'To', +]; /*traditional japanese musical notes, seems like they do not use falts or sharps*/ + +const english = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']; + +export const sol2note = (n, notation = 'letters') => { + const pc = + notation === 'solfeggio' + ? solfeggio /*check if its is any of the following*/ + : notation === 'indian' + ? indian + : notation === 'german' + ? german + : notation === 'byzantine' + ? byzantine + : notation === 'japanese' + ? japanese + : english; /*if not use standard version*/ + const note = pc[n % 12]; /*calculating the midi value to the note*/ + const oct = Math.floor(n / 12) - 1; + return note + oct; +}; diff --git a/packages/react/src/components/CodeMirror6.jsx b/packages/react/src/components/CodeMirror6.jsx index e3fdc87a..18306d31 100644 --- a/packages/react/src/components/CodeMirror6.jsx +++ b/packages/react/src/components/CodeMirror6.jsx @@ -2,12 +2,12 @@ import React, { useMemo } from 'react'; import _CodeMirror from '@uiw/react-codemirror'; import { EditorView, Decoration } from '@codemirror/view'; import { StateField, StateEffect } from '@codemirror/state'; -import { javascript } from '@codemirror/lang-javascript'; +import { javascript, javascriptLanguage } from '@codemirror/lang-javascript'; import strudelTheme from '../themes/strudel-theme'; import './style.css'; import { useCallback } from 'react'; import { autocompletion } from '@codemirror/autocomplete'; -//import { strudelAutocomplete } from './Autocomplete'; +import { strudelAutocomplete } from './Autocomplete'; import { vim } from '@replit/codemirror-vim'; import { emacs } from '@replit/codemirror-emacs'; @@ -92,10 +92,8 @@ const staticExtensions = [ javascript(), highlightField, flashField, + javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }), EditorView.lineWrapping, - // javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }), - // autocompletion({ override: [strudelAutocomplete] }), - autocompletion({ override: [] }), // wait for https://github.com/uiwjs/react-codemirror/pull/458 ]; export default function CodeMirror({ @@ -105,6 +103,8 @@ export default function CodeMirror({ onSelectionChange, theme, keybindings, + isLineNumbersDisplayed, + isAutoCompletionEnabled, fontSize = 18, fontFamily = 'monospace', options, @@ -116,12 +116,14 @@ export default function CodeMirror({ }, [onChange], ); + const handleOnCreateEditor = useCallback( (view) => { onViewChanged?.(view); }, [onViewChanged], ); + const handleOnUpdate = useCallback( (viewUpdate) => { if (viewUpdate.selectionSet && onSelectionChange) { @@ -130,16 +132,27 @@ export default function CodeMirror({ }, [onSelectionChange], ); + const extensions = useMemo(() => { + let _extensions = [...staticExtensions]; let bindings = { vim, emacs, }; + if (bindings[keybindings]) { - return [...staticExtensions, bindings[keybindings]()]; + _extensions.push(bindings[keybindings]()); } - return staticExtensions; - }, [keybindings]); + + if (isAutoCompletionEnabled) { + _extensions.push(javascriptLanguage.data.of({ autocomplete: strudelAutocomplete })); + } else { + _extensions.push(autocompletion({ override: [] })); + } + + return _extensions; + }, [keybindings, isAutoCompletionEnabled]); + return (
<_CodeMirror @@ -149,6 +162,7 @@ export default function CodeMirror({ onCreateEditor={handleOnCreateEditor} onUpdate={handleOnUpdate} extensions={extensions} + basicSetup={{ lineNumbers: isLineNumbersDisplayed }} />
); diff --git a/packages/react/src/components/MiniRepl.jsx b/packages/react/src/components/MiniRepl.jsx index a8de7978..1a6cce94 100644 --- a/packages/react/src/components/MiniRepl.jsx +++ b/packages/react/src/components/MiniRepl.jsx @@ -18,16 +18,24 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, + onTrigger, drawTime, punchcard, + punchcardLabels, + onPaint, canvasHeight = 200, + fontSize = 18, + fontFamily, + hideHeader = false, theme, + keybindings, + isLineNumbersDisplayed, }) { drawTime = drawTime || (punchcard ? [0, 4] : undefined); const evalOnMount = !!drawTime; const drawContext = useCallback( - !!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null, - [drawTime], + punchcard ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null, + [punchcard], ); const { code, @@ -47,7 +55,18 @@ export function MiniRepl({ } = useStrudel({ initialCode: tune, defaultOutput: webaudioOutput, - editPattern: (pat) => (punchcard ? pat.punchcard() : pat), + editPattern: (pat, id) => { + //pat = pat.withContext((ctx) => ({ ...ctx, id })); + if (onTrigger) { + pat = pat.onTrigger(onTrigger, false); + } + if (onPaint) { + pat = pat.onPaint(onPaint); + } else if (punchcard) { + pat = pat.punchcard({ labels: punchcardLabels }); + } + return pat; + }, getTime, evalOnMount, drawContext, @@ -82,7 +101,7 @@ export function MiniRepl({ e.preventDefault(); flash(view); await activateCode(); - } else if (e.key === '.') { + } else if (e.key === '.' || e.code === 'Period') { stop(); e.preventDefault(); } @@ -101,7 +120,7 @@ export function MiniRepl({ // const logId = data?.pattern?.meta?.id; if (logId === replId) { setLog((l) => { - return l.concat([e.detail]).slice(-10); + return l.concat([e.detail]).slice(-8); }); } }, []), @@ -109,33 +128,46 @@ export function MiniRepl({ return (
-
-
- - + {!hideHeader && ( +
+
+ + +
- {error &&
{error.message}
} -
+ )}
- {show && } + {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/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); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a135d89..8090c960 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,9 +28,6 @@ importers: dependency-tree: specifier: ^9.0.0 version: 9.0.0 - vitest: - specifier: ^0.28.0 - version: 0.28.0(@vitest/ui@0.28.0) devDependencies: '@vitest/ui': specifier: ^0.28.0 @@ -65,6 +62,9 @@ importers: rollup-plugin-visualizer: specifier: ^5.8.1 version: 5.9.0 + vitest: + specifier: ^0.28.0 + version: 0.28.0(@vitest/ui@0.28.0) packages/codemirror: dependencies: @@ -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 @@ -3534,6 +3537,7 @@ packages: /@polka/url@1.0.0-next.21: resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} + dev: true /@proload/core@0.3.3: resolution: {integrity: sha512-7dAFWsIK84C90AMl24+N/ProHKm4iw0akcnoKjRvbfHifJZBLhaDsDus1QJmhG12lXj4e/uB/8mB/0aduCW+NQ==} @@ -3981,9 +3985,11 @@ packages: resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: '@types/chai': 4.3.4 + dev: true /@types/chai@4.3.4: resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} + dev: true /@types/debug@4.1.7: resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} @@ -4060,6 +4066,7 @@ packages: /@types/node@18.11.18: resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==} + dev: true /@types/node@18.16.3: resolution: {integrity: sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==} @@ -4502,7 +4509,7 @@ packages: '@babel/plugin-transform-react-jsx-self': 7.21.0(@babel/core@7.21.5) '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.21.5) react-refresh: 0.14.0 - vite: 4.3.3(@types/node@18.16.3) + vite: 4.3.3(@types/node@18.11.18) transitivePeerDependencies: - supports-color dev: true @@ -4513,6 +4520,7 @@ packages: '@vitest/spy': 0.28.0 '@vitest/utils': 0.28.0 chai: 4.3.7 + dev: true /@vitest/runner@0.28.0: resolution: {integrity: sha512-SXQO9aubp7Hg4DV4D5DP70wJ/4o0krH1gAPrSt+rhEZQbQvMaBJAHWOxEibwzLkklgoHreaMEvETFILkGQWXww==} @@ -4520,11 +4528,13 @@ packages: '@vitest/utils': 0.28.0 p-limit: 4.0.0 pathe: 1.1.0 + dev: true /@vitest/spy@0.28.0: resolution: {integrity: sha512-gYBDQIP0QDvxrscl2Id0BTbzLUbuAzFiFur3eHxH9Yt5cM6YCH/kxBrSHhmXTbu92UenLx53Gwq17u5N0zGNDQ==} dependencies: tinyspy: 1.0.2 + dev: true /@vitest/ui@0.28.0: resolution: {integrity: sha512-ihcVEx8t1gZXMboPGcIvoHk+PxiW5USxDMqnZOeUVIUm+XrRCtoJ96YDXdeR6MyPWeYLBPXfBWSxp5gMqoNSkw==} @@ -4534,6 +4544,7 @@ packages: pathe: 1.1.0 picocolors: 1.0.0 sirv: 2.0.2 + dev: true /@vitest/utils@0.28.0: resolution: {integrity: sha512-Dt+jDZbwriZWzJ5Hi9nAUnz9IPgNb+ACE96tWiXPp/u9NmCYWIWcuNoUOYS8HQyGFz31GiNYGvaZ4ZEDjAgi1g==} @@ -4543,6 +4554,7 @@ packages: loupe: 2.3.6 picocolors: 1.0.0 pretty-format: 27.5.1 + dev: true /@vscode/emmet-helper@2.8.6: resolution: {integrity: sha512-IIB8jbiKy37zN8bAIHx59YmnIelY78CGHtThnibD/d3tQOKRY83bYVi9blwmZVUZh6l9nfkYH3tvReaiNxY9EQ==} @@ -4608,6 +4620,7 @@ packages: /acorn-walk@8.2.0: resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} engines: {node: '>=0.4.0'} + dev: true /acorn@8.8.2: resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} @@ -4732,6 +4745,7 @@ packages: /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + dev: true /ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} @@ -4881,6 +4895,7 @@ packages: /assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true /ast-module-types@3.0.0: resolution: {integrity: sha512-CMxMCOCS+4D+DkOQfuZf+vLrSEmY/7xtORwdxs4wtcC1wVgvk2MqFFTwQCFhvWsI4KPU9lcWXPI8DgRiz+xetQ==} @@ -5150,6 +5165,7 @@ packages: /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true /buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -5200,6 +5216,7 @@ packages: /cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + dev: true /cacache@16.1.3: resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} @@ -5329,6 +5346,7 @@ packages: loupe: 2.3.6 pathval: 1.1.1 type-detect: 4.0.8 + dev: true /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -5376,6 +5394,7 @@ packages: /check-error@1.0.2: resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + dev: true /chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} @@ -5413,6 +5432,10 @@ packages: resolution: {integrity: sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==} engines: {node: '>=8'} + /claviature@0.1.0: + resolution: {integrity: sha512-Ai12axNwQ7x/F9QAj64RYKsgvi5Y33+X3GUSKAC/9s/adEws8TSSc0efeiqhKNGKBo6rT/c+CSCwSXzXxwxZzQ==} + dev: false + /clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -5450,6 +5473,7 @@ packages: dependencies: slice-ansi: 5.0.0 string-width: 5.1.2 + dev: true /cli-width@3.0.0: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} @@ -5929,6 +5953,7 @@ packages: engines: {node: '>=6'} dependencies: type-detect: 4.0.8 + dev: true /deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} @@ -7127,6 +7152,7 @@ packages: /get-func-name@2.0.0: resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + dev: true /get-intrinsic@1.2.0: resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} @@ -7958,6 +7984,7 @@ packages: /is-fullwidth-code-point@4.0.0: resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} engines: {node: '>=12'} + dev: true /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} @@ -8601,6 +8628,7 @@ packages: /local-pkg@0.4.3: resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} engines: {node: '>=14'} + dev: true /locate-path@2.0.0: resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} @@ -8693,6 +8721,7 @@ packages: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} dependencies: get-func-name: 2.0.0 + dev: true /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -9587,6 +9616,7 @@ packages: pathe: 1.1.0 pkg-types: 1.0.2 ufo: 1.1.1 + dev: true /modify-values@1.0.1: resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} @@ -9632,6 +9662,7 @@ packages: /mrmime@1.0.1: resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} engines: {node: '>=10'} + dev: true /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -10312,6 +10343,7 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: yocto-queue: 1.0.0 + dev: true /p-locate@2.0.0: resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} @@ -10564,9 +10596,11 @@ packages: /pathe@1.1.0: resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==} + dev: true /pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true /peggy@3.0.2: resolution: {integrity: sha512-n7chtCbEoGYRwZZ0i/O3t1cPr6o+d9Xx4Zwy2LYfzv0vjchMBU0tO+qYYyvZloBPcgRgzYvALzGWHe609JjEpg==} @@ -10643,6 +10677,7 @@ packages: jsonc-parser: 3.2.0 mlly: 1.2.0 pathe: 1.1.0 + dev: true /pkg@5.8.1: resolution: {integrity: sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==} @@ -10844,6 +10879,7 @@ packages: ansi-regex: 5.0.1 ansi-styles: 5.2.0 react-is: 17.0.2 + dev: true /pretty-format@29.4.3: resolution: {integrity: sha512-cvpcHTc42lcsvOOAzd3XuNWTcvk1Jmnzqeu+WsOuiPmxUJTnkbAcFNsRKvEpBEUFVUgy/GTZLulZDcDEi+CIlA==} @@ -11003,6 +11039,7 @@ packages: /react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: true /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} @@ -11756,6 +11793,7 @@ packages: /siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -11803,6 +11841,7 @@ packages: '@polka/url': 1.0.0-next.21 mrmime: 1.0.1 totalist: 3.0.0 + dev: true /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -11821,6 +11860,7 @@ packages: dependencies: ansi-styles: 6.2.1 is-fullwidth-code-point: 4.0.0 + dev: true /smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} @@ -11879,6 +11919,7 @@ packages: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 + dev: true /source-map@0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} @@ -11961,6 +12002,7 @@ packages: /stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true /standardized-audio-context@25.3.37: resolution: {integrity: sha512-lr0+RH/IJXYMts95oYKIJ+orTmstOZN3GXWVGmlkbMj8OLahREkRh7DhNGLYgBGDkBkhhc4ev5pYGSFN3gltHw==} @@ -11972,6 +12014,7 @@ packages: /std-env@3.3.2: resolution: {integrity: sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==} + dev: true /stdopt@2.2.0: resolution: {integrity: sha512-D/p41NgXOkcj1SeGhfXOwv9z1K6EV3sjAUY5aeepVbgEHv7DpKWLTjhjScyzMWAQCAgUQys1mjH0eArm4cjRGw==} @@ -12142,6 +12185,7 @@ packages: resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==} dependencies: acorn: 8.8.2 + dev: true /strong-log-transformer@2.1.0: resolution: {integrity: sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==} @@ -12428,14 +12472,17 @@ packages: /tinybench@2.5.0: resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} + dev: true /tinypool@0.3.1: resolution: {integrity: sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==} engines: {node: '>=14.0.0'} + dev: true /tinyspy@1.0.2: resolution: {integrity: sha512-bSGlgwLBYf7PnUsQ6WOc6SJ3pGOcd+d8AA6EUnLDDM0kWEstC1JIlSZA3UNliDXhd9ABoS7hiRBDCu+XP/sf1Q==} engines: {node: '>=14.0.0'} + dev: true /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} @@ -12471,6 +12518,7 @@ packages: /totalist@3.0.0: resolution: {integrity: sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==} engines: {node: '>=6'} + dev: true /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -12577,6 +12625,7 @@ packages: /type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + dev: true /type-fest@0.13.1: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} @@ -12677,6 +12726,7 @@ packages: /ufo@1.1.1: resolution: {integrity: sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==} + dev: true /uglify-js@3.17.4: resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} @@ -13052,6 +13102,7 @@ packages: - sugarss - supports-color - terser + dev: true /vite-plugin-pwa@0.14.7(vite@4.3.3)(workbox-build@6.5.4)(workbox-window@6.5.4): resolution: {integrity: sha512-dNJaf0fYOWncmjxv9HiSa2xrSjipjff7IkYE5oIUJ2x5HKu3cXgA8LRgzOwTc5MhwyFYRSU0xyN0Phbx3NsQYw==} @@ -13103,6 +13154,7 @@ packages: rollup: 3.21.0 optionalDependencies: fsevents: 2.3.2 + dev: true /vite@4.3.3(@types/node@18.16.3): resolution: {integrity: sha512-MwFlLBO4udZXd+VBcezo3u8mC77YQk+ik+fbc0GZWGgzfbPP+8Kf0fldhARqvSYmtIWoAJ5BXPClUbMTlqFxrA==} @@ -13200,6 +13252,7 @@ packages: - sugarss - supports-color - terser + dev: true /vscode-css-languageservice@6.2.3: resolution: {integrity: sha512-EAyhyIVHpEaf+GjtI+tVe7SekdoANfG0aubnspsQwak3Qkimn/97FpAufNyXk636ngW05pjNKAR9zyTCzo6avQ==} @@ -13387,6 +13440,7 @@ packages: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + dev: true /wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} @@ -13720,6 +13774,7 @@ packages: /yocto-queue@1.0.0: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} + dev: true /zod@3.21.4: resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index da9bda11..557516e3 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3193,6 +3193,23 @@ exports[`runs examples > example "rarely" example index 0 1`] = ` ] `; +exports[`runs examples > example "ratio" example index 0 1`] = ` +[ + "[ (0/1 → 1/1) ⇝ 2/1 | freq:110 s:piano ]", + "[ (0/1 → 1/1) ⇝ 2/1 | freq:137.5 s:piano ]", + "[ (0/1 → 1/1) ⇝ 2/1 | freq:165 s:piano ]", + "[ 0/1 ⇜ (1/1 → 2/1) | freq:110 s:piano ]", + "[ 0/1 ⇜ (1/1 → 2/1) | freq:137.5 s:piano ]", + "[ 0/1 ⇜ (1/1 → 2/1) | freq:165 s:piano ]", + "[ (2/1 → 3/1) ⇝ 4/1 | freq:110 s:piano ]", + "[ (2/1 → 3/1) ⇝ 4/1 | freq:137.5 s:piano ]", + "[ (2/1 → 3/1) ⇝ 4/1 | freq:165 s:piano ]", + "[ 2/1 ⇜ (3/1 → 4/1) | freq:110 s:piano ]", + "[ 2/1 ⇜ (3/1 → 4/1) | freq:137.5 s:piano ]", + "[ 2/1 ⇜ (3/1 → 4/1) | freq:165 s:piano ]", +] +`; + exports[`runs examples > example "release" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c3 release:0 ]", diff --git a/test/metadata.test.mjs b/test/metadata.test.mjs new file mode 100644 index 00000000..cbd0f8a3 --- /dev/null +++ b/test/metadata.test.mjs @@ -0,0 +1,246 @@ +import { describe, expect, it } from 'vitest'; +import { getMetadata } from '../website/src/pages/metadata_parser'; + +describe.concurrent('Metadata parser', () => { + it('loads a tag from inline comment', async () => { + const tune = `// @title Awesome song`; + expect(getMetadata(tune)).toStrictEqual({ + title: 'Awesome song', + }); + }); + + it('loads many tags from inline comments', async () => { + const tune = `// @title Awesome song +// @by Sam`; + expect(getMetadata(tune)).toStrictEqual({ + title: 'Awesome song', + by: ['Sam'], + }); + }); + + it('loads many tags from one inline comment', async () => { + const tune = `// @title Awesome song @by Sam`; + expect(getMetadata(tune)).toStrictEqual({ + title: 'Awesome song', + by: ['Sam'], + }); + }); + + it('loads a tag from a block comment', async () => { + const tune = `/* @title Awesome song */`; + expect(getMetadata(tune)).toStrictEqual({ + title: 'Awesome song', + }); + }); + + it('loads many tags from a block comment', async () => { + const tune = `/* +@title Awesome song +@by Sam +*/`; + expect(getMetadata(tune)).toStrictEqual({ + title: 'Awesome song', + by: ['Sam'], + }); + }); + + it('loads many tags from many block comments', async () => { + const tune = `/* @title Awesome song */ +/* @by Sam */`; + expect(getMetadata(tune)).toStrictEqual({ + title: 'Awesome song', + by: ['Sam'], + }); + }); + + it('loads many tags from mixed comments', async () => { + const tune = `/* @title Awesome song */ +// @by Sam +`; + expect(getMetadata(tune)).toStrictEqual({ + title: 'Awesome song', + by: ['Sam'], + }); + }); + + it('loads a title tag with quotes syntax', async () => { + const tune = `// "Awesome song"`; + expect(getMetadata(tune)).toStrictEqual({ + title: 'Awesome song', + }); + }); + + it('loads a title tag with quotes syntax among other tags', async () => { + const tune = `// "Awesome song" made @by Sam`; + expect(getMetadata(tune)).toStrictEqual({ + title: 'Awesome song', + by: ['Sam'], + }); + }); + + it('loads a title tag with quotes syntax from block comment', async () => { + const tune = `/* "Awesome song" +@by Sam */`; + expect(getMetadata(tune)).toStrictEqual({ + title: 'Awesome song', + by: ['Sam'], + }); + }); + + it('does not load a title tag with quotes syntax after a prefix', async () => { + const tune = `// I don't care about those "metadata".`; + expect(getMetadata(tune)).toStrictEqual({}); + }); + + it('does not load a title tag with quotes syntax after an other comment', async () => { + const tune = `// I don't care about those +// "metadata"`; + expect(getMetadata(tune)).toStrictEqual({}); + }); + + it('does not load a title tag with quotes syntax after other tags', async () => { + const tune = `/* +@by Sam aka "Lady Strudel" + "Sandyyy" +*/`; + expect(getMetadata(tune)).toStrictEqual({ + by: ['Sam aka "Lady Strudel"', '"Sandyyy"'], + }); + }); + + it('loads a tag list with comma-separated values syntax', async () => { + const tune = `// @by Sam, Sandy`; + expect(getMetadata(tune)).toStrictEqual({ + by: ['Sam', 'Sandy'], + }); + }); + + it('loads a tag list with duplicate keys syntax', async () => { + const tune = `// @by Sam +// @by Sandy`; + expect(getMetadata(tune)).toStrictEqual({ + by: ['Sam', 'Sandy'], + }); + }); + + it('loads a tag list with duplicate keys syntax, with prefixes', async () => { + const tune = `// song @by Sam +// samples @by Sandy`; + expect(getMetadata(tune)).toStrictEqual({ + by: ['Sam', 'Sandy'], + }); + }); + + it('loads many tag lists with duplicate keys syntax, within code', async () => { + const tune = `note("a3 c#4 e4 a4") // @by Sam @license CC0 + s("bd hh sd hh") // @by Sandy @license CC BY-NC-SA`; + expect(getMetadata(tune)).toStrictEqual({ + by: ['Sam', 'Sandy'], + license: ['CC0', 'CC BY-NC-SA'], + }); + }); + + it('loads a tag list with duplicate keys syntax from block comment', async () => { + const tune = `/* @by Sam +@by Sandy */`; + expect(getMetadata(tune)).toStrictEqual({ + by: ['Sam', 'Sandy'], + }); + }); + + it('loads a tag list with newline syntax', async () => { + const tune = `/* +@by Sam + Sandy */`; + expect(getMetadata(tune)).toStrictEqual({ + by: ['Sam', 'Sandy'], + }); + }); + + it('loads a multiline tag from block comment', async () => { + const tune = `/* +@details I wrote this song in February 19th, 2023. + It was around midnight and I was lying on + the sofa in the living room. +*/`; + expect(getMetadata(tune)).toStrictEqual({ + details: + 'I wrote this song in February 19th, 2023. ' + + 'It was around midnight and I was lying on the sofa in the living room.', + }); + }); + + it('loads a multiline tag from block comment with duplicate keys', async () => { + const tune = `/* +@details I wrote this song in February 19th, 2023. +@details It was around midnight and I was lying on + the sofa in the living room. +*/`; + expect(getMetadata(tune)).toStrictEqual({ + details: + 'I wrote this song in February 19th, 2023. ' + + 'It was around midnight and I was lying on the sofa in the living room.', + }); + }); + + it('loads a multiline tag from inline comments', async () => { + const tune = `// @details I wrote this song in February 19th, 2023. +// @details It was around midnight and I was lying on +// @details the sofa in the living room. +*/`; + expect(getMetadata(tune)).toStrictEqual({ + details: + 'I wrote this song in February 19th, 2023. ' + + 'It was around midnight and I was lying on the sofa in the living room.', + }); + }); + + it('loads empty tags from inline comments', async () => { + const tune = `// @title +// @by`; + expect(getMetadata(tune)).toStrictEqual({ + title: '', + by: [], + }); + }); + + it('loads tags with whitespaces from inline comments', async () => { + const tune = ` // @title Awesome song + // @by Sam Tagada `; + expect(getMetadata(tune)).toStrictEqual({ + title: 'Awesome song', + by: ['Sam Tagada'], + }); + }); + + it('loads tags with whitespaces from block comment', async () => { + const tune = ` /* @title Awesome song + @by Sam Tagada */ `; + expect(getMetadata(tune)).toStrictEqual({ + title: 'Awesome song', + by: ['Sam Tagada'], + }); + }); + + it('loads empty tags from block comment', async () => { + const tune = `/* @title +@by */`; + expect(getMetadata(tune)).toStrictEqual({ + title: '', + by: [], + }); + }); + + it('does not load tags if there is not', async () => { + const tune = `note("a3 c#4 e4 a4")`; + expect(getMetadata(tune)).toStrictEqual({}); + }); + + it('does not load code that looks like a metadata tag', async () => { + const tune = `const str1 = '@title Awesome song'`; + // need a lexer to avoid this one, but it's a pretty rare use case: + // const tune = `const str1 = '// @title Awesome song'`; + + expect(getMetadata(tune)).toStrictEqual({}); + }); +}); diff --git a/website/package.json b/website/package.json index 6b8b7a6c..c595f5a6 100644 --- a/website/package.json +++ b/website/package.json @@ -4,16 +4,16 @@ "version": "0.6.0", "private": true, "scripts": { - "dev": "astro dev", + "dev": "astro dev --host 0.0.0.0", "start": "astro dev", "check": "astro check && tsc", "build": "astro build", - "preview": "astro preview --port 3009", + "preview": "astro preview --port 3009 --host 0.0.0.0", "astro": "astro" }, "dependencies": { "@algolia/client-search": "^4.17.0", - "@astrojs/mdx": "^0.19.0", + "@astrojs/mdx": "^0.19.0", "@astrojs/react": "^2.1.1", "@astrojs/tailwind": "^3.1.1", "@docsearch/css": "^3.3.4", @@ -43,6 +43,7 @@ "@uiw/codemirror-themes-all": "^4.19.16", "astro": "^2.3.2", "canvas": "^2.11.2", + "claviature": "^0.1.0", "fraction.js": "^4.2.0", "nanoid": "^4.0.2", "nanostores": "^0.8.1", diff --git a/website/public/icons/strudel_icon.png b/website/public/icons/strudel_icon.png new file mode 100644 index 00000000..ec9ad8e1 Binary files /dev/null and b/website/public/icons/strudel_icon.png differ diff --git a/website/src/components/Box.astro b/website/src/components/Box.astro new file mode 100644 index 00000000..d27671ea --- /dev/null +++ b/website/src/components/Box.astro @@ -0,0 +1,10 @@ +--- +import LightBulbIcon from '@heroicons/react/20/solid/LightBulbIcon'; +//import MusicalNoteIcon from '@heroicons/react/20/solid/MusicalNoteIcon'; +--- + +
+
+ + +
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/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/components/QA.tsx b/website/src/components/QA.tsx new file mode 100644 index 00000000..7d2ac53d --- /dev/null +++ b/website/src/components/QA.tsx @@ -0,0 +1,19 @@ +import ChevronDownIcon from '@heroicons/react/20/solid/ChevronDownIcon'; +import ChevronUpIcon from '@heroicons/react/20/solid/ChevronUpIcon'; +import React from 'react'; +import { useState } from 'react'; + +export default function QA({ children, q }) { + const [visible, setVisible] = useState(false); + return ( +
+
setVisible((v) => !v)}> +
{q}
+ + {visible ? : } + +
+ {visible &&
{children}
} +
+ ); +} diff --git a/website/src/config.ts b/website/src/config.ts index bf26fff1..e32e243d 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); @@ -38,25 +39,49 @@ export const ALGOLIA = { apiKey: 'd5044f9d21b80e7721e5b0067a8730b1', }; -export type Sidebar = Record<(typeof KNOWN_LANGUAGE_CODES)[number], Record>; +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/getting-started' }, + { text: 'Erste Sounds', link: 'de/workshop/first-sounds' }, + { text: 'Erste Töne', link: 'de/workshop/first-notes' }, + { text: 'Erste Effekte', link: 'de/workshop/first-effects' }, + { text: 'Pattern Effekte', link: 'de/workshop/pattern-effects' }, + { text: 'Rückblick', link: 'de/workshop/recap' }, + { text: 'Mehr Seiten auf Englisch', link: 'workshop/getting-started' }, + ], + }, en: { - Tutorial: [ - { text: 'Getting Started', link: 'learn/getting-started' }, - { text: 'Notes', link: 'learn/notes' }, - { text: 'Sounds', link: 'learn/sounds' }, - { text: 'Coding syntax', link: 'learn/code' }, - { text: 'Mini-Notation', link: 'learn/mini-notation' }, + Workshop: [ + { text: 'Getting Started', link: 'workshop/getting-started' }, + { text: 'First Sounds', link: 'workshop/first-sounds' }, + { text: 'First Notes', link: 'workshop/first-notes' }, + { text: 'First Effects', link: 'workshop/first-effects' }, + { text: 'Pattern Effects', link: 'workshop/pattern-effects' }, + { text: 'Recap', link: 'workshop/recap' }, + { text: 'Workshop in German', link: 'de/workshop/getting-started' }, ], 'Making Sound': [ { text: 'Samples', link: 'learn/samples' }, { text: 'Synths', link: 'learn/synths' }, { text: 'Audio Effects', link: 'learn/effects' }, + { text: 'MIDI & OSC', link: 'learn/input-output' }, + ], + More: [ + { text: 'Mini-Notation', link: 'learn/mini-notation' }, + { text: 'Coding syntax', link: 'learn/code' }, + { text: 'Offline', link: 'learn/pwa' }, + { text: 'Patterns', link: 'technical-manual/patterns' }, + { text: 'Pattern Alignment', link: 'technical-manual/alignment' }, + { text: 'Strudel vs Tidal', link: 'learn/strudel-vs-tidal' }, + { text: 'Music metadata', link: 'learn/metadata' }, { text: 'CSound', link: 'learn/csound' }, ], 'Pattern Functions': [ { text: 'Introduction', link: 'functions/intro' }, - { text: 'Pattern Constructors', link: 'learn/factories' }, + { text: 'Creating Patterns', link: 'learn/factories' }, { text: 'Time Modifiers', link: 'learn/time-modifiers' }, { text: 'Control Parameters', link: 'functions/value-modifiers' }, { text: 'Signals', link: 'learn/signals' }, @@ -64,13 +89,6 @@ export const SIDEBAR: Sidebar = { { text: 'Accumulation', link: 'learn/accumulation' }, { text: 'Tonal Modifiers', link: 'learn/tonal' }, ], - More: [ - { text: 'MIDI & OSC', link: 'learn/input-output' }, - { text: 'Offline', link: 'learn/pwa' }, - { text: 'Patterns', link: 'technical-manual/patterns' }, - { text: 'Pattern Alignment', link: 'technical-manual/alignment' }, - { text: 'Strudel vs Tidal', link: 'learn/strudel-vs-tidal' }, - ], Development: [ { text: 'REPL', link: 'technical-manual/repl' }, { text: 'Sounds', link: 'technical-manual/sounds' }, diff --git a/website/src/docs/MiniRepl.css b/website/src/docs/MiniRepl.css index e9b49af8..84927a88 100644 --- a/website/src/docs/MiniRepl.css +++ b/website/src/docs/MiniRepl.css @@ -1,4 +1,5 @@ -.cm-activeLine { +.cm-activeLine, +.cm-activeLineGutter { background-color: transparent !important; } @@ -7,3 +8,19 @@ border: 1px solid var(--lineHighlight); padding: 2px; } + +.cm-scroller { + font-family: inherit !important; +} + +.cm-gutters { + display: none !important; +} + +.cm-cursorLayer { + animation-name: inherit !important; +} + +.cm-cursor { + border-left: 2px solid currentcolor !important; +} diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index cce24623..7c5079a0 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,20 @@ if (typeof window !== 'undefined') { prebake(); } -export function MiniRepl({ tune, drawTime, punchcard, canvasHeight = 100 }) { +export function MiniRepl({ + tune, + drawTime, + punchcard, + punchcardLabels = true, + span = [0, 4], + canvasHeight = 100, + hideHeader, + claviature, + claviatureLabels, +}) { const [Repl, setRepl] = useState(); - const { theme } = useSettings(); + const { theme, keybindings, fontSize, fontFamily, isLineNumbersDisplayed } = useSettings(); + const [activeNotes, setActiveNotes] = useState([]); useEffect(() => { // we have to load this package on the client // because codemirror throws an error on the server @@ -42,11 +54,39 @@ export function MiniRepl({ tune, drawTime, punchcard, canvasHeight = 100 }) { { + 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/pages/de/workshop/first-effects.mdx b/website/src/pages/de/workshop/first-effects.mdx new file mode 100644 index 00000000..7719249a --- /dev/null +++ b/website/src/pages/de/workshop/first-effects.mdx @@ -0,0 +1,336 @@ +--- +title: Erste Effekte +layout: ../../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../../docs/MiniRepl'; +import QA from '@components/QA'; + +# Erste Effekte + +import Box from '@components/Box.astro'; + +## Ein paar grundlegende Effekte + +**low-pass filter** + +/2") +.sound("sawtooth").lpf(800)`} +/> + + + +lpf = **l**ow **p**ass **f**ilter + +- Ändere `lpf` in 200. Hörst du wie der Bass dumpfer klingt? Es klingt so ähnlich als würde die Musik hinter einer geschlossenen Tür laufen 🚪 +- Lass uns nun die Tür öffnen: Ändere `lpf` in 5000. Der Klang wird dadurch viel heller und schärfer ✨🪩 + + + +**filter automatisieren** + +/2") +.sound("sawtooth").lpf("200 1000")`} +/> + + + +- Füg noch mehr `lpf` Werte hinzu +- Das pattern in `lpf` ändert nicht den Rhythmus der Bassline + +Später sehen wir wie man mit Wellenformen Dinge automatisieren kann. + + + +**vowel = Vokal** + +/2") +.sound("sawtooth").vowel("/2")`} +/> + +**gain = Verstärkung** + + + + + +Bei Rhythmen ist die Dynamik (= Veränderungen der Lautstärke) sehr wichtig. + +- Entferne `.gain(...)` und achte darauf wie es viel flacher klingt. +- Mach es rückgängig (strg+z dann strg+enter) + + + +**stacks in stacks** + +Lass uns die obigen Beispiele kombinieren: + +/2") + .sound("sawtooth").lpf("200 1000"), + note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>/2") + .sound("sawtooth").vowel("/2") +) `} +/> + + + +Versuche die einzelnen Teile innerhalb `stack` zu erkennen, schau dir an wie die Kommas gesetzt sind. + +Die 3 Teile (Drums, Bass, Akkorde) sind genau wie vorher, nur in einem `stack`, getrennt durch Kommas + + + +**Den Sound formen mit ADSR Hüllkurve** + +") +.sound("sawtooth").lpf(600) +.attack(.1) +.decay(.1) +.sustain(.25) +.release(.2)`} +/> + + + +Versuche herauszufinden was die Zahlen machen. Probier folgendes: + +- attack: `.5` vs `0` +- decay: `.5` vs `0` +- sustain: `1` vs `.25` vs `0` +- release: `0` vs `.5` vs `1` + +Kannst du erraten was die einzelnen Werte machen? + + + + + +- attack (anschlagen): Zeit des Aufbaus +- decay (zerfallen): Zeit des Abfalls +- sustain (erhalten): Lautstärke nach Abfall +- release (loslassen): Zeit des Abfalls nach dem Ende + +![ADSR](https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/ADSR_parameter.svg/1920px-ADSR_parameter.svg.png) + + + +**adsr Kurznotation** + +") +.sound("sawtooth").lpf(600) +.adsr(".1:.1:.5:.2") +`} +/> + +**delay = Verzögerung** + + ~]") + .sound("gm_electric_guitar_muted"), + sound("").bank("RolandTR707") +).delay(".5")`} +/> + + + +Probier verschiedene `delay` Werte zwischen 0 und 1. Übrigens: `.5` ist kurz für `0.5`. + +Was passiert wenn du `.delay(".8:.125")` schreibst? Kannst du erraten was die zweite Zahl macht? + +Was passiert wenn du `.delay(".8:.06:.8")` schreibst? Kannst du erraten was die dritte Zahl macht? + + + + + +`delay("a:b:c")`: + +- a: Lautstärke des Delays +- b: Verzögerungszeit +- c: Feedback (je kleiner desto schneller verschwindet das Delay) + + + +**room aka reverb = Hall** + + ~@16] ~>/2") +.scale("D4:minor").sound("gm_accordion:2") +.room(2)`} +/> + + + +Spiel mit verschiedenen Werten. + +Füg auch ein Delay hinzu! + + + +**kleiner 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) +)`} +/> + +Für echten Dub fehlt noch der Bass: + + ~]") + .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) +)`} +/> + + + +Füg `.hush()` ans ende eines Patterns im stack... + + + +**pan = Panorama** + + + +**speed = Geschwindigkeit** + +").room(.2)`} /> + +**fast and slow = schnell und langsam** + +Mit `fast` und `slow` kann man das tempo eines patterns außerhalb der Mini-Notation ändern: + + + + + +Ändere den `slow` Wert. Tausche `slow` durch `fast`. + +Was passiert wenn du den Wert automatisierst? z.b. `.fast("<1 [2 4]>")` ? + + + +Übrigens, innerhalb der Mini-Notation, `fast` ist `*` und `slow` ist `/`. + +")`} /> + +## Automation mit Signalen + +Anstatt Werte schrittweise zu automatisieren können wir auch sogenannte Signale benutzen: + + + + + +Die grundlegenden Wellenformen sind `sine`, `saw`, `square`, `tri` 🌊 + +Probiere auch die zufälligen Signale `rand` und `perlin`! + +Der `gain`-Wert (Verstärkung) wird in der Visualisierung als Transparenz dargestellt. + + + +**Bereich ändern mit `range`** + +Signale bewegen sich standardmäßig zwischen 0 und 1. Wir können das mit `range` ändern: + + + +`range` ist nützlich wenn wir Funktionen mit einem anderen Wertebereich als 0 und 1 automatisieren wollen (z.b. lpf) + + + +Was passiert wenn du die beiden Werte vertauschst? + + + +Wir können die Geschwindigkeit der Automation mit slow / fast ändern: + +/2") +.sound("sawtooth") +.lpf(sine.range(100, 2000).slow(8))`} +/> + + + +Die ganze Automation braucht nun 8 cycle bis sie sich wiederholt. + + + +## Rückblick + +| name | example | +| ----- | -------------------------------------------------------------------------------------------------- | +| lpf | ")`} /> | +| vowel | ")`} /> | +| gain | | +| delay | | +| room | | +| pan | | +| speed | ")`} /> | +| range | | + +Lass uns nun die für Tidal typischen [Pattern Effekte anschauen](/de/workshop/pattern-effects). 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..c44969df --- /dev/null +++ b/website/src/pages/de/workshop/first-notes.mdx @@ -0,0 +1,408 @@ +--- +title: Erste Töne +layout: ../../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '@src/docs/MiniRepl'; +import { midi2note } from '@strudel.cycles/core/'; +import Box from '@components/Box.astro'; +import QA from '@components/QA'; + +# Erste Töne + +Jetzt schauen wir uns an wie man mit Tönen mit der `note` Funktion spielt. + +## Zahlen und Noten + +**Töne mit Zahlen** + + [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 + +**Sequenzen verlangsamen mit `/`** + +{/* [c2 bb1 f2 eb2] */} + + + + + +Das `/4` spielt die Sequenz 4 mal so langsam, also insgesamt 4 cycles = 4s. + +Jede Note ist nun also 1s lang. + +Schreib noch mehr Töne in die Klammern und achte darauf dass es schneller wird. + + + +Wenn eine Sequenz unabhängig von ihrem Inhalt immer gleich schnell bleiben soll, gibt es noch eine andere Art Klammern: + +**Eins pro Cycle per \< \>** + +").sound("gm_acoustic_bass")`} punchcard /> + + + +Füg noch mehr Töne hinzu und achte darauf wie das Tempo gleich bleibt! + +Tatsächlich sind diese Klammern nur eine Abkürzung: + +`` = `[a b c]/3` + +`` = `[a b c d]/4` + +usw.. + + + +**Eine Sequenz pro Cycle** + +") +.sound("gm_acoustic_bass")`} + punchcard +/> + +oder auch... + +/2") +.sound("gm_acoustic_bass")`} + punchcard +/> + +**Alternativen** + +Ähnlich wie Unter-Sequenzen, kann auch `<...>` innerhalb einer Sequenz verwendet werden: + +") +.sound("gm_xylophone")`} + punchcard +/> + +Das ist auch praktisch für atonale Sounds: + +, [~ hh]*2") +.bank("RolandTR909")`} + punchcard +/> + +## Skalen + +Es kann mühsam sein die richtigen Noten zu finden wenn man alle zur Verfügung hat. +Mit Skalen ist das einfacher: + +") +.scale("C:minor").sound("piano")`} + punchcard +/> + + + +Probier verschiedene Zahlen aus. Jede klingt gut! + +Probier verschiedene Skalen: + +- C:major +- A2:minor +- D:dorian +- G:mixolydian +- A2:minor:pentatonic +- F:major:pentatonic + + + +**Automatisierte Skalen** + +Wie alle Funktionen können auch Skalen mit einem Pattern automatisiert werden: + +, 2 4 <[6,8] [7,9]>") +.scale("/4") +.sound("piano")`} + punchcard +/> + + + +Wenn du keine Ahnung hast was die Skalen bedeuten, keine Sorge. +Es sind einfach nur Namen für verschiedene Gruppen von Tönen die gut zusammenpassen. + +Nimm dir Zeit um herauszufinden welche Skalen du magst. + + + +## Wiederholen und Verlängern + +**Verlängern mit @** + + + + + +Ein Element ohne `@` ist gleichbedeutend mit `@1`. Im Beispiel ist `c` drei Einheiten lang, `eb` nur eine. + +Spiel mit der Länge! + + + +**Unter-Sequenzen verlängern** + +*2") +.scale("/4") +.sound("gm_acoustic_bass")`} + punchcard +/> + + + +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. + + + +**Wiederholen** + +]").sound("piano")`} punchcard /> + + + +Wechsel zwischen `!`, `*` und `@` hin und her. + +Was ist der Unterschied? + + + +## Rückblick + +Das haben wir in diesem Kapitel gelernt: + +| Concept | Syntax | Example | +| ------------ | ------ | ------------------------------------------------------------------- | +| Verlangsamen | \/ | | +| Alternativen | \<\> | ")`} /> | +| Verlängern | @ | | +| Wiederholen | ! | | + +Neue Funktionen: + +| Name | Description | Example | +| ----- | --------------------------------------- | -------------------------------------------------------------------------------------------- | +| note | Tonhöhe als Buchstabe oder Zahl | | +| scale | Interpretiert `n` als Skalenstufe | | +| stack | Spiele mehrere Patterns parallel (s.u.) | | + +## Beispiele + +**Bassline** + +/2") +.sound("gm_synth_bass_1") +.lpf(800) // <-- we'll learn about this soon`} +/> + +**Melodie** + +*2\`).scale("C4:minor") +.sound("gm_synth_strings_1")`} +/> + +**Drums** + +, [~ hh]*2") +.bank("RolandTR909")`} +/> + +**Wenn es doch nur einen Weg gäbe das alles gleichzeitig zu spielen.......** + + + +Das geht mit `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") +)`} +/> + +Das hört sich doch langsam nach echter Musik an! +Wir haben Sounds, wir haben Töne.. noch ein Puzzleteil fehlt: [Effekte](/de/workshop/first-effects) 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..fea64cbd --- /dev/null +++ b/website/src/pages/de/workshop/first-sounds.mdx @@ -0,0 +1,363 @@ +--- +title: Erste Sounds +layout: ../../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '@src/docs/MiniRepl'; +import Box from '@components/Box.astro'; +import QA from '@components/QA'; + +# Erste Sounds + +Dies ist das erste Kapitel im Strudel Workshop, schön dich an Bord zu haben! + +## Textfelder + +Der Workshop ist voller interaktiver Textfelder. Lass uns lernen wie sie funktionieren. Hier ist eins: + + + + + +1. ⬆️ Klicke in das obige Textfeld ⬆️ +2. Drücke `Strg`+`Enter` zum Abspielen +3. Ändere `casio` in `metal` +4. Drücke `Strg`+`Enter` zum Aktualisieren +5. Drücke `Strg`+`Punkt` zum Stoppen + +Mac: `Strg` = `control` oder auch `option` + + + +Glückwunsch, du kannst jetzt live coden! + +## Sounds + +Gerade haben wir schon den ersten sound mit `sound` abgespielt: + + + + + +`casio` ist einer von vielen Standard Sounds. + +Probier ein paar andere Sounds aus: + +``` +insect wind jazz metal east crow casio space numbers +``` + +Es kann sein, dass du kurz nichts hörst während ein neuer Sound lädt. + + + +**Sample Nummer ändern mit :** + +Ein Sound kann mehrere Samples (Audio Dateien) enthalten. + +Du kannst ein anderes Sample wählen, indem du `:` und eine Zahl an den Sound-Namen anhängst: + + + + + +Probiere verschiedene Sound / Zahlen Kombinationen. + +Ohne Zahl ist gleichbedeutend mit `:0` + + + +Jetzt weißt du wie man Sounds abspielt und ihre Sample Nummer einstellt. +Vorerst bleiben wir bei den voreingestellten Sounds, später erfahren wir noch wie man eigene benutzt. + +## Drum Sounds + +Strudel kommt von Haus aus mit einer breiten Auswahl an Drum Sounds: + + + + + +Diese 2-Buchstaben Kombinationen stehen für verschiedene Teile eines Schlagzeugs: + +- `bd` = **b**ass **d**rum - Basstrommel +- `sd` = **s**nare **d**rum - Schnarrtrommel +- `rim` = **rim**shot - Rahmenschlag +- `hh` = **h**i**h**at - Hallo Hut +- `oh` = **o**pen **h**ihat - Offener Hallo Hut + +Probier verschiedene Sounds aus! + + + +Wir können den Charakter des Drum Sounds verändern, indem wir mit `bank` die Drum Machine auswählen: + + + +In diesem Beispiel ist `RolandTR909` der Name der Drum Machine, die 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.. + +🦥 Tipp für faule: Mach Doppel-Klick auf einen Namen um ihn zu markieren. +Dann kannst du ihn mit `Strg`+`C` kopieren und mit `Strg`+`V` einfügen. + + + +## 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. + +`cpm` ist angelehnt an `bpm` (=beats per minute). + + + +Wir werden später noch mehr Möglichkeiten kennen lernen das Tempo zu verändern. + +**Pausen mit '~'** + + + + + +Tilde tippen: + +- Windows / Linux: `Alt Gr` + `~` +- Mac: `option` + `N` + + + +**Unter-Sequenzen mit [Klammern]** + + + + + +Der Inhalt der Klammer wird ebenfalls zusammengequetscht! + +Füge noch mehr Sounds in die Klammern ein! + + + +Genau wie bei der ganzen Sequence wird eine Unter-Sequence schneller je mehr drin ist. + +**Multiplikation: Dinge schneller machen** + + + +**Multiplikation: Vieeeeeeel schneller** + + + + + +Tonhöhe = sehr schneller Rhythmus + + + +**Multiplikation: Ganze Unter-Sequences schneller machen** + + + +Bolero: + + + +**Unter-Unter-Sequenzen mit [[Klammern]]** + + + + + +Du kannst so tief verschachteln wie du willst! + + + +**Parallele Sequenzen mit Komma** + + + +Du kannst so viele Kommas benutzen wie du möchtest: + + + +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 schreiben mit \` (backtick)** + + + + + +Ob man " oder \` benutzt ist nur eine Frage der Übersichtlichkeit. + + + +**Sample Nummer separat auswählen** + +Benutzt man nur einen Sound mit unterschiedlichen Sample Nummer sieht das so aus: + + + +Das gleiche kann man auch so schreiben: + + + +## 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](/de/workshop/first-notes). diff --git a/website/src/pages/de/workshop/getting-started.mdx b/website/src/pages/de/workshop/getting-started.mdx new file mode 100644 index 00000000..c86fa668 --- /dev/null +++ b/website/src/pages/de/workshop/getting-started.mdx @@ -0,0 +1,74 @@ +--- +title: Intro +layout: ../../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../../docs/MiniRepl'; + +# Willkommen + + + +Willkommen zum Strudel Workshop! +Du hast den richtigen Ort gefunden wenn du lernen möchtest wie man mit Code Musik macht. + +## Was ist Strudel + +Mit Strudel kann man dynamische Musikstücke in Echtzeit schreiben. +Es ist eine in JavaScript entwickelte Version von [Tidal Cycles](https://tidalcycles.org/) und wurde 2022 von Alex McLean und Felix Roos initiiert. +Tidal Cycles, auch bekannt unter dem Namen Tidal, ist eine Computersprache für algorithmische Muster. +Obwohl sie meistens für die Erzeugung von Musik eingesetzt wird, kann sie für jede Art von Tätigkeit eingesetzt werden, +in der Muster eine Rolle spielen. + +Du brauchst keine Erfahrung in JavaScript oder Tidal Cycles um mit Strudel Musik zu machen. +Dieser interaktive Workshop leitet dich spielerisch durch die Grundlagen von Strudel. +Der beste Ort um mit Strudel Musik zu machen ist das [Strudel REPL](https://strudel.tidalcycles.org/). + +## Was kann man mit Strudel machen? + +- Musik Live Coding: In Echtzeit mit Code Musik machen +- Algorithmische Komposition: Schreibe Musik mithilfe Tidals einzigartiger Sprache für Muster +- Lehren: Strudel eignet sich gut für Lehrende, da keine Installation nötig ist und die Sprache kein theoretisches Vorwissen erfordert. +- Mit anderen Musik-Anwendungen kombinieren: Per MIDI oder OSC kann Strudel als flexibler Sequencer mit jedem Setup kombiniert werden + +## Beispiel + +Hier ist ein Beispiel wie Strudel klingen kann: + +],hh*8") + .speed(perlin.range(.8,.9)), // random sample speed variation + // bassline + "" + .off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps + .add(perlin.range(0,.5)) // random pitch variation + .superimpose(add(.05)) // add second, slightly detuned voice + .note() // wrap in "note" + .decay(.15).sustain(0) // make each note of equal length + .s('sawtooth') // waveform + .gain(.4) // turn down + .cutoff(sine.slow(7).range(300,5000)), // automate cutoff + // chords + ">".voicings('lefthand') + .superimpose(x=>x.add(.04)) // add second, slightly detuned voice + .add(perlin.range(0,.5)) // random pitch variation + .note() // wrap in "note" + .s('sawtooth') // waveform + .gain(.16) // turn down + .cutoff(500) // fixed cutoff + .attack(1) // slowly fade in +) +.slow(3/2)`} +/> + +Mehr Beispiele gibt es [hier](/examples). + +Du kannst auch im [Strudel REPL](https://strudel.tidalcycles.org/) auf `shuffle` klicken um ein zufälliges Beispiel zu hören. + +## Workshop + +Der beste Weg um Strudel zu lernen ist der nun folgende Workshop. +Wenn du bereit bist, lass uns loslegen mit deinen [ersten Sounds](/de/workshop/first-sounds). 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/pattern-effects.mdx b/website/src/pages/de/workshop/pattern-effects.mdx new file mode 100644 index 00000000..b701958a --- /dev/null +++ b/website/src/pages/de/workshop/pattern-effects.mdx @@ -0,0 +1,183 @@ +--- +title: Pattern Effekte +layout: ../../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '@src/docs/MiniRepl'; +import Box from '@components/Box.astro'; +import QA from '@components/QA'; + +# Pattern Effekte + +Bis jetzt sind die meisten Funktionen die wir kennengelernt haben ähnlich wie Funktionen in anderen Musik Programmen: Sequencing von Sounds, Noten und Effekten. + +In diesem Kapitel beschäftigen wir uns mit Funktionen die weniger herkömmlich oder auch enzigartig sind. + +**rev = rückwärts abspielen** + + + +**jux = einen stereo kanal modifizieren** + + + +So würde man das ohne jux schreiben: + + + +Lass uns visualisieren was hier passiert: + + + + + +Schreibe `//` vor eine der beiden Zeilen im `stack`! + + + +**mehrere tempos** + + + +Das hat den gleichen Effekt wie: + + + + + +Schreibe wieder `//` vor eine oder mehrere Zeilen im `stack`. + + + +**add = addieren** + +>")) +.color(">").adsr("[.1 0]:.2:[1 0]") +.sound("gm_acoustic_bass").room(.5)`} + punchcard +/> + + + +Addiert man eine Zahl zu einer Note, verhält sich diese wie eine Zahl. + +z.B. c4 = 60, also ist c4 + 2 = 62 + + + +Man kann auch mehrmals addieren: + +>").add("0,7")) +.color(">").adsr("[.1 0]:.2:[1 0]") +.sound("gm_acoustic_bass").room(.5)`} + punchcard +/> + +**add + scale** + + [~ <4 1>]>*2".add("<0 [0,2,4]>/4")) +.scale("C5:minor").release(.5) +.sound("gm_xylophone").room(.5)`} + punchcard +/> + +**Alles zusammen** + + [~ <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** + + + +das ist wie: + + + + + +Probier `ply` mit einem pattern zu automatisieren, z.b. `"<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 der notation `x=>x.`, das `x` ist das Pattern das wir bearbeiten. + + + +`off` ist auch nützlich für sounds: + +x.speed(1.5).gain(.25))`} +/> + +| name | description | example | +| ---- | --------------------------------- | ---------------------------------------------------------------------------------------------- | +| rev | rückwärts | | +| jux | ein stereo-kanal modifizieren | | +| add | addiert zahlen oder noten | ")).scale("C:minor")`} /> | +| ply | multipliziert jedes element x mal | ")`} /> | +| off | verzögert eine modifizierte kopie | 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..db392b8b --- /dev/null +++ b/website/src/pages/de/workshop/recap.mdx @@ -0,0 +1,68 @@ +--- +title: Recap +layout: ../../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../../docs/MiniRepl'; + +# Workshop Rückblick + +Diese Seite ist eine Auflistung aller im Workshop enthaltenen Funktionen. + +## Mini Notation + +| Concept | Syntax | Example | +| --------------------- | -------- | -------------------------------------------------------------------------------- | +| Sequence | space | | +| Sample Nummer | :x | | +| Pausen | ~ | | +| Unter-Sequences | \[\] | | +| Unter-Unter-Sequences | \[\[\]\] | | +| Schneller | \* | | +| Slow down | \/ | | +| Parallel | , | | +| Alternieren | \<\> | ")`} /> | +| Verlängern | @ | | +| Wiederholen | ! | | + +## Sounds + +| Name | Description | Example | +| ----- | -------------------------- | ---------------------------------------------------------------------------------- | +| sound | spielt den sound mit namen | | +| bank | wählt die soundbank | | +| n | wählt sample mit nummer | | + +## Notes + +| Name | Description | Example | +| --------- | ---------------------------------- | -------------------------------------------------------------------------------------------- | +| note | wählt note per zahl oder buchstabe | | +| n + scale | wählt note n in skala | | +| stack | spielt mehrere patterns parallel | | + +## Audio Effekte + +| name | example | +| ----- | -------------------------------------------------------------------------------------------------- | +| lpf | ")`} /> | +| vowel | ")`} /> | +| gain | | +| delay | | +| room | | +| pan | | +| speed | ")`} /> | +| range | | + +## Pattern Effects + +| name | description | example | +| ---- | --------------------------------- | ---------------------------------------------------------------------------------------------- | +| cpm | tempo in cycles pro minute | | +| fast | schneller | | +| slow | langsamer | | +| rev | rückwärts | | +| jux | ein stereo-kanal modifizieren | | +| add | addiert zahlen oder noten | ")).scale("C:minor")`} /> | +| ply | jedes element schneller machen | ")`} /> | +| off | verzögert eine modifizierte kopie | x.speed(2))`} /> | 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/functions/intro.mdx b/website/src/pages/functions/intro.mdx index d396543c..2e1bb7eb 100644 --- a/website/src/pages/functions/intro.mdx +++ b/website/src/pages/functions/intro.mdx @@ -6,23 +6,24 @@ layout: ../../layouts/MainLayout.astro import { MiniRepl } from '../../docs/MiniRepl'; import { JsDoc } from '../../docs/JsDoc'; -# Functional JavaScript API +# Pattern Functions -While the mini notation is powerful on its own, there is much more to discover. -Internally, the mini notation will expand to use the actual functional JavaScript API. +Let's learn all about functions to create and modify patterns. +At the core of Strudel, everything is made of functions. -For example, this Pattern in Mini Notation: +For example, everything you can do with the Mini-Notation can also be done with a function. +This Pattern in Mini Notation: is equivalent to this Pattern without Mini Notation: - + Similarly, there is an equivalent function for every aspect of the mini notation. -Which representation to use is a matter of context. As a rule of thumb, you can think of the JavaScript API -to fit better for the larger context, while mini notation is more practical for individiual rhythms. +Which representation to use is a matter of context. As a rule of thumb, functions +are better suited in a larger context, while mini notation is more practical for individiual rhythms. ## Limits of Mini Notation @@ -46,10 +47,10 @@ You can freely mix JS patterns, mini patterns and values! For example, this patt @@ -72,4 +73,4 @@ You can freely mix JS patterns, mini patterns and values! For example, this patt While mini notation is almost always shorter, it only has a handful of modifiers: \* / ! @. When using JS patterns, there is a lot more you can do. -What [Pattern Constructors](/learn/factories) does Strudel offer? +Next, let's look at how you can [create patterns](/learn/factories) diff --git a/website/src/pages/functions/value-modifiers.mdx b/website/src/pages/functions/value-modifiers.mdx index 81874672..94372e75 100644 --- a/website/src/pages/functions/value-modifiers.mdx +++ b/website/src/pages/functions/value-modifiers.mdx @@ -139,6 +139,10 @@ This group of functions allows to modify the value of events. +## ratio + + + # Custom Parameters You can also create your own parameters: diff --git a/website/src/pages/learn/code.mdx b/website/src/pages/learn/code.mdx index 678d4ccc..ca2564de 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -6,31 +6,31 @@ layout: ../../layouts/MainLayout.astro import { MiniRepl } from '../../docs/MiniRepl'; import { JsDoc } from '../../docs/JsDoc'; -# Strudel Code +# Coding Syntax -Now that we have played some notes using different sounds, let's take a step back and look how we actually achieved this using _code_. +Let's take a step back and understand how the syntax in Strudel works. -Let's look at this simple example again. What do we notice? +Take a look at this simple example: - + -- We have a word `freq` which is followed by some brackets `()` with some words/letters/numbers inside, surrounded by quotes `"220 275 330 440"` (corresponding to the pitches a3, c#4, e4, a4). -- Then we have a dot `.` followed by another similar piece of code `s("sine")`. -- We can also see these texts are _highlighted_ using colours: word `freq` is purple, the brackets `()` are grey, and the content inside the `""` are green. +- We have a word `note` which is followed by some brackets `()` with some words/letters/numbers inside, surrounded by quotes `"c a f e"` +- Then we have a dot `.` followed by another similar piece of code `s("piano")`. +- We can also see these texts are _highlighted_ using colours: word `note` is purple, the brackets `()` are grey, and the content inside the `""` are green. (The colors could be different if you've changed the default theme) What happens if we try to 'break' this pattern in different ways? - + - + - + Ok, none of these seem to work... - + -This one does work, but now we can't hear the four different events and frequencies anymore. +This one does work, but now we only hear the first note... So what is going on here? @@ -67,16 +67,17 @@ It is a handy way to quickly turn code on and off. Try uncommenting this line by deleting `//` and refreshing the pattern. You can also use the keyboard shortcut `cmd-/` to toggle comments on and off. +You might noticed that some comments in the REPL samples include some words starting with a "@", like `@by` or `@license`. +Those are just a convention to define some information about the music. We will talk about it in the [Music metadata](/learn/metadata) section. + # Strings -Ok, so what about the content inside the quotes (e.g. `"a3 c#4 e4 a4"`)? +Ok, so what about the content inside the quotes (e.g. `"c a f e"`)? In JavaScript, as in most programming languages, this content is referred to as being a [_string_](). A string is simply a sequence of individual characters. In TidalCycles, double quoted strings are used to write _patterns_ using the mini-notation, and you may hear the phrase _pattern string_ from time to time. If you want to create a regular string and not a pattern, you can use single quotes, e.g. `'C minor'` will not be parsed as Mini Notation. -The good news is, that this covers 99% of the JavaScript syntax needed for Strudel! - -Let's now look at the way we can express [Rhythms](/learn/mini-notation)... +The good news is, that this covers most of the JavaScript syntax needed for Strudel!
diff --git a/website/src/pages/learn/factories.mdx b/website/src/pages/learn/factories.mdx index 46567e34..0d93fb21 100644 --- a/website/src/pages/learn/factories.mdx +++ b/website/src/pages/learn/factories.mdx @@ -1,12 +1,12 @@ --- -title: Pattern Constructors +title: Creating Patterns layout: ../../layouts/MainLayout.astro --- import { MiniRepl } from '../../docs/MiniRepl'; import { JsDoc } from '../../docs/JsDoc'; -# Pattern Constructors +# Creating Patterns The following functions will return a pattern. These are the equivalents used by the Mini Notation: diff --git a/website/src/pages/learn/metadata.mdx b/website/src/pages/learn/metadata.mdx new file mode 100644 index 00000000..27f46921 --- /dev/null +++ b/website/src/pages/learn/metadata.mdx @@ -0,0 +1,94 @@ +--- +title: Music metadata +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Music metadata + +You can optionally add some music metadata in your Strudel code, by using tags in code comments: + +```js +// @title Hey Hoo +// @by Sam Tagada +// @license CC BY-NC-SA +``` + +Like other comments, those are ignored by Strudel, but it can be used by other tools to retrieve some information about the music. + +It is for instance used by the [swatch tool](https://github.com/tidalcycles/strudel/tree/main/my-patterns) to display pattern titles in the [examples page](https://strudel.tidalcycles.org/examples/). + +## Alternative syntax + +You can also use comment blocks: + +```js +/* +@title Hey Hoo +@by Sam Tagada +@license CC BY-NC-SA +*/ +``` + +Or define multiple tags in one line: + +```js +// @title Hey Hoo @by Sam Tagada @license CC BY-NC-SA +``` + +The `title` tag has an alternative syntax using quotes (must be defined at the very begining): + +```js +// "Hey Hoo" @by Sam Tagada +``` + +## Tags list + +Available tags are: + +- `@title`: music title +- `@by`: music author(s), separated by comma, eventually followed with a link in `<>` (ex: `@by John Doe `) +- `@license`: music license(s) +- `@details`: some additional information about the music +- `@url`: web page(s) related to the music (git repo, soundcloud link, etc.) +- `@genre`: music genre(s) (pop, jazz, etc) +- `@album`: music album name + +## Multiple values + +Some of them accepts several values, using the comma or new line separator, or duplicating the tag: + +```js +/* +@by Sam Tagada + Jimmy +@genre pop, jazz +@url https://example.com +@url https://example.org +*/ +``` + +You can also add optional prefixes and use tags where you want: + +```js +/* +song @by Sam Tagada +samples @by Jimmy +*/ +... +note("a3 c#4 e4 a4") // @by Sandy +``` + +## Multiline + +If a tag doesn't accept a list, it can take multi-line values: + +```js +/* +@details I wrote this song in February 19th, 2023. + It was around midnight and I was lying on + the sofa in the living room. +*/ +``` diff --git a/website/src/pages/learn/mini-notation.mdx b/website/src/pages/learn/mini-notation.mdx index 79e81fc4..d0844f45 100644 --- a/website/src/pages/learn/mini-notation.mdx +++ b/website/src/pages/learn/mini-notation.mdx @@ -8,11 +8,17 @@ import { JsDoc } from '../../docs/JsDoc'; # Mini-notation -Similar to [Haskell Tidal Cycles](https://tidalcycles.org/docs/), Strudel has an "embedded mini-notation" (also called a [domain-specific language, or DSL](https://en.wikipedia.org/wiki/Domain-specific_language)) that is designed for writing rhythmic patterns using little amounts of text. -If you've seen any Tidal code before, you may have noticed the mini-notation and wondered what it's all about. -It's one of the main features of Tidal, and although it might look like a strange way to notate music and other patterns, you will soon see how powerful it can be. +Just like [Tidal Cycles](https://tidalcycles.org/), Strudel uses a so called "Mini-Notation", which is a custom language that is designed for writing rhythmic patterns using little amounts of text. -Before diving deeper into the details, here is a flavour of how the mini-notation looks like: +## Note + +This page just explains the entirety of the Mini-Notation syntax. +If you are just getting started with Strudel, you can learn the basics of the Mini-Notation in a more practical manner in the [workshop](http://localhost:3000/workshop/first-sounds). +After that, you can come back here if you want to understand every little detail. + +## Example + +Before diving deeper into the details, here is a flavour of how the Mini-Notation looks like: (c[1] || c[2] || '').trim()); + const tags = {}; + + const [prefix, title] = (comments[0] || '').split('"'); + if (prefix.trim() === '' && title !== undefined) { + tags['title'] = title; + } + + for (const comment of comments) { + const tag_matches = comment.split('@').slice(1); + for (const tag_match of tag_matches) { + let [tag, tag_value] = tag_match.split(/ (.*)/s); + tag = tag.trim(); + tag_value = (tag_value || '').replaceAll(/ +/g, ' ').trim(); + + if (ALLOW_MANY.includes(tag)) { + const tag_list = tag_value + .split(/[,\n]/) + .map((t) => t.trim()) + .filter((t) => t !== ''); + tags[tag] = tag in tags ? tags[tag].concat(tag_list) : tag_list; + } else { + tag_value = tag_value.replaceAll(/\s+/g, ' '); + tags[tag] = tag in tags ? tags[tag] + ' ' + tag_value : tag_value; + } + } + } + + return tags; +} diff --git a/website/src/pages/swatch/list.json.js b/website/src/pages/swatch/list.json.js index 6d86e384..4bf6bb4a 100644 --- a/website/src/pages/swatch/list.json.js +++ b/website/src/pages/swatch/list.json.js @@ -1,9 +1,11 @@ +import { getMetadata } from '../metadata_parser'; + export function getMyPatterns() { const my = import.meta.glob('../../../../my-patterns/**', { as: 'raw', eager: true }); return Object.fromEntries( - Object.entries(my) // - .filter(([name]) => name.endsWith('.txt')) // - .map(([name, raw]) => [name.split('/').slice(-1), raw]), // + Object.entries(my) + .filter(([name]) => name.endsWith('.txt')) + .map(([name, raw]) => [getMetadata(raw)['title'] || name.split('/').slice(-1), raw]), ); } diff --git a/website/src/pages/workshop/first-effects.mdx b/website/src/pages/workshop/first-effects.mdx new file mode 100644 index 00000000..e3ce5487 --- /dev/null +++ b/website/src/pages/workshop/first-effects.mdx @@ -0,0 +1,335 @@ +--- +title: First Effects +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import QA from '@components/QA'; + +# First Effects + +import Box from '@components/Box.astro'; + +We have sounds, we have notes, now let's look at effects! + +## Some basic effects + +**low-pass filter** + +/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? + + + + + +`delay("a:b:c")`: + +- a: delay volume +- b: delay time +- c: feedback (smaller number = quicker fade) + + + +**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 | | + +Let us now take a look at some of Tidal's typical [pattern effects](/workshop/pattern-effects). diff --git a/website/src/pages/workshop/first-notes.mdx b/website/src/pages/workshop/first-notes.mdx new file mode 100644 index 00000000..330dd072 --- /dev/null +++ b/website/src/pages/workshop/first-notes.mdx @@ -0,0 +1,390 @@ +--- +title: First Notes +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '@src/docs/MiniRepl'; +import { midi2note } from '@strudel.cycles/core/'; +import Box from '@components/Box.astro'; +import QA from '@components/QA'; + +# First Notes + +Let's look at how we can play notes + +## numbers and notes + +**play notes with numbers** + + [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) + + + +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 */} + + + +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). + +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/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx new file mode 100644 index 00000000..893a563d --- /dev/null +++ b/website/src/pages/workshop/first-sounds.mdx @@ -0,0 +1,330 @@ +--- +title: First Sounds +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '@src/docs/MiniRepl'; +import Box from '@components/Box.astro'; +import QA from '@components/QA'; + +# First Sounds + +This is the first chapter of the Strudel Workshop, nice to have you on board! + +## Code Fields + +The workshop is full of interactive code fields. Let's learn how to use those. Here is one: + + + + + +1. ⬆️ click into the text field above ⬆️ +2. press `ctrl`+`enter` to play +3. change `casio` to `metal` +4. press `ctrl`+`enter` to update +5. press `ctrl`+`.` to stop + + + +Congratulations, you are now live coding! + +## Sounds + +We have just played a sound with `sound` like this: + + + + + +`casio` is one of many standard sounds. + +Try out a few other sounds: + +``` +insect wind jazz metal east crow casio space numbers +``` + +You might hear a little pause while the sound is loading + + + +**Change Sample Number with :** + +One Sound can contain multiple samples (audio files). + +You can select the sample by appending `:` followed by a number to the name: + + + + + +Try different sound / sample number combinations. + +Not adding a number is like doing `:0` + + + +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 +- `sd` = **sd**are +- `rim` = **rim**shot +- `hh` = **h**i**h**at +- `oh` = **o**pen **h**ihat + +Try out different drum sounds! + + + +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` + +There are a lot more, but let's keep it simple for now + +🦥 Pro-Tip: Mark a name via double click. Then just copy and paste! + + + +## Sequences + +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. + + + +Try adding more sounds to the sequence! + + + +**The longer the sequence, the faster it runs** + + + +The content of a sequence will be squished into what's called a cycle. + +**One way to change the tempo is using `cpm`** + + + + + +cpm = cycles per minute + +By default, the tempo is 60 cycles per minute = 1 cycle per second. + + + +We will look at other ways to change the tempo later! + +**Add a rests in a sequence with '~'** + + + +**Sub-Sequences with [brackets]** + + + + + +Try adding more sounds inside a bracket! + + + +Similar to the whole sequence, the content of a sub-sequence will be squished to the its own length. + +**Multiplication: Speed things up** + + + +**Multiplication: Speed up sequences** + + + +**Multiplication: Speeeeeeeeed things up** + + + + + +Pitch = really fast rhythm + + + +**Sub-Sub-Sequences with [[brackets]]** + + + + + +You can go as deep as you want! + + + +**Play sequences in parallel with comma** + + + +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** + + + +**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. +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 | , | | + +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 | | +| n | select sample number | | + +## Examples + +**Basic rock beat** + + + +**Classic house** + + + + + +Notice that the two patterns are extremely similar. +Certain drum patterns are reused across genres. + + + +We Will Rock you + + + +**Yellow Magic Orchestra - Firecracker** + + + +**Imitation of a 16 step sequencer** + + + +**Another one** + + + +**Not your average drums** + + + +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/getting-started.mdx b/website/src/pages/workshop/getting-started.mdx new file mode 100644 index 00000000..66eecdce --- /dev/null +++ b/website/src/pages/workshop/getting-started.mdx @@ -0,0 +1,70 @@ +--- +title: Getting Started +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; + +# Welcome + + + +Welcome to the Strudel documentation pages! +You've come to the right place if you want to learn how to make music with code. + +## What is Strudel? + +With Strudel, you can expressively write dynamic music pieces.
+It is an official port of the [Tidal Cycles](https://tidalcycles.org/) pattern language to JavaScript.
+You don't need to know JavaScript or Tidal Cycles to make music with Strudel. +This interactive tutorial will guide you through the basics of Strudel.
+The best place to actually make music with Strudel is the [Strudel REPL](https://strudel.tidalcycles.org/) + +
+ +## What can you do with Strudel? + +- live code music: make music with code in real time +- algorithmic composition: compose music using tidal's unique approach to pattern manipulation +- teaching: focussing on a low barrier of entry, Strudel is a good fit for teaching music and code at the same time. +- integrate into your existing music setup: either via MIDI or OSC, you can use Strudel as a really flexible sequencer + +## Example + +Here is an example of how strudel can sound: + +],hh*8") + .speed(perlin.range(.8,.9)), // random sample speed variation + // bassline + "" + .off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps + .add(perlin.range(0,.5)) // random pitch variation + .superimpose(add(.05)) // add second, slightly detuned voice + .note() // wrap in "note" + .decay(.15).sustain(0) // make each note of equal length + .s('sawtooth') // waveform + .gain(.4) // turn down + .cutoff(sine.slow(7).range(300,5000)), // automate cutoff + // chords + ">".voicings('lefthand') + .superimpose(x=>x.add(.04)) // add second, slightly detuned voice + .add(perlin.range(0,.5)) // random pitch variation + .note() // wrap in "note" + .s('sawtooth') // waveform + .gain(.16) // turn down + .cutoff(500) // fixed cutoff + .attack(1) // slowly fade in +) +.slow(3/2)`} +/> + +To hear more, go to the [Strudel REPL](https://strudel.tidalcycles.org/) and press shuffle to hear a random example pattern. + +## Getting Started + +The best way to start learning Strudel is the workshop. +If you're ready to dive in, let's start with your [first sounds](/workshop/first-sounds) diff --git a/website/src/pages/workshop/index.astro b/website/src/pages/workshop/index.astro new file mode 100644 index 00000000..9f79e4c2 --- /dev/null +++ b/website/src/pages/workshop/index.astro @@ -0,0 +1,3 @@ + diff --git a/website/src/pages/workshop/pattern-effects.mdx b/website/src/pages/workshop/pattern-effects.mdx new file mode 100644 index 00000000..23346ccb --- /dev/null +++ b/website/src/pages/workshop/pattern-effects.mdx @@ -0,0 +1,181 @@ +--- +title: Pattern Effects +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '@src/docs/MiniRepl'; +import Box from '@components/Box.astro'; +import QA from '@components/QA'; + +# Pattern Effects + +Up until now, most of the functions we've seen are what other music programs are typically capable of: sequencing sounds, playing notes, controlling effects. + +In this chapter, we are going to look at functions that are more unique to tidal. + +**reverse patterns with rev** + + + +**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. + + + +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..fad14fb4 --- /dev/null +++ b/website/src/pages/workshop/recap.mdx @@ -0,0 +1,68 @@ +--- +title: Recap +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; + +# Workshop Recap + +This page is just a listing of all functions covered in the workshop! + +## Mini Notation + +| Concept | Syntax | Example | +| ----------------- | -------- | -------------------------------------------------------------------------------- | +| Sequence | space | | +| 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 | | + +## 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))`} /> | diff --git a/website/src/repl/Footer.jsx b/website/src/repl/Footer.jsx index 4b3399ad..30e2f3a1 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,8 @@ const fontFamilyOptions = { }; function SettingsTab({ scheduler }) { - const { theme, keybindings, fontSize, fontFamily } = useSettings(); + const { theme, keybindings, isLineNumbersDisplayed, isAutoCompletionEnabled, fontSize, fontFamily } = useSettings(); + return (
{/* @@ -397,13 +407,25 @@ 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} + /> + settingsMap.setKey('isAutoCompletionEnabled', cbEvent.target.checked)} + value={isAutoCompletionEnabled} + /> +