use vite for repl
This commit is contained in:
parent
0d63ec9146
commit
645ea30b53
39 changed files with 3230 additions and 42511 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -28,4 +28,5 @@ repl-parcel
|
||||||
mytunes.ts
|
mytunes.ts
|
||||||
doc
|
doc
|
||||||
out
|
out
|
||||||
.parcel-cache
|
.parcel-cache
|
||||||
|
repl_old
|
||||||
2161
package-lock.json
generated
2161
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,7 @@
|
||||||
"test": "npm run test --workspaces --if-present && cd repl && npm run test",
|
"test": "npm run test --workspaces --if-present && cd repl && npm run test",
|
||||||
"bootstrap": "lerna bootstrap",
|
"bootstrap": "lerna bootstrap",
|
||||||
"setup": "npm i && npm run bootstrap && cd repl && npm i",
|
"setup": "npm i && npm run bootstrap && cd repl && npm i",
|
||||||
"repl": "cd repl && npm run start",
|
"repl": "cd repl && npm run dev",
|
||||||
"osc": "cd packages/osc && npm run server",
|
"osc": "cd packages/osc && npm run server",
|
||||||
"build": "cd repl && npm run build",
|
"build": "cd repl && npm run build",
|
||||||
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
||||||
|
|
|
||||||
41
repl/.gitignore
vendored
41
repl/.gitignore
vendored
|
|
@ -1,25 +1,24 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# Logs
|
||||||
|
logs
|
||||||
# dependencies
|
*.log
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
.parcel-cache
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
# Strudel REPL
|
|
||||||
|
|
||||||
This is the REPL for Strudel. REPL stands for
|
|
||||||
|
|
||||||
- Read
|
|
||||||
- Evaluate
|
|
||||||
- Play!
|
|
||||||
- Loop
|
|
||||||
|
|
||||||
The REPL is deployed at [strudel.tidalcycles.org](https://strudel.tidalcycles.org/).
|
|
||||||
|
|
||||||
## Run REPL locally
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# from project root
|
|
||||||
npm run setup
|
|
||||||
npm run repl
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build REPL
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd repl
|
|
||||||
npm run build # <- builds repl + tutorial to ../docs
|
|
||||||
npm run static # <- test static build
|
|
||||||
```
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
|
|
||||||
Strudel - javascript-based environment for live coding algorithmic (musical) patterns
|
|
||||||
https://strudel.tidalcycles.org / https://github.com/tidalcycles/strudel/
|
|
||||||
|
|
||||||
Copyright (C) Strudel contributors
|
|
||||||
https://github.com/tidalcycles/strudel/graphs/contributors
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" type="image/svg+xml" href="/src/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="Strudel REPL" />
|
<meta name="description" content="Strudel REPL" />
|
||||||
<!-- TODO: add manifest images -->
|
<!-- TODO: add manifest images -->
|
||||||
|
|
@ -13,5 +13,6 @@
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
41269
repl/package-lock.json
generated
41269
repl/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,65 +1,24 @@
|
||||||
{
|
{
|
||||||
"name": "@strudel.cycles/repl",
|
"name": "repl2",
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"version": "0.0.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"dependencies": {
|
|
||||||
"@codemirror/lang-javascript": "^0.19.0",
|
|
||||||
"@testing-library/jest-dom": "^5.16.3",
|
|
||||||
"@testing-library/react": "^12.1.4",
|
|
||||||
"@testing-library/user-event": "^13.5.0",
|
|
||||||
"codemirror6-themes": "^0.1.2",
|
|
||||||
"events": "^3.3.0",
|
|
||||||
"gh-pages": "^3.2.3",
|
|
||||||
"react": "^17.0.2",
|
|
||||||
"react-codemirror6": "^1.1.0",
|
|
||||||
"react-dom": "^17.0.2",
|
|
||||||
"react-hook-inview": "^4.4.1",
|
|
||||||
"react-scripts": "5.0.0",
|
|
||||||
"tone": "^14.7.77",
|
|
||||||
"web-vitals": "^2.1.4"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"dev": "vite",
|
||||||
"build": "BUILD_PATH='../docs' react-scripts build && npm run build-tutorial && npm run add-license",
|
"build": "vite build",
|
||||||
"test": "mocha ./src/test --colors",
|
"preview": "vite preview"
|
||||||
"snapshot": "cd ./src/ && rm -f ./tunes.snapshot.mjs && node ./shoot.mjs > ./tunes.snapshot.mjs",
|
|
||||||
"eject": "react-scripts eject",
|
|
||||||
"tutorial": "parcel src/tutorial/index.html --no-cache",
|
|
||||||
"build-tutorial": "rm -rf ../docs/tutorial && parcel build src/tutorial/index.html --dist-dir ../docs/tutorial --public-url /tutorial --no-scope-hoist --no-cache",
|
|
||||||
"add-license": "cat etc/agpl-header.txt ../docs/static/js/*LICENSE.txt > /tmp/strudel-license.txt && cp /tmp/strudel-license.txt ../docs/static/js/*LICENSE.txt",
|
|
||||||
"predeploy": "npm run build",
|
|
||||||
"deploy": "gh-pages -d ../docs",
|
|
||||||
"static": "npx serve ../docs"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"dependencies": {
|
||||||
"extends": [
|
"react": "^17.0.2",
|
||||||
"react-app",
|
"react-dom": "^17.0.2"
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mdx-js/react": "^1.6.22",
|
|
||||||
"@parcel/transformer-mdx": "^2.3.1",
|
|
||||||
"@tailwindcss/typography": "^0.5.2",
|
"@tailwindcss/typography": "^0.5.2",
|
||||||
"autoprefixer": "^10.4.4",
|
"@types/react": "^17.0.2",
|
||||||
"parcel": "^2.3.1",
|
"@types/react-dom": "^17.0.2",
|
||||||
"postcss": "^8.4.12",
|
"@vitejs/plugin-react": "^1.3.0",
|
||||||
"process": "^0.11.10",
|
"autoprefixer": "^10.4.7",
|
||||||
"serve": "^13.0.2",
|
"postcss": "^8.4.13",
|
||||||
"tailwindcss": "^3.0.23"
|
"tailwindcss": "^3.0.24",
|
||||||
|
"vite": "^2.9.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
strudel.tidalcycles.org
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"short_name": "Strudel REPL",
|
|
||||||
"name": "Strudel REPL - Tidal Patterns in JavaScript",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
|
|
@ -4,15 +4,16 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import CodeMirror6, { setHighlights } from './CodeMirror6';
|
import CodeMirror6, { setHighlights } from '@strudel.cycles/react/src/components/CodeMirror6';
|
||||||
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import cx from './cx';
|
import cx from '@strudel.cycles/react/src/cx';
|
||||||
import logo from './logo.svg';
|
import logo from './logo.svg';
|
||||||
import playStatic from './static.mjs';
|
// import playStatic from './static.mjs';
|
||||||
import { getDefaultSynth } from '@strudel.cycles/tone';
|
import { getDefaultSynth } from '@strudel.cycles/tone';
|
||||||
import * as tunes from './tunes.mjs';
|
import * as tunes from './tunes.mjs';
|
||||||
import useRepl from './useRepl.mjs';
|
import useRepl from '@strudel.cycles/react/src/hooks/useRepl.mjs';
|
||||||
import { useWebMidi } from './useWebMidi';
|
import { useWebMidi } from '@strudel.cycles/react/src/hooks/useWebMidi.mjs';
|
||||||
|
import useHighlighting from '@strudel.cycles/react/src/hooks/useHighlighting';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
// eval stuff start
|
// eval stuff start
|
||||||
import { evaluate, extend } from '@strudel.cycles/eval';
|
import { evaluate, extend } from '@strudel.cycles/eval';
|
||||||
|
|
@ -39,7 +40,6 @@ import '@strudel.cycles/osc/osc.mjs';
|
||||||
import '@strudel.cycles/webaudio/webaudio.mjs';
|
import '@strudel.cycles/webaudio/webaudio.mjs';
|
||||||
import '@strudel.cycles/serial/serial.mjs';
|
import '@strudel.cycles/serial/serial.mjs';
|
||||||
import controls from '@strudel.cycles/core/controls.mjs';
|
import controls from '@strudel.cycles/core/controls.mjs';
|
||||||
import useHighlighting from './useHighlighting';
|
|
||||||
|
|
||||||
extend(
|
extend(
|
||||||
Tone,
|
Tone,
|
||||||
|
|
@ -254,11 +254,11 @@ function App() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
{!isEmbedded && (
|
{/* !isEmbedded && (
|
||||||
<button className="fixed right-4 bottom-2 z-[11]" onClick={() => playStatic(code)}>
|
<button className="fixed right-4 bottom-2 z-[11]" onClick={() => playStatic(code)}>
|
||||||
static
|
static
|
||||||
</button>
|
</button>
|
||||||
)}
|
) */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { CodeMirror as _CodeMirror } from 'react-codemirror6';
|
|
||||||
import { EditorView, Decoration } from '@codemirror/view';
|
|
||||||
import { StateField, StateEffect } from '@codemirror/state';
|
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
|
||||||
// import { materialPalenight } from 'codemirror6-themes';
|
|
||||||
import { materialPalenight } from './themes/material-palenight';
|
|
||||||
|
|
||||||
export const setHighlights = StateEffect.define();
|
|
||||||
const highlightField = StateField.define({
|
|
||||||
create() {
|
|
||||||
return Decoration.none;
|
|
||||||
},
|
|
||||||
update(highlights, tr) {
|
|
||||||
try {
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(setHighlights)) {
|
|
||||||
highlights = Decoration.set(
|
|
||||||
e.value
|
|
||||||
.flatMap((hap) =>
|
|
||||||
(hap.context.locations || []).map(({ start, end }) => {
|
|
||||||
const color = hap.context.color || '#FFCA28';
|
|
||||||
let from = tr.newDoc.line(start.line).from + start.column;
|
|
||||||
let to = tr.newDoc.line(end.line).from + end.column;
|
|
||||||
const l = tr.newDoc.length;
|
|
||||||
if (from > l || to > l) {
|
|
||||||
return; // dont mark outside of range, as it will throw an error
|
|
||||||
}
|
|
||||||
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
|
|
||||||
return mark.range(from, to);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.filter(Boolean),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return highlights;
|
|
||||||
} catch (err) {
|
|
||||||
// console.warn('highlighting error', err);
|
|
||||||
return highlights;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
provide: (f) => EditorView.decorations.from(f),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<_CodeMirror
|
|
||||||
onViewChange={onViewChanged}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
flex: '1 0 auto',
|
|
||||||
}}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
extensions={[
|
|
||||||
javascript(),
|
|
||||||
materialPalenight,
|
|
||||||
highlightField,
|
|
||||||
// theme, language, ...
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let parenMark;
|
|
||||||
export const markParens = (editor, data) => {
|
|
||||||
const v = editor.getDoc().getValue();
|
|
||||||
const marked = getCurrentParenArea(v, data);
|
|
||||||
parenMark?.clear();
|
|
||||||
parenMark = editor.getDoc().markText(...marked, { css: 'background-color: #00007720' }); //
|
|
||||||
};
|
|
||||||
|
|
||||||
// returns { line, ch } from absolute character offset
|
|
||||||
export function offsetToPosition(offset, code) {
|
|
||||||
const lines = code.split('\n');
|
|
||||||
let line = 0;
|
|
||||||
let ch = 0;
|
|
||||||
for (let i = 0; i < offset; i++) {
|
|
||||||
if (ch === lines[line].length) {
|
|
||||||
line++;
|
|
||||||
ch = 0;
|
|
||||||
} else {
|
|
||||||
ch++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { line, ch };
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns absolute character offset from { line, ch }
|
|
||||||
export function positionToOffset(position, code) {
|
|
||||||
const lines = code.split('\n');
|
|
||||||
if (position.line > lines.length) {
|
|
||||||
// throw new Error('positionToOffset: position.line > lines.length');
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let offset = 0;
|
|
||||||
for (let i = 0; i < position.line; i++) {
|
|
||||||
offset += lines[i].length + 1;
|
|
||||||
}
|
|
||||||
offset += position.ch;
|
|
||||||
return offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
// given code and caret position, the functions returns the indices of the parens we are in
|
|
||||||
export function getCurrentParenArea(code, caretPosition) {
|
|
||||||
const caret = positionToOffset(caretPosition, code);
|
|
||||||
let open, i, begin, end;
|
|
||||||
// walk left
|
|
||||||
i = caret;
|
|
||||||
open = 0;
|
|
||||||
while (i > 0) {
|
|
||||||
if (code[i - 1] === '(') {
|
|
||||||
open--;
|
|
||||||
} else if (code[i - 1] === ')') {
|
|
||||||
open++;
|
|
||||||
}
|
|
||||||
if (open === -1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
begin = i;
|
|
||||||
// walk right
|
|
||||||
i = caret;
|
|
||||||
open = 0;
|
|
||||||
while (i < code.length) {
|
|
||||||
if (code[i] === '(') {
|
|
||||||
open--;
|
|
||||||
} else if (code[i] === ')') {
|
|
||||||
open++;
|
|
||||||
}
|
|
||||||
if (open === 1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
end = i;
|
|
||||||
return [begin, end].map((o) => offsetToPosition(o, code));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
export const markEvent = (editor) => (time, event) => {
|
|
||||||
const locs = event.context.locations;
|
|
||||||
if (!locs || !editor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const col = event.context?.color || '#FFCA28';
|
|
||||||
// mark active event
|
|
||||||
const marks = locs.map(({ start, end }) =>
|
|
||||||
editor.getDoc().markText(
|
|
||||||
{ line: start.line - 1, ch: start.column },
|
|
||||||
{ line: end.line - 1, ch: end.column },
|
|
||||||
//{ css: 'background-color: #FFCA28; color: black' } // background-color is now used by parent marking
|
|
||||||
{ css: 'outline: 1px solid ' + col + '; box-sizing:border-box' },
|
|
||||||
//{ css: `background-color: ${col};border-radius:5px` },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
//Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler...
|
|
||||||
setTimeout(() => {
|
|
||||||
marks.forEach((mark) => mark.clear());
|
|
||||||
// }, '+' + event.duration * 0.5);
|
|
||||||
}, event.duration * 1000);
|
|
||||||
}; */
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
/*
|
|
||||||
cx.js - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/cx.js>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function cx(...classes) { // : Array<string | undefined>
|
|
||||||
return classes.filter(Boolean).join(' ');
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
index.js - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/index.js>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import './index.css';
|
|
||||||
import App from './App';
|
|
||||||
import reportWebVitals from './reportWebVitals';
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
|
||||||
document.getElementById('root')
|
|
||||||
);
|
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
||||||
reportWebVitals();
|
|
||||||
11
repl/src/main.jsx
Normal file
11
repl/src/main.jsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('root'),
|
||||||
|
);
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
/*
|
|
||||||
oldtunes.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/oldtunes.mjs>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const timeCatMini = `stack(
|
|
||||||
"c3@3 [eb3, g3, [c4 d4]/2]",
|
|
||||||
"c2 g2",
|
|
||||||
"[eb4@5 [f4 eb4 d4]@3] [eb4 c4]/2".slow(8)
|
|
||||||
)`;
|
|
||||||
|
|
||||||
export const timeCat = `stack(
|
|
||||||
timeCat([3, c3], [1, stack(eb3, g3, cat(c4, d4).slow(2))]),
|
|
||||||
cat(c2, g2),
|
|
||||||
sequence(
|
|
||||||
timeCat([5, eb4], [3, cat(f4, eb4, d4)]),
|
|
||||||
cat(eb4, c4).slow(2)
|
|
||||||
).slow(4)
|
|
||||||
)`;
|
|
||||||
|
|
||||||
export const shapeShifted = `stack(
|
|
||||||
sequence(
|
|
||||||
e5, [b4, c5], d5, [c5, b4],
|
|
||||||
a4, [a4, c5], e5, [d5, c5],
|
|
||||||
b4, [r, c5], d5, e5,
|
|
||||||
c5, a4, a4, r,
|
|
||||||
[r, d5], [r, f5], a5, [g5, f5],
|
|
||||||
e5, [r, c5], e5, [d5, c5],
|
|
||||||
b4, [b4, c5], d5, e5,
|
|
||||||
c5, a4, a4, r,
|
|
||||||
).rev(),
|
|
||||||
sequence(
|
|
||||||
e2, e3, e2, e3, e2, e3, e2, e3,
|
|
||||||
a2, a3, a2, a3, a2, a3, a2, a3,
|
|
||||||
gs2, gs3, gs2, gs3, e2, e3, e2, e3,
|
|
||||||
a2, a3, a2, a3, a2, a3, b1, c2,
|
|
||||||
d2, d3, d2, d3, d2, d3, d2, d3,
|
|
||||||
c2, c3, c2, c3, c2, c3, c2, c3,
|
|
||||||
b1, b2, b1, b2, e2, e3, e2, e3,
|
|
||||||
a1, a2, a1, a2, a1, a2, a1, a2,
|
|
||||||
).rev()
|
|
||||||
).slow(16)`;
|
|
||||||
|
|
||||||
export const tetrisWithFunctions = `stack(sequence(
|
|
||||||
'e5', sequence('b4', 'c5'), 'd5', sequence('c5', 'b4'),
|
|
||||||
'a4', sequence('a4', 'c5'), 'e5', sequence('d5', 'c5'),
|
|
||||||
'b4', sequence(silence, 'c5'), 'd5', 'e5',
|
|
||||||
'c5', 'a4', 'a4', silence,
|
|
||||||
sequence(silence, 'd5'), sequence(silence, 'f5'), 'a5', sequence('g5', 'f5'),
|
|
||||||
'e5', sequence(silence, 'c5'), 'e5', sequence('d5', 'c5'),
|
|
||||||
'b4', sequence('b4', 'c5'), 'd5', 'e5',
|
|
||||||
'c5', 'a4', 'a4', silence),
|
|
||||||
sequence(
|
|
||||||
'e2', 'e3', 'e2', 'e3', 'e2', 'e3', 'e2', 'e3',
|
|
||||||
'a2', 'a3', 'a2', 'a3', 'a2', 'a3', 'a2', 'a3',
|
|
||||||
'g#2', 'g#3', 'g#2', 'g#3', 'e2', 'e3', 'e2', 'e3',
|
|
||||||
'a2', 'a3', 'a2', 'a3', 'a2', 'a3', 'b1', 'c2',
|
|
||||||
'd2', 'd3', 'd2', 'd3', 'd2', 'd3', 'd2', 'd3',
|
|
||||||
'c2', 'c3', 'c2', 'c3', 'c2', 'c3', 'c2', 'c3',
|
|
||||||
'b1', 'b2', 'b1', 'b2', 'e2', 'e3', 'e2', 'e3',
|
|
||||||
'a1', 'a2', 'a1', 'a2', 'a1', 'a2', 'a1', 'a2',
|
|
||||||
)
|
|
||||||
).slow(16)`;
|
|
||||||
|
|
||||||
export const tetris = `stack(
|
|
||||||
cat(
|
|
||||||
"e5 [b4 c5] d5 [c5 b4]",
|
|
||||||
"a4 [a4 c5] e5 [d5 c5]",
|
|
||||||
"b4 [~ c5] d5 e5",
|
|
||||||
"c5 a4 a4 ~",
|
|
||||||
"[~ d5] [~ f5] a5 [g5 f5]",
|
|
||||||
"e5 [~ c5] e5 [d5 c5]",
|
|
||||||
"b4 [b4 c5] d5 e5",
|
|
||||||
"c5 a4 a4 ~"
|
|
||||||
),
|
|
||||||
cat(
|
|
||||||
"e2 e3 e2 e3 e2 e3 e2 e3",
|
|
||||||
"a2 a3 a2 a3 a2 a3 a2 a3",
|
|
||||||
"g#2 g#3 g#2 g#3 e2 e3 e2 e3",
|
|
||||||
"a2 a3 a2 a3 a2 a3 b1 c2",
|
|
||||||
"d2 d3 d2 d3 d2 d3 d2 d3",
|
|
||||||
"c2 c3 c2 c3 c2 c3 c2 c3",
|
|
||||||
"b1 b2 b1 b2 e2 e3 e2 e3",
|
|
||||||
"a1 a2 a1 a2 a1 a2 a1 a2",
|
|
||||||
)
|
|
||||||
).slow(16)`;
|
|
||||||
|
|
||||||
export const tetrisMini = `\`[[e5 [b4 c5] d5 [c5 b4]]
|
|
||||||
[a4 [a4 c5] e5 [d5 c5]]
|
|
||||||
[b4 [~ c5] d5 e5]
|
|
||||||
[c5 a4 a4 ~]
|
|
||||||
[[~ d5] [~ f5] a5 [g5 f5]]
|
|
||||||
[e5 [~ c5] e5 [d5 c5]]
|
|
||||||
[b4 [b4 c5] d5 e5]
|
|
||||||
[c5 a4 a4 ~]],
|
|
||||||
[[e2 e3]*4]
|
|
||||||
[[a2 a3]*4]
|
|
||||||
[[g#2 g#3]*2 [e2 e3]*2]
|
|
||||||
[a2 a3 a2 a3 a2 a3 b1 c2]
|
|
||||||
[[d2 d3]*4]
|
|
||||||
[[c2 c3]*4]
|
|
||||||
[[b1 b2]*2 [e2 e3]*2]
|
|
||||||
[[a1 a2]*4]\`.slow(16)
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const whirlyStrudel = `sequence(e4, [b2, b3], c4)
|
|
||||||
.every(4, fast(2))
|
|
||||||
.every(3, slow(1.5))
|
|
||||||
.fast(slowcat(1.25, 1, 1.5))
|
|
||||||
.every(2, _ => sequence(e4, r, e3, d4, r))`;
|
|
||||||
|
|
||||||
export const giantSteps = `stack(
|
|
||||||
// melody
|
|
||||||
cat(
|
|
||||||
"[F#5 D5] [B4 G4] Bb4 [B4 A4]",
|
|
||||||
"[D5 Bb4] [G4 Eb4] F#4 [G4 F4]",
|
|
||||||
"Bb4 [B4 A4] D5 [D#5 C#5]",
|
|
||||||
"F#5 [G5 F5] Bb5 [F#5 F#5]",
|
|
||||||
),
|
|
||||||
// chords
|
|
||||||
cat(
|
|
||||||
"[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]",
|
|
||||||
"[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]",
|
|
||||||
"Eb^7 [Am7 D7] G^7 [C#m7 F#7]",
|
|
||||||
"B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]"
|
|
||||||
).voicings(['E3', 'G4']),
|
|
||||||
// bass
|
|
||||||
cat(
|
|
||||||
"[B2 D2] [G2 Bb2] [Eb2 Bb3] [A2 D2]",
|
|
||||||
"[G2 Bb2] [Eb2 F#2] [B2 F#2] [F2 Bb2]",
|
|
||||||
"[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]",
|
|
||||||
"[B2 F#2] [F2 Bb2] [Eb2 Bb3] [C#2 F#2]"
|
|
||||||
)
|
|
||||||
).slow(20);`;
|
|
||||||
|
|
||||||
export const transposedChordsHacked = `stack(
|
|
||||||
"c2 eb2 g2",
|
|
||||||
"Cm7".voicings(['g2','c4']).slow(2)
|
|
||||||
).transpose(
|
|
||||||
slowcat(1, 2, 3, 2).slow(2)
|
|
||||||
).transpose(5)`;
|
|
||||||
|
|
||||||
export const scaleTranspose = `stack(f2, f3, c4, ab4)
|
|
||||||
.scale(sequence('F minor', 'F harmonic minor').slow(4))
|
|
||||||
.scaleTranspose(sequence(0, -1, -2, -3).slow(4))
|
|
||||||
.transpose(sequence(0, 1).slow(16))`;
|
|
||||||
|
|
||||||
export const struct = `stack(
|
|
||||||
"c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]",
|
|
||||||
"[C^7 A7] [Dm7 G7]".struct("[x@2 x] [~@2 x] [~ x@2]@2 [x ~@2] ~ [~@2 x@4]@2")
|
|
||||||
.voicings(['G3','A4'])
|
|
||||||
).slow(4)`;
|
|
||||||
|
|
||||||
export const magicSofa = `stack(
|
|
||||||
"<C^7 F^7 ~> <Dm7 G7 A7 ~>"
|
|
||||||
.every(2, fast(2))
|
|
||||||
.voicings(),
|
|
||||||
"<c2 f2 g2> <d2 g2 a2 e2>"
|
|
||||||
).slow(1).transpose.slowcat(0, 2, 3, 4)`;
|
|
||||||
|
|
||||||
|
|
||||||
export const primalEnemy = `()=>{
|
|
||||||
const f = fast("<1 <2 [4 8]>>");
|
|
||||||
return stack(
|
|
||||||
"c3,g3,c4".struct("[x ~]*2").apply(f).transpose("<0 <3 [5 [7 [9 [11 13]]]]>>"),
|
|
||||||
"c2 [c2 ~]*2".tone(synth(osc('sawtooth8')).chain(vol(0.8),out())),
|
|
||||||
"c1*2".tone(membrane().chain(vol(0.8),out()))
|
|
||||||
).slow(1)
|
|
||||||
}`;
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
reportWebVitals.js - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/reportWebVitals.js>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const reportWebVitals = onPerfEntry => {
|
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
||||||
getCLS(onPerfEntry);
|
|
||||||
getFID(onPerfEntry);
|
|
||||||
getFCP(onPerfEntry);
|
|
||||||
getLCP(onPerfEntry);
|
|
||||||
getTTFB(onPerfEntry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reportWebVitals;
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
// this file contains a runtime scope for testing all the tunes
|
|
||||||
// it mocks all the functions that won't work in node (who are not important for testing values / structure)
|
|
||||||
// it might require mocking more stuff when tunes added that use other functions
|
|
||||||
|
|
||||||
// import * as tunes from './tunes.mjs';
|
|
||||||
import { evaluate } from '@strudel.cycles/eval';
|
|
||||||
import { extend } from '@strudel.cycles/eval';
|
|
||||||
import * as strudel from '@strudel.cycles/core';
|
|
||||||
// import gist from '@strudel.cycles/core/gist.js';
|
|
||||||
import { mini } from '@strudel.cycles/mini/mini.mjs';
|
|
||||||
// import { Tone } from '@strudel.cycles/tone';
|
|
||||||
// import * as toneHelpers from '@strudel.cycles/tone/tone.mjs';
|
|
||||||
// import * as voicingHelpers from '@strudel.cycles/tonal/voicings.mjs';
|
|
||||||
// import * as uiHelpers from '@strudel.cycles/tone/ui.mjs';
|
|
||||||
// import * as drawHelpers from '@strudel.cycles/tone/draw.mjs';
|
|
||||||
// import euclid from '@strudel.cycles/core/euclid.mjs';
|
|
||||||
// import '@strudel.cycles/tone/tone.mjs';
|
|
||||||
// import '@strudel.cycles/midi/midi.mjs';
|
|
||||||
import '@strudel.cycles/tonal/voicings.mjs';
|
|
||||||
import '@strudel.cycles/tonal/tonal.mjs';
|
|
||||||
import '@strudel.cycles/xen/xen.mjs';
|
|
||||||
// import '@strudel.cycles/xen/tune.mjs';
|
|
||||||
// import '@strudel.cycles/core/euclid.mjs';
|
|
||||||
// import '@strudel.cycles/core/speak.mjs'; // window is not defined
|
|
||||||
// import '@strudel.cycles/tone/pianoroll.mjs';
|
|
||||||
// import '@strudel.cycles/tone/draw.mjs';
|
|
||||||
// import '@strudel.cycles/osc/osc.mjs';
|
|
||||||
// import '@strudel.cycles/webaudio/webaudio.mjs';
|
|
||||||
// import '@strudel.cycles/serial/serial.mjs';
|
|
||||||
// import controls from '@strudel.cycles/core/controls.mjs';
|
|
||||||
|
|
||||||
class MockedNode {
|
|
||||||
chain() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
connect() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
toDestination() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
set() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
start() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockNode = () => new MockedNode();
|
|
||||||
|
|
||||||
const id = (x) => x;
|
|
||||||
|
|
||||||
const toneHelpersMocked = {
|
|
||||||
FeedbackDelay: MockedNode,
|
|
||||||
MembraneSynth: MockedNode,
|
|
||||||
NoiseSynth: MockedNode,
|
|
||||||
MetalSynth: MockedNode,
|
|
||||||
Synth: MockedNode,
|
|
||||||
PolySynth: MockedNode,
|
|
||||||
Chorus: MockedNode,
|
|
||||||
Freeverb: MockedNode,
|
|
||||||
Gain: MockedNode,
|
|
||||||
vol: mockNode,
|
|
||||||
out: id,
|
|
||||||
osc: id,
|
|
||||||
adsr: id,
|
|
||||||
getDestination: id,
|
|
||||||
players: mockNode,
|
|
||||||
sampler: mockNode,
|
|
||||||
synth: mockNode,
|
|
||||||
piano: mockNode,
|
|
||||||
polysynth: mockNode,
|
|
||||||
fmsynth: mockNode,
|
|
||||||
membrane: mockNode,
|
|
||||||
noise: mockNode,
|
|
||||||
metal: mockNode,
|
|
||||||
lowpass: mockNode,
|
|
||||||
highpass: mockNode,
|
|
||||||
};
|
|
||||||
|
|
||||||
// tone mock
|
|
||||||
strudel.Pattern.prototype.tone = function () {
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
// draw mock
|
|
||||||
strudel.Pattern.prototype.pianoroll = function () {
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
// speak mock
|
|
||||||
strudel.Pattern.prototype.speak = function () {
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
// webaudio mock
|
|
||||||
strudel.Pattern.prototype.wave = function () {
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
strudel.Pattern.prototype.filter = function () {
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
strudel.Pattern.prototype.adsr = function () {
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
strudel.Pattern.prototype.out = function () {
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
// tune mock
|
|
||||||
strudel.Pattern.prototype.tune = function () {
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
const uiHelpersMocked = {
|
|
||||||
backgroundImage: id,
|
|
||||||
};
|
|
||||||
|
|
||||||
extend(
|
|
||||||
// Tone,
|
|
||||||
strudel,
|
|
||||||
strudel.Pattern.prototype.bootstrap(),
|
|
||||||
toneHelpersMocked,
|
|
||||||
uiHelpersMocked,
|
|
||||||
/* controls,
|
|
||||||
toneHelpers,
|
|
||||||
voicingHelpers,
|
|
||||||
drawHelpers,
|
|
||||||
uiHelpers,
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
// gist,
|
|
||||||
// euclid,
|
|
||||||
mini,
|
|
||||||
// Tone,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const queryCode = async (code, cycles = 1) => {
|
|
||||||
const { pattern } = await evaluate(code);
|
|
||||||
const haps = pattern.queryArc(0, cycles);
|
|
||||||
return haps.map((h) => h.showWhole());
|
|
||||||
};
|
|
||||||
|
|
||||||
export const testCycles = {
|
|
||||||
timeCatMini: 16,
|
|
||||||
timeCat: 8,
|
|
||||||
shapeShifted: 16,
|
|
||||||
tetrisMini: 16,
|
|
||||||
whirlyStrudel: 16,
|
|
||||||
swimming: 51,
|
|
||||||
giantSteps: 20,
|
|
||||||
giantStepsReggae: 25,
|
|
||||||
transposedChordsHacked: 8,
|
|
||||||
scaleTranspose: 16,
|
|
||||||
struct: 4,
|
|
||||||
magicSofa: 8,
|
|
||||||
confusedPhone: 8,
|
|
||||||
zeldasRescue: 48,
|
|
||||||
technoDrums: 4,
|
|
||||||
caverave: 60,
|
|
||||||
callcenterhero: 22,
|
|
||||||
primalEnemy: 4,
|
|
||||||
synthDrums: 4,
|
|
||||||
sampleDrums: 4,
|
|
||||||
xylophoneCalling: 60,
|
|
||||||
sowhatelse: 60,
|
|
||||||
barryHarris: 64,
|
|
||||||
wavyKalimba: 64,
|
|
||||||
jemblung: 12,
|
|
||||||
risingEnemy: 12,
|
|
||||||
festivalOfFingers: 16,
|
|
||||||
festivalOfFingers2: 22,
|
|
||||||
undergroundPlumber: 20,
|
|
||||||
bridgeIsOver: 16,
|
|
||||||
goodTimes: 16,
|
|
||||||
echoPiano: 8,
|
|
||||||
sml1: 48,
|
|
||||||
speakerman: 48,
|
|
||||||
randomBells: 24,
|
|
||||||
waa: 16,
|
|
||||||
waar: 16,
|
|
||||||
hyperpop: 60,
|
|
||||||
festivalOfFingers3: 16,
|
|
||||||
};
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
/*
|
|
||||||
setupTests.js - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/setupTests.js>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
||||||
// allows you to do things like:
|
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
// this script will render all example tunes and log them to the console.
|
|
||||||
// it is intended to be written to tunes.snapshot.mjs using `npm run snapshot`
|
|
||||||
|
|
||||||
import * as tunes from './tunes.mjs';
|
|
||||||
import { queryCode, testCycles } from './runtime.mjs';
|
|
||||||
|
|
||||||
Object.entries(tunes).forEach(([key, code]) => {
|
|
||||||
queryCode(code, testCycles[key] || 1).then((haps) => {
|
|
||||||
console.log(`export const ${key} = ${JSON.stringify(haps)}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
static.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/static.mjs>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Tone } from '@strudel.cycles/tone';
|
|
||||||
import { State, TimeSpan } from '@strudel.cycles/core';
|
|
||||||
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
|
|
||||||
import { evaluate } from '@strudel.cycles/eval';
|
|
||||||
import { getDefaultSynth } from '@strudel.cycles/tone';
|
|
||||||
|
|
||||||
const defaultSynth = getDefaultSynth();
|
|
||||||
|
|
||||||
// this is a test to play back events with as less runtime code as possible..
|
|
||||||
// the code asks for the number of seconds to prequery
|
|
||||||
// after the querying is done, the events are scheduled
|
|
||||||
// after the scheduling is done, the transport is started
|
|
||||||
// nothing happens while tone.js runs except the schedule callback, which is a thin wrapper around triggerAttackRelease calls
|
|
||||||
// so all glitches that appear here should have nothing to do with strudel and or the repl
|
|
||||||
|
|
||||||
async function playStatic(code) {
|
|
||||||
Tone.getTransport().cancel();
|
|
||||||
Tone.getTransport().stop();
|
|
||||||
let start, took;
|
|
||||||
const seconds = Number(prompt('How many seconds to run?')) || 60;
|
|
||||||
start = performance.now();
|
|
||||||
console.log('evaluating..');
|
|
||||||
const { pattern: pat } = await evaluate(code);
|
|
||||||
took = performance.now() - start;
|
|
||||||
console.log('evaluate took', took, 'ms');
|
|
||||||
console.log('querying..');
|
|
||||||
start = performance.now();
|
|
||||||
const events = pat
|
|
||||||
?.query(new State(new TimeSpan(0, seconds)))
|
|
||||||
?.filter((event) => event.part.begin.equals(event.whole.begin))
|
|
||||||
?.map((event) => ({
|
|
||||||
time: event.whole.begin.valueOf(),
|
|
||||||
duration: event.whole.end.sub(event.whole.begin).valueOf(),
|
|
||||||
value: event.value,
|
|
||||||
context: event.context,
|
|
||||||
}));
|
|
||||||
took = performance.now() - start;
|
|
||||||
console.log('query took', took, 'ms');
|
|
||||||
console.log('scheduling..');
|
|
||||||
start = performance.now();
|
|
||||||
events.forEach((event) => {
|
|
||||||
Tone.getTransport().schedule((time) => {
|
|
||||||
try {
|
|
||||||
const { onTrigger, velocity } = event.context;
|
|
||||||
if (!onTrigger) {
|
|
||||||
if (defaultSynth) {
|
|
||||||
const note = getPlayableNoteValue(event);
|
|
||||||
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
|
||||||
} else {
|
|
||||||
throw new Error('no defaultSynth passed to useRepl.');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onTrigger(time, event);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(err);
|
|
||||||
err.message = 'unplayable event: ' + err?.message;
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}, event.time);
|
|
||||||
});
|
|
||||||
took = performance.now() - start;
|
|
||||||
console.log('scheduling took', took, 'ms');
|
|
||||||
console.log('now starting!');
|
|
||||||
|
|
||||||
Tone.getTransport().start('+0.5');
|
|
||||||
}
|
|
||||||
|
|
||||||
export default playStatic;
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { queryCode, testCycles } from '../runtime.mjs';
|
|
||||||
import * as snaps from '../tunes.snapshot.mjs';
|
|
||||||
import * as tunes from '../tunes.mjs';
|
|
||||||
import { strict as assert } from 'assert';
|
|
||||||
|
|
||||||
async function testTune(key) {
|
|
||||||
// console.log('test tune', key);
|
|
||||||
const haps = await queryCode(tunes[key], testCycles[key] || 1);
|
|
||||||
assert.deepStrictEqual(haps, snaps[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('renders tunes', () => {
|
|
||||||
Object.keys(tunes).forEach((key) => {
|
|
||||||
it(`tune: ${key}`, async () => {
|
|
||||||
await testTune(key);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
import { EditorView } from '@codemirror/view';
|
|
||||||
import { HighlightStyle, tags as t } from '@codemirror/highlight';
|
|
||||||
|
|
||||||
/*
|
|
||||||
Credits for color palette:
|
|
||||||
|
|
||||||
Author: Mattia Astorino (http://github.com/equinusocio)
|
|
||||||
Website: https://material-theme.site/
|
|
||||||
*/
|
|
||||||
|
|
||||||
const ivory = '#abb2bf',
|
|
||||||
stone = '#7d8799', // Brightened compared to original to increase contrast
|
|
||||||
invalid = '#ffffff',
|
|
||||||
darkBackground = '#21252b',
|
|
||||||
highlightBackground = 'rgba(0, 0, 0, 0.5)',
|
|
||||||
// background = '#292d3e',
|
|
||||||
background = 'transparent',
|
|
||||||
tooltipBackground = '#353a42',
|
|
||||||
selection = 'rgba(128, 203, 196, 0.2)',
|
|
||||||
cursor = '#ffcc00';
|
|
||||||
|
|
||||||
/// The editor theme styles for Material Palenight.
|
|
||||||
export const materialPalenightTheme = EditorView.theme(
|
|
||||||
{
|
|
||||||
// done
|
|
||||||
'&': {
|
|
||||||
color: '#ffffff',
|
|
||||||
backgroundColor: background,
|
|
||||||
fontSize: '15px',
|
|
||||||
'z-index': 11,
|
|
||||||
},
|
|
||||||
|
|
||||||
// done
|
|
||||||
'.cm-content': {
|
|
||||||
caretColor: cursor,
|
|
||||||
lineHeight: '22px',
|
|
||||||
},
|
|
||||||
'.cm-line': {
|
|
||||||
background: '#2C323699',
|
|
||||||
},
|
|
||||||
// done
|
|
||||||
'&.cm-focused .cm-cursor': {
|
|
||||||
borderLeftColor: cursor,
|
|
||||||
},
|
|
||||||
|
|
||||||
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
|
|
||||||
backgroundColor: selection,
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-panels': { backgroundColor: darkBackground, color: '#ffffff' },
|
|
||||||
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
|
||||||
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
|
||||||
|
|
||||||
// done, use onedarktheme
|
|
||||||
'.cm-searchMatch': {
|
|
||||||
backgroundColor: '#72a1ff59',
|
|
||||||
outline: '1px solid #457dff',
|
|
||||||
},
|
|
||||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
|
||||||
backgroundColor: '#6199ff2f',
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-activeLine': { backgroundColor: highlightBackground },
|
|
||||||
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
|
||||||
|
|
||||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
|
||||||
backgroundColor: '#bad0f847',
|
|
||||||
outline: '1px solid #515a6b',
|
|
||||||
},
|
|
||||||
|
|
||||||
// done
|
|
||||||
'.cm-gutters': {
|
|
||||||
background: '#2C323699',
|
|
||||||
color: '#676e95',
|
|
||||||
border: 'none',
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-activeLineGutter': {
|
|
||||||
backgroundColor: highlightBackground,
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-foldPlaceholder': {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
color: '#ddd',
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-tooltip': {
|
|
||||||
border: 'none',
|
|
||||||
backgroundColor: tooltipBackground,
|
|
||||||
},
|
|
||||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
|
||||||
borderTopColor: 'transparent',
|
|
||||||
borderBottomColor: 'transparent',
|
|
||||||
},
|
|
||||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
|
||||||
borderTopColor: tooltipBackground,
|
|
||||||
borderBottomColor: tooltipBackground,
|
|
||||||
},
|
|
||||||
'.cm-tooltip-autocomplete': {
|
|
||||||
'& > ul > li[aria-selected]': {
|
|
||||||
backgroundColor: highlightBackground,
|
|
||||||
color: ivory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ dark: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
/// The highlighting style for code in the Material Palenight theme.
|
|
||||||
export const materialPalenightHighlightStyle = HighlightStyle.define([
|
|
||||||
{ tag: t.keyword, color: '#c792ea' },
|
|
||||||
{ tag: t.operator, color: '#89ddff' },
|
|
||||||
{ tag: t.special(t.variableName), color: '#eeffff' },
|
|
||||||
{ tag: t.typeName, color: '#f07178' },
|
|
||||||
{ tag: t.atom, color: '#f78c6c' },
|
|
||||||
{ tag: t.number, color: '#ff5370' },
|
|
||||||
{ tag: t.definition(t.variableName), color: '#82aaff' },
|
|
||||||
{ tag: t.string, color: '#c3e88d' },
|
|
||||||
{ tag: t.special(t.string), color: '#f07178' },
|
|
||||||
{ tag: t.comment, color: stone },
|
|
||||||
{ tag: t.variableName, color: '#f07178' },
|
|
||||||
{ tag: t.tagName, color: '#ff5370' },
|
|
||||||
{ tag: t.bracket, color: '#a2a1a4' },
|
|
||||||
{ tag: t.meta, color: '#ffcb6b' },
|
|
||||||
{ tag: t.attributeName, color: '#c792ea' },
|
|
||||||
{ tag: t.propertyName, color: '#c792ea' },
|
|
||||||
{ tag: t.className, color: '#decb6b' },
|
|
||||||
{ tag: t.invalid, color: invalid },
|
|
||||||
]);
|
|
||||||
|
|
||||||
/// Extension to enable the Material Palenight theme (both the editor theme and
|
|
||||||
/// the highlight style).
|
|
||||||
// : Extension
|
|
||||||
export const materialPalenight = [materialPalenightTheme, materialPalenightHighlightStyle];
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,124 +0,0 @@
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { Tone } from '@strudel.cycles/tone';
|
|
||||||
import useRepl from '../useRepl.mjs';
|
|
||||||
import cx from '../cx';
|
|
||||||
import useHighlighting from '../useHighlighting';
|
|
||||||
import { useInView } from 'react-hook-inview';
|
|
||||||
|
|
||||||
// eval stuff start
|
|
||||||
import { extend } from '@strudel.cycles/eval';
|
|
||||||
import * as strudel from '@strudel.cycles/core';
|
|
||||||
import gist from '@strudel.cycles/core/gist.js';
|
|
||||||
import { mini } from '@strudel.cycles/mini/mini.mjs';
|
|
||||||
import { Tone } from '@strudel.cycles/tone';
|
|
||||||
import * as toneHelpers from '@strudel.cycles/tone/tone.mjs';
|
|
||||||
import * as voicingHelpers from '@strudel.cycles/tonal/voicings.mjs';
|
|
||||||
import * as uiHelpers from '@strudel.cycles/tone/ui.mjs';
|
|
||||||
import * as drawHelpers from '@strudel.cycles/tone/draw.mjs';
|
|
||||||
import euclid from '@strudel.cycles/core/euclid.mjs';
|
|
||||||
import '@strudel.cycles/tone/tone.mjs';
|
|
||||||
import '@strudel.cycles/midi/midi.mjs';
|
|
||||||
import '@strudel.cycles/tonal/voicings.mjs';
|
|
||||||
import '@strudel.cycles/tonal/tonal.mjs';
|
|
||||||
import '@strudel.cycles/xen/xen.mjs';
|
|
||||||
import '@strudel.cycles/xen/tune.mjs';
|
|
||||||
import '@strudel.cycles/core/euclid.mjs';
|
|
||||||
import '@strudel.cycles/tone/pianoroll.mjs';
|
|
||||||
import '@strudel.cycles/tone/draw.mjs';
|
|
||||||
import CodeMirror6 from '../CodeMirror6';
|
|
||||||
|
|
||||||
extend(Tone, strudel, strudel.Pattern.prototype.bootstrap(), toneHelpers, voicingHelpers, drawHelpers, uiHelpers, {
|
|
||||||
gist,
|
|
||||||
euclid,
|
|
||||||
mini,
|
|
||||||
Tone,
|
|
||||||
});
|
|
||||||
// eval stuff end
|
|
||||||
|
|
||||||
const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.Destination).set({
|
|
||||||
oscillator: { type: 'triangle' },
|
|
||||||
envelope: {
|
|
||||||
release: 0.01,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// "balanced" | "interactive" | "playback";
|
|
||||||
// Tone.setContext(new Tone.Context({ latencyHint: 'playback', lookAhead: 1 }));
|
|
||||||
function MiniRepl({ tune, maxHeight = 500 }) {
|
|
||||||
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
|
|
||||||
tune,
|
|
||||||
defaultSynth,
|
|
||||||
autolink: false,
|
|
||||||
});
|
|
||||||
const lines = code.split('\n').length;
|
|
||||||
const [view, setView] = useState();
|
|
||||||
const [ref, isVisible] = useInView({
|
|
||||||
threshold: 0.01,
|
|
||||||
});
|
|
||||||
useHighlighting({ view, pattern, active: cycle.started });
|
|
||||||
return (
|
|
||||||
<div className="rounded-md overflow-hidden bg-[#444C57]" ref={ref}>
|
|
||||||
<div className="flex justify-between bg-slate-700 border-t border-slate-500">
|
|
||||||
<div className="flex">
|
|
||||||
<button
|
|
||||||
className={cx(
|
|
||||||
'w-16 flex items-center justify-center p-1 bg-slate-700 border-r border-slate-500 text-white hover:bg-slate-600',
|
|
||||||
cycle.started ? 'animate-pulse' : '',
|
|
||||||
)}
|
|
||||||
onClick={() => togglePlay()}
|
|
||||||
>
|
|
||||||
{!cycle.started ? (
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={cx(
|
|
||||||
'w-16 flex items-center justify-center p-1 border-slate-500 hover:bg-slate-600',
|
|
||||||
dirty
|
|
||||||
? 'bg-slate-700 border-r border-slate-500 text-white'
|
|
||||||
: 'bg-slate-600 text-slate-400 cursor-not-allowed',
|
|
||||||
)}
|
|
||||||
onClick={() => activateCode()}
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="text-right p-1 text-sm">{error && <span className="text-red-200">{error.message}</span>}</div>{' '}
|
|
||||||
</div>
|
|
||||||
<div className="flex space-y-0 overflow-auto relative">
|
|
||||||
{isVisible && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
|
|
||||||
</div>
|
|
||||||
{/* <div className="bg-slate-700 border-t border-slate-500 content-right pr-2 text-right">
|
|
||||||
<a href={`https://strudel.tidalcycles.org/#${hash}`} className="text-white items-center inline-flex">
|
|
||||||
<span>open in REPL</span>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
|
|
||||||
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MiniRepl;
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
/*
|
|
||||||
Tutorial.js - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/tutorial/Tutorial.js>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import Tutorial from './tutorial.mdx';
|
|
||||||
// import logo from '../logo.svg';
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<div className="min-h-screen">
|
|
||||||
<header className="flex-none flex justify-center w-full h-16 px-2 items-center border-b border-gray-200 bg-white">
|
|
||||||
<div className="p-4 w-full max-w-3xl flex justify-between">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<img src={'https://tidalcycles.org/img/logo.svg'} className="Tidal-logo w-16 h-16" alt="logo" />
|
|
||||||
<h1 className="text-2xl">Strudel Tutorial</h1>
|
|
||||||
</div>
|
|
||||||
{!window.location.href.includes('localhost') && (
|
|
||||||
<div className="flex space-x-4">
|
|
||||||
<a href="../">go to REPL</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main className="p-4 max-w-3xl prose">
|
|
||||||
<Tutorial />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</React.StrictMode>,
|
|
||||||
document.getElementById('root')
|
|
||||||
);
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="../../public/favicon.ico" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="./style.css" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="description" content="Strudel REPL" />
|
|
||||||
<title>Strudel Tutorial</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<script type="module" src="Tutorial.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
.cm-editor {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
@ -1,693 +0,0 @@
|
||||||
import MiniRepl from './MiniRepl';
|
|
||||||
|
|
||||||
# What is Strudel?
|
|
||||||
|
|
||||||
With Strudel, you can expressively write dynamic music pieces.
|
|
||||||
It aims to be [Tidal Cycles](https://tidalcycles.org/) for JavaScript (started by the same author).
|
|
||||||
|
|
||||||
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/).
|
|
||||||
|
|
||||||
## Show me a Demo
|
|
||||||
|
|
||||||
To get a taste of what Strudel can do, check out this track:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`const delay = new FeedbackDelay(1/8, .4).chain(vol(0.5), out());
|
|
||||||
const kick = new MembraneSynth().chain(vol(.8), out());
|
|
||||||
const snare = new NoiseSynth().chain(vol(.8), out());
|
|
||||||
const hihat = new MetalSynth().set(adsr(0, .08, 0, .1)).chain(vol(.3).connect(delay),out());
|
|
||||||
const bass = new Synth().set({ ...osc('sawtooth'), ...adsr(0, .1, .4) }).chain(lowpass(900), vol(.5), out());
|
|
||||||
const keys = new PolySynth().set({ ...osc('sawtooth'), ...adsr(0, .5, .2, .7) }).chain(lowpass(1200), vol(.5), out());
|
|
||||||
const drums = stack(
|
|
||||||
"c1*2".tone(kick).bypass("<0@7 1>/8"),
|
|
||||||
"~ <x!7 [x@3 x]>".tone(snare).bypass("<0@7 1>/4"),
|
|
||||||
"[~ c4]*2".tone(hihat)
|
|
||||||
);
|
|
||||||
const thru = (x) => x.transpose("<0 1>/8").transpose(-1);
|
|
||||||
const synths = stack(
|
|
||||||
"<eb4 d4 c4 b3>/2".scale(timeCat([3,'C minor'],[1,'C melodic minor']).slow(8)).struct("[~ x]\*2")
|
|
||||||
.edit(
|
|
||||||
scaleTranspose(0).early(0),
|
|
||||||
scaleTranspose(2).early(1/8),
|
|
||||||
scaleTranspose(7).early(1/4),
|
|
||||||
scaleTranspose(8).early(3/8)
|
|
||||||
).edit(thru).tone(keys).bypass("<1 0>/16"),
|
|
||||||
"<C2 Bb1 Ab1 [G1 [G2 G1]]>/2".struct("[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2".fast(2)).edit(thru).tone(bass),
|
|
||||||
"<Cm7 Bb7 Fm7 G7b13>/2".struct("~ [x@0.1 ~]".fast(2)).voicings().edit(thru).every(2, early(1/8)).tone(keys).bypass("<0@7 1>/8".early(1/4))
|
|
||||||
)
|
|
||||||
stack(
|
|
||||||
drums.fast(2),
|
|
||||||
synths
|
|
||||||
).slow(2);
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
[Open this track in the REPL](https://strudel.tidalcycles.org/#KCkgPT4gewogIGNvbnN0IGRlbGF5ID0gbmV3IEZlZWRiYWNrRGVsYXkoMS84LCAuNCkuY2hhaW4odm9sKDAuNSksIG91dCk7CiAgY29uc3Qga2ljayA9IG5ldyBNZW1icmFuZVN5bnRoKCkuY2hhaW4odm9sKC44KSwgb3V0KTsKICBjb25zdCBzbmFyZSA9IG5ldyBOb2lzZVN5bnRoKCkuY2hhaW4odm9sKC44KSwgb3V0KTsKICBjb25zdCBoaWhhdCA9IG5ldyBNZXRhbFN5bnRoKCkuc2V0KGFkc3IoMCwgLjA4LCAwLCAuMSkpLmNoYWluKHZvbCguMykuY29ubmVjdChkZWxheSksb3V0KTsKICBjb25zdCBiYXNzID0gbmV3IFN5bnRoKCkuc2V0KHsgLi4ub3NjKCdzYXd0b290aCcpLCAuLi5hZHNyKDAsIC4xLCAuNCkgfSkuY2hhaW4obG93cGFzcyg5MDApLCB2b2woLjUpLCBvdXQpOwogIGNvbnN0IGtleXMgPSBuZXcgUG9seVN5bnRoKCkuc2V0KHsgLi4ub3NjKCdzYXd0b290aCcpLCAuLi5hZHNyKDAsIC41LCAuMiwgLjcpIH0pLmNoYWluKGxvd3Bhc3MoMTIwMCksIHZvbCguNSksIG91dCk7CiAgCiAgY29uc3QgZHJ1bXMgPSBzdGFjaygKICAgICdjMSoyJy5tLnRvbmUoa2ljaykuYnlwYXNzKCc8MEA3IDE%2BLzgnLm0pLAogICAgJ34gPHghNyBbeEAzIHhdPicubS50b25lKHNuYXJlKS5ieXBhc3MoJzwwQDcgMT4vNCcubSksCiAgICAnW34gYzRdKjInLm0udG9uZShoaWhhdCkKICApOwogIAogIGNvbnN0IHRocnUgPSAoeCkgPT4geC50cmFuc3Bvc2UoJzwwIDE%2BLzgnLm0pLnRyYW5zcG9zZSgtMSk7CiAgY29uc3Qgc3ludGhzID0gc3RhY2soCiAgICAnPGViNCBkNCBjNCBiMz4vMicubS5zY2FsZSh0aW1lQ2F0KFszLCdDIG1pbm9yJ10sWzEsJ0MgbWVsb2RpYyBtaW5vciddKS5zbG93KDgpKS5ncm9vdmUoJ1t%2BIHhdKjInLm0pCiAgICAuZWRpdCgKICAgICAgc2NhbGVUcmFuc3Bvc2UoMCkuZWFybHkoMCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDIpLmVhcmx5KDEvOCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDcpLmVhcmx5KDEvNCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDgpLmVhcmx5KDMvOCkKICAgICkuZWRpdCh0aHJ1KS50b25lKGtleXMpLmJ5cGFzcygnPDEgMD4vMTYnLm0pLAogICAgJzxDMiBCYjEgQWIxIFtHMSBbRzIgRzFdXT4vMicubS5ncm9vdmUoJ1t4IFt%2BIHhdIDxbfiBbfiB4XV0hMyBbeCB4XT5AMl0vMicubS5mYXN0KDIpKS5lZGl0KHRocnUpLnRvbmUoYmFzcyksCiAgICAnPENtNyBCYjcgRm03IEc3YjEzPi8yJy5tLmdyb292ZSgnfiBbeEAwLjEgfl0nLm0uZmFzdCgyKSkudm9pY2luZ3MoKS5lZGl0KHRocnUpLmV2ZXJ5KDIsIGVhcmx5KDEvOCkpLnRvbmUoa2V5cykuYnlwYXNzKCc8MEA3IDE%2BLzgnLm0uZWFybHkoMS80KSkKICApCiAgcmV0dXJuIHN0YWNrKAogICAgZHJ1bXMuZmFzdCgyKSwgCiAgICBzeW50aHMKICApLnNsb3coMik7Cn0%3D)
|
|
||||||
|
|
||||||
## Disclaimer
|
|
||||||
|
|
||||||
- This project is still in its experimental state. In the future, parts of it might change significantly.
|
|
||||||
- This tutorial is far from complete.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
# Mini Notation
|
|
||||||
|
|
||||||
Similar to Tidal Cycles, Strudel has an embedded mini language that is designed to write rhythmic patterns in a short manner.
|
|
||||||
Before diving deeper into the details, here is a flavor of how the mini language looks like:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`\`[
|
|
||||||
[
|
|
||||||
[e5 [b4 c5] d5 [c5 b4]]
|
|
||||||
[a4 [a4 c5] e5 [d5 c5]]
|
|
||||||
[b4 [~ c5] d5 e5]
|
|
||||||
[c5 a4 a4 ~]
|
|
||||||
[[~ d5] [~ f5] a5 [g5 f5]]
|
|
||||||
[e5 [~ c5] e5 [d5 c5]]
|
|
||||||
[b4 [b4 c5] d5 e5]
|
|
||||||
[c5 a4 a4 ~]
|
|
||||||
],[
|
|
||||||
[[e2 e3]*4]
|
|
||||||
[[a2 a3]*4]
|
|
||||||
[[g#2 g#3]*2 [e2 e3]*2]
|
|
||||||
[a2 a3 a2 a3 a2 a3 b1 c2]
|
|
||||||
[[d2 d3]*4]
|
|
||||||
[[c2 c3]*4]
|
|
||||||
[[b1 b2]*2 [e2 e3]*2]
|
|
||||||
[[a1 a2]*4]
|
|
||||||
]
|
|
||||||
]/16\``}
|
|
||||||
/>
|
|
||||||
|
|
||||||
The snippet above is enclosed in backticks (`), which allows you to write multi-line strings.
|
|
||||||
You can also use double quotes (") for single line mini notation.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
Notes are notated with the note letter, followed by the octave number. You can notate flats with `b` and sharps with `#`.
|
|
||||||
|
|
||||||
<MiniRepl tune={`"e5"`} />
|
|
||||||
|
|
||||||
Here, the same note is played over and over again, once a second. This one second is the default length of one so called "cycle".
|
|
||||||
|
|
||||||
By the way, you can edit the contents of the player, and press "update" to hear your change!
|
|
||||||
You can also press "play" on the next player without needing to stop the last one.
|
|
||||||
|
|
||||||
## Sequences
|
|
||||||
|
|
||||||
We can play more notes by separating them with spaces:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"e5 b4 d5 c5"`} />
|
|
||||||
|
|
||||||
Here, those four notes are squashed into one cycle, so each note is a quarter second long.
|
|
||||||
|
|
||||||
## Division
|
|
||||||
|
|
||||||
We can slow the sequence down by enclosing it in brackets and dividing it by a number:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"[e5 b4 d5 c5]/2"`} />
|
|
||||||
|
|
||||||
The division by two means that the sequence will be played over the course of two cycles.
|
|
||||||
You can also use decimal numbers for any tempo you like.
|
|
||||||
|
|
||||||
## Angle Brackets
|
|
||||||
|
|
||||||
Using angle brackets, we can define the sequence length based on the number of children:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"<e5 b4 d5 c5>"`} />
|
|
||||||
|
|
||||||
The above snippet is the same as:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"[e5 b4 d5 c5]/4"`} />
|
|
||||||
|
|
||||||
The advantage of the angle brackets, is that we can add more children without needing to change the number at the end.
|
|
||||||
|
|
||||||
## Multiplication
|
|
||||||
|
|
||||||
Contrary to division, a sequence can be sped up by multiplying it by a number:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"[e5 b4 d5 c5]*2"`} />
|
|
||||||
|
|
||||||
The multiplication by 2 here means that the sequence will play twice a cycle.
|
|
||||||
|
|
||||||
## Bracket Nesting
|
|
||||||
|
|
||||||
To create more interesting rhythms, you can nest sequences with brackets, like this:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"e5 [b4 c5] d5 [c5 b4]"`} />
|
|
||||||
|
|
||||||
## Rests
|
|
||||||
|
|
||||||
The "~" represents a rest:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"[b4 [~ c5] d5 e5]"`} />
|
|
||||||
|
|
||||||
## Parallel
|
|
||||||
|
|
||||||
Using commas, we can play chords:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"g3,b3,e4"`} />
|
|
||||||
|
|
||||||
To play multiple chords in a sequence, we have to wrap them in brackets:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"<[g3,b3,e4] [a3,c3,e4] [b3,d3,f#4] [b3,e4,g4]>"`} />
|
|
||||||
|
|
||||||
## Elongation
|
|
||||||
|
|
||||||
With the "@" symbol, we can specify temporal "weight" of a sequence child:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>"`} />
|
|
||||||
|
|
||||||
Here, the first chord has a weight of 2, making it twice the length of the other chords. The default weight is 1.
|
|
||||||
|
|
||||||
## Replication
|
|
||||||
|
|
||||||
Using "!" we can repeat without speeding up:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>"`} />
|
|
||||||
|
|
||||||
In essence, the `x!n` is like a shortcut for `[x*n]@n`.
|
|
||||||
|
|
||||||
## Mini Notation TODO
|
|
||||||
|
|
||||||
Compared to [tidal mini notation](https://tidalcycles.org/docs/patternlib/tutorials/mini_notation/), the following mini notation features are missing from Strudel:
|
|
||||||
|
|
||||||
- [x] Euclidean algorithm "c3(3,2,1)" TODO: document
|
|
||||||
- [ ] Tie symbols "\_"
|
|
||||||
- [ ] feet marking "."
|
|
||||||
- [ ] random choice "|"
|
|
||||||
- [ ] Random removal "?"
|
|
||||||
- [ ] Polymetric sequences "{ ... }"
|
|
||||||
- [ ] Fixed steps using "%"
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
# Core API
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
Notes are automatically available as variables:
|
|
||||||
|
|
||||||
<MiniRepl tune={`seq(d4, fs4, a4)`} />
|
|
||||||
|
|
||||||
An important difference to the mini notation:
|
|
||||||
For sharp notes, the letter "s" is used instead of "#", because JavaScript does not support "#" in a variable name.
|
|
||||||
|
|
||||||
The above is the same as:
|
|
||||||
|
|
||||||
<MiniRepl tune={`seq('d4', 'f#4', 'a4')`} />
|
|
||||||
|
|
||||||
Using strings, you can also use "#".
|
|
||||||
|
|
||||||
## Functions that create Patterns
|
|
||||||
|
|
||||||
The following functions will return a pattern. We will see later what that means.
|
|
||||||
|
|
||||||
## pure(value)
|
|
||||||
|
|
||||||
To create a pattern from a value, you can wrap the value in pure:
|
|
||||||
|
|
||||||
<MiniRepl tune={`pure('e4')`} />
|
|
||||||
|
|
||||||
Most of the time, you won't need that function as input values of pattern creating functions are purified by default.
|
|
||||||
|
|
||||||
### cat(...values)
|
|
||||||
|
|
||||||
The given items are con**cat**enated, where each one takes one cycle:
|
|
||||||
|
|
||||||
<MiniRepl tune={`cat(e5, b4, [d5, c5])`} />
|
|
||||||
|
|
||||||
- Square brackets will create a subsequence
|
|
||||||
- The function **slowcat** does the same as **cat**.
|
|
||||||
|
|
||||||
### seq(...values)
|
|
||||||
|
|
||||||
Like **cat**, but the items are crammed into one cycle:
|
|
||||||
|
|
||||||
<MiniRepl tune={`seq(e5, b4, [d5, c5])`} />
|
|
||||||
|
|
||||||
- Synonyms: **fastcat**, **sequence**
|
|
||||||
|
|
||||||
### stack(...values)
|
|
||||||
|
|
||||||
The given items are played at the same time at the same length:
|
|
||||||
|
|
||||||
<MiniRepl tune={`stack(g3, b3, [e4, d4])`} />
|
|
||||||
|
|
||||||
- Square Brackets will create a subsequence
|
|
||||||
|
|
||||||
### Nesting functions
|
|
||||||
|
|
||||||
You can nest functions inside one another:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`cat(
|
|
||||||
stack(g3,b3,e4),
|
|
||||||
stack(a3,c3,e4),
|
|
||||||
stack(b3,d3,fs4),
|
|
||||||
stack(b3,e4,g4)
|
|
||||||
)`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
The above is equivalent to
|
|
||||||
|
|
||||||
<MiniRepl tune={`"<[g3,b3,e4] [a3,c3,e4] [b3,d3,f#4] [b3,e4,g4]>"`} />
|
|
||||||
|
|
||||||
### timeCat(...[weight,value])
|
|
||||||
|
|
||||||
Like with "@" in mini notation, we can specify weights to the items in a sequence:
|
|
||||||
|
|
||||||
<MiniRepl tune={`timeCat([3,e3],[1, g3])`} />
|
|
||||||
|
|
||||||
<!-- ## polymeter
|
|
||||||
|
|
||||||
how to use?
|
|
||||||
|
|
||||||
<MiniRepl tune={`polymeter(3, e3, g3, b3)`} /> -->
|
|
||||||
|
|
||||||
### polyrhythm(...[...values])
|
|
||||||
|
|
||||||
Plays the given items at the same time, within the same length:
|
|
||||||
|
|
||||||
<MiniRepl tune={`polyrhythm([e3, g3], [e4, g4, b4])`} />
|
|
||||||
|
|
||||||
We can write the same with **stack** and **cat**:
|
|
||||||
|
|
||||||
<MiniRepl tune={`stack(seq(e3, g3), seq(e4, g4, b4))`} />
|
|
||||||
|
|
||||||
You can also use the shorthand **pr** instead of **polyrhythm**.
|
|
||||||
|
|
||||||
## Pattern modifier functions
|
|
||||||
|
|
||||||
The following functions modify a pattern.
|
|
||||||
|
|
||||||
### slow(factor)
|
|
||||||
|
|
||||||
Like "/" in mini notation, **slow** will slow down a pattern over the given number of cycles:
|
|
||||||
|
|
||||||
<MiniRepl tune={`seq(e5, b4, d5, c5).slow(2)`} />
|
|
||||||
|
|
||||||
The same in mini notation:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"[e5 b4 d5 c5]/2"`} />
|
|
||||||
|
|
||||||
### fast(factor)
|
|
||||||
|
|
||||||
Like "\*" in mini notation, **fast** will play a pattern times the given number in one cycle:
|
|
||||||
|
|
||||||
<MiniRepl tune={`seq(e5, b4, d5, c5).fast(2)`} />
|
|
||||||
|
|
||||||
### early(cycles)
|
|
||||||
|
|
||||||
With early, you can nudge a pattern to start earlier in time:
|
|
||||||
|
|
||||||
<MiniRepl tune={`seq(e5, b4.early(0.5))`} />
|
|
||||||
|
|
||||||
### late(cycles)
|
|
||||||
|
|
||||||
Like early, but in the other direction:
|
|
||||||
|
|
||||||
<MiniRepl tune={`seq(e5, b4.late(0.5))`} />
|
|
||||||
|
|
||||||
<!-- TODO: shouldn't it sound different? -->
|
|
||||||
|
|
||||||
### rev()
|
|
||||||
|
|
||||||
Will reverse the pattern:
|
|
||||||
|
|
||||||
<MiniRepl tune={`seq(c3,d3,e3,f3).rev()`} />
|
|
||||||
|
|
||||||
### every(n, func)
|
|
||||||
|
|
||||||
Will apply the given function every n cycles:
|
|
||||||
|
|
||||||
<MiniRepl tune={`seq(e5, "b4".every(4, late(0.5)))`} />
|
|
||||||
|
|
||||||
<!-- TODO: should be able to do b4.every => like already possible with fast slow etc.. -->
|
|
||||||
|
|
||||||
Note that late is called directly. This is a shortcut for:
|
|
||||||
|
|
||||||
<MiniRepl tune={`seq(e5, "b4".every(4, x => x.late(0.5)))`} />
|
|
||||||
|
|
||||||
<!-- TODO: should the function really run the first cycle? -->
|
|
||||||
|
|
||||||
### add(n)
|
|
||||||
|
|
||||||
Adds the given number to each item in the pattern:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"0 2 4".add("<0 3 4 0>").scale('C major')`} />
|
|
||||||
|
|
||||||
Here, the triad `0, 2, 4` is shifted by different amounts. Without add, the equivalent would be:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>".scale('C major')`} />
|
|
||||||
|
|
||||||
You can also use add with notes:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"c3 e3 g3".add("<0 5 7 0>")`} />
|
|
||||||
|
|
||||||
Behind the scenes, the notes are converted to midi numbers as soon before add is applied, which is equivalent to:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"48 52 55".add("<0 5 7 0>")`} />
|
|
||||||
|
|
||||||
### sub(n)
|
|
||||||
|
|
||||||
Like add, but the given numbers are subtracted:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"0 2 4".sub("<0 1 2 3>").scale('C4 minor')`} />
|
|
||||||
|
|
||||||
See add for more information.
|
|
||||||
|
|
||||||
### mul(n)
|
|
||||||
|
|
||||||
Multiplies each number by the given factor:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"0,1,2".mul("<2 3 4 3>").scale('C4 minor')`} />
|
|
||||||
|
|
||||||
... is equivalent to:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"<[0,2,4] [0,3,6] [0,4,8] [0,3,6]>".scale('C4 minor')`} />
|
|
||||||
|
|
||||||
This function is really useful in combination with signals:
|
|
||||||
|
|
||||||
<MiniRepl tune={`sine.struct("x*16").mul(7).round().scale('C major')`} />
|
|
||||||
|
|
||||||
Here, we sample a sine wave 16 times, and multiply each sample by 7. This way, we let values oscillate between 0 and 7.
|
|
||||||
|
|
||||||
### div(n)
|
|
||||||
|
|
||||||
Like mul, but dividing by the given number.
|
|
||||||
|
|
||||||
### round()
|
|
||||||
|
|
||||||
Rounds all values to the nearest integer:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"0.5 1.5 2.5".round().scale('C major')`} />
|
|
||||||
|
|
||||||
### struct(binary_pat)
|
|
||||||
|
|
||||||
Applies the given structure to the pattern:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"c3,eb3,g3".struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~").slow(4)`} />
|
|
||||||
|
|
||||||
This is also useful to sample signals:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`sine.struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~").mul(7).round()
|
|
||||||
.scale('C minor').slow(4)`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
### when(binary_pat, func)
|
|
||||||
|
|
||||||
Applies the given function whenever the given pattern is in a true state.
|
|
||||||
|
|
||||||
<MiniRepl tune={`"c3 eb3 g3".when("<0 1>/2", sub(5))`} />
|
|
||||||
|
|
||||||
### superimpose(...func)
|
|
||||||
|
|
||||||
Superimposes the result of the given function(s) on top of the original pattern:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"<c3 eb3 g3>".scale('C minor').superimpose(scaleTranspose("2,4"))`} />
|
|
||||||
|
|
||||||
### layer(...func)
|
|
||||||
|
|
||||||
Layers the result of the given function(s) on top of each other. Like superimpose, but the original pattern is not part of the result.
|
|
||||||
|
|
||||||
<MiniRepl tune={`"<c3 eb3 g3>".scale('C minor').layer(scaleTranspose("0,2,4"))`} />
|
|
||||||
|
|
||||||
### apply(func)
|
|
||||||
|
|
||||||
Like layer, but with a single function:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"<c3 eb3 g3>".scale('C minor').apply(scaleTranspose("0,2,4"))`} />
|
|
||||||
|
|
||||||
### off(time, func)
|
|
||||||
|
|
||||||
Applies the given function by the given time offset:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"c3 eb3 g3".off(1/8, add(7))`} />
|
|
||||||
|
|
||||||
### append(pat)
|
|
||||||
|
|
||||||
Appends the given pattern after the current pattern:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"c4,eb4,g4".append("c4,f4,ab4")`} />
|
|
||||||
|
|
||||||
### stack(pat)
|
|
||||||
|
|
||||||
Stacks the given pattern to the current pattern:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"c4,eb4,g4".stack("bb4,d5")`} />
|
|
||||||
|
|
||||||
## Tone API
|
|
||||||
|
|
||||||
To make the sounds more interesting, we can use Tone.js instruments ands effects.
|
|
||||||
|
|
||||||
[Show Source on Github](https://github.com/tidalcycles/strudel/blob/main/repl/src/tone.ts)
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`stack(
|
|
||||||
"[c5 c5 bb4 c5] [~ g4 ~ g4] [c5 f5 e5 c5] ~"
|
|
||||||
.tone(synth(adsr(0,.1,0,0)).chain(out())),
|
|
||||||
"[c2 c3]*8"
|
|
||||||
.tone(synth({
|
|
||||||
...osc('sawtooth'),
|
|
||||||
...adsr(0,.1,0.4,0)
|
|
||||||
}).chain(lowpass(300), out()))
|
|
||||||
).slow(4)`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
### tone(instrument)
|
|
||||||
|
|
||||||
To change the instrument of a pattern, you can pass any [Tone.js Source](https://tonejs.github.io/docs/14.7.77/index.html) to .tone:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
|
|
||||||
.tone(new FMSynth().toDestination())`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
While this works, it is a little bit verbose. To simplify things, all Tone Synths have a shortcut:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const amsynth = (options) => new AMSynth(options);
|
|
||||||
const duosynth = (options) => new DuoSynth(options);
|
|
||||||
const fmsynth = (options) => new FMSynth(options);
|
|
||||||
const membrane = (options) => new MembraneSynth(options);
|
|
||||||
const metal = (options) => new MetalSynth(options);
|
|
||||||
const monosynth = (options) => new MonoSynth(options);
|
|
||||||
const noise = (options) => new NoiseSynth(options);
|
|
||||||
const pluck = (options) => new PluckSynth(options);
|
|
||||||
const polysynth = (options) => new PolySynth(options);
|
|
||||||
const synth = (options) => new Synth(options);
|
|
||||||
const sampler = (options, baseUrl?) => new Sampler(options); // promisified, see below
|
|
||||||
const players = (options, baseUrl?) => new Sampler(options); // promisified, see below
|
|
||||||
```
|
|
||||||
|
|
||||||
### sampler
|
|
||||||
|
|
||||||
With sampler, you can create tonal instruments from samples:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`sampler({
|
|
||||||
C5: 'https://freesound.org/data/previews/536/536549_11935698-lq.mp3'
|
|
||||||
}).then(kalimba =>
|
|
||||||
saw.struct("x*8").mul(16).round()
|
|
||||||
.legato(4).scale('D dorian').slow(2)
|
|
||||||
.tone(kalimba.toDestination())
|
|
||||||
)`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
The sampler function promisifies [Tone.js Sampler](https://tonejs.github.io/docs/14.7.77/Sampler).
|
|
||||||
|
|
||||||
Note that this function currently only works with this promise notation, but in the future,
|
|
||||||
it will be possible to use async instruments in a synchronous fashion.
|
|
||||||
|
|
||||||
### players
|
|
||||||
|
|
||||||
With players, you can create sound banks:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`players({
|
|
||||||
bd: 'samples/tidal/bd/BT0A0D0.wav',
|
|
||||||
sn: 'samples/tidal/sn/ST0T0S3.wav',
|
|
||||||
hh: 'samples/tidal/hh/000_hh3closedhh.wav'
|
|
||||||
}, 'https://loophole-letters.vercel.app/')
|
|
||||||
.then(drums=>
|
|
||||||
"bd hh sn hh".tone(drums.toDestination())
|
|
||||||
)
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
The sampler function promisifies [Tone.js Players](https://tonejs.github.io/docs/14.7.77/Players).
|
|
||||||
|
|
||||||
Note that this function currently only works with this promise notation, but in the future,
|
|
||||||
it will be possible to use async instruments in a synchronous fashion.
|
|
||||||
|
|
||||||
### out
|
|
||||||
|
|
||||||
Shortcut for Tone.Destination. Intended to be used with Tone's .chain:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
|
|
||||||
.tone(membrane().chain(out()))`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
This alone is not really useful, so read on..
|
|
||||||
|
|
||||||
### vol(volume)
|
|
||||||
|
|
||||||
Helper that returns a Gain Node with the given volume. Intended to be used with Tone's .chain:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
|
|
||||||
.tone(noise().chain(vol(0.5), out()))`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
### osc(type)
|
|
||||||
|
|
||||||
Helper to set the waveform of a synth, monosynth or polysynth:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
|
|
||||||
.tone(synth(osc('sawtooth4')).chain(out()))`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
The base types are `sine`, `square`, `sawtooth`, `triangle`. You can also append a number between 1 and 32 to reduce the harmonic partials.
|
|
||||||
|
|
||||||
### lowpass(cutoff)
|
|
||||||
|
|
||||||
Helper that returns a Filter Node of type lowpass with the given cutoff. Intended to be used with Tone's .chain:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
|
|
||||||
.tone(synth(osc('sawtooth')).chain(lowpass(800), out()))`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
### highpass(cutoff)
|
|
||||||
|
|
||||||
Helper that returns a Filter Node of type highpass with the given cutoff. Intended to be used with Tone's .chain:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
|
|
||||||
.tone(synth(osc('sawtooth')).chain(highpass(2000), out()))`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
### adsr(attack, decay?, sustain?, release?)
|
|
||||||
|
|
||||||
Helper to set the envelope of a Tone.js instrument. Intended to be used with Tone's .set:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
|
|
||||||
.tone(synth(adsr(0,.1,0,0)).chain(out()))`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
## Tonal API
|
|
||||||
|
|
||||||
The Tonal API, uses [tonaljs](https://github.com/tonaljs/tonal) to provide helpers for musical operations.
|
|
||||||
|
|
||||||
### transpose(semitones)
|
|
||||||
|
|
||||||
Transposes all notes to the given number of semitones:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).transpose(0)`} />
|
|
||||||
|
|
||||||
This method gets really exciting when we use it with a pattern as above.
|
|
||||||
|
|
||||||
Instead of numbers, scientific interval notation can be used as well:
|
|
||||||
|
|
||||||
<MiniRepl tune={`"c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).transpose(1)`} />
|
|
||||||
|
|
||||||
### scale(name)
|
|
||||||
|
|
||||||
Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like scaleTranpose.
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`"0 2 4 6 4 2"
|
|
||||||
.scale(seq('C2 major', 'C2 minor').slow(2))`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
Note that the scale root is octaved here. You can also omit the octave, then index zero will default to octave 3.
|
|
||||||
|
|
||||||
All the available scale names can be found [here](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
|
|
||||||
|
|
||||||
### scaleTranspose(steps)
|
|
||||||
|
|
||||||
Transposes notes inside the scale by the number of steps:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`"-8 [2,4,6]"
|
|
||||||
.scale('C4 bebop major')
|
|
||||||
.scaleTranspose("<0 -1 -2 -3 -4 -5 -6 -4>")`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
### voicings(range?)
|
|
||||||
|
|
||||||
Turns chord symbols into voicings, using the smoothest voice leading possible:
|
|
||||||
|
|
||||||
<MiniRepl tune={`stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>")`} />
|
|
||||||
|
|
||||||
<!-- TODO: use voicing collection as first param + patternify. -->
|
|
||||||
|
|
||||||
### rootNotes(octave = 2)
|
|
||||||
|
|
||||||
Turns chord symbols into root notes of chords in given octave.
|
|
||||||
|
|
||||||
<MiniRepl tune={`"<C^7 A7b13 Dm7 G7>".rootNotes(3)`} />
|
|
||||||
|
|
||||||
Together with edit, struct and voicings, this can be used to create a basic backing track:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
tune={`"<C^7 A7b13 Dm7 G7>".edit(
|
|
||||||
x => x.voicings(['d3','g4']).struct("~ x"),
|
|
||||||
x => x.rootNotes(2).tone(synth(osc('sawtooth4')).chain(out()))
|
|
||||||
)`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- TODO: use range instead of octave. -->
|
|
||||||
<!-- TODO: find out why composition does not work -->
|
|
||||||
|
|
||||||
## Microtonal API
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## MIDI API
|
|
||||||
|
|
||||||
Strudel also supports midi via [webmidi](https://npmjs.com/package/webmidi).
|
|
||||||
|
|
||||||
### midi(outputName?)
|
|
||||||
|
|
||||||
Make sure to have a midi device connected or to use an IAC Driver.
|
|
||||||
If no outputName is given, it uses the first midi output it finds.
|
|
||||||
|
|
||||||
Midi is currently not supported by the mini repl used here, but you can [open the midi example in the repl](https://strudel.tidalcycles.org/#c3RhY2soIjxDXjcgQTcgRG03IEc3PiIubS52b2ljaW5ncygpLCAnPEMzIEEyIEQzIEcyPicubSkKICAubWlkaSgp).
|
|
||||||
|
|
||||||
In the REPL, you will se a log of the available MIDI devices.
|
|
||||||
|
|
||||||
<!--<MiniRepl
|
|
||||||
tune={`stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>")
|
|
||||||
.midi()`}
|
|
||||||
/>-->
|
|
||||||
|
|
||||||
# Contributing
|
|
||||||
|
|
||||||
Contributions of any sort are very welcome! You can contribute by editing [this file](https://github.com/tidalcycles/strudel/blob/main/repl/src/tutorial/tutorial.mdx).
|
|
||||||
All you need is a github account.
|
|
||||||
|
|
||||||
If you want to run the tutorial locally, you can clone the and run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd repl && npm i && npm run tutorial
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to contribute in another way, either
|
|
||||||
|
|
||||||
- [fork strudel repo on GitHub](https://github.com/tidalcycles/strudel)
|
|
||||||
- [Join the Discord Channel](https://discord.gg/remJ6gQA)
|
|
||||||
- [play with the Strudel REPL](https://strudel.tidalcycles.org/)
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
/*
|
|
||||||
useCycle.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useCycle.mjs>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Tone } from '@strudel.cycles/tone';
|
|
||||||
import { State, TimeSpan } from '@strudel.cycles/core';
|
|
||||||
|
|
||||||
/* export declare interface UseCycleProps {
|
|
||||||
onEvent: ToneEventCallback<any>;
|
|
||||||
onQuery?: (state: State) => Hap[];
|
|
||||||
onSchedule?: (events: Hap[], cycle: number) => void;
|
|
||||||
onDraw?: ToneEventCallback<any>;
|
|
||||||
ready?: boolean; // if false, query will not be called on change props
|
|
||||||
} */
|
|
||||||
|
|
||||||
// function useCycle(props: UseCycleProps) {
|
|
||||||
function useCycle(props) {
|
|
||||||
// onX must use useCallback!
|
|
||||||
const { onEvent, onQuery, onSchedule, ready = true, onDraw } = props;
|
|
||||||
const [started, setStarted] = useState(false);
|
|
||||||
const cycleDuration = 1;
|
|
||||||
const activeCycle = () => Math.floor(Tone.getTransport().seconds / cycleDuration);
|
|
||||||
|
|
||||||
// pull events with onQuery + count up to next cycle
|
|
||||||
const query = (cycle = activeCycle()) => {
|
|
||||||
const timespan = new TimeSpan(cycle, cycle + 1);
|
|
||||||
const events = onQuery?.(new State(timespan)) || [];
|
|
||||||
onSchedule?.(events, cycle);
|
|
||||||
// cancel events after current query. makes sure no old events are player for rescheduled cycles
|
|
||||||
// console.log('schedule', cycle);
|
|
||||||
// query next cycle in the middle of the current
|
|
||||||
const cancelFrom = timespan.begin.valueOf();
|
|
||||||
Tone.getTransport().cancel(cancelFrom);
|
|
||||||
// const queryNextTime = (cycle + 1) * cycleDuration - 0.1;
|
|
||||||
const queryNextTime = (cycle + 1) * cycleDuration - 0.5;
|
|
||||||
|
|
||||||
// if queryNextTime would be before current time, execute directly (+0.1 for safety that it won't miss)
|
|
||||||
const t = Math.max(Tone.getTransport().seconds, queryNextTime) + 0.1;
|
|
||||||
Tone.getTransport().schedule(() => {
|
|
||||||
query(cycle + 1);
|
|
||||||
}, t);
|
|
||||||
|
|
||||||
// schedule events for next cycle
|
|
||||||
events
|
|
||||||
?.filter((event) => event.part.begin.equals(event.whole?.begin))
|
|
||||||
.forEach((event) => {
|
|
||||||
Tone.getTransport().schedule((time) => {
|
|
||||||
onEvent(time, event, Tone.getContext().currentTime);
|
|
||||||
Tone.Draw.schedule(() => {
|
|
||||||
// do drawing or DOM manipulation here
|
|
||||||
onDraw?.(time, event);
|
|
||||||
}, time);
|
|
||||||
}, event.part.begin.valueOf());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
ready && query();
|
|
||||||
}, [onEvent, onSchedule, onQuery, onDraw, ready]);
|
|
||||||
|
|
||||||
const start = async () => {
|
|
||||||
setStarted(true);
|
|
||||||
await Tone.start();
|
|
||||||
Tone.getTransport().start('+0.1');
|
|
||||||
};
|
|
||||||
const stop = () => {
|
|
||||||
Tone.getTransport().pause();
|
|
||||||
setStarted(false);
|
|
||||||
};
|
|
||||||
const toggle = () => (started ? stop() : start());
|
|
||||||
return {
|
|
||||||
start,
|
|
||||||
stop,
|
|
||||||
onEvent,
|
|
||||||
started,
|
|
||||||
setStarted,
|
|
||||||
toggle,
|
|
||||||
query,
|
|
||||||
activeCycle,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useCycle;
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { setHighlights } from './CodeMirror6';
|
|
||||||
import { Tone } from '@strudel.cycles/tone';
|
|
||||||
|
|
||||||
let highlights = []; // actively highlighted events
|
|
||||||
let lastEnd;
|
|
||||||
|
|
||||||
function useHighlighting({ view, pattern, active }) {
|
|
||||||
useEffect(() => {
|
|
||||||
if (view) {
|
|
||||||
if (pattern && active) {
|
|
||||||
let frame = requestAnimationFrame(updateHighlights);
|
|
||||||
|
|
||||||
function updateHighlights() {
|
|
||||||
try {
|
|
||||||
const audioTime = Tone.getTransport().seconds;
|
|
||||||
const span = [lastEnd || audioTime, audioTime + 1 / 60];
|
|
||||||
lastEnd = audioTime + 1 / 60;
|
|
||||||
highlights = highlights.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
|
|
||||||
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
|
||||||
highlights = highlights.concat(haps); // add potential new onsets
|
|
||||||
view.dispatch({ effects: setHighlights.of(highlights) }); // highlight all still active + new active haps
|
|
||||||
} catch (err) {
|
|
||||||
// console.log('error in updateHighlights', err);
|
|
||||||
view.dispatch({ effects: setHighlights.of([]) });
|
|
||||||
}
|
|
||||||
frame = requestAnimationFrame(updateHighlights);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelAnimationFrame(frame);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
highlights = [];
|
|
||||||
view.dispatch({ effects: setHighlights.of([]) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [pattern, active, view]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useHighlighting;
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
/*
|
|
||||||
usePostMessage.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/usePostMessage.mjs>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect, useCallback } from 'react';
|
|
||||||
|
|
||||||
function usePostMessage(listener) {
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener('message', listener);
|
|
||||||
return () => window.removeEventListener('message', listener);
|
|
||||||
}, [listener]);
|
|
||||||
return useCallback((data) => window.postMessage(data, '*'), []);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default usePostMessage;
|
|
||||||
|
|
@ -1,157 +0,0 @@
|
||||||
/*
|
|
||||||
useRepl.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useRepl.mjs>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useCallback, useState, useMemo } from 'react';
|
|
||||||
import { evaluate } from '@strudel.cycles/eval';
|
|
||||||
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
|
|
||||||
import useCycle from './useCycle.mjs';
|
|
||||||
import usePostMessage from './usePostMessage.mjs';
|
|
||||||
|
|
||||||
let s4 = () => {
|
|
||||||
return Math.floor((1 + Math.random()) * 0x10000)
|
|
||||||
.toString(16)
|
|
||||||
.substring(1);
|
|
||||||
};
|
|
||||||
const generateHash = (code) => encodeURIComponent(btoa(code));
|
|
||||||
|
|
||||||
function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawProp }) {
|
|
||||||
const id = useMemo(() => s4(), []);
|
|
||||||
const [code, setCode] = useState(tune);
|
|
||||||
const [activeCode, setActiveCode] = useState();
|
|
||||||
const [log, setLog] = useState('');
|
|
||||||
const [error, setError] = useState();
|
|
||||||
const [pending, setPending] = useState(false);
|
|
||||||
const [hash, setHash] = useState('');
|
|
||||||
const [pattern, setPattern] = useState();
|
|
||||||
const dirty = useMemo(() => code !== activeCode || error, [code, activeCode, error]);
|
|
||||||
const pushLog = useCallback((message) => setLog((log) => log + `${log ? '\n\n' : ''}${message}`), []);
|
|
||||||
|
|
||||||
// below block allows disabling the highlighting by including "strudel disable-highlighting" in the code (as comment)
|
|
||||||
const onDraw = useMemo(() => {
|
|
||||||
if (activeCode && !activeCode.includes('strudel disable-highlighting')) {
|
|
||||||
return (time, event) => onDrawProp?.(time, event, activeCode);
|
|
||||||
}
|
|
||||||
}, [activeCode, onDrawProp]);
|
|
||||||
|
|
||||||
// cycle hook to control scheduling
|
|
||||||
const cycle = useCycle({
|
|
||||||
onDraw,
|
|
||||||
onEvent: useCallback(
|
|
||||||
(time, event, currentTime) => {
|
|
||||||
try {
|
|
||||||
onEvent?.(event);
|
|
||||||
if (event.context.logs?.length) {
|
|
||||||
event.context.logs.forEach(pushLog);
|
|
||||||
}
|
|
||||||
const { onTrigger, velocity } = event.context;
|
|
||||||
if (!onTrigger) {
|
|
||||||
if (defaultSynth) {
|
|
||||||
const note = getPlayableNoteValue(event);
|
|
||||||
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
|
||||||
} else {
|
|
||||||
throw new Error('no defaultSynth passed to useRepl.');
|
|
||||||
}
|
|
||||||
/* console.warn('no instrument chosen', event);
|
|
||||||
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
|
|
||||||
} else {
|
|
||||||
onTrigger(time, event, currentTime);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(err);
|
|
||||||
err.message = 'unplayable event: ' + err?.message;
|
|
||||||
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onEvent, pushLog, defaultSynth],
|
|
||||||
),
|
|
||||||
onQuery: useCallback(
|
|
||||||
(state) => {
|
|
||||||
try {
|
|
||||||
return pattern?.query(state) || [];
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(err);
|
|
||||||
err.message = 'query error: ' + err.message;
|
|
||||||
setError(err);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[pattern],
|
|
||||||
),
|
|
||||||
onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), []),
|
|
||||||
ready: !!pattern && !!activeCode,
|
|
||||||
});
|
|
||||||
|
|
||||||
const broadcast = usePostMessage(({ data: { from, type } }) => {
|
|
||||||
if (type === 'start' && from !== id) {
|
|
||||||
// console.log('message', from, type);
|
|
||||||
cycle.setStarted(false);
|
|
||||||
setActiveCode(undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const activateCode = useCallback(
|
|
||||||
async (_code = code) => {
|
|
||||||
if (activeCode && !dirty) {
|
|
||||||
setError(undefined);
|
|
||||||
cycle.start();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
setPending(true);
|
|
||||||
const parsed = await evaluate(_code);
|
|
||||||
cycle.start();
|
|
||||||
broadcast({ type: 'start', from: id });
|
|
||||||
setPattern(() => parsed.pattern);
|
|
||||||
if (autolink) {
|
|
||||||
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
|
||||||
}
|
|
||||||
setHash(generateHash(code));
|
|
||||||
setError(undefined);
|
|
||||||
setActiveCode(_code);
|
|
||||||
setPending(false);
|
|
||||||
} catch (err) {
|
|
||||||
err.message = 'evaluation error: ' + err.message;
|
|
||||||
console.warn(err);
|
|
||||||
setError(err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[activeCode, dirty, code, cycle, autolink, id, broadcast],
|
|
||||||
);
|
|
||||||
// logs events of cycle
|
|
||||||
const logCycle = (_events, cycle) => {
|
|
||||||
if (_events.length) {
|
|
||||||
// pushLog(`# cycle ${cycle}\n` + _events.map((e: any) => e.show()).join('\n'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const togglePlay = () => {
|
|
||||||
if (!cycle.started) {
|
|
||||||
activateCode();
|
|
||||||
} else {
|
|
||||||
cycle.stop();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
pending,
|
|
||||||
code,
|
|
||||||
setCode,
|
|
||||||
pattern,
|
|
||||||
error,
|
|
||||||
cycle,
|
|
||||||
setPattern,
|
|
||||||
dirty,
|
|
||||||
log,
|
|
||||||
togglePlay,
|
|
||||||
setActiveCode,
|
|
||||||
activateCode,
|
|
||||||
activeCode,
|
|
||||||
pushLog,
|
|
||||||
hash,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useRepl;
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
/*
|
|
||||||
useWebMidi.js - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useWebMidi.js>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { enableWebMidi, WebMidi } from '@strudel.cycles/midi'
|
|
||||||
|
|
||||||
export function useWebMidi(props) {
|
|
||||||
const { ready, connected, disconnected } = props;
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [outputs, setOutputs] = useState(WebMidi?.outputs || []);
|
|
||||||
useEffect(() => {
|
|
||||||
enableWebMidi()
|
|
||||||
.then(() => {
|
|
||||||
// Reacting when a new device becomes available
|
|
||||||
WebMidi.addListener('connected', (e) => {
|
|
||||||
setOutputs([...WebMidi.outputs]);
|
|
||||||
connected?.(WebMidi, e);
|
|
||||||
});
|
|
||||||
// Reacting when a device becomes unavailable
|
|
||||||
WebMidi.addListener('disconnected', (e) => {
|
|
||||||
setOutputs([...WebMidi.outputs]);
|
|
||||||
disconnected?.(WebMidi, e);
|
|
||||||
});
|
|
||||||
ready?.(WebMidi);
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(err);
|
|
||||||
//throw new Error("Web Midi could not be enabled...");
|
|
||||||
console.warn('Web Midi could not be enabled..');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [ready, connected, disconnected, outputs]);
|
|
||||||
const outputByName = (name) => WebMidi.getOutputByName(name);
|
|
||||||
return { loading, outputs, outputByName };
|
|
||||||
}
|
|
||||||
7
repl/vite.config.js
Normal file
7
repl/vite.config.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()]
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue