Compare commits
225 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f54d93a00 | |||
| 284f6293a2 | |||
| a2b99f94bd | |||
| 8a0f9c6e36 | |||
| d38a8ab1fa | |||
| 6a451be50c | |||
| dac2996b3d | |||
| d689d71ee6 | |||
| a4a6321afb | |||
| 8b13badb67 | |||
| 15a6747b2c | |||
| a96ff66f63 | |||
| 123323e32b | |||
| 8ca850f9fe | |||
| d77725cf1d | |||
| b016d115f5 | |||
| 1a57a86497 | |||
| d433e68fb2 | |||
| c26f6b72d9 | |||
| 6cd3cc96a3 | |||
| c138cf0b76 | |||
| c90bc80e6d | |||
| d3c4fd70dc | |||
| 87cab96893 | |||
| 0248d15470 | |||
| d4801c545e | |||
| ce28acaf18 | |||
| 8edb086178 | |||
| 0f74b5a4b1 | |||
| 6de2feff05 | |||
| a9811ee955 | |||
| 98b6c11a82 | |||
| 3df2929d12 | |||
| 30d1f423cb | |||
| e0c5566f35 | |||
| c5a2ae47d7 | |||
| 17654b885b | |||
| acaf0ca0af | |||
| 6a201be1fd | |||
| 8617b9164e | |||
| 991e5f1b3c | |||
| 55c533c947 | |||
| d17543d5d9 | |||
| 73169563c4 | |||
| bf0336404d | |||
| 7c3d8e5c8d | |||
| 3dad55e266 | |||
| d351cb9f7f | |||
| 251be60630 | |||
| 18d7222804 | |||
| ff1c37d361 | |||
| 3abe6db3de | |||
| b9ca9d3b06 | |||
| 9bbc04843d | |||
| ddf61a6438 | |||
| ba35a81e9b | |||
| 13133583ca | |||
| e91b981740 | |||
| 1458ee64ec | |||
| 4e2a5e1663 | |||
| c98b0e9687 | |||
| 4e25990de9 | |||
| 4b51f1a3fc | |||
| 5490b01004 | |||
| 43176e1f1d | |||
| 3d9ac37efe | |||
| d4619362bd | |||
| fffd3c07ca | |||
| 952bd38e70 | |||
| 8589f5cc89 | |||
| ec594d52f8 | |||
| 848cbf9c60 | |||
| e02be7dd9a | |||
| 33d35e47a6 | |||
| 18d62b32c2 | |||
| 58ed51b49c | |||
| 9f9fe529e4 | |||
| aad721890a | |||
| e63a5d10c8 | |||
| 15681262de | |||
| b5d3aa81f9 | |||
| a688061616 | |||
| f788d347ec | |||
| a33ce67dd4 | |||
| 8a22c5627a | |||
| 7b529a30f4 | |||
| 4910b34f26 | |||
| 6a73b43e3a | |||
| b59b26460d | |||
| aa324ae25d | |||
| ff412f6396 | |||
| ac148b2f32 | |||
| 6f5d096e6d | |||
| ff1e6714c6 | |||
| 41d82eaadf | |||
| be6b011204 | |||
| 65e48c05f0 | |||
| cee08ea67d | |||
| 35ef26c013 | |||
| b08a0b8102 | |||
| 220342154b | |||
| ef7182cd76 | |||
| 6059c69995 | |||
| 892d8ba152 | |||
| da11069f75 | |||
| bf72908dc9 | |||
| 81ab5c78e2 | |||
| de503c7d63 | |||
| 487ccb90a1 | |||
| 4bfeaa47bf | |||
| 9a4d8036a6 | |||
| a50bd6375d | |||
| 14508e91a9 | |||
| 2d5064bd5b | |||
| fdc4819e39 | |||
| efddbb616b | |||
| db4d0f26a8 | |||
| 06cffbc31b | |||
| 3cfb0f4c76 | |||
| 436ba067bc | |||
| f8724bf47e | |||
| 1f7e293204 | |||
| 86d2652258 | |||
| f5c0cc2e2d | |||
| 4463d4a417 | |||
| df35a5c9b1 | |||
| dda93b3c13 | |||
| 5de40b6bf5 | |||
| dcbe799d25 | |||
| a5641aef03 | |||
| 668f97a4c8 | |||
| e9d577496b | |||
| 66fba947ac | |||
| 18c7e02609 | |||
| 7e1351507a | |||
| 032acbd27c | |||
| 8b51f0fbd2 | |||
| ead5942ef0 | |||
| cfe369d9fe | |||
| 2750a3c130 | |||
| a7f6d6ecac | |||
| 3680282450 | |||
| 3b3435c999 | |||
| e2ae6278c2 | |||
| 37d6723e3c | |||
| 961bbf6f55 | |||
| fc71d349ed | |||
| 97c8ab0f71 | |||
| 28534ea547 | |||
| 1a75b79a33 | |||
| 75dfb0af35 | |||
| 073fa09648 | |||
| 3015e7b203 | |||
| 7ac0cdc0f9 | |||
| 49234183a3 | |||
| ed7763df92 | |||
| b818a02f76 | |||
| 886f8449fd | |||
| 5de6643604 | |||
| 6283273d81 | |||
| 5506d18bc5 | |||
| c8f8f02a46 | |||
| 4c5d6f1d6c | |||
| f349e36345 | |||
| 1574c4ab13 | |||
| 0dc9bc3ab6 | |||
| 0006d57a4a | |||
| 70f7e73b9a | |||
| b67b049802 | |||
| ff99dbcd22 | |||
| 0198697737 | |||
| a742a71f67 | |||
| 8f6c1531fa | |||
| d9f56f11cb | |||
| 7ff877464f | |||
| 7c367eb1e8 | |||
| 20848aac09 | |||
| 7716574076 | |||
| 89cd0c769b | |||
| a21b3d788f | |||
| 7994ba8b38 | |||
| 81b4cb2f16 | |||
| 14af1df6a3 | |||
| ac3bd7d7db | |||
| 632e8e9634 | |||
| 1e3f09f69b | |||
| f3f18ffca7 | |||
| ceb3aa0627 | |||
| 014555fe5d | |||
| fdb76867a7 | |||
| 57157fde9c | |||
| 4a3540cf2b | |||
| 540bd938f2 | |||
| c6fbebd996 | |||
| 18d3a7e23a | |||
| cbae355896 | |||
| 924e8a764a | |||
| 40e629cb0e | |||
| 99ec261f79 | |||
| 9daea5bbc2 | |||
| 4704c03d48 | |||
| 9dbf52bedb | |||
| cfd04ab7fa | |||
| 33dbf75e3b | |||
| 8ffaeeb171 | |||
| 451828e5d4 | |||
| 176877f2b8 | |||
| 61b0d08b5f | |||
| f50a6ce1ae | |||
| 8549d6ea8b | |||
| 90a90a87a5 | |||
| 760bdea521 | |||
| 0fd783e83f | |||
| 42caa9058b | |||
| 9ab315ac2c | |||
| a08ad07537 | |||
| c6ce763f7c | |||
| 70d1eb17a7 | |||
| 0393205eda | |||
| 5c05a5a63e | |||
| b01d9f849b | |||
| 0ab904275c | |||
| 5f82a86801 | |||
| 13a5d8de86 | |||
| f024b71808 |
@@ -7,7 +7,7 @@
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"proseWrap": "preserve",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
|
||||
@@ -82,6 +82,10 @@ Please report any problems you've had with the setup instructions!
|
||||
## Code Style
|
||||
|
||||
To make sure the code changes only where it should, we are using prettier to unify the code style.
|
||||
|
||||
- You can format all files at once by running `pnpm prettier` from the project root
|
||||
- Run `pnpm format-check` from the project root to check if all files are well formatted
|
||||
|
||||
If you use VSCode, you can
|
||||
|
||||
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||
@@ -89,6 +93,24 @@ If you use VSCode, you can
|
||||
3. Choose "Configure Default Formatter..."
|
||||
4. Select prettier
|
||||
|
||||
## ESLint
|
||||
|
||||
To prevent unwanted runtime errors, this project uses [eslint](https://eslint.org/).
|
||||
|
||||
- You can check for lint errors by running `pnpm lint`
|
||||
|
||||
There are also eslint extensions / plugins for most editors.
|
||||
|
||||
## Running Tests
|
||||
|
||||
- Run all tests with `pnpm test`
|
||||
- Run all tests with UI using `pnpm test-ui`
|
||||
|
||||
## Running all CI Checks
|
||||
|
||||
When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests.
|
||||
You can run the same check with `pnpm check`
|
||||
|
||||
## Package Workflow
|
||||
|
||||
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
|
||||
@@ -101,7 +123,14 @@ To publish all packages that have been changed since the last release, run:
|
||||
|
||||
```sh
|
||||
npm login
|
||||
npx lerna publish
|
||||
|
||||
# this will increment all the versions in package.json files of non private packages to selected versions
|
||||
npx lerna version --no-private
|
||||
|
||||
# publish all packages inside /packages using pnpm! don't use lerna to publish!!
|
||||
pnpm --filter "./packages/**" publish --dry-run
|
||||
|
||||
# the last command was only a dry-run, make sure everything looks ok, if yes, run the same command without flag
|
||||
```
|
||||
|
||||
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
|
||||
|
||||
@@ -14,8 +14,8 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
|
||||
After cloning the project, you can run the REPL locally:
|
||||
|
||||
```bash
|
||||
npm run setup
|
||||
npm run repl
|
||||
pnpm run setup
|
||||
pnpm run repl
|
||||
```
|
||||
|
||||
## Using Strudel In Your Project
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
public/samples
|
||||
@@ -0,0 +1,22 @@
|
||||
# Usage
|
||||
|
||||
```sh
|
||||
cd iclc2023/slides
|
||||
pnpm i
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- ctrl+click or ctrl+(left|right) to switch slides
|
||||
|
||||
## Samples
|
||||
|
||||
To make sure all the samples are available, clone:
|
||||
|
||||
```sh
|
||||
cd public/samples
|
||||
git clone https://github.com/ritchse/tidal-drum-machines.git
|
||||
git clone https://github.com/tidalcycles/Dirt-Samples.git
|
||||
git clone https://github.com/felixroos/webaudiofontdata.git
|
||||
```
|
||||
|
||||
or use symlinks..
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Slides</title>
|
||||
</head>
|
||||
<body style="background-color: black">
|
||||
<div id="root">
|
||||
<span style="color: white">loading... </span>
|
||||
</div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "slides",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdx-js/rollup": "^2.3.0",
|
||||
"@nanostores/persistent": "^0.7.0",
|
||||
"@nanostores/react": "^0.4.1",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/react": "workspace:*",
|
||||
"@strudel.cycles/soundfonts": "workspace:*",
|
||||
"@strudel.cycles/tonal": "workspace:*",
|
||||
"@strudel.cycles/csound": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"@uiw/codemirror-themes-all": "^4.19.8",
|
||||
"escodegen": "^2.0.0",
|
||||
"nanostores": "^0.7.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"vite-plugin-top-level-await": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.21",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"vite": "^4.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 103 KiB |
@@ -0,0 +1,46 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { atomone } from '@uiw/codemirror-themes-all';
|
||||
import { CodeMirror, flash, useKeydown } from '@strudel.cycles/react';
|
||||
import { initAudioOnFirstClick, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
import { TimeSpan } from '@strudel.cycles/core';
|
||||
|
||||
const initAudio = initAudioOnFirstClick();
|
||||
|
||||
function EventEditor({ code: initialCode }) {
|
||||
const [code, setCode] = useState(initialCode);
|
||||
const [view, setView] = useState();
|
||||
useKeydown(
|
||||
useCallback(
|
||||
async (e) => {
|
||||
if (view?.hasFocus) {
|
||||
if (e.ctrlKey || e.altKey) {
|
||||
if (e.code === 'Enter') {
|
||||
/* if (getAudioContext().state !== 'running') {
|
||||
alert('please click play to initialize the audio. you can use shortcuts after that!');
|
||||
return;
|
||||
} */
|
||||
e.preventDefault();
|
||||
flash(view);
|
||||
let json = JSON.parse(code);
|
||||
if (!Array.isArray(json)) {
|
||||
json = [json];
|
||||
}
|
||||
await initAudio;
|
||||
json.forEach((value) => {
|
||||
const hap = new Hap(new TimeSpan(0, 1), new TimeSpan(0, 1), value);
|
||||
webaudioOutput(hap, 0.01, 1, 1);
|
||||
});
|
||||
} else if (e.key === '.') {
|
||||
stop();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[code, view],
|
||||
),
|
||||
);
|
||||
return <CodeMirror value={code} onChange={setCode} theme={atomone} onViewChanged={(v) => setView(v)} fontSize={32} />;
|
||||
}
|
||||
|
||||
export default EventEditor;
|
||||
@@ -0,0 +1,39 @@
|
||||
import EventEditor from './EventEditor.jsx';
|
||||
import Stepper from './Stepper.jsx';
|
||||
|
||||
const snippets = [
|
||||
[`{ "s": "cp", "crush": 4 }`, 'Samples'],
|
||||
[`{ "s": "sawtooth", "note": "e3" }`, 'Oscillators'],
|
||||
[
|
||||
`[
|
||||
{ "s": "gm_epiano2", "note": "c4" },
|
||||
{ "s": "gm_epiano2", "note": "eb4" },
|
||||
{ "s": "gm_epiano2", "note": "g4" }
|
||||
]`,
|
||||
'Soundfonts',
|
||||
],
|
||||
[
|
||||
`{
|
||||
"s": "toys",
|
||||
"cutoff": 2000,
|
||||
"delay": 0.5,
|
||||
"crush": 4
|
||||
}`,
|
||||
'Effects',
|
||||
],
|
||||
];
|
||||
|
||||
function EventsDemo() {
|
||||
return (
|
||||
<Stepper
|
||||
steps={snippets.map(([code, label]) => (
|
||||
<div className="py-4 space-y-4">
|
||||
<h3>{label}</h3>
|
||||
<EventEditor code={code} />
|
||||
</div>
|
||||
))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventsDemo;
|
||||
@@ -0,0 +1,21 @@
|
||||
//import {Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
//import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
//import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { atomOneDark } from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
||||
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import js from 'react-syntax-highlighter/dist/esm/languages/hljs/javascript';
|
||||
import hs from 'react-syntax-highlighter/dist/esm/languages/hljs/haskell';
|
||||
SyntaxHighlighter.registerLanguage('javascript', js);
|
||||
SyntaxHighlighter.registerLanguage('haskell', hs);
|
||||
|
||||
function Highlight({ code, language = 'javascript' }) {
|
||||
return (
|
||||
<div className="text-[32px] leading-10 rounded-xl overflow-hidden">
|
||||
<SyntaxHighlighter language={language} style={atomOneDark} customStyle={{ padding: '8px 10px' }}>
|
||||
{code}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Highlight;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { SlideRepl } from './SlideRepl.jsx';
|
||||
import Stepper from './Stepper.jsx';
|
||||
|
||||
const snippets = [
|
||||
[`sound("bd [hh sd]")`, `sound(seq("bd", ["hh", "sd"]))`, 'Nested Sequences'], //
|
||||
[`sound("<bd sd>")`, `sound(cat("bd", "sd"))`, 'Cyclewise Sequences'], //
|
||||
[`sound("bd,hh")`, `sound(stack("bd", "hh"))`, 'Stack'], //
|
||||
[`sound("bd@3 sd@1")`, `sound(timeCat([3, "bd"], [1, "sd"]))`, 'Weighted Sequence'], //
|
||||
[`sound("{lt ht mt, bd sd}")`, `sound(polymeter(["lt","ht","mt"], ["bd", "sd"]))`, 'Polymeter'], //
|
||||
[`sound("{bd hh sd ht}%2")`, `sound(polymeterSteps(2, ["bd", "hh", "sd", "ht"]))`, 'Steps per Cycle'],
|
||||
];
|
||||
|
||||
function MiniComparison() {
|
||||
return (
|
||||
<>
|
||||
<h1>User Code: Mini Notation -> JavaScript</h1>
|
||||
<Stepper
|
||||
steps={snippets.map(([a, b, label]) => (
|
||||
<div className="py-4">
|
||||
<h3 className="pb-4">{label}</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex space-x-2">
|
||||
<img src="./img/tidalcycles.svg" className={`h-10 mt-2`} />
|
||||
<SlideRepl tune={a} hideHeader />
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<img src="./img/js.jpg" className={`h-10 mt-2`} />
|
||||
<SlideRepl tune={b} hideHeader />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MiniComparison;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { SlideRepl } from './SlideRepl.jsx';
|
||||
import Highlight from './Highlight.jsx';
|
||||
import Stepper from './Stepper.jsx';
|
||||
|
||||
const snippets = [
|
||||
[
|
||||
<>
|
||||
{' '}
|
||||
<SlideRepl tune={`s("bd [hh sd]").crush(4).log()`} hideHeader />
|
||||
</>,
|
||||
'Logging Events',
|
||||
],
|
||||
[
|
||||
<>
|
||||
<Highlight
|
||||
code={`let pattern = s("bd [hh sd]").crush(4);
|
||||
let events = pattern.queryArc(0, 1);
|
||||
console.log(events.map(e => e.show()))`}
|
||||
language="javascript"
|
||||
/>
|
||||
<Highlight
|
||||
language="json"
|
||||
code={`[
|
||||
"0/1 -> 1/2 | s:bd crush:4",
|
||||
"1/2 -> 3/4 | s:hh crush:4",
|
||||
"3/4 -> 1/1 | s:sd crush:4"
|
||||
]`}
|
||||
/>
|
||||
</>,
|
||||
'Querying Events Manually',
|
||||
],
|
||||
];
|
||||
|
||||
function QueryDemo() {
|
||||
return (
|
||||
<>
|
||||
<img src="./img/queryflow.png" className="w-[800px]" />
|
||||
<Stepper
|
||||
steps={snippets.map(([snippet, label]) => (
|
||||
<div className="py-4 space-y-4">
|
||||
<h3>{label}</h3>
|
||||
{snippet}
|
||||
</div>
|
||||
))}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueryDemo;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { evalScope, controls } from '@strudel.cycles/core';
|
||||
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
||||
import { MiniRepl as _MiniRepl } from '@strudel.cycles/react';
|
||||
import { prebake } from './prebake';
|
||||
import { atomone } from '@uiw/codemirror-themes-all';
|
||||
import blackscreen from '@strudel.cycles/react/src/themes/blackscreen';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
await evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/soundfonts'),
|
||||
import('@strudel.cycles/csound'),
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
initAudioOnFirstClick();
|
||||
prebake();
|
||||
}
|
||||
|
||||
export function SlideRepl({
|
||||
tune,
|
||||
drawTime,
|
||||
punchcard,
|
||||
canvasHeight = 100,
|
||||
hideHeader = false,
|
||||
fontSize = 32,
|
||||
disabled = false,
|
||||
}) {
|
||||
return (
|
||||
<div className="not-prose rounded-xl overflow-hidden">
|
||||
<_MiniRepl
|
||||
hideHeader={hideHeader}
|
||||
tune={tune}
|
||||
hideOutsideView={true}
|
||||
drawTime={drawTime}
|
||||
punchcard={punchcard}
|
||||
canvasHeight={canvasHeight}
|
||||
fontSize={fontSize}
|
||||
theme={disabled ? blackscreen : atomone}
|
||||
// theme={themes[theme]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { persistentAtom } from '@nanostores/persistent';
|
||||
import { useStore } from '@nanostores/react'; // or '@nanostores/preact'
|
||||
import useEvent from './useEvent';
|
||||
|
||||
function cx(...classes) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
// https://vitejs.dev/guide/features.html#glob-import
|
||||
const slideImports = import.meta.glob('./slides/*.mdx');
|
||||
//const slideImports = import.meta.glob('./slides/*.mdx', { eager: true });
|
||||
console.log('load slides...', slideImports);
|
||||
|
||||
const loadedMDXFiles = await Promise.all(
|
||||
Object.entries(slideImports).map(async ([path, load]) => {
|
||||
//Object.entries(slideImports).map(async ([path, m]) => {
|
||||
const segments = path.split('/');
|
||||
const filename = segments[segments.length - 1].slice(0, -4); // expects .mdx at the end
|
||||
return [filename, (await load()).default];
|
||||
//return [filename, m.default];
|
||||
}),
|
||||
);
|
||||
console.log('load done!');
|
||||
|
||||
const order = [
|
||||
'cover',
|
||||
'whatIsStrudel',
|
||||
'history',
|
||||
'hs2js',
|
||||
|
||||
'usage',
|
||||
'swatch',
|
||||
'repl',
|
||||
//'repl-features',
|
||||
'viz',
|
||||
|
||||
'replflow',
|
||||
|
||||
// User Code
|
||||
'transpilation',
|
||||
'mininotation',
|
||||
// 'mini-comparison', // remove?
|
||||
// Patterns
|
||||
'patterns-events', // remove?
|
||||
// scheduler?
|
||||
// Output
|
||||
'events-audio', // remove?
|
||||
'outputs',
|
||||
// 'csound', // remove?
|
||||
//
|
||||
// pattern alignment?
|
||||
'bad',
|
||||
'good',
|
||||
'future',
|
||||
'end',
|
||||
];
|
||||
// examples swatch
|
||||
// draw examples!!
|
||||
|
||||
const slideEntries = order.map((name) => loadedMDXFiles.find(([file]) => file === name));
|
||||
|
||||
// current slide index is persisted, so it's safe to refresh the browser
|
||||
export const slideIndex = persistentAtom('slideIndex', '0');
|
||||
|
||||
export const prev = () => slideIndex.set((parseInt(slideIndex.get()) - 1 + slideEntries.length) % slideEntries.length);
|
||||
export const next = () => slideIndex.set((parseInt(slideIndex.get()) + 1) % slideEntries.length);
|
||||
|
||||
function Slides() {
|
||||
const activeIndex = parseInt(useStore(slideIndex));
|
||||
|
||||
/*useEvent('click', (e) => {
|
||||
if (!e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
const leftHalf = e.clientX / document.body.clientWidth < 0.5;
|
||||
if (leftHalf) {
|
||||
prev();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});*/
|
||||
/*useEvent('keydown', (e) => {
|
||||
if (e.key === 'Home') {
|
||||
prev();
|
||||
} else if (e.key === 'End') {
|
||||
next();
|
||||
}
|
||||
});*/
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-hidden relative">
|
||||
{slideEntries.map(([path, Slide], i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cx(
|
||||
`bg-slate-900 w-full h-full flex justify-center absolute top-0 transition-all ease-in-out duration-500 overflow-auto`,
|
||||
i < activeIndex && '-translate-x-full',
|
||||
i > activeIndex && 'translate-x-full',
|
||||
)}
|
||||
>
|
||||
<div className="prose prose-invert w-[1400px] p-12 max-w-full" style={{ fontSize: '2em' }}>
|
||||
{Math.abs(i - activeIndex) <= 1 && <Slide />}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="text-white text-2xl text-bold cursor-pointer select-none">
|
||||
<div className="fixed left-0 p-4 h-full items-start flex hover:bg-[#00000050]" onClick={() => prev()}>
|
||||
<
|
||||
</div>
|
||||
<div className="fixed right-0 p-4 h-full items-start flex hover:bg-[#00000050]" onClick={() => next()}>
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-0 w-full">
|
||||
<div className="h-[5px] bg-slate-500 w-full z-100">
|
||||
<div
|
||||
style={{ width: ((activeIndex / (slideEntries.length - 1)) * 100).toFixed(0) + '%' }}
|
||||
className="h-full bg-yellow-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Slides;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
function Stepper({ steps }) {
|
||||
const [step, setStep] = useState(1);
|
||||
return (
|
||||
<div className="not-prose">
|
||||
{steps.slice(0, step).map((snippet, i) => {
|
||||
const isActive = i === step - 1;
|
||||
return (
|
||||
<div key={i} className={`border-l-4 pl-8 border-gray-500 ${isActive ? 'border-yellow-500' : ''}`}>
|
||||
{snippet}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{step < steps.length && <button onClick={() => setStep((r) => r + 1)}>next</button>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Stepper;
|
||||
@@ -0,0 +1,73 @@
|
||||
import { SlideRepl } from './SlideRepl.jsx';
|
||||
import Highlight from './Highlight.jsx';
|
||||
import Stepper from './Stepper.jsx';
|
||||
|
||||
const snippets = [
|
||||
[`sound "bd ~ [sd cp]"`, [`sound("bd ~ [sd cp]")`], 'Mini Notation'], //
|
||||
[`sound "bd ~ [sd cp]" # speed "1 2"`, [`sound("bd ~ [sd cp]").speed("1 2")`], 'Composing Patterns'],
|
||||
[
|
||||
`fast 2 $ sound "bd ~ [sd cp]"`,
|
||||
[`fast(2, sound("bd ~ [sd cp]"))`, `sound("bd ~ [sd cp]").fast(2)`],
|
||||
'Patterns Transformations',
|
||||
],
|
||||
[
|
||||
`speed "1 2 3" + "4 5" # s "bd"`,
|
||||
['speed("1 2 3".add("4 5")).s("bd")', '"1 2 3".add("4 5").speed().s("bd")'],
|
||||
'Pattern Arithmetic',
|
||||
],
|
||||
[
|
||||
`jux rev
|
||||
$ every 3 (fast 2)
|
||||
$ sound "bd sd"`,
|
||||
[
|
||||
`sound("bd sd")
|
||||
.every(3, fast("2"))
|
||||
.jux(rev)`,
|
||||
],
|
||||
'Higher order transformations (with partial application)',
|
||||
],
|
||||
[
|
||||
`jux rev
|
||||
$ every "<3 5>" (fast "1 2")
|
||||
$ sound "bd sd cp mt"`,
|
||||
[
|
||||
`sound("bd sd cp mt")
|
||||
.every("<3 5>", fast("1 2"))
|
||||
.jux(rev)`,
|
||||
],
|
||||
'Patterns all the way down',
|
||||
],
|
||||
];
|
||||
|
||||
function SyntaxComparison() {
|
||||
return (
|
||||
<>
|
||||
<h1>Haskell -> JavaScript</h1>
|
||||
<Stepper
|
||||
steps={snippets
|
||||
.map(([hs, js, label]) => (
|
||||
<div className="py-4 space-y-4">
|
||||
<h3>{label}</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex space-x-4">
|
||||
<img src="./img/haskell.png" className="h-10 mt-2" />
|
||||
<Highlight language="haskell" code={hs} />
|
||||
</div>
|
||||
<div>
|
||||
{js.map((c, j) => (
|
||||
<div className="flex space-x-4" key={j}>
|
||||
<img src="./img/js.jpg" className={`h-10 mt-2`} />
|
||||
<SlideRepl tune={c} hideHeader />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
.concat([<>Fluent Interface: Just chain all the things!</>])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SyntaxComparison;
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { atomone } from '@uiw/codemirror-themes-all';
|
||||
import { CodeMirror, flash, useKeydown } from '@strudel.cycles/react';
|
||||
import { transpiler } from '@strudel.cycles/transpiler';
|
||||
import Highlight from './Highlight';
|
||||
import Stepper from './Stepper';
|
||||
import escodegen from 'escodegen';
|
||||
|
||||
function getTranspiled(code) {
|
||||
return transpiler(code, {
|
||||
addReturn: false,
|
||||
codegenOptions: {
|
||||
format: escodegen.FORMAT_MINIFY,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function TranspilationEditor({ code: initialCode }) {
|
||||
const [code, setCode] = useState(initialCode);
|
||||
const [transpiled, setTranspiled] = useState(getTranspiled(initialCode));
|
||||
const [view, setView] = useState();
|
||||
useKeydown(
|
||||
useCallback(
|
||||
async (e) => {
|
||||
if (view?.hasFocus) {
|
||||
if (e.ctrlKey || e.altKey) {
|
||||
if (e.code === 'Enter') {
|
||||
/* if (getAudioContext().state !== 'running') {
|
||||
alert('please click play to initialize the audio. you can use shortcuts after that!');
|
||||
return;
|
||||
} */
|
||||
e.preventDefault();
|
||||
flash(view);
|
||||
const t = getTranspiled(code);
|
||||
setTranspiled(t);
|
||||
console.log('transpiled', t);
|
||||
} else if (e.key === '.') {
|
||||
stop();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[code, view],
|
||||
),
|
||||
);
|
||||
return (
|
||||
<div className="space-y-2 py-2">
|
||||
<CodeMirror
|
||||
value={code}
|
||||
onChange={(v) => {
|
||||
setCode(v);
|
||||
}}
|
||||
theme={atomone}
|
||||
onViewChanged={(v) => setView(v)}
|
||||
fontSize={32}
|
||||
/>
|
||||
<span>⬇️ transpiles to</span>
|
||||
<Highlight language="javascript" code={transpiled} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TranspilationDemo() {
|
||||
return (
|
||||
<div className="not-prose space-y-8">
|
||||
<Stepper
|
||||
steps={[
|
||||
<>
|
||||
<img src="./img/transpileflow.png" className="h-[500px]" />
|
||||
<ul className="list-decimal pl-8">
|
||||
<li>Parse JS Code with acorn to get AST</li>
|
||||
<li>Modify AST with estree-walker</li>
|
||||
<li>Convert modified AST back to JS with escodegen</li>
|
||||
</ul>
|
||||
</>,
|
||||
<TranspilationEditor code={`"bd [hh sd]"`} />,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TranspilationDemo;
|
||||
@@ -0,0 +1,21 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.cm-activeLine {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.cm-theme {
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--lineHighlight);
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.cm-gutters {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
img {
|
||||
@apply rounded-xl overflow-hidden;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import Slides from './Slides';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<Slides />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
import { registerSynthSounds, samples } from '@strudel.cycles/webaudio';
|
||||
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
|
||||
|
||||
export async function prebake() {
|
||||
// https://archive.org/details/SalamanderGrandPianoV3
|
||||
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
|
||||
await Promise.all([
|
||||
registerSynthSounds(),
|
||||
registerSoundfonts(`./samples/webaudiofontdata/sound/`),
|
||||
/* samples(`./piano.json`, `./piano/`, { prebake: true }),
|
||||
samples(`./vcsl.json`, 'github:sgossner/VCSL/master/', { prebake: true }),
|
||||
*/
|
||||
//samples(`./tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/', {
|
||||
samples(`./tidal-drum-machines.json`, './samples/tidal-drum-machines/machines/', {
|
||||
prebake: true,
|
||||
tag: 'drum-machines',
|
||||
}) /*
|
||||
samples(`./EmuSP12.json`, `./EmuSP12/`, { prebake: true, tag: 'drum-machines' }), */,
|
||||
// samples('github:tidalcycles/Dirt-Samples/master'),
|
||||
samples('./Dirt-Samples.json', './samples/Dirt-Samples/'),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# What's bad
|
||||
|
||||
- Dynamic Typing in JS
|
||||
- Fractions and Hot Paths
|
||||
- Verbose function composition
|
||||
- Method-chaining tradeoffs
|
||||
@@ -0,0 +1,7 @@
|
||||
<div className="flex flex-col justify-between h-full overflow-hidden max-h-screen space-y-8">
|
||||
<h2 className="m-0">Strudel: live coding patterns on the Web</h2>
|
||||
<div className="justify-center flex overflow-hidden not-prose">
|
||||
<img src="./img/strudel_icon.png" className="h-full" />
|
||||
</div>
|
||||
<span>Felix Roos, Alex McLean</span>
|
||||
</div>
|
||||
@@ -0,0 +1,65 @@
|
||||
import { SlideRepl } from '../SlideRepl.jsx';
|
||||
|
||||
# CSound Example
|
||||
|
||||
## Loading ORC instruments
|
||||
|
||||
<SlideRepl
|
||||
tune={`// livecode.orc by Steven Yi
|
||||
await loadOrc('github:kunstmusik/csound-live-code/master/livecode.orc')
|
||||
|
||||
stack(
|
||||
note("<C^7 A7 Dm7 Fm7>/2".voicings('lefthand'))
|
||||
.cutoff(sine.range(500,2000).round().slow(16))
|
||||
.euclidLegato(3,8).csound('FM1'),
|
||||
|
||||
note("<C2 A1 D2 F2>/2").ply(8).csound('Bass')
|
||||
.gain("1 4 1 4"),
|
||||
|
||||
note("0 7 [4 3] 2".fast(2/3)
|
||||
.off(".25 .125", add("<2 4 -3 -1>"))
|
||||
.slow(2).scale('A4 minor'))
|
||||
.legato(.25).csound('SynHarp'),
|
||||
|
||||
s("bd*2,[~ hh]*2,~ cp").bank('RolandTR909')
|
||||
)`}
|
||||
hideHeader
|
||||
/>
|
||||
|
||||
## Synthesis
|
||||
|
||||
<SlideRepl
|
||||
tune={`await loadCsound\`
|
||||
instr CoolSynth
|
||||
iduration = p3
|
||||
ifreq = p4
|
||||
igain = p5
|
||||
ioct = octcps(ifreq)
|
||||
|
||||
kpwm = oscili(.05, 8)
|
||||
asig = vco2(igain, ifreq, 4, .5 + kpwm)
|
||||
asig += vco2(igain, ifreq * 2)
|
||||
|
||||
idepth = 2
|
||||
acut = transegr:a(0, .005, 0, idepth, .06, -4.2, 0.001, .01, -4.2, 0) ; filter envelope
|
||||
asig = zdf_2pole(asig, cpsoct(ioct + acut + 2), 0.5)
|
||||
|
||||
iattack = .01
|
||||
isustain = .5
|
||||
idecay = .1
|
||||
irelease = .1
|
||||
asig *= linsegr:a(0, iattack, 1, idecay, isustain, iduration, isustain, irelease, 0)
|
||||
|
||||
out(asig, asig)
|
||||
|
||||
endin\`
|
||||
|
||||
"<0 2 [4 6](3,4,2) 3\*2>"
|
||||
.off(1/4, add(2))
|
||||
.off(1/2, add(6))
|
||||
.scale('D minor')
|
||||
.note()
|
||||
//.pianoroll()
|
||||
.csound('CoolSynth')`}
|
||||
hideHeader
|
||||
/>
|
||||
@@ -0,0 +1 @@
|
||||
# Strudel Docs
|
||||
@@ -0,0 +1,4 @@
|
||||
# Thanks
|
||||
|
||||
- ICLC organizers
|
||||
- Tidal Community
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SlideRepl } from '../SlideRepl.jsx';
|
||||
import Highlight from '../Highlight.jsx';
|
||||
import EventsDemo from '../EventsDemo.jsx';
|
||||
|
||||
# Webaudio Output: Values -> Sound
|
||||
|
||||
<EventsDemo />
|
||||
@@ -0,0 +1,15 @@
|
||||
import { SlideRepl } from '../SlideRepl.jsx';
|
||||
|
||||
# Pattern Example
|
||||
|
||||
<SlideRepl
|
||||
tune={`"<0 2 [4 6](3,4,1) 3>"
|
||||
.off(1/4, add(2))
|
||||
.off(1/2, add(6))
|
||||
.scale('D minor')
|
||||
.legato(.25)
|
||||
.note().s("sawtooth square")
|
||||
.delay(.8).delaytime(.125)`}
|
||||
hideHeader
|
||||
punchcard
|
||||
/>
|
||||
@@ -0,0 +1,10 @@
|
||||
# Future Outlook
|
||||
|
||||
- (slow) community growth
|
||||
- block based evaluation
|
||||
- live collaboration
|
||||
- more audio backends
|
||||
- interface experiments, e.g.:
|
||||
- alternative syntax, visual notations
|
||||
- time scrubbing
|
||||
- ui controls
|
||||
@@ -0,0 +1,9 @@
|
||||
# What's good
|
||||
|
||||
- Zero-install, multi-platform
|
||||
- works (surprisingly) well on mobile devices
|
||||
- visual feedback good for teaching + learning
|
||||
- loads of potential contributors
|
||||
- instant pattern sharing
|
||||
- two-way flow of features between strudel and tidal
|
||||
- full mini-notation in hydra and gibber
|
||||
@@ -0,0 +1,3 @@
|
||||
# The journey
|
||||
|
||||

|
||||
@@ -0,0 +1,3 @@
|
||||
import SyntaxComparison from '../SyntaxComparison.jsx';
|
||||
|
||||
<SyntaxComparison />
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SlideRepl } from '../SlideRepl.jsx';
|
||||
import Highlight from '../Highlight.jsx';
|
||||
import MiniComparison from '../MiniComparison.jsx';
|
||||
|
||||
<MiniComparison />
|
||||
@@ -0,0 +1,29 @@
|
||||
import Highlight from '../Highlight';
|
||||
import Stepper from '../Stepper';
|
||||
|
||||
# User Code: Mini Notation
|
||||
|
||||
<img src="./img/pegflow.png" />
|
||||
|
||||
<Stepper
|
||||
steps={[
|
||||
<ul className="list-decimal pl-8">
|
||||
<li>Parse Mini Notation to get AST</li>
|
||||
<li>walk AST and construct function calls</li>
|
||||
</ul>,
|
||||
<div className="space-y-2 py-2">
|
||||
<Highlight code={`mini('bd [hh sd]')`} />
|
||||
<span>⬇️ resulting calls</span>
|
||||
<Highlight
|
||||
code={`seq(
|
||||
reify('bd').withLocation([1,1,1], [1,4,4]),
|
||||
seq(
|
||||
reify('hh').withLocation([1,5,5], [1,8,8]),
|
||||
reify('sd').withLocation([1,8,8], [1,10,10]),
|
||||
)
|
||||
)`}
|
||||
/>
|
||||
<span>➡️ location can be used for highlighting</span>
|
||||
</div>,
|
||||
]}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
# What can I pattern?
|
||||
|
||||
- Default: Web Audio API
|
||||
- OSC via osc-js, e.g. SuperDirt
|
||||
- Csound via the Csound WebAssembly build
|
||||
- MIDI via WebMIDI
|
||||
- Serial via WebSerial, e.g. for robot dancing
|
||||
@@ -0,0 +1,5 @@
|
||||
import QueryDemo from '../QueryDemo.jsx';
|
||||
|
||||
# Querying: From Patterns to Events
|
||||
|
||||
<QueryDemo />
|
||||
@@ -0,0 +1,12 @@
|
||||
# REPL: Features
|
||||
|
||||
- Visual Feedback
|
||||
- Example shuffler (Hydra-style)
|
||||
- Tutorial + reference
|
||||
- Shareable links
|
||||
- Sound Browser
|
||||
- Offline Support (+ installable PWA)
|
||||
- Outputs: Web Audio, MIDI, OSC, CSound, Serial, ..
|
||||
- Themes
|
||||
|
||||

|
||||
@@ -0,0 +1,7 @@
|
||||
# REPL - live coding editor
|
||||
|
||||
[strudel.tidalcycles.org](https://strudel.tidalcycles.org/)
|
||||
|
||||
{/* <iframe className="w-full h-[600px]" src="https://strudel.tidalcycles.org/#c2FtcGxlcyh7CiAgYmQ6IFsnYmQvQlQwQUFEQS53YXYnLCdiZC9CVDBBQUQwLndhdicsJ2JkL0JUMEEwREEud2F2JywnYmQvQlQwQTBEMy53YXYnLCdiZC9CVDBBMEQwLndhdicsJ2JkL0JUMEEwQTcud2F2J10sCiAgc2Q6IFsnc2Qvcnl0bS0wMS1jbGFzc2ljLndhdicsJ3NkL3J5dG0tMDAtaGFyZC53YXYnXSwKICBoaDogWydoaDI3LzAwMF9oaDI3Y2xvc2VkaGgud2F2JywnaGgvMDAwX2hoM2Nsb3NlZGhoLndhdiddLAp9LCAnZ2l0aHViOnRpZGFsY3ljbGVzL0RpcnQtU2FtcGxlcy9tYXN0ZXIvJyk7CnN0YWNrKAogIHMoImJkLFt%2BIDxzZCEzIHNkKDMsNCwyKT5dLGhoKjQiKSAvLyBkcnVtcwogIC5zcGVlZChwZXJsaW4ucmFuZ2UoLjcsMSkpIC8vIHJhbmRvbSBzYW1wbGUgc3BlZWQgdmFyaWF0aW9uCiAgLy8uaHVzaCgpCiAgLCI8YTEgYjEqMiBhMSgzLDgpIGUyPiIgLy8gYmFzc2xpbmUKICAub2ZmKDEvOCx4PT54LmFkZCgxMikuZGVncmFkZUJ5KC41KSkgLy8gcmFuZG9tIG9jdGF2ZSBqdW1wcwogIC5hZGQocGVybGluLnJhbmdlKDAsLjUpKSAvLyByYW5kb20gcGl0Y2ggdmFyaWF0aW9uCiAgLnN1cGVyaW1wb3NlKGFkZCguMDUpKSAvLyBhZGQgc2Vjb25kLCBzbGlnaHRseSBkZXR1bmVkIHZvaWNlCiAgLm4oKSAvLyB3cmFwIGluICJuIgogIC5kZWNheSguMTUpLnN1c3RhaW4oMCkgLy8gbWFrZSBlYWNoIG5vdGUgb2YgZXF1YWwgbGVuZ3RoCiAgLnMoJ3Nhd3Rvb3RoJykgLy8gd2F2ZWZvcm0KICAuZ2FpbiguNCkgLy8gdHVybiBkb3duCiAgLmN1dG9mZihzaW5lLnNsb3coNykucmFuZ2UoMzAwLDUwMDApKSAvLyBhdXRvbWF0ZSBjdXRvZmYKICAvLy5odXNoKCkKICAsIjxBbTchMyA8RW03IEU3YjEzIEVtNyBFYm03YjU%2BPiIudm9pY2luZ3MoJ2xlZnRoYW5kJykgLy8gY2hvcmRzCiAgLnN1cGVyaW1wb3NlKHg9PnguYWRkKC4wNCkpIC8vIGFkZCBzZWNvbmQsIHNsaWdodGx5IGRldHVuZWQgdm9pY2UKICAuYWRkKHBlcmxpbi5yYW5nZSgwLC41KSkgLy8gcmFuZG9tIHBpdGNoIHZhcmlhdGlvbgogIC5uKCkgLy8gd3JhcCBpbiAibiIKICAucygnc2F3dG9vdGgnKSAvLyB3YXZlZm9ybQogIC5nYWluKC4xNikgLy8gdHVybiBkb3duCiAgLmN1dG9mZig1MDApIC8vIGZpeGVkIGN1dG9mZgogIC5hdHRhY2soMSkgLy8gc2xvd2x5IGZhZGUgaW4KICAvLy5odXNoKCkKICAsImE0IGM1IDxlNiBhNj4iLnN0cnVjdCgieCg1LDgpIikKICAuc3VwZXJpbXBvc2UoeD0%2BeC5hZGQoLjA0KSkgLy8gYWRkIHNlY29uZCwgc2xpZ2h0bHkgZGV0dW5lZCB2b2ljZQogIC5hZGQocGVybGluLnJhbmdlKDAsLjUpKSAvLyByYW5kb20gcGl0Y2ggdmFyaWF0aW9uCiAgLm4oKSAvLyB3cmFwIGluICJuIgogIC5kZWNheSguMSkuc3VzdGFpbigwKSAvLyBtYWtlIG5vdGVzIHNob3J0CiAgLnMoJ3RyaWFuZ2xlJykgLy8gd2F2ZWZvcm0KICAuZGVncmFkZUJ5KHBlcmxpbi5yYW5nZSgwLC41KSkgLy8gcmFuZG9tbHkgY29udHJvbGxlZCByYW5kb20gcmVtb3ZhbCA6KQogIC5lY2hvV2l0aCg0LC4xMjUsKHgsbik9PnguZ2FpbiguMTUqMS8obisxKSkpIC8vIGVjaG8gbm90ZXMKICAvLy5odXNoKCkKKQogIC5zbG93KDEuNCk%3D" /> */}
|
||||
|
||||

|
||||
@@ -0,0 +1,5 @@
|
||||
# REPL: System Overview
|
||||
|
||||
<center>
|
||||
<img src="./img/strudelflow.png" className="w-[900px]" />
|
||||
</center>
|
||||
@@ -0,0 +1,3 @@
|
||||
# Strudel Docs
|
||||
|
||||

|
||||
@@ -0,0 +1,5 @@
|
||||
# Pattern Swatches
|
||||
|
||||
fork strudel - publish your own patterns with a fixed version
|
||||
|
||||

|
||||
@@ -0,0 +1,5 @@
|
||||
import TranspilationDemo from '../TranspilationDemo';
|
||||
|
||||
# User Code: Transpilation
|
||||
|
||||
<TranspilationDemo />
|
||||
@@ -0,0 +1,14 @@
|
||||
import Highlight from '../Highlight.jsx';
|
||||
|
||||
# Ways to use Strudel
|
||||
|
||||
- "Strudel REPL" at [strudel.tidalcycles.org](https://strudel.tidalcycles.org)
|
||||
- strudel on [flok.cc](https://flok.cc/) for collaboration
|
||||
- `@strudel.cycles/*` npm packages for your own projects
|
||||
- Embeddable [Web Component](https://github.com/tidalcycles/strudel/tree/main/packages/embed#strudelcyclesembed)
|
||||
- Browser Plugin for [Mastodon](https://github.com/mortuosplango/topfenstrudel) + Discourse Plugin:
|
||||
|
||||
<div className="flex">
|
||||
<img src="./img/topfenstrudel.png" />
|
||||
<img src="./img/discourse.png" />
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { SlideRepl } from '../SlideRepl.jsx';
|
||||
|
||||
# REPL: Visual Feedback
|
||||
|
||||
Highlighting + Piano Roll
|
||||
|
||||
<SlideRepl
|
||||
hideHeader
|
||||
punchcard
|
||||
canvasHeight={300}
|
||||
fontSize={24}
|
||||
tune={`// Steve Reich - Piano Phase (1967)
|
||||
note("E4 F#4 B4 C#5 D5 F#4 E4 C#5 B4 F#4 D5 C#5")
|
||||
.layer(
|
||||
x=>x.color('#00ff0050').pan(0).fast(1), // left speaker
|
||||
x=>x.color('#ff00ff50').pan(1).fast(1.01) // right speaker
|
||||
)
|
||||
.slow(2).room(.5)
|
||||
`}
|
||||
/>
|
||||
@@ -0,0 +1,28 @@
|
||||
import { SlideRepl } from '../SlideRepl.jsx';
|
||||
|
||||
# What is Strudel
|
||||
|
||||
- TidalCycles faithfully ported to JavaScript
|
||||
- zero-install live coding environment for the browser
|
||||
- free & open source, modular, hackable, sharable, embeddable:
|
||||
|
||||
<SlideRepl
|
||||
hideHeader
|
||||
fontSize={24}
|
||||
tune={`// froos - freakuency - 2023/03/29
|
||||
stack(
|
||||
stack(
|
||||
s("bd, ~ <rim cp>,~ hh*2").off(1/8, x=>x.speed(2).gain(.5)).fast(2),
|
||||
s("oh*6").end(.01).speed(.5).room(.5).fast("1 2")
|
||||
).bank('RolandTR909'),
|
||||
//
|
||||
note("[c a f e](<3 5>,8,<0 1>)".add("0,.1").sub(24)).s('sawtooth').lpf(500),
|
||||
//
|
||||
"[~ <Cm7 Fm7 Bb7 Db7>]*2".voicings('lefthand').note().legato(.2)
|
||||
.echoWith(6, .1, (p,n)=>p.add(note(n*.6)))
|
||||
.lpf(sine.range(300,1000).slow(8)).lpq(6)
|
||||
.add(note(perlin.range(0,.8))).s('gm_epiano2')
|
||||
.mask("<0 1>")
|
||||
).restart("<1@3 1*[4 [8 16]]>")
|
||||
.slow(2)`}
|
||||
/>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
function useEvent(name, onTrigger, useCapture = false) {
|
||||
useEffect(() => {
|
||||
document.addEventListener(name, onTrigger, useCapture);
|
||||
return () => {
|
||||
document.removeEventListener(name, onTrigger, useCapture);
|
||||
};
|
||||
}, [onTrigger]);
|
||||
}
|
||||
|
||||
export default useEvent;
|
||||
@@ -0,0 +1,26 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./index.html',
|
||||
'./src/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'../../packages/react/src/**/*.{html,js,jsx,md,mdx,ts,tsx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// codemirror-theme settings
|
||||
background: 'var(--background)',
|
||||
lineBackground: 'var(--lineBackground)',
|
||||
foreground: 'var(--foreground)',
|
||||
caret: 'var(--caret)',
|
||||
selection: 'var(--selection)',
|
||||
selectionMatch: 'var(--selectionMatch)',
|
||||
gutterBackground: 'var(--gutterBackground)',
|
||||
gutterForeground: 'var(--gutterForeground)',
|
||||
gutterBorder: 'var(--gutterBorder)',
|
||||
lineHighlight: 'var(--lineHighlight)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography')],
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import mdx from '@mdx-js/rollup';
|
||||
import gfm from 'remark-gfm';
|
||||
import topLevelAwait from 'vite-plugin-top-level-await';
|
||||
|
||||
const options = {
|
||||
// See https://mdxjs.com/advanced/plugins
|
||||
remarkPlugins: [gfm],
|
||||
rehypePlugins: [],
|
||||
};
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
mdx(options),
|
||||
topLevelAwait({
|
||||
// The export name of top-level await promise for each chunk module
|
||||
promiseExportName: '__tla',
|
||||
// The function to generate import names of top-level await promise in each chunk module
|
||||
promiseImportName: (i) => `__tla_${i}`,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
target: 'esnext', //browsers can handle the latest ES features
|
||||
},
|
||||
});
|
||||
@@ -15,4 +15,3 @@ export * from './packages/transpiler/index.mjs';
|
||||
export * from './packages/webaudio/index.mjs';
|
||||
export * from './packages/webdirt/index.mjs';
|
||||
export * from './packages/xen/index.mjs';
|
||||
export * from './packages/wam/index.mjs';
|
||||
|
||||
@@ -46,14 +46,15 @@
|
||||
},
|
||||
"homepage": "https://strudel.tidalcycles.org",
|
||||
"dependencies": {
|
||||
"dependency-tree": "^9.0.0",
|
||||
"vitest": "^0.25.7",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/mini": "workspace:*",
|
||||
"@strudel.cycles/tonal": "workspace:*",
|
||||
"@strudel.cycles/transpiler": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"@strudel.cycles/xen": "workspace:*"
|
||||
"@strudel.cycles/xen": "workspace:*",
|
||||
"acorn": "^8.8.1",
|
||||
"dependency-tree": "^9.0.0",
|
||||
"vitest": "^0.25.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/ui": "^0.25.7",
|
||||
@@ -66,7 +67,7 @@
|
||||
"jsdoc": "^3.6.10",
|
||||
"jsdoc-json": "^2.0.2",
|
||||
"jsdoc-to-markdown": "^7.1.1",
|
||||
"lerna": "^4.0.0",
|
||||
"lerna": "^6.5.1",
|
||||
"prettier": "^2.8.1",
|
||||
"rollup-plugin-visualizer": "^5.8.1"
|
||||
}
|
||||
|
||||
@@ -5,19 +5,33 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { Pattern, sequence } from './pattern.mjs';
|
||||
import { zipWith } from './util.mjs';
|
||||
|
||||
const controls = {};
|
||||
const generic_params = [
|
||||
/**
|
||||
* Select a sound / sample by name.
|
||||
* Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters
|
||||
* separated by ':'.
|
||||
*
|
||||
* @name s
|
||||
* @param {string | Pattern} sound The sound / pattern of sounds to pick
|
||||
* @synonyms sound
|
||||
* @example
|
||||
* s("bd hh")
|
||||
* @example
|
||||
* s("bd:0 bd:1 bd:0:0.3 bd:1:1.4")
|
||||
*
|
||||
*/
|
||||
['s', 's', 'sound'],
|
||||
[['s', 'n', 'gain'], 'sound'],
|
||||
/**
|
||||
* Define a custom webaudio node to use as a sound source.
|
||||
*
|
||||
* @name source
|
||||
* @param {function} getSource
|
||||
* @synonyms src
|
||||
*
|
||||
*/
|
||||
['source', 'src'],
|
||||
/**
|
||||
* Selects the given index from the sample map.
|
||||
* Numbers too high will wrap around.
|
||||
@@ -29,7 +43,7 @@ const generic_params = [
|
||||
* s("bd sd,hh*3").n("<0 1>")
|
||||
*/
|
||||
// also see https://github.com/tidalcycles/strudel/pull/63
|
||||
['f', 'n', 'The sample number to choose for a synth or sampleset'],
|
||||
['n'],
|
||||
/**
|
||||
* Plays the given note name or midi number. A note name consists of
|
||||
*
|
||||
@@ -49,10 +63,8 @@ const generic_params = [
|
||||
* @example
|
||||
* note("60 69 65 64")
|
||||
*/
|
||||
['f', 'note', 'The note or pitch to play a sound or synth with'],
|
||||
//['s', 'toArg', 'for internal sound routing'],
|
||||
// ["f", "from", "for internal sound routing"),
|
||||
//['f', 'to', 'for internal sound routing'],
|
||||
[['note', 'n']],
|
||||
|
||||
/**
|
||||
* A pattern of numbers that speed up (or slow down) samples while they play. Currently only supported by osc / superdirt.
|
||||
*
|
||||
@@ -63,7 +75,7 @@ const generic_params = [
|
||||
* s("sax").accelerate("<0 1 2 4 8 16>").slow(2).osc()
|
||||
*
|
||||
*/
|
||||
['f', 'accelerate', 'a pattern of numbers that speed up (or slow down) samples while they play.'],
|
||||
['accelerate'],
|
||||
/**
|
||||
* Controls the gain by an exponential amount.
|
||||
*
|
||||
@@ -73,11 +85,7 @@ const generic_params = [
|
||||
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1")
|
||||
*
|
||||
*/
|
||||
[
|
||||
'f',
|
||||
'gain',
|
||||
'a pattern of numbers that specify volume. Values less than 1 make the sound quieter. Values greater than 1 make the sound louder. For the linear equivalent, see @amp@.',
|
||||
],
|
||||
['gain'],
|
||||
/**
|
||||
* Like {@link gain}, but linear.
|
||||
*
|
||||
@@ -88,17 +96,18 @@ const generic_params = [
|
||||
* s("bd*8").amp(".1*2 .5 .1*2 .5 .1 .5").osc()
|
||||
*
|
||||
*/
|
||||
['f', 'amp', 'like @gain@, but linear.'],
|
||||
['amp'],
|
||||
/**
|
||||
* Amplitude envelope attack time: Specifies how long it takes for the sound to reach its peak value, relative to the onset.
|
||||
*
|
||||
* @name attack
|
||||
* @param {number | Pattern} attack time in seconds.
|
||||
* @synonyms att
|
||||
* @example
|
||||
* note("c3 e3").attack("<0 .1 .5>")
|
||||
*
|
||||
*/
|
||||
['f', 'attack'],
|
||||
['attack', 'att'],
|
||||
|
||||
/**
|
||||
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
|
||||
@@ -109,7 +118,7 @@ const generic_params = [
|
||||
* s("bd sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
|
||||
*
|
||||
*/
|
||||
['f', 'bank', 'selects sound bank to use'],
|
||||
['bank'],
|
||||
|
||||
/**
|
||||
* Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level.
|
||||
@@ -121,52 +130,46 @@ const generic_params = [
|
||||
* note("c3 e3").decay("<.1 .2 .3 .4>").sustain(0)
|
||||
*
|
||||
*/
|
||||
['f', 'decay', ''],
|
||||
['decay'],
|
||||
/**
|
||||
* Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset.
|
||||
*
|
||||
* @name sustain
|
||||
* @param {number | Pattern} gain sustain level between 0 and 1
|
||||
* @synonyms sus
|
||||
* @example
|
||||
* note("c3 e3").decay(.2).sustain("<0 .1 .4 .6 1>")
|
||||
*
|
||||
*/
|
||||
['f', 'sustain', ''],
|
||||
['sustain', 'sus'],
|
||||
/**
|
||||
* Amplitude envelope release time: The time it takes after the offset to go from sustain level to zero.
|
||||
*
|
||||
* @name release
|
||||
* @param {number | Pattern} time release time in seconds
|
||||
* @synonyms rel
|
||||
* @example
|
||||
* note("c3 e3 g3 c4").release("<0 .1 .4 .6 1>/2")
|
||||
*
|
||||
*/
|
||||
[
|
||||
'f',
|
||||
'release',
|
||||
'a pattern of numbers to specify the release time (in seconds) of an envelope applied to each sample.',
|
||||
],
|
||||
[
|
||||
'f',
|
||||
'hold',
|
||||
'a pattern of numbers to specify the hold time (in seconds) of an envelope applied to each sample. Only takes effect if `attack` and `release` are also specified.',
|
||||
],
|
||||
['release', 'rel'],
|
||||
['hold'],
|
||||
// TODO: in tidal, it seems to be normalized
|
||||
/**
|
||||
* Sets the center frequency of the **b**and-**p**ass **f**ilter.
|
||||
* Sets the center frequency of the **b**and-**p**ass **f**ilter. When using mininotation, you
|
||||
* can also optionally supply the 'bpq' parameter separated by ':'.
|
||||
*
|
||||
* @name bpf
|
||||
* @param {number | Pattern} frequency center frequency
|
||||
* @synonyms bandf
|
||||
* @synonyms bandf, bp
|
||||
* @example
|
||||
* s("bd sd,hh*3").bpf("<1000 2000 4000 8000>")
|
||||
*
|
||||
*/
|
||||
['f', 'bpf', ''],
|
||||
['f', 'bandf', 'A pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
|
||||
[['bandf', 'bandq'], 'bpf', 'bp'],
|
||||
// TODO: in tidal, it seems to be normalized
|
||||
/**
|
||||
* Sets the **b**and-**p**ass **q**-factor (resonance)
|
||||
* Sets the **b**and-**p**ass **q**-factor (resonance).
|
||||
*
|
||||
* @name bpq
|
||||
* @param {number | Pattern} q q factor
|
||||
@@ -175,8 +178,9 @@ const generic_params = [
|
||||
* s("bd sd").bpf(500).bpq("<0 1 2 3>")
|
||||
*
|
||||
*/
|
||||
['f', 'bpq', ''],
|
||||
['f', 'bandq', 'a pattern of anumbers from 0 to 1. Sets the q-factor of the band-pass filter.'],
|
||||
// currently an alias of 'bandq' https://github.com/tidalcycles/strudel/issues/496
|
||||
// ['bpq'],
|
||||
['bandq', 'bpq'],
|
||||
/**
|
||||
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
||||
*
|
||||
@@ -188,11 +192,7 @@ const generic_params = [
|
||||
* s("rave").begin("<0 .25 .5 .75>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
'f',
|
||||
'begin',
|
||||
'a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.',
|
||||
],
|
||||
['begin'],
|
||||
/**
|
||||
* The same as .begin, but cuts off the end off each sample.
|
||||
*
|
||||
@@ -203,11 +203,7 @@ const generic_params = [
|
||||
* s("bd*2,oh*4").end("<.1 .2 .5 1>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
'f',
|
||||
'end',
|
||||
'the same as `begin`, but cuts the end off samples, shortening them; e.g. `0.75` to cut off the last quarter of each sample.',
|
||||
],
|
||||
['end'],
|
||||
/**
|
||||
* Loops the sample (from `begin` to `end`) the specified number of times.
|
||||
* Note that the tempo of the loop is not synced with the cycle tempo.
|
||||
@@ -218,7 +214,7 @@ const generic_params = [
|
||||
* s("bd").loop("<1 2 3 4>").osc()
|
||||
*
|
||||
*/
|
||||
['f', 'loop', 'loops the sample (from `begin` to `end`) the specified number of times.'],
|
||||
['loop'],
|
||||
// TODO: currently duplicated with "native" legato
|
||||
// TODO: superdirt legato will do more: https://youtu.be/dQPmE1WaD1k?t=419
|
||||
/**
|
||||
@@ -230,8 +226,8 @@ const generic_params = [
|
||||
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
|
||||
*
|
||||
*/
|
||||
// ['f', 'legato', 'controls the amount of overlap between two adjacent sounds'],
|
||||
// ['f', 'clhatdecay', ''],
|
||||
// ['legato'],
|
||||
// ['clhatdecay'],
|
||||
/**
|
||||
* bit crusher effect.
|
||||
*
|
||||
@@ -241,11 +237,7 @@ const generic_params = [
|
||||
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
'f',
|
||||
'crush',
|
||||
'bit crushing, a pattern of numbers from 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).',
|
||||
],
|
||||
['crush'],
|
||||
/**
|
||||
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
|
||||
*
|
||||
@@ -255,11 +247,7 @@ const generic_params = [
|
||||
* s("bd sd,hh*4").coarse("<1 4 8 16 32>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
'f',
|
||||
'coarse',
|
||||
'fake-resampling, a pattern of numbers for lowering the sample rate, i.e. 1 for original 2 for half, 3 for a third and so on.',
|
||||
],
|
||||
['coarse'],
|
||||
|
||||
/**
|
||||
* choose the channel the pattern is sent to in superdirt
|
||||
@@ -268,7 +256,7 @@ const generic_params = [
|
||||
* @param {number | Pattern} channel channel number
|
||||
*
|
||||
*/
|
||||
['i', 'channel', 'choose the channel the pattern is sent to in superdirt'],
|
||||
['channel'],
|
||||
/**
|
||||
* In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.
|
||||
*
|
||||
@@ -278,35 +266,39 @@ const generic_params = [
|
||||
* s("rd*4").cut(1)
|
||||
*
|
||||
*/
|
||||
[
|
||||
'i',
|
||||
'cut',
|
||||
'In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.',
|
||||
],
|
||||
['cut'],
|
||||
/**
|
||||
* Applies the cutoff frequency of the **l**ow-**p**ass **f**ilter.
|
||||
*
|
||||
* When using mininotation, you can also optionally add the 'lpq' parameter, separated by ':'.
|
||||
*
|
||||
* @name lpf
|
||||
* @param {number | Pattern} frequency audible between 0 and 20000
|
||||
* @synonyms cutoff
|
||||
* @synonyms cutoff, ctf, lp
|
||||
* @example
|
||||
* s("bd sd,hh*3").lpf("<4000 2000 1000 500 200 100>")
|
||||
* @example
|
||||
* s("bd*8").lpf("1000:0 1000:10 1000:20 1000:30")
|
||||
*
|
||||
*/
|
||||
['f', 'lpf'],
|
||||
['f', 'cutoff', 'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the low-pass filter.'],
|
||||
[['cutoff', 'resonance'], 'ctf', 'lpf', 'lp'],
|
||||
/**
|
||||
* Applies the cutoff frequency of the **h**igh-**p**ass **f**ilter.
|
||||
*
|
||||
* When using mininotation, you can also optionally add the 'hpq' parameter, separated by ':'.
|
||||
*
|
||||
* @name hpf
|
||||
* @param {number | Pattern} frequency audible between 0 and 20000
|
||||
* @synonyms hcutoff
|
||||
* @synonyms hp, hcutoff
|
||||
* @example
|
||||
* s("bd sd,hh*4").hpf("<4000 2000 1000 500 200 100>")
|
||||
* @example
|
||||
* s("bd sd,hh*4").hpf("<2000 2000:25>")
|
||||
*
|
||||
*/
|
||||
['f', 'hpf', ''],
|
||||
['f', 'hcutoff', ''],
|
||||
// currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496
|
||||
// ['hpf'],
|
||||
[['hcutoff', 'hresonance'], 'hpf', 'hp'],
|
||||
/**
|
||||
* Controls the **h**igh-**p**ass **q**-value.
|
||||
*
|
||||
@@ -317,8 +309,7 @@ const generic_params = [
|
||||
* s("bd sd,hh*4").hpf(2000).hpq("<0 10 20 30>")
|
||||
*
|
||||
*/
|
||||
['f', 'hresonance', ''],
|
||||
['f', 'hpq', ''],
|
||||
['hresonance', 'hpq'],
|
||||
/**
|
||||
* Controls the **l**ow-**p**ass **q**-value.
|
||||
*
|
||||
@@ -329,8 +320,8 @@ const generic_params = [
|
||||
* s("bd sd,hh*4").lpf(2000).lpq("<0 10 20 30>")
|
||||
*
|
||||
*/
|
||||
['f', 'lpq'],
|
||||
['f', 'resonance', ''],
|
||||
// currently an alias of 'resonance' https://github.com/tidalcycles/strudel/issues/496
|
||||
['resonance', 'lpq'],
|
||||
/**
|
||||
* DJ filter, below 0.5 is low pass filter, above is high pass filter.
|
||||
*
|
||||
@@ -340,40 +331,48 @@ const generic_params = [
|
||||
* n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc()
|
||||
*
|
||||
*/
|
||||
['f', 'djf', 'DJ filter, below 0.5 is low pass filter, above is high pass filter.'],
|
||||
// ['f', 'cutoffegint', ''],
|
||||
['djf'],
|
||||
// ['cutoffegint'],
|
||||
// TODO: does not seem to work
|
||||
/**
|
||||
* Sets the level of the delay signal.
|
||||
*
|
||||
* When using mininotation, you can also optionally add the 'delaytime' and 'delayfeedback' parameter,
|
||||
* separated by ':'.
|
||||
*
|
||||
*
|
||||
* @name delay
|
||||
* @param {number | Pattern} level between 0 and 1
|
||||
* @example
|
||||
* s("bd").delay("<0 .25 .5 1>")
|
||||
* @example
|
||||
* s("bd bd").delay("0.65:0.25:0.9 0.65:0.125:0.7")
|
||||
*
|
||||
*/
|
||||
['f', 'delay', 'a pattern of numbers from 0 to 1. Sets the level of the delay signal.'],
|
||||
[['delay', 'delaytime', 'delayfeedback']],
|
||||
/**
|
||||
* Sets the level of the signal that is fed back into the delay.
|
||||
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
|
||||
*
|
||||
* @name delayfeedback
|
||||
* @param {number | Pattern} feedback between 0 and 1
|
||||
* @synonyms delayfb, dfb
|
||||
* @example
|
||||
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>").slow(2)
|
||||
*
|
||||
*/
|
||||
['f', 'delayfeedback', 'a pattern of numbers from 0 to 1. Sets the amount of delay feedback.'],
|
||||
['delayfeedback', 'delayfb', 'dfb'],
|
||||
/**
|
||||
* Sets the time of the delay effect.
|
||||
*
|
||||
* @name delaytime
|
||||
* @param {number | Pattern} seconds between 0 and Infinity
|
||||
* @synonyms delayt, dt
|
||||
* @example
|
||||
* s("bd").delay(.25).delaytime("<.125 .25 .5 1>").slow(2)
|
||||
*
|
||||
*/
|
||||
['f', 'delaytime', 'a pattern of numbers from 0 to 1. Sets the length of the delay.'],
|
||||
['delaytime', 'delayt', 'dt'],
|
||||
/* // TODO: test
|
||||
* Specifies whether delaytime is calculated relative to cps.
|
||||
*
|
||||
@@ -383,22 +382,19 @@ const generic_params = [
|
||||
* s("sd").delay().lock(1).osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
'f',
|
||||
'lock',
|
||||
'A pattern of numbers. Specifies whether delaytime is calculated relative to cps. When set to 1, delaytime is a direct multiple of a cycle.',
|
||||
],
|
||||
['lock'],
|
||||
/**
|
||||
* Set detune of oscillators. Works only with some synths, see <a target="_blank" href="https://tidalcycles.org/docs/patternlib/tutorials/synthesizers">tidal doc</a>
|
||||
*
|
||||
* @name detune
|
||||
* @param {number | Pattern} amount between 0 and 1
|
||||
* @synonyms det
|
||||
* @superdirtOnly
|
||||
* @example
|
||||
* n("0 3 7").s('superzow').octave(3).detune("<0 .25 .5 1 2>").osc()
|
||||
*
|
||||
*/
|
||||
['f', 'detune', ''],
|
||||
['detune', 'det'],
|
||||
/**
|
||||
* Set dryness of reverb. See {@link room} and {@link size} for more information about reverb.
|
||||
*
|
||||
@@ -409,11 +405,7 @@ const generic_params = [
|
||||
* @superdirtOnly
|
||||
*
|
||||
*/
|
||||
[
|
||||
'f',
|
||||
'dry',
|
||||
'when set to `1` will disable all reverb for this pattern. See `room` and `size` for more information about reverb.',
|
||||
],
|
||||
['dry'],
|
||||
// TODO: does not seem to do anything
|
||||
/*
|
||||
* Used when using {@link begin}/{@link end} or {@link chop}/{@link striate} and friends, to change the fade out time of the 'grain' envelope.
|
||||
@@ -424,17 +416,9 @@ const generic_params = [
|
||||
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
'f',
|
||||
'fadeTime',
|
||||
"Used when using begin/end or chop/striate and friends, to change the fade out time of the 'grain' envelope.",
|
||||
],
|
||||
['fadeTime', 'fadeOutTime'],
|
||||
// TODO: see above
|
||||
[
|
||||
'f',
|
||||
'fadeInTime',
|
||||
'As with fadeTime, but controls the fade in time of the grain envelope. Not used if the grain begins at position 0 in the sample.',
|
||||
],
|
||||
['fadeInTime'],
|
||||
/**
|
||||
* Set frequency of sound.
|
||||
*
|
||||
@@ -446,15 +430,15 @@ const generic_params = [
|
||||
* freq("110".mul.out(".5 1.5 .6 [2 3]")).s("superzow").osc()
|
||||
*
|
||||
*/
|
||||
['f', 'freq', ''],
|
||||
['freq'],
|
||||
// TODO: https://tidalcycles.org/docs/configuration/MIDIOSC/control-voltage/#gate
|
||||
['f', 'gate', ''],
|
||||
// ['f', 'hatgrain', ''],
|
||||
// ['f', 'lagogo', ''],
|
||||
// ['f', 'lclap', ''],
|
||||
// ['f', 'lclaves', ''],
|
||||
// ['f', 'lclhat', ''],
|
||||
// ['f', 'lcrash', ''],
|
||||
['gate', 'gat'],
|
||||
// ['hatgrain'],
|
||||
// ['lagogo'],
|
||||
// ['lclap'],
|
||||
// ['lclaves'],
|
||||
// ['lclhat'],
|
||||
// ['lcrash'],
|
||||
// TODO:
|
||||
// https://tidalcycles.org/docs/reference/audio_effects/#leslie-1
|
||||
// https://tidalcycles.org/docs/reference/audio_effects/#leslie
|
||||
@@ -468,7 +452,7 @@ const generic_params = [
|
||||
* @superdirtOnly
|
||||
*
|
||||
*/
|
||||
['f', 'leslie', ''],
|
||||
['leslie'],
|
||||
/**
|
||||
* Rate of modulation / rotation for leslie effect
|
||||
*
|
||||
@@ -480,7 +464,7 @@ const generic_params = [
|
||||
*
|
||||
*/
|
||||
// TODO: the rate seems to "lag" (in the example, 1 will be fast)
|
||||
['f', 'lrate', ''],
|
||||
['lrate'],
|
||||
/**
|
||||
* Physical size of the cabinet in meters. Be careful, it might be slightly larger than your computer. Affects the Doppler amount (pitch warble)
|
||||
*
|
||||
@@ -491,31 +475,28 @@ const generic_params = [
|
||||
* @superdirtOnly
|
||||
*
|
||||
*/
|
||||
['f', 'lsize', ''],
|
||||
// ['f', 'lfo', ''],
|
||||
// ['f', 'lfocutoffint', ''],
|
||||
// ['f', 'lfodelay', ''],
|
||||
// ['f', 'lfoint', ''],
|
||||
// ['f', 'lfopitchint', ''],
|
||||
// ['f', 'lfoshape', ''],
|
||||
// ['f', 'lfosync', ''],
|
||||
// ['f', 'lhitom', ''],
|
||||
// ['f', 'lkick', ''],
|
||||
// ['f', 'llotom', ''],
|
||||
// ['f', 'lophat', ''],
|
||||
// ['f', 'lsnare', ''],
|
||||
['f', 'degree', ''], // TODO: what is this? not found in tidal doc
|
||||
['f', 'mtranspose', ''], // TODO: what is this? not found in tidal doc
|
||||
['f', 'ctranspose', ''], // TODO: what is this? not found in tidal doc
|
||||
['f', 'harmonic', ''], // TODO: what is this? not found in tidal doc
|
||||
['f', 'stepsPerOctave', ''], // TODO: what is this? not found in tidal doc
|
||||
['f', 'octaveR', ''], // TODO: what is this? not found in tidal doc
|
||||
// TODO: why is this needed? what's the difference to late / early?
|
||||
[
|
||||
'f',
|
||||
'nudge',
|
||||
'Nudges events into the future by the specified number of seconds. Negative numbers work up to a point as well (due to internal latency)',
|
||||
],
|
||||
['lsize'],
|
||||
// ['lfo'],
|
||||
// ['lfocutoffint'],
|
||||
// ['lfodelay'],
|
||||
// ['lfoint'],
|
||||
// ['lfopitchint'],
|
||||
// ['lfoshape'],
|
||||
// ['lfosync'],
|
||||
// ['lhitom'],
|
||||
// ['lkick'],
|
||||
// ['llotom'],
|
||||
// ['lophat'],
|
||||
// ['lsnare'],
|
||||
['degree'], // TODO: what is this? not found in tidal doc
|
||||
['mtranspose'], // TODO: what is this? not found in tidal doc
|
||||
['ctranspose'], // TODO: what is this? not found in tidal doc
|
||||
['harmonic'], // TODO: what is this? not found in tidal doc
|
||||
['stepsPerOctave'], // TODO: what is this? not found in tidal doc
|
||||
['octaveR'], // TODO: what is this? not found in tidal doc
|
||||
// TODO: why is this needed? what's the difference to late / early? Answer: it's in seconds, and delays the message at
|
||||
// OSC time (so can't be negative, at least not beyond the latency value)
|
||||
['nudge'],
|
||||
// TODO: the following doc is just a guess, it's not documented in tidal doc.
|
||||
/**
|
||||
* Sets the default octave of a synth.
|
||||
@@ -526,9 +507,9 @@ const generic_params = [
|
||||
* n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc()
|
||||
* @superDirtOnly
|
||||
*/
|
||||
['i', 'octave', ''],
|
||||
['f', 'offset', ''], // TODO: what is this? not found in tidal doc
|
||||
// ['f', 'ophatdecay', ''],
|
||||
['octave'],
|
||||
['offset'], // TODO: what is this? not found in tidal doc
|
||||
// ['ophatdecay'],
|
||||
// TODO: example
|
||||
/**
|
||||
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
|
||||
@@ -541,13 +522,9 @@ const generic_params = [
|
||||
* s("~ sd").delay(.5).delaytime(.125).orbit(2)
|
||||
* )
|
||||
*/
|
||||
[
|
||||
'i',
|
||||
'orbit',
|
||||
'a pattern of numbers. An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share hardware output bus offset and global effects, e.g. reverb and delay. The maximum number of orbits is specified in the superdirt startup, numbers higher than maximum will wrap around.',
|
||||
],
|
||||
['f', 'overgain', ''], // TODO: what is this? not found in tidal doc
|
||||
['f', 'overshape', ''], // TODO: what is this? not found in tidal doc
|
||||
['orbit'],
|
||||
['overgain'], // TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that
|
||||
['overshape'], // TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
|
||||
/**
|
||||
* Sets position in stereo.
|
||||
*
|
||||
@@ -557,11 +534,7 @@ const generic_params = [
|
||||
* s("[bd hh]*2").pan("<.5 1 .5 0>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
'f',
|
||||
'pan',
|
||||
'a pattern of numbers between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)',
|
||||
],
|
||||
['pan'],
|
||||
// TODO: this has no effect (see example)
|
||||
/*
|
||||
* Controls how much multichannel output is fanned out
|
||||
@@ -572,11 +545,7 @@ const generic_params = [
|
||||
* s("[bd hh]*2").pan("<.5 1 .5 0>").panspan("<0 .5 1>").osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
'f',
|
||||
'panspan',
|
||||
'a pattern of numbers between -inf and inf, which controls how much multichannel output is fanned out (negative is backwards ordering)',
|
||||
],
|
||||
['panspan'],
|
||||
// TODO: this has no effect (see example)
|
||||
/*
|
||||
* Controls how much multichannel output is spread
|
||||
@@ -587,50 +556,42 @@ const generic_params = [
|
||||
* s("[bd hh]*2").pan("<.5 1 .5 0>").pansplay("<0 .5 1>").osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
'f',
|
||||
'pansplay',
|
||||
'a pattern of numbers between 0.0 and 1.0, which controls the multichannel spread range (multichannel only)',
|
||||
],
|
||||
[
|
||||
'f',
|
||||
'panwidth',
|
||||
'a pattern of numbers between 0.0 and inf, which controls how much each channel is distributed over neighbours (multichannel only)',
|
||||
],
|
||||
[
|
||||
'f',
|
||||
'panorient',
|
||||
'a pattern of numbers between -1.0 and 1.0, which controls the relative position of the centre pan in a pair of adjacent speakers (multichannel only)',
|
||||
],
|
||||
// ['f', 'pitch1', ''],
|
||||
// ['f', 'pitch2', ''],
|
||||
// ['f', 'pitch3', ''],
|
||||
// ['f', 'portamento', ''],
|
||||
['pansplay'],
|
||||
['panwidth'],
|
||||
['panorient'],
|
||||
// ['pitch1'],
|
||||
// ['pitch2'],
|
||||
// ['pitch3'],
|
||||
// ['portamento'],
|
||||
// TODO: LFO rate see https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
||||
['f', 'rate', "used in SuperDirt softsynths as a control rate or 'speed'"],
|
||||
['rate'],
|
||||
// TODO: slide param for certain synths
|
||||
['f', 'slide', ''],
|
||||
['slide'],
|
||||
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
||||
['f', 'semitone', ''],
|
||||
['semitone'],
|
||||
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
|
||||
// ['f', 'velocity', ''],
|
||||
['f', 'voice', ''], // TODO: synth param
|
||||
// ['velocity'],
|
||||
['voice'], // TODO: synth param
|
||||
/**
|
||||
* Sets the level of reverb.
|
||||
*
|
||||
* When using mininotation, you can also optionally add the 'size' parameter, separated by ':'.
|
||||
*
|
||||
* @name room
|
||||
* @param {number | Pattern} level between 0 and 1
|
||||
* @example
|
||||
* s("bd sd").room("<0 .2 .4 .6 .8 1>")
|
||||
* @example
|
||||
* s("bd sd").room("<0.9:1 0.9:4>")
|
||||
*
|
||||
*/
|
||||
['f', 'room', 'a pattern of numbers from 0 to 1. Sets the level of reverb.'],
|
||||
[['room', 'size']],
|
||||
/**
|
||||
* Sets the room size of the reverb, see {@link room}.
|
||||
*
|
||||
* @name roomsize
|
||||
* @synonyms size
|
||||
* @param {number | Pattern} size between 0 and 10
|
||||
* @synonyms size, sz
|
||||
* @example
|
||||
* s("bd sd").room(.8).roomsize("<0 1 2 4 8>")
|
||||
*
|
||||
@@ -638,20 +599,11 @@ const generic_params = [
|
||||
// TODO: find out why :
|
||||
// s("bd sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc()
|
||||
// .. does not work. Is it because room is only one effect?
|
||||
[
|
||||
'f',
|
||||
'size',
|
||||
'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.',
|
||||
],
|
||||
[
|
||||
'f',
|
||||
'roomsize',
|
||||
'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.',
|
||||
],
|
||||
// ['f', 'sagogo', ''],
|
||||
// ['f', 'sclap', ''],
|
||||
// ['f', 'sclaves', ''],
|
||||
// ['f', 'scrash', ''],
|
||||
['size', 'sz', 'roomsize'],
|
||||
// ['sagogo'],
|
||||
// ['sclap'],
|
||||
// ['sclaves'],
|
||||
// ['scrash'],
|
||||
/**
|
||||
* Wave shaping distortion. CAUTION: it might get loud
|
||||
*
|
||||
@@ -661,11 +613,7 @@ const generic_params = [
|
||||
* s("bd sd,hh*4").shape("<0 .2 .4 .6 .8>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
'f',
|
||||
'shape',
|
||||
'wave shaping distortion, a pattern of numbers from 0 for no distortion up to 1 for loads of distortion.',
|
||||
],
|
||||
['shape'],
|
||||
/**
|
||||
* Changes the speed of sample playback, i.e. a cheap way of changing pitch.
|
||||
*
|
||||
@@ -677,11 +625,7 @@ const generic_params = [
|
||||
* speed("1 1.5*2 [2 1.1]").s("piano").clip(1)
|
||||
*
|
||||
*/
|
||||
[
|
||||
'f',
|
||||
'speed',
|
||||
'a pattern of numbers which changes the speed of sample playback, i.e. a cheap way of changing pitch. Negative values will play the sample backwards!',
|
||||
],
|
||||
['speed'],
|
||||
/**
|
||||
* Used in conjunction with {@link speed}, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`.
|
||||
*
|
||||
@@ -692,11 +636,7 @@ const generic_params = [
|
||||
* @superdirtOnly
|
||||
*
|
||||
*/
|
||||
[
|
||||
's',
|
||||
'unit',
|
||||
'used in conjunction with `speed`, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`.',
|
||||
],
|
||||
['unit'],
|
||||
/**
|
||||
* Made by Calum Gunn. Reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter. The SuperCollider manual defines Squiz as:
|
||||
*
|
||||
@@ -709,14 +649,14 @@ const generic_params = [
|
||||
* @superdirtOnly
|
||||
*
|
||||
*/
|
||||
['f', 'squiz', ''],
|
||||
['f', 'stutterdepth', ''], // TODO: what is this? not found in tidal doc
|
||||
['f', 'stuttertime', ''], // TODO: what is this? not found in tidal doc
|
||||
['f', 'timescale', ''], // TODO: what is this? not found in tidal doc
|
||||
['f', 'timescalewin', ''], // TODO: what is this? not found in tidal doc
|
||||
// ['f', 'tomdecay', ''],
|
||||
// ['f', 'vcfegint', ''],
|
||||
// ['f', 'vcoegint', ''],
|
||||
['squiz'],
|
||||
// ['stutterdepth'], // TODO: what is this? not found in tidal doc
|
||||
// ['stuttertime'], // TODO: what is this? not found in tidal doc
|
||||
// ['timescale'], // TODO: what is this? not found in tidal doc
|
||||
// ['timescalewin'], // TODO: what is this? not found in tidal doc
|
||||
// ['tomdecay'],
|
||||
// ['vcfegint'],
|
||||
// ['vcoegint'],
|
||||
// TODO: Use a rest (~) to override the effect <- vowel
|
||||
/**
|
||||
*
|
||||
@@ -729,11 +669,7 @@ const generic_params = [
|
||||
* .vowel("<a e i <o u>>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
's',
|
||||
'vowel',
|
||||
'formant filter to make things sound like vowels, a pattern of either `a`, `e`, `i`, `o` or `u`. Use a rest (`~`) for no effect.',
|
||||
],
|
||||
['vowel'],
|
||||
/* // TODO: find out how it works
|
||||
* Made by Calum Gunn. Divides an audio stream into tiny segments, using the signal's zero-crossings as segment boundaries, and discards a fraction of them. Takes a number between 1 and 100, denoted the percentage of segments to drop. The SuperCollider manual describes the Waveloss effect this way:
|
||||
*
|
||||
@@ -744,12 +680,12 @@ const generic_params = [
|
||||
*
|
||||
* @name waveloss
|
||||
*/
|
||||
['f', 'waveloss', ''],
|
||||
['waveloss'],
|
||||
// TODO: midi effects?
|
||||
['f', 'dur', ''],
|
||||
// ['f', 'modwheel', ''],
|
||||
['f', 'expression', ''],
|
||||
['f', 'sustainpedal', ''],
|
||||
['dur'],
|
||||
// ['modwheel'],
|
||||
['expression'],
|
||||
['sustainpedal'],
|
||||
/* // TODO: doesn't seem to do anything
|
||||
*
|
||||
* Tremolo Audio DSP effect
|
||||
@@ -760,59 +696,58 @@ const generic_params = [
|
||||
* n("0,4,7").tremolodepth("<0 .3 .6 .9>").osc()
|
||||
*
|
||||
*/
|
||||
// TODO: tremdp alias
|
||||
['f', 'tremolodepth', "Tremolo Audio DSP effect | params are 'tremolorate' and 'tremolodepth'"],
|
||||
['f', 'tremolorate', "Tremolo Audio DSP effect | params are 'tremolorate' and 'tremolodepth'"],
|
||||
['tremolodepth', 'tremdp'],
|
||||
['tremolorate', 'tremr'],
|
||||
// TODO: doesn't seem to do anything
|
||||
['f', 'phaserdepth', "Phaser Audio DSP effect | params are 'phaserrate' and 'phaserdepth'"],
|
||||
['f', 'phaserrate', "Phaser Audio DSP effect | params are 'phaserrate' and 'phaserdepth'"],
|
||||
['phaserdepth', 'phasdp'],
|
||||
['phaserrate', 'phasr'],
|
||||
|
||||
['f', 'fshift', 'frequency shifter'],
|
||||
['f', 'fshiftnote', 'frequency shifter'],
|
||||
['f', 'fshiftphase', 'frequency shifter'],
|
||||
['fshift'],
|
||||
['fshiftnote'],
|
||||
['fshiftphase'],
|
||||
|
||||
['f', 'triode', 'tube distortion'],
|
||||
['f', 'krush', 'shape/bass enhancer'],
|
||||
['f', 'kcutoff', ''],
|
||||
['f', 'octer', 'octaver effect'],
|
||||
['f', 'octersub', 'octaver effect'],
|
||||
['f', 'octersubsub', 'octaver effect'],
|
||||
['f', 'ring', 'ring modulation'],
|
||||
['f', 'ringf', 'ring modulation'],
|
||||
['f', 'ringdf', 'ring modulation'],
|
||||
['f', 'distort', 'noisy fuzzy distortion'],
|
||||
['f', 'freeze', 'Spectral freeze'],
|
||||
['f', 'xsdelay', ''],
|
||||
['f', 'tsdelay', ''],
|
||||
['f', 'real', 'Spectral conform'],
|
||||
['f', 'imag', ''],
|
||||
['f', 'enhance', 'Spectral enhance'],
|
||||
['f', 'partials', ''],
|
||||
['f', 'comb', 'Spectral comb'],
|
||||
['f', 'smear', 'Spectral smear'],
|
||||
['f', 'scram', 'Spectral scramble'],
|
||||
['f', 'binshift', 'Spectral binshift'],
|
||||
['f', 'hbrick', 'High pass sort of spectral filter'],
|
||||
['f', 'lbrick', 'Low pass sort of spectral filter'],
|
||||
['f', 'midichan', ''],
|
||||
['f', 'control', ''],
|
||||
['f', 'ccn', ''],
|
||||
['f', 'ccv', ''],
|
||||
['f', 'polyTouch', ''],
|
||||
['f', 'midibend', ''],
|
||||
['f', 'miditouch', ''],
|
||||
['f', 'ctlNum', ''],
|
||||
['f', 'frameRate', ''],
|
||||
['f', 'frames', ''],
|
||||
['f', 'hours', ''],
|
||||
['s', 'midicmd', ''],
|
||||
['f', 'minutes', ''],
|
||||
['f', 'progNum', ''],
|
||||
['f', 'seconds', ''],
|
||||
['f', 'songPtr', ''],
|
||||
['f', 'uid', ''],
|
||||
['f', 'val', ''],
|
||||
['f', 'cps', ''],
|
||||
['triode'],
|
||||
['krush'],
|
||||
['kcutoff'],
|
||||
['octer'],
|
||||
['octersub'],
|
||||
['octersubsub'],
|
||||
['ring'],
|
||||
['ringf'],
|
||||
['ringdf'],
|
||||
['distort'],
|
||||
['freeze'],
|
||||
['xsdelay'],
|
||||
['tsdelay'],
|
||||
['real'],
|
||||
['imag'],
|
||||
['enhance'],
|
||||
['partials'],
|
||||
['comb'],
|
||||
['smear'],
|
||||
['scram'],
|
||||
['binshift'],
|
||||
['hbrick'],
|
||||
['lbrick'],
|
||||
['midichan'],
|
||||
['control'],
|
||||
['ccn'],
|
||||
['ccv'],
|
||||
['polyTouch'],
|
||||
['midibend'],
|
||||
['miditouch'],
|
||||
['ctlNum'],
|
||||
['frameRate'],
|
||||
['frames'],
|
||||
['hours'],
|
||||
['midicmd'],
|
||||
['minutes'],
|
||||
['progNum'],
|
||||
['seconds'],
|
||||
['songPtr'],
|
||||
['uid'],
|
||||
['val'],
|
||||
['cps'],
|
||||
/**
|
||||
* If set to 1, samples will be cut to the duration of their event.
|
||||
* In tidal, this would be done with legato, which [is about to land in strudel too](https://github.com/tidalcycles/strudel/issues/111)
|
||||
@@ -823,33 +758,55 @@ const generic_params = [
|
||||
* note("c a f e ~").s("piano").clip(1)
|
||||
*
|
||||
*/
|
||||
['f', 'clip', ''],
|
||||
['clip'],
|
||||
];
|
||||
|
||||
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
|
||||
|
||||
const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
|
||||
controls.createParam = function (names) {
|
||||
const name = Array.isArray(names) ? names[0] : names;
|
||||
|
||||
const _setter = (func, name) =>
|
||||
function (...pats) {
|
||||
var withVal;
|
||||
if (Array.isArray(names)) {
|
||||
withVal = (xs) => {
|
||||
if (Array.isArray(xs)) {
|
||||
const result = {};
|
||||
xs.forEach((x, i) => {
|
||||
if (i < names.length) {
|
||||
result[names[i]] = x;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
} else {
|
||||
return { [name]: xs };
|
||||
}
|
||||
};
|
||||
} else {
|
||||
withVal = (x) => ({ [name]: x });
|
||||
}
|
||||
|
||||
const func = (...pats) => sequence(...pats).withValue(withVal);
|
||||
|
||||
const setter = function (...pats) {
|
||||
if (!pats.length) {
|
||||
return this.fmap((value) => ({ [name]: value }));
|
||||
return this.fmap(withVal);
|
||||
}
|
||||
return this.set(func(...pats));
|
||||
};
|
||||
|
||||
generic_params.forEach(([type, name, description]) => {
|
||||
controls[name] = (...pats) => _name(name, ...pats);
|
||||
Pattern.prototype[name] = _setter(controls[name], name);
|
||||
});
|
||||
|
||||
// create custom param
|
||||
controls.createParam = (name) => {
|
||||
const func = (...pats) => _name(name, ...pats);
|
||||
Pattern.prototype[name] = _setter(func, name);
|
||||
return (...pats) => _name(name, ...pats);
|
||||
Pattern.prototype[name] = setter;
|
||||
return func;
|
||||
};
|
||||
|
||||
generic_params.forEach(([names, ...aliases]) => {
|
||||
const name = Array.isArray(names) ? names[0] : names;
|
||||
controls[name] = controls.createParam(names);
|
||||
|
||||
aliases.forEach((alias) => {
|
||||
controls[alias] = controls[name];
|
||||
Pattern.prototype[alias] = Pattern.prototype[name];
|
||||
});
|
||||
});
|
||||
|
||||
controls.createParams = (...names) =>
|
||||
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
|
||||
|
||||
|
||||
@@ -10,44 +10,49 @@ import { logger } from './logger.mjs';
|
||||
export class Cyclist {
|
||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||
this.started = false;
|
||||
this.cps = 1; // TODO
|
||||
this.phase = 0;
|
||||
this.getTime = getTime;
|
||||
this.cps = 1;
|
||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||
this.lastBegin = 0; // query begin of last tick
|
||||
this.lastEnd = 0; // query end of last tick
|
||||
this.getTime = getTime; // get absolute time
|
||||
this.onToggle = onToggle;
|
||||
this.latency = latency;
|
||||
this.latency = latency; // fixed trigger time offset
|
||||
const round = (x) => Math.round(x * 1000) / 1000;
|
||||
this.clock = createClock(
|
||||
getTime,
|
||||
// called slightly before each cycle
|
||||
(phase, duration, tick) => {
|
||||
if (tick === 0) {
|
||||
this.origin = phase;
|
||||
}
|
||||
const begin = round(phase - this.origin);
|
||||
this.phase = begin - latency;
|
||||
const end = round(begin + duration);
|
||||
const time = getTime();
|
||||
try {
|
||||
const haps = this.pattern.queryArc(begin, end); // get Haps
|
||||
const time = getTime();
|
||||
const begin = this.lastEnd;
|
||||
this.lastBegin = begin;
|
||||
const end = round(begin + duration * this.cps);
|
||||
this.lastEnd = end;
|
||||
const haps = this.pattern.queryArc(begin, end);
|
||||
const tickdeadline = phase - time; // time left till phase begins
|
||||
this.lastTick = time + tickdeadline;
|
||||
|
||||
haps.forEach((hap) => {
|
||||
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||
const deadline = hap.whole.begin + this.origin - time + latency;
|
||||
const duration = hap.duration * 1;
|
||||
onTrigger?.(hap, deadline, duration);
|
||||
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
|
||||
const duration = hap.duration / this.cps;
|
||||
onTrigger?.(hap, deadline, duration, this.cps);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger(`[cyclist] error: ${e.message}`);
|
||||
onError?.(e);
|
||||
}
|
||||
}, // called slightly before each cycle
|
||||
},
|
||||
interval, // duration of each cycle
|
||||
);
|
||||
}
|
||||
getPhase() {
|
||||
return this.getTime() - this.origin - this.latency;
|
||||
}
|
||||
now() {
|
||||
return this.getTime() - this.origin + this.clock.minLatency;
|
||||
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
|
||||
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
|
||||
}
|
||||
setStarted(v) {
|
||||
this.started = v;
|
||||
@@ -69,6 +74,7 @@ export class Cyclist {
|
||||
stop() {
|
||||
logger('[cyclist] stop');
|
||||
this.clock.stop();
|
||||
this.lastEnd = 0;
|
||||
this.setStarted(false);
|
||||
}
|
||||
setPattern(pat, autostart = false) {
|
||||
|
||||
@@ -11,15 +11,9 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { Pattern, timeCat, register, silence } from './pattern.mjs';
|
||||
import { rotate, flatten } from './util.mjs';
|
||||
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
|
||||
const splitAt = function (index, value) {
|
||||
return [value.slice(0, index), value.slice(index)];
|
||||
};
|
||||
|
||||
const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||
|
||||
const left = function (n, x) {
|
||||
const [ons, offs] = n;
|
||||
const [xs, ys] = x;
|
||||
|
||||
@@ -6,12 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import { isPattern } from './index.mjs';
|
||||
|
||||
let scoped = false;
|
||||
export const evalScope = async (...args) => {
|
||||
if (scoped) {
|
||||
console.warn('evalScope was called more than once.');
|
||||
}
|
||||
scoped = true;
|
||||
const results = await Promise.allSettled(args);
|
||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||
results.forEach((result, i) => {
|
||||
@@ -42,9 +37,6 @@ function safeEval(str, options = {}) {
|
||||
}
|
||||
|
||||
export const evaluate = async (code, transpiler) => {
|
||||
if (!scoped) {
|
||||
await evalScope(); // at least scope Pattern.prototype.boostrap
|
||||
}
|
||||
if (transpiler) {
|
||||
code = transpiler(code); // transform syntactically correct js code to semantically usable code
|
||||
}
|
||||
|
||||
@@ -126,6 +126,19 @@ export class Hap {
|
||||
setContext(context) {
|
||||
return new Hap(this.whole, this.part, this.value, context);
|
||||
}
|
||||
|
||||
ensureObjectValue() {
|
||||
/* if (isNote(hap.value)) {
|
||||
// supports primitive hap values that look like notes
|
||||
hap.value = { note: hap.value };
|
||||
} */
|
||||
if (typeof this.value !== 'object') {
|
||||
throw new Error(
|
||||
`expected hap.value to be an object, but got "${this.value}". Hint: append .note() or .s() to the end`,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Hap;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.6.8",
|
||||
"version": "0.7.2",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -820,22 +820,6 @@ export class Pattern {
|
||||
);
|
||||
}
|
||||
|
||||
addTrigger(onTrigger) {
|
||||
return this.withHap((hap) =>
|
||||
hap.setContext({
|
||||
...hap.context,
|
||||
onTrigger: (...args) => {
|
||||
if (hap.context.onTrigger) {
|
||||
hap.context.onTrigger(...args);
|
||||
}
|
||||
onTrigger(...args);
|
||||
},
|
||||
// this flag causes the default output to be ignored
|
||||
dominantTrigger: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) {
|
||||
return this.onTrigger((...args) => {
|
||||
logger(func(...args), undefined, getData(...args));
|
||||
@@ -856,7 +840,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// functions relating to chords/patterns of lists
|
||||
// functions relating to chords/patterns of lists/lists of patterns
|
||||
|
||||
// returns Array<Hap[]> where each list of haps satisfies eq
|
||||
function groupHapsBy(eq, haps) {
|
||||
@@ -905,6 +889,35 @@ Pattern.prototype.arp = function (pat) {
|
||||
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
|
||||
};
|
||||
|
||||
/*
|
||||
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
|
||||
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
|
||||
* @name weave
|
||||
* @memberof Pattern
|
||||
* @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd"))
|
||||
* @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd"))
|
||||
|
||||
addToPrototype('weave', function (t, ...pats) {
|
||||
return this.weaveWith(t, ...pats.map((x) => set.out(x)));
|
||||
});
|
||||
|
||||
*/
|
||||
/*
|
||||
* Like 'weave', but accepts functions rather than patterns, which are applied to the pattern.
|
||||
* @name weaveWith
|
||||
* @memberof Pattern
|
||||
|
||||
addToPrototype('weaveWith', function (t, ...funcs) {
|
||||
const pat = this;
|
||||
const l = funcs.length;
|
||||
t = Fraction(t);
|
||||
if (l == 0) {
|
||||
return silence;
|
||||
}
|
||||
return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t);
|
||||
});
|
||||
*/
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// compose matrix functions
|
||||
|
||||
@@ -1114,6 +1127,8 @@ function _composeOp(a, b, func) {
|
||||
export const polyrhythm = stack;
|
||||
export const pr = stack;
|
||||
|
||||
export const pm = polymeter;
|
||||
|
||||
// methods that create patterns, which are added to patternified Pattern methods
|
||||
// TODO: remove? this is only used in old transpiler (shapeshifter)
|
||||
Pattern.prototype.factories = {
|
||||
@@ -1271,6 +1286,20 @@ export function timeCat(...timepats) {
|
||||
return stack(...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to arrange multiple patterns together over multiple cycles.
|
||||
* Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* arrange([4, "<c a f e>(3,8)"],[2, "<g a>(5,8)"]).note()
|
||||
*/
|
||||
export function arrange(...sections) {
|
||||
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
|
||||
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
|
||||
return timeCat(...sections).slow(total);
|
||||
}
|
||||
|
||||
export function fastcat(...pats) {
|
||||
return slowcat(...pats)._fast(pats.length);
|
||||
}
|
||||
@@ -1347,11 +1376,6 @@ export function polymeter(...args) {
|
||||
return polymeterSteps(0, ...args);
|
||||
}
|
||||
|
||||
// alias
|
||||
export function pm(...args) {
|
||||
polymeter(...args);
|
||||
}
|
||||
|
||||
export const mask = curry((a, b) => reify(b).mask(a));
|
||||
export const struct = curry((a, b) => reify(b).struct(a));
|
||||
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
|
||||
@@ -1391,39 +1415,45 @@ export const func = curry((a, b) => reify(b).func(a));
|
||||
* @noAutocomplete
|
||||
*
|
||||
*/
|
||||
export function register(name, func) {
|
||||
export function register(name, func, patternify = true) {
|
||||
if (Array.isArray(name)) {
|
||||
const result = {};
|
||||
for (const name_item of name) {
|
||||
result[name_item] = register(name_item, func);
|
||||
result[name_item] = register(name_item, func, patternify);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
const arity = func.length;
|
||||
var pfunc; // the patternified function
|
||||
|
||||
pfunc = function (...args) {
|
||||
args = args.map(reify);
|
||||
const pat = args[args.length - 1];
|
||||
if (arity === 1) {
|
||||
return func(pat);
|
||||
}
|
||||
const [left, ...right] = args.slice(0, -1);
|
||||
let mapFn = (...args) => {
|
||||
// make sure to call func with the correct argument count
|
||||
// args.length is expected to be <= arity-1
|
||||
// so we set undefined args explicitly undefined
|
||||
Array(arity - 1)
|
||||
.fill()
|
||||
.map((_, i) => args[i] ?? undefined);
|
||||
return func(...args, pat);
|
||||
if (patternify) {
|
||||
pfunc = function (...args) {
|
||||
args = args.map(reify);
|
||||
const pat = args[args.length - 1];
|
||||
if (arity === 1) {
|
||||
return func(pat);
|
||||
}
|
||||
const [left, ...right] = args.slice(0, -1);
|
||||
let mapFn = (...args) => {
|
||||
// make sure to call func with the correct argument count
|
||||
// args.length is expected to be <= arity-1
|
||||
// so we set undefined args explicitly undefined
|
||||
Array(arity - 1)
|
||||
.fill()
|
||||
.map((_, i) => args[i] ?? undefined);
|
||||
return func(...args, pat);
|
||||
};
|
||||
mapFn = curry(mapFn, null, arity - 1);
|
||||
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
|
||||
};
|
||||
mapFn = curry(mapFn, null, arity - 1);
|
||||
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
|
||||
};
|
||||
} else {
|
||||
pfunc = function (...args) {
|
||||
args = args.map(reify);
|
||||
return func(...args);
|
||||
};
|
||||
}
|
||||
|
||||
Pattern.prototype[name] = function (...args) {
|
||||
args = args.map(reify);
|
||||
// For methods that take a single argument (plus 'this'), allow
|
||||
// multiple arguments but sequence them
|
||||
if (arity === 2 && args.length !== 1) {
|
||||
@@ -1431,6 +1461,7 @@ export function register(name, func) {
|
||||
} else if (arity !== args.length + 1) {
|
||||
throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`);
|
||||
}
|
||||
args = args.map(reify);
|
||||
return pfunc(...args, this);
|
||||
};
|
||||
|
||||
@@ -2210,6 +2241,61 @@ const _loopAt = function (factor, pat, cps = 1) {
|
||||
.slow(factor);
|
||||
};
|
||||
|
||||
/*
|
||||
* Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers.
|
||||
* @name slice
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
|
||||
*/
|
||||
|
||||
const slice = register(
|
||||
'slice',
|
||||
function (npat, ipat, opat) {
|
||||
return npat.innerBind((n) =>
|
||||
ipat.outerBind((i) =>
|
||||
opat.outerBind((o) => {
|
||||
// If it's not an object, assume it's a string and make it a 's' control parameter
|
||||
o = o instanceof Object ? o : { s: o };
|
||||
// Remember we must stay pure and avoid editing the object directly
|
||||
const toAdd = { begin: i / n, end: (i + 1) / n, _slices: n };
|
||||
return pure({ ...toAdd, ...o });
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
false, // turns off auto-patternification
|
||||
);
|
||||
|
||||
/*
|
||||
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
|
||||
* @name splice
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
|
||||
*/
|
||||
|
||||
const splice = register(
|
||||
'splice',
|
||||
function (npat, ipat, opat) {
|
||||
const sliced = slice(npat, ipat, opat);
|
||||
return sliced.withHap(function (hap) {
|
||||
return hap.withValue((v) => ({
|
||||
...{
|
||||
speed: (1 / v._slices / hap.whole.duration) * (v.speed || 1),
|
||||
unit: 'c',
|
||||
},
|
||||
...v,
|
||||
}));
|
||||
});
|
||||
},
|
||||
false, // turns off auto-patternification
|
||||
);
|
||||
|
||||
const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
|
||||
return _loopAt(factor, pat, 1);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
||||
import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
||||
|
||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||
const getValue = (e) => {
|
||||
@@ -18,7 +18,7 @@ const getValue = (e) => {
|
||||
}
|
||||
note = note ?? n;
|
||||
if (typeof note === 'string') {
|
||||
return toMidi(note);
|
||||
return noteToMidi(note);
|
||||
}
|
||||
if (typeof note === 'number') {
|
||||
return note;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Cyclist } from './cyclist.mjs';
|
||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { setTime } from './time.mjs';
|
||||
import { evalScope } from './evaluate.mjs';
|
||||
|
||||
export function repl({
|
||||
interval,
|
||||
@@ -15,15 +16,15 @@ export function repl({
|
||||
onToggle,
|
||||
editPattern,
|
||||
}) {
|
||||
const id = s4();
|
||||
const scheduler = new Cyclist({
|
||||
interval,
|
||||
onTrigger: async (hap, deadline, duration) => {
|
||||
onTrigger: async (hap, deadline, duration, cps) => {
|
||||
try {
|
||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||
await defaultOutput(hap, deadline, duration);
|
||||
await defaultOutput(hap, deadline, duration, cps);
|
||||
}
|
||||
if (hap.context.onTrigger) {
|
||||
const cps = 1;
|
||||
// call signature of output / onTrigger is different...
|
||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
||||
}
|
||||
@@ -41,11 +42,11 @@ export function repl({
|
||||
throw new Error('no code to evaluate');
|
||||
}
|
||||
try {
|
||||
beforeEval?.({ code });
|
||||
await beforeEval?.({ code });
|
||||
let { pattern } = await _evaluate(code, transpiler);
|
||||
|
||||
logger(`[eval] code updated`);
|
||||
pattern = editPattern?.(pattern) || pattern;
|
||||
pattern = editPattern?.(pattern, id) || pattern;
|
||||
scheduler.setPattern(pattern, autostart);
|
||||
afterEval?.({ code, pattern });
|
||||
return pattern;
|
||||
@@ -58,5 +59,16 @@ export function repl({
|
||||
const stop = () => scheduler.stop();
|
||||
const start = () => scheduler.start();
|
||||
const pause = () => scheduler.pause();
|
||||
return { scheduler, evaluate, start, stop, pause };
|
||||
const setCps = (cps) => scheduler.setCps(cps);
|
||||
evalScope({
|
||||
setCps,
|
||||
setcps: setCps,
|
||||
});
|
||||
return { scheduler, evaluate, start, stop, pause, setCps, id };
|
||||
}
|
||||
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
controls.test.mjs - <short description TODO>
|
||||
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/controls.test.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 controls from '../controls.mjs';
|
||||
import { mini } from '../../mini/mini.mjs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('controls', () => {
|
||||
it('should support controls', () => {
|
||||
expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
|
||||
});
|
||||
it('should support compound controls', () => {
|
||||
expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
|
||||
expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
|
||||
{ s: 'bd', n: 3 },
|
||||
{ s: 'sd', n: 4, gain: 1.4 },
|
||||
]);
|
||||
});
|
||||
it('should support ignore extra elements in compound controls', () => {
|
||||
expect(controls.s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
|
||||
{ s: 'bd', n: 3, gain: 0.4 },
|
||||
{ s: 'sd', n: 4, gain: 0.5 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -959,4 +959,48 @@ describe('Pattern', () => {
|
||||
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
|
||||
});
|
||||
});
|
||||
/*describe('composable functions', () => {
|
||||
it('Can compose functions', () => {
|
||||
sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
|
||||
});
|
||||
it('Can compose by method chaining operators with controls', () => {
|
||||
sameFirst(s('bd').apply(set.n(3).fast(2)), s('bd').set.n(3).fast(2));
|
||||
});
|
||||
it('Can compose by method chaining operators and alignments with controls', () => {
|
||||
sameFirst(s('bd').apply(set.in.n(3).fast(2)), s('bd').set.n(3).fast(2));
|
||||
// sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2));
|
||||
});
|
||||
});
|
||||
describe('weave', () => {
|
||||
it('Can distribute patterns along a pattern', () => {
|
||||
sameFirst(n(0, 1).weave(2, s('bd', silence), s(silence, 'sd')), sequence(s('bd').n(0), s('sd').n(1)));
|
||||
});
|
||||
});
|
||||
*/
|
||||
describe('slice', () => {
|
||||
it('Can slice a sample', () => {
|
||||
sameFirst(
|
||||
s('break').slice(4, sequence(0, 1, 2, 3)),
|
||||
sequence(
|
||||
{ begin: 0, end: 0.25, s: 'break', _slices: 4 },
|
||||
{ begin: 0.25, end: 0.5, s: 'break', _slices: 4 },
|
||||
{ begin: 0.5, end: 0.75, s: 'break', _slices: 4 },
|
||||
{ begin: 0.75, end: 1, s: 'break', _slices: 4 },
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('splice', () => {
|
||||
it('Can splice a sample', () => {
|
||||
sameFirst(
|
||||
s('break').splice(4, sequence(0, 1, 2, 3)),
|
||||
sequence(
|
||||
{ begin: 0, end: 0.25, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||
{ begin: 0.25, end: 0.5, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||
{ begin: 0.5, end: 0.75, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||
{ begin: 0.75, end: 1, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,8 +8,8 @@ import { pure } from '../pattern.mjs';
|
||||
import {
|
||||
isNote,
|
||||
tokenizeNote,
|
||||
toMidi,
|
||||
fromMidi,
|
||||
noteToMidi,
|
||||
midiToFreq,
|
||||
freqToMidi,
|
||||
_mod,
|
||||
compose,
|
||||
@@ -75,27 +75,27 @@ describe('isNote', () => {
|
||||
expect(tokenizeNote(123)).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
describe('toMidi', () => {
|
||||
describe('noteToMidi', () => {
|
||||
it('should turn notes into midi', () => {
|
||||
expect(toMidi('A4')).toEqual(69);
|
||||
expect(toMidi('C4')).toEqual(60);
|
||||
expect(toMidi('Db4')).toEqual(61);
|
||||
expect(toMidi('C3')).toEqual(48);
|
||||
expect(toMidi('Cb3')).toEqual(47);
|
||||
expect(toMidi('Cbb3')).toEqual(46);
|
||||
expect(toMidi('C#3')).toEqual(49);
|
||||
expect(toMidi('C#3')).toEqual(49);
|
||||
expect(toMidi('C##3')).toEqual(50);
|
||||
expect(noteToMidi('A4')).toEqual(69);
|
||||
expect(noteToMidi('C4')).toEqual(60);
|
||||
expect(noteToMidi('Db4')).toEqual(61);
|
||||
expect(noteToMidi('C3')).toEqual(48);
|
||||
expect(noteToMidi('Cb3')).toEqual(47);
|
||||
expect(noteToMidi('Cbb3')).toEqual(46);
|
||||
expect(noteToMidi('C#3')).toEqual(49);
|
||||
expect(noteToMidi('C#3')).toEqual(49);
|
||||
expect(noteToMidi('C##3')).toEqual(50);
|
||||
});
|
||||
it('should throw an error when given a non-note', () => {
|
||||
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||
expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||
expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||
});
|
||||
});
|
||||
describe('fromMidi', () => {
|
||||
describe('midiToFreq', () => {
|
||||
it('should turn midi into frequency', () => {
|
||||
expect(fromMidi(69)).toEqual(440);
|
||||
expect(fromMidi(57)).toEqual(220);
|
||||
expect(midiToFreq(69)).toEqual(440);
|
||||
expect(midiToFreq(57)).toEqual(220);
|
||||
});
|
||||
});
|
||||
describe('freqToMidi', () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ export const tokenizeNote = (note) => {
|
||||
};
|
||||
|
||||
// turns the given note into its midi number representation
|
||||
export const toMidi = (note) => {
|
||||
export const noteToMidi = (note) => {
|
||||
const [pc, acc, oct = 3] = tokenizeNote(note);
|
||||
if (!pc) {
|
||||
throw new Error('not a note: "' + note + '"');
|
||||
@@ -28,7 +28,7 @@ export const toMidi = (note) => {
|
||||
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
|
||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||
};
|
||||
export const fromMidi = (n) => {
|
||||
export const midiToFreq = (n) => {
|
||||
return Math.pow(2, (n - 69) / 12) * 440;
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ export const valueToMidi = (value, fallbackValue) => {
|
||||
return freqToMidi(freq);
|
||||
}
|
||||
if (typeof note === 'string') {
|
||||
return toMidi(note);
|
||||
return noteToMidi(note);
|
||||
}
|
||||
if (typeof note === 'number') {
|
||||
return note;
|
||||
@@ -62,9 +62,9 @@ export const valueToMidi = (value, fallbackValue) => {
|
||||
*/
|
||||
export const getFreq = (noteOrMidi) => {
|
||||
if (typeof noteOrMidi === 'number') {
|
||||
return fromMidi(noteOrMidi);
|
||||
return midiToFreq(noteOrMidi);
|
||||
}
|
||||
return fromMidi(toMidi(noteOrMidi));
|
||||
return midiToFreq(noteToMidi(noteOrMidi));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -91,7 +91,7 @@ export const getPlayableNoteValue = (hap) => {
|
||||
}
|
||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||
note = fromMidi(hap.value);
|
||||
note = midiToFreq(hap.value);
|
||||
} else if (typeof note === 'number' && context.type === 'frequency') {
|
||||
note = hap.value; // legacy workaround.. will be removed in the future
|
||||
} else if (typeof note !== 'string' || !isNote(note)) {
|
||||
@@ -110,9 +110,9 @@ export const getFrequency = (hap) => {
|
||||
return getFreq(value.note || value.n || value.value);
|
||||
}
|
||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||
value = fromMidi(hap.value);
|
||||
value = midiToFreq(hap.value);
|
||||
} else if (typeof value === 'string' && isNote(value)) {
|
||||
value = fromMidi(toMidi(hap.value));
|
||||
value = midiToFreq(noteToMidi(hap.value));
|
||||
} else if (typeof value !== 'number') {
|
||||
throw new Error('not a note or frequency: ' + value);
|
||||
}
|
||||
@@ -170,7 +170,7 @@ export function parseNumeral(numOrString) {
|
||||
return asNumber;
|
||||
}
|
||||
if (isNote(numOrString)) {
|
||||
return toMidi(numOrString);
|
||||
return noteToMidi(numOrString);
|
||||
}
|
||||
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
||||
}
|
||||
@@ -206,3 +206,9 @@ export function parseFractional(numOrString) {
|
||||
}
|
||||
|
||||
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
|
||||
|
||||
export const splitAt = function (index, value) {
|
||||
return [value.slice(0, index), value.slice(index)];
|
||||
};
|
||||
|
||||
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||
|
||||
@@ -44,6 +44,6 @@ function createClock(
|
||||
};
|
||||
const getPhase = () => phase;
|
||||
// setCallback
|
||||
return { setDuration, start, stop, pause, duration, getPhase, minLatency };
|
||||
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
|
||||
}
|
||||
export default createClock;
|
||||
|
||||
@@ -28,9 +28,7 @@ export const csound = register('csound', (instrument, pat) => {
|
||||
logger('[csound] not loaded yet', 'warning');
|
||||
return;
|
||||
}
|
||||
if (typeof hap.value !== 'object') {
|
||||
throw new Error('csound only support objects as hap values');
|
||||
}
|
||||
hap.ensureObjectValue();
|
||||
let { gain = 0.8 } = hap.value;
|
||||
gain *= 0.2;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/csound",
|
||||
"version": "0.6.2",
|
||||
"version": "0.7.1",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.6.2",
|
||||
"version": "0.7.1",
|
||||
"description": "Code evaluator for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -5,8 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import * as _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern, isNote, getPlayableNoteValue, logger } from '@strudel.cycles/core';
|
||||
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
|
||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||
import { noteToMidi } from '@strudel.cycles/core';
|
||||
|
||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||
export const { WebMidi } = _WebMidi;
|
||||
@@ -63,7 +64,7 @@ function getDevice(output, outputs) {
|
||||
}
|
||||
|
||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||
Pattern.prototype.midi = function (output, channel = 1) {
|
||||
Pattern.prototype.midi = function (output) {
|
||||
if (!supportsMidi()) {
|
||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||
}
|
||||
@@ -90,11 +91,6 @@ Pattern.prototype.midi = function (output, channel = 1) {
|
||||
);
|
||||
}
|
||||
return this.onTrigger((time, hap) => {
|
||||
let note = getPlayableNoteValue(hap);
|
||||
const velocity = hap.context?.velocity ?? 0.9;
|
||||
if (!isNote(note)) {
|
||||
throw new Error('not a note: ' + note);
|
||||
}
|
||||
if (!midiReady) {
|
||||
return;
|
||||
}
|
||||
@@ -106,15 +102,34 @@ Pattern.prototype.midi = function (output, channel = 1) {
|
||||
.join(' | ')}`,
|
||||
);
|
||||
}
|
||||
// console.log('midi', value, output);
|
||||
hap.ensureObjectValue();
|
||||
|
||||
// calculate time
|
||||
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
|
||||
time = time * 1000 + timingOffset;
|
||||
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
||||
// await enableWebMidi()
|
||||
device.playNote(note, channel, {
|
||||
time,
|
||||
duration: hap.duration.valueOf() * 1000 - 5,
|
||||
attack: velocity,
|
||||
});
|
||||
|
||||
// destructure value
|
||||
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
|
||||
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
|
||||
const duration = hap.duration.valueOf() * 1000 - 5;
|
||||
|
||||
if (note) {
|
||||
const midiNumber = noteToMidi(note);
|
||||
device.playNote(midiNumber, midichan, {
|
||||
time,
|
||||
duration,
|
||||
attack: velocity,
|
||||
});
|
||||
}
|
||||
if (ccv && ccn) {
|
||||
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
||||
throw new Error('expected ccv to be a number between 0 and 1');
|
||||
}
|
||||
if (!['string', 'number'].includes(typeof ccn)) {
|
||||
throw new Error('expected ccn to be a number or a string');
|
||||
}
|
||||
const scaled = Math.round(ccv * 127);
|
||||
device.sendControlChange(ccn, scaled, midichan, { time });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.1",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -182,21 +182,21 @@ function peg$parse(input, options) {
|
||||
var peg$c8 = "#";
|
||||
var peg$c9 = "^";
|
||||
var peg$c10 = "_";
|
||||
var peg$c11 = ":";
|
||||
var peg$c12 = "[";
|
||||
var peg$c13 = "]";
|
||||
var peg$c14 = "{";
|
||||
var peg$c15 = "}";
|
||||
var peg$c16 = "%";
|
||||
var peg$c17 = "<";
|
||||
var peg$c18 = ">";
|
||||
var peg$c19 = "@";
|
||||
var peg$c20 = "!";
|
||||
var peg$c21 = "(";
|
||||
var peg$c22 = ")";
|
||||
var peg$c23 = "/";
|
||||
var peg$c24 = "*";
|
||||
var peg$c25 = "?";
|
||||
var peg$c11 = "[";
|
||||
var peg$c12 = "]";
|
||||
var peg$c13 = "{";
|
||||
var peg$c14 = "}";
|
||||
var peg$c15 = "%";
|
||||
var peg$c16 = "<";
|
||||
var peg$c17 = ">";
|
||||
var peg$c18 = "@";
|
||||
var peg$c19 = "!";
|
||||
var peg$c20 = "(";
|
||||
var peg$c21 = ")";
|
||||
var peg$c22 = "/";
|
||||
var peg$c23 = "*";
|
||||
var peg$c24 = "?";
|
||||
var peg$c25 = ":";
|
||||
var peg$c26 = "struct";
|
||||
var peg$c27 = "target";
|
||||
var peg$c28 = "euclid";
|
||||
@@ -237,21 +237,21 @@ function peg$parse(input, options) {
|
||||
var peg$e15 = peg$literalExpectation("#", false);
|
||||
var peg$e16 = peg$literalExpectation("^", false);
|
||||
var peg$e17 = peg$literalExpectation("_", false);
|
||||
var peg$e18 = peg$literalExpectation(":", false);
|
||||
var peg$e19 = peg$literalExpectation("[", false);
|
||||
var peg$e20 = peg$literalExpectation("]", false);
|
||||
var peg$e21 = peg$literalExpectation("{", false);
|
||||
var peg$e22 = peg$literalExpectation("}", false);
|
||||
var peg$e23 = peg$literalExpectation("%", false);
|
||||
var peg$e24 = peg$literalExpectation("<", false);
|
||||
var peg$e25 = peg$literalExpectation(">", false);
|
||||
var peg$e26 = peg$literalExpectation("@", false);
|
||||
var peg$e27 = peg$literalExpectation("!", false);
|
||||
var peg$e28 = peg$literalExpectation("(", false);
|
||||
var peg$e29 = peg$literalExpectation(")", false);
|
||||
var peg$e30 = peg$literalExpectation("/", false);
|
||||
var peg$e31 = peg$literalExpectation("*", false);
|
||||
var peg$e32 = peg$literalExpectation("?", false);
|
||||
var peg$e18 = peg$literalExpectation("[", false);
|
||||
var peg$e19 = peg$literalExpectation("]", false);
|
||||
var peg$e20 = peg$literalExpectation("{", false);
|
||||
var peg$e21 = peg$literalExpectation("}", false);
|
||||
var peg$e22 = peg$literalExpectation("%", false);
|
||||
var peg$e23 = peg$literalExpectation("<", false);
|
||||
var peg$e24 = peg$literalExpectation(">", false);
|
||||
var peg$e25 = peg$literalExpectation("@", false);
|
||||
var peg$e26 = peg$literalExpectation("!", false);
|
||||
var peg$e27 = peg$literalExpectation("(", false);
|
||||
var peg$e28 = peg$literalExpectation(")", false);
|
||||
var peg$e29 = peg$literalExpectation("/", false);
|
||||
var peg$e30 = peg$literalExpectation("*", false);
|
||||
var peg$e31 = peg$literalExpectation("?", false);
|
||||
var peg$e32 = peg$literalExpectation(":", false);
|
||||
var peg$e33 = peg$literalExpectation("struct", false);
|
||||
var peg$e34 = peg$literalExpectation("target", false);
|
||||
var peg$e35 = peg$literalExpectation("euclid", false);
|
||||
@@ -279,36 +279,37 @@ function peg$parse(input, options) {
|
||||
var peg$f8 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
|
||||
var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
|
||||
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
|
||||
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) };
|
||||
var peg$f12 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
|
||||
var peg$f12 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
|
||||
var peg$f13 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||
for (const op of ops) {
|
||||
op(result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
var peg$f13 = function(s) { return new PatternStub(s, 'fastcat'); };
|
||||
var peg$f14 = function(tail) { return { alignment: 'stack', list: tail }; };
|
||||
var peg$f15 = function(tail) { return { alignment: 'rand', list: tail }; };
|
||||
var peg$f16 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } };
|
||||
var peg$f17 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
||||
var peg$f18 = function(sc) { return sc; };
|
||||
var peg$f19 = function(s) { return { name: "struct", args: { mini:s }}};
|
||||
var peg$f20 = function(s) { return { name: "target", args : { name:s}}};
|
||||
var peg$f21 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
|
||||
var peg$f22 = function(a) { return { name: "stretch", args :{ amount: a}}};
|
||||
var peg$f23 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
|
||||
var peg$f24 = function(a) { return { name: "shift", args :{ amount: a}}};
|
||||
var peg$f25 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
|
||||
var peg$f26 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
|
||||
var peg$f27 = function(s, v) { return v};
|
||||
var peg$f28 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
|
||||
var peg$f29 = function(sg) {return sg};
|
||||
var peg$f30 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
|
||||
var peg$f31 = function(sc) { return sc };
|
||||
var peg$f32 = function(c) { return c };
|
||||
var peg$f33 = function(v) { return new CommandStub("setcps", { value: v})};
|
||||
var peg$f34 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
|
||||
var peg$f35 = function() { return new CommandStub("hush")};
|
||||
var peg$f14 = function(s) { return new PatternStub(s, 'fastcat'); };
|
||||
var peg$f15 = function(tail) { return { alignment: 'stack', list: tail }; };
|
||||
var peg$f16 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
|
||||
var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
|
||||
var peg$f18 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
||||
var peg$f19 = function(sc) { return sc; };
|
||||
var peg$f20 = function(s) { return { name: "struct", args: { mini:s }}};
|
||||
var peg$f21 = function(s) { return { name: "target", args : { name:s}}};
|
||||
var peg$f22 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
|
||||
var peg$f23 = function(a) { return { name: "stretch", args :{ amount: a}}};
|
||||
var peg$f24 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
|
||||
var peg$f25 = function(a) { return { name: "shift", args :{ amount: a}}};
|
||||
var peg$f26 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
|
||||
var peg$f27 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
|
||||
var peg$f28 = function(s, v) { return v};
|
||||
var peg$f29 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
|
||||
var peg$f30 = function(sg) {return sg};
|
||||
var peg$f31 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
|
||||
var peg$f32 = function(sc) { return sc };
|
||||
var peg$f33 = function(c) { return c };
|
||||
var peg$f34 = function(v) { return new CommandStub("setcps", { value: v})};
|
||||
var peg$f35 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
|
||||
var peg$f36 = function() { return new CommandStub("hush")};
|
||||
var peg$currPos = 0;
|
||||
var peg$savedPos = 0;
|
||||
var peg$posDetailsCache = [{ line: 1, column: 1 }];
|
||||
@@ -848,15 +849,6 @@ function peg$parse(input, options) {
|
||||
s0 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e17); }
|
||||
}
|
||||
if (s0 === peg$FAILED) {
|
||||
if (input.charCodeAt(peg$currPos) === 58) {
|
||||
s0 = peg$c11;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s0 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -899,11 +891,11 @@ function peg$parse(input, options) {
|
||||
s0 = peg$currPos;
|
||||
s1 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 91) {
|
||||
s2 = peg$c12;
|
||||
s2 = peg$c11;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s2 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||
}
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
@@ -911,11 +903,11 @@ function peg$parse(input, options) {
|
||||
if (s4 !== peg$FAILED) {
|
||||
s5 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 93) {
|
||||
s6 = peg$c13;
|
||||
s6 = peg$c12;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s6 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
}
|
||||
if (s6 !== peg$FAILED) {
|
||||
s7 = peg$parsews();
|
||||
@@ -943,11 +935,11 @@ function peg$parse(input, options) {
|
||||
s0 = peg$currPos;
|
||||
s1 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 123) {
|
||||
s2 = peg$c14;
|
||||
s2 = peg$c13;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s2 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e21); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
||||
}
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
@@ -955,11 +947,11 @@ function peg$parse(input, options) {
|
||||
if (s4 !== peg$FAILED) {
|
||||
s5 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 125) {
|
||||
s6 = peg$c15;
|
||||
s6 = peg$c14;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s6 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e22); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e21); }
|
||||
}
|
||||
if (s6 !== peg$FAILED) {
|
||||
s7 = peg$parsepolymeter_steps();
|
||||
@@ -990,11 +982,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 37) {
|
||||
s1 = peg$c16;
|
||||
s1 = peg$c15;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e22); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
@@ -1019,11 +1011,11 @@ function peg$parse(input, options) {
|
||||
s0 = peg$currPos;
|
||||
s1 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 60) {
|
||||
s2 = peg$c17;
|
||||
s2 = peg$c16;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s2 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
||||
}
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
@@ -1031,11 +1023,11 @@ function peg$parse(input, options) {
|
||||
if (s4 !== peg$FAILED) {
|
||||
s5 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 62) {
|
||||
s6 = peg$c18;
|
||||
s6 = peg$c17;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s6 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e25); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
||||
}
|
||||
if (s6 !== peg$FAILED) {
|
||||
s7 = peg$parsews();
|
||||
@@ -1088,6 +1080,9 @@ function peg$parse(input, options) {
|
||||
s0 = peg$parseop_replicate();
|
||||
if (s0 === peg$FAILED) {
|
||||
s0 = peg$parseop_degrade();
|
||||
if (s0 === peg$FAILED) {
|
||||
s0 = peg$parseop_tail();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1102,11 +1097,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 64) {
|
||||
s1 = peg$c19;
|
||||
s1 = peg$c18;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e25); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
@@ -1130,11 +1125,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 33) {
|
||||
s1 = peg$c20;
|
||||
s1 = peg$c19;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e27); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
@@ -1158,11 +1153,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 40) {
|
||||
s1 = peg$c21;
|
||||
s1 = peg$c20;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e27); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
@@ -1186,11 +1181,11 @@ function peg$parse(input, options) {
|
||||
}
|
||||
s12 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 41) {
|
||||
s13 = peg$c22;
|
||||
s13 = peg$c21;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s13 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||
}
|
||||
if (s13 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
@@ -1224,11 +1219,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 47) {
|
||||
s1 = peg$c23;
|
||||
s1 = peg$c22;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e30); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
@@ -1252,11 +1247,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 42) {
|
||||
s1 = peg$c24;
|
||||
s1 = peg$c23;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e31); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e30); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
@@ -1280,11 +1275,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 63) {
|
||||
s1 = peg$c25;
|
||||
s1 = peg$c24;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e32); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e31); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
@@ -1301,6 +1296,34 @@ function peg$parse(input, options) {
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parseop_tail() {
|
||||
var s0, s1, s2;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 58) {
|
||||
s1 = peg$c25;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e32); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f12(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parseslice_with_ops() {
|
||||
var s0, s1, s2, s3;
|
||||
|
||||
@@ -1314,7 +1337,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parseslice_op();
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f12(s1, s2);
|
||||
s0 = peg$f13(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1339,7 +1362,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f13(s1);
|
||||
s1 = peg$f14(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1388,7 +1411,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f14(s1);
|
||||
s1 = peg$f15(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1437,7 +1460,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f15(s1);
|
||||
s1 = peg$f16(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1458,7 +1481,7 @@ function peg$parse(input, options) {
|
||||
s2 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f16(s1, s2);
|
||||
s0 = peg$f17(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1478,7 +1501,7 @@ function peg$parse(input, options) {
|
||||
s2 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f17(s1, s2);
|
||||
s0 = peg$f18(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1488,18 +1511,20 @@ function peg$parse(input, options) {
|
||||
}
|
||||
|
||||
function peg$parsemini() {
|
||||
var s0, s1, s2, s3, s4;
|
||||
var s0, s1, s2, s3, s4, s5, s6;
|
||||
|
||||
s0 = peg$currPos;
|
||||
s1 = peg$parsews();
|
||||
s2 = peg$parsequote();
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsestack_or_choose();
|
||||
if (s3 !== peg$FAILED) {
|
||||
s4 = peg$parsequote();
|
||||
if (s4 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
s4 = peg$parsestack_or_choose();
|
||||
if (s4 !== peg$FAILED) {
|
||||
s5 = peg$parsews();
|
||||
s6 = peg$parsequote();
|
||||
if (s6 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f18(s3);
|
||||
s0 = peg$f19(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1561,7 +1586,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsemini_or_operator();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f19(s3);
|
||||
s0 = peg$f20(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1594,7 +1619,7 @@ function peg$parse(input, options) {
|
||||
s5 = peg$parsequote();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f20(s4);
|
||||
s0 = peg$f21(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1639,7 +1664,7 @@ function peg$parse(input, options) {
|
||||
s7 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f21(s3, s5, s7);
|
||||
s0 = peg$f22(s3, s5, s7);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1672,7 +1697,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f22(s3);
|
||||
s0 = peg$f23(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1701,7 +1726,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f23(s3);
|
||||
s0 = peg$f24(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1730,7 +1755,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f24(s3);
|
||||
s0 = peg$f25(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1759,7 +1784,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f25(s3);
|
||||
s0 = peg$f26(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1801,7 +1826,7 @@ function peg$parse(input, options) {
|
||||
s5 = peg$parsequote();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f26(s4);
|
||||
s0 = peg$f27(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1876,11 +1901,11 @@ function peg$parse(input, options) {
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 91) {
|
||||
s3 = peg$c12;
|
||||
s3 = peg$c11;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s3 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||
}
|
||||
if (s3 !== peg$FAILED) {
|
||||
s4 = peg$parsews();
|
||||
@@ -1893,7 +1918,7 @@ function peg$parse(input, options) {
|
||||
s9 = peg$parsemini_or_operator();
|
||||
if (s9 !== peg$FAILED) {
|
||||
peg$savedPos = s7;
|
||||
s7 = peg$f27(s5, s9);
|
||||
s7 = peg$f28(s5, s9);
|
||||
} else {
|
||||
peg$currPos = s7;
|
||||
s7 = peg$FAILED;
|
||||
@@ -1910,7 +1935,7 @@ function peg$parse(input, options) {
|
||||
s9 = peg$parsemini_or_operator();
|
||||
if (s9 !== peg$FAILED) {
|
||||
peg$savedPos = s7;
|
||||
s7 = peg$f27(s5, s9);
|
||||
s7 = peg$f28(s5, s9);
|
||||
} else {
|
||||
peg$currPos = s7;
|
||||
s7 = peg$FAILED;
|
||||
@@ -1922,15 +1947,15 @@ function peg$parse(input, options) {
|
||||
}
|
||||
s7 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 93) {
|
||||
s8 = peg$c13;
|
||||
s8 = peg$c12;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s8 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
}
|
||||
if (s8 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f28(s5, s6);
|
||||
s0 = peg$f29(s5, s6);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1976,7 +2001,7 @@ function peg$parse(input, options) {
|
||||
s4 = peg$parsecomment();
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f29(s1);
|
||||
s0 = peg$f30(s1);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1998,7 +2023,7 @@ function peg$parse(input, options) {
|
||||
s5 = peg$parsemini_or_operator();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f30(s1, s5);
|
||||
s0 = peg$f31(s1, s5);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2023,7 +2048,7 @@ function peg$parse(input, options) {
|
||||
s1 = peg$parsemini_or_operator();
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f31(s1);
|
||||
s1 = peg$f32(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
if (s0 === peg$FAILED) {
|
||||
@@ -2056,7 +2081,7 @@ function peg$parse(input, options) {
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f32(s2);
|
||||
s0 = peg$f33(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2081,7 +2106,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f33(s3);
|
||||
s0 = peg$f34(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2110,7 +2135,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f34(s3);
|
||||
s0 = peg$f35(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2136,7 +2161,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f35();
|
||||
s1 = peg$f36();
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -2162,10 +2187,13 @@ function peg$parse(input, options) {
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var PatternStub = function(source, alignment)
|
||||
var PatternStub = function(source, alignment, seed)
|
||||
{
|
||||
this.type_ = "pattern";
|
||||
this.arguments_ = { alignment : alignment};
|
||||
this.arguments_ = { alignment: alignment };
|
||||
if (seed !== undefined) {
|
||||
this.arguments_.seed = seed;
|
||||
}
|
||||
this.source_ = source;
|
||||
}
|
||||
|
||||
@@ -2191,6 +2219,7 @@ function peg$parse(input, options) {
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
var seed = 0;
|
||||
|
||||
peg$result = peg$startRuleFunction();
|
||||
|
||||
|
||||
@@ -19,10 +19,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var PatternStub = function(source, alignment)
|
||||
var PatternStub = function(source, alignment, seed)
|
||||
{
|
||||
this.type_ = "pattern";
|
||||
this.arguments_ = { alignment : alignment};
|
||||
this.arguments_ = { alignment: alignment };
|
||||
if (seed !== undefined) {
|
||||
this.arguments_.seed = seed;
|
||||
}
|
||||
this.source_ = source;
|
||||
}
|
||||
|
||||
@@ -48,6 +51,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
var seed = 0;
|
||||
}
|
||||
|
||||
start = statement
|
||||
@@ -96,7 +100,7 @@ quote = '"' / "'"
|
||||
// ------------------ steps and cycles ---------------------------
|
||||
|
||||
// single step definition (e.g bd)
|
||||
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
|
||||
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_"
|
||||
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
|
||||
|
||||
// define a sub cycle e.g. [1 2, 3 [4]]
|
||||
@@ -119,7 +123,7 @@ slice = step / sub_cycle / polymeter / slow_sequence
|
||||
|
||||
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
||||
// at this point, we assume we can represent them as regular sequence operators
|
||||
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade
|
||||
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail
|
||||
|
||||
op_weight = "@" a:number
|
||||
{ return x => x.options_['weight'] = a }
|
||||
@@ -137,7 +141,10 @@ op_fast = "*"a:slice
|
||||
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
|
||||
|
||||
op_degrade = "?"a:number?
|
||||
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
|
||||
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) }
|
||||
|
||||
op_tail = ":" s:slice
|
||||
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
|
||||
|
||||
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
||||
slice_with_ops = s:slice ops:slice_op*
|
||||
@@ -159,12 +166,12 @@ stack_tail = tail:(comma @sequence)+
|
||||
// a choose is a series of pipe-separated sequence, one of which is
|
||||
// chosen at random, each cycle
|
||||
choose_tail = tail:(pipe @sequence)+
|
||||
{ return { alignment: 'rand', list: tail }; }
|
||||
{ return { alignment: 'rand', list: tail, seed: seed++ }; }
|
||||
|
||||
// if the stack contains only one element, we don't create a stack but return the
|
||||
// underlying element
|
||||
stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
|
||||
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
|
||||
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
|
||||
|
||||
polymeter_stack = head:sequence tail:stack_tail?
|
||||
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
|
||||
@@ -175,7 +182,7 @@ polymeter_stack = head:sequence tail:stack_tail?
|
||||
// Experimental haskellish parser begins
|
||||
|
||||
// mini-notation = a quoted stack
|
||||
mini = ws quote sc:stack_or_choose quote
|
||||
mini = ws quote ws sc:stack_or_choose ws quote
|
||||
{ return sc; }
|
||||
|
||||
// ------------------ operators ---------------------------
|
||||
|
||||
@@ -7,17 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
import * as krill from './krill-parser.js';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
/* var _seedState = 0;
|
||||
const randOffset = 0.0002;
|
||||
|
||||
function _nextSeed() {
|
||||
return _seedState++;
|
||||
} */
|
||||
const randOffset = 0.0003;
|
||||
|
||||
const applyOptions = (parent, code) => (pat, i) => {
|
||||
const ast = parent.source_[i];
|
||||
const options = ast.options_;
|
||||
const ops = options?.ops;
|
||||
|
||||
if (ops) {
|
||||
for (const op of ops) {
|
||||
switch (op.type_) {
|
||||
@@ -43,27 +39,14 @@ const applyOptions = (parent, code) => (pat, i) => {
|
||||
break;
|
||||
}
|
||||
case 'degradeBy': {
|
||||
// TODO: find out what is right here
|
||||
// example:
|
||||
/*
|
||||
stack(
|
||||
s("hh*8").degrade(),
|
||||
s("[ht*8]?")
|
||||
)
|
||||
*/
|
||||
// above example will only be in sync when _degradeBy is used...
|
||||
// it also seems that the nextSeed will create undeterministic behaviour
|
||||
// as it uses a global _seedState. This is probably the reason for
|
||||
// https://github.com/tidalcycles/strudel/issues/245
|
||||
|
||||
// this is how it was:
|
||||
/*
|
||||
return strudel.reify(pat)._degradeByWith(
|
||||
strudel.rand.early(randOffset * _nextSeed()).segment(1),
|
||||
op.arguments_.amount ?? 0.5,
|
||||
);
|
||||
*/
|
||||
pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
|
||||
pat = strudel
|
||||
.reify(pat)
|
||||
._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5);
|
||||
break;
|
||||
}
|
||||
case 'tail': {
|
||||
const friend = patternifyAST(op.arguments_.element, code);
|
||||
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -108,9 +91,7 @@ export function patternifyAST(ast, code) {
|
||||
return strudel.stack(...aligned);
|
||||
}
|
||||
if (alignment === 'rand') {
|
||||
// https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422
|
||||
// return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
|
||||
return strudel.chooseCycles(...children);
|
||||
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
|
||||
}
|
||||
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
||||
if (!weightedChildren && alignment === 'slowcat') {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.2",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -140,7 +140,22 @@ describe('mini', () => {
|
||||
expect(haps.length < 230).toBe(true);
|
||||
// 'Had too many cycles remaining after degradeBy 0.8');
|
||||
});
|
||||
/*it('supports the random choice operator ("|") with nesting', () => {
|
||||
it('supports multiple independent uses of the random choice operator ("|")', () => {
|
||||
const numCycles = 1000;
|
||||
const values = mini('[a|b] [a|b]')
|
||||
.queryArc(0, numCycles)
|
||||
.map((e) => e.value);
|
||||
const observed = { aa: 0, ab: 0, ba: 0, bb: 0 };
|
||||
for (let i = 0; i < values.length; i += 2) {
|
||||
const chunk = values.slice(i, i + 2);
|
||||
observed[chunk.join('')]++;
|
||||
}
|
||||
for (const count of Object.values(observed)) {
|
||||
// Should fall within 99% confidence interval for binomial with p=0.25.
|
||||
expect(215 <= count && count <= 286).toBe(true);
|
||||
}
|
||||
});
|
||||
it('supports the random choice operator ("|") with nesting', () => {
|
||||
const numCycles = 900;
|
||||
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
|
||||
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
|
||||
@@ -165,6 +180,8 @@ describe('mini', () => {
|
||||
// 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
|
||||
// PRNG, this test should succeed
|
||||
expect(chisq <= 15.086).toBe(true);
|
||||
// assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
|
||||
});*/
|
||||
});
|
||||
it('supports lists', () => {
|
||||
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,9 +45,10 @@ let startedAt = -1;
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
*/
|
||||
Pattern.prototype.osc = async function () {
|
||||
const osc = await connect();
|
||||
return this.onTrigger((time, hap, currentTime, cps = 1) => {
|
||||
Pattern.prototype.osc = function () {
|
||||
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
|
||||
hap.ensureObjectValue();
|
||||
const osc = await connect();
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
// time should be audio time of onset
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.1",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -13,24 +13,43 @@ npm i @strudel.cycles/react
|
||||
Here is a minimal example of how to set up a MiniRepl:
|
||||
|
||||
```jsx
|
||||
import { evalScope, controls } from '@strudel.cycles/core';
|
||||
import * as React from 'react';
|
||||
import '@strudel.cycles/react/dist/style.css';
|
||||
import { MiniRepl } from '@strudel.cycles/react';
|
||||
import { prebake } from '../repl/src/prebake.mjs';
|
||||
import { evalScope, controls } from '@strudel.cycles/core';
|
||||
import { samples, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
||||
|
||||
evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
/* probably import other strudel packages */
|
||||
);
|
||||
async function prebake() {
|
||||
await samples(
|
||||
'https://strudel.tidalcycles.org/tidal-drum-machines.json',
|
||||
'github:ritchse/tidal-drum-machines/main/machines/'
|
||||
);
|
||||
await samples(
|
||||
'https://strudel.tidalcycles.org/EmuSP12.json',
|
||||
'https://strudel.tidalcycles.org/EmuSP12/'
|
||||
);
|
||||
}
|
||||
|
||||
prebake();
|
||||
async function init() {
|
||||
await evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/tonal')
|
||||
);
|
||||
await prebake();
|
||||
initAudioOnFirstClick();
|
||||
}
|
||||
|
||||
export function Repl({ tune }) {
|
||||
return <MiniRepl tune={tune} hideOutsideView={true} />;
|
||||
if (typeof window !== 'undefined') {
|
||||
init();
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <MiniRepl tune={`s("bd sd,hh*4")`} />;
|
||||
}
|
||||
```
|
||||
|
||||
For a more sophisticated example, check out the [nano-repl](./examples/nano-repl/)!
|
||||
- Open [example on stackblitz](https://stackblitz.com/edit/react-ts-saaair?file=tune.tsx,App.tsx)
|
||||
- Also check out the [nano-repl](./examples/nano-repl/) for a more sophisticated example
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# nano-repl
|
||||
|
||||
this is an example of how to create a repl with strudel and react.
|
||||
|
||||
## Usage
|
||||
|
||||
after cloning the strudel repo:
|
||||
|
||||
```sh
|
||||
pnpm i
|
||||
cd packages/react/examples/nano-repl
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
you should now have a repl running at `http://localhost:5173/`
|
||||
@@ -66,7 +66,7 @@ function App() {
|
||||
const [code, setCode] = useState(defaultTune);
|
||||
const [view, setView] = useState();
|
||||
// const [code, setCode] = useState(`"c3".note().slow(2)`);
|
||||
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern } = useStrudel({
|
||||
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern, started } = useStrudel({
|
||||
code,
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime,
|
||||
@@ -75,8 +75,8 @@ function App() {
|
||||
useHighlighting({
|
||||
view,
|
||||
pattern,
|
||||
active: !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.getPhase(),
|
||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.now(),
|
||||
});
|
||||
|
||||
const error = evalError || schedulerError;
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: #222;
|
||||
--lineBackground: #22222250;
|
||||
--foreground: #fff;
|
||||
--caret: #ffcc00;
|
||||
--selection: rgba(128, 203, 196, 0.5);
|
||||
--selectionMatch: #036dd626;
|
||||
--lineHighlight: #00000050;
|
||||
--gutterBackground: transparent;
|
||||
--gutterForeground: #8a919966;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #123;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,23 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
module.exports = {
|
||||
// TODO: find out if leaving out tutorial path works now
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}', '../../src/**/*.{html,js,jsx,md,mdx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
colors: {
|
||||
// codemirror-theme settings
|
||||
background: 'var(--background)',
|
||||
lineBackground: 'var(--lineBackground)',
|
||||
foreground: 'var(--foreground)',
|
||||
caret: 'var(--caret)',
|
||||
selection: 'var(--selection)',
|
||||
selectionMatch: 'var(--selectionMatch)',
|
||||
gutterBackground: 'var(--gutterBackground)',
|
||||
gutterForeground: 'var(--gutterForeground)',
|
||||
gutterBorder: 'var(--gutterBorder)',
|
||||
lineHighlight: 'var(--lineHighlight)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/react",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.1",
|
||||
"description": "React components for strudel",
|
||||
"main": "src/index.js",
|
||||
"publishConfig": {
|
||||
@@ -37,6 +37,8 @@
|
||||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/view": "^6.7.3",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@replit/codemirror-emacs": "^6.0.0",
|
||||
"@replit/codemirror-vim": "^6.0.6",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/transpiler": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
@@ -45,17 +47,17 @@
|
||||
"react-hook-inview": "^4.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"postcss": "^8.4.18",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"vite": "^3.2.2"
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ evalScope(
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/midi'),
|
||||
import('@strudel.cycles/xen'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import _CodeMirror from '@uiw/react-codemirror';
|
||||
import { EditorView, Decoration } from '@codemirror/view';
|
||||
import { StateField, StateEffect } from '@codemirror/state';
|
||||
@@ -7,7 +7,9 @@ import strudelTheme from '../themes/strudel-theme';
|
||||
import './style.css';
|
||||
import { useCallback } from 'react';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { strudelAutocomplete } from './Autocomplete';
|
||||
//import { strudelAutocomplete } from './Autocomplete';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
|
||||
export const setFlash = StateEffect.define();
|
||||
const flashField = StateField.define({
|
||||
@@ -56,15 +58,19 @@ const highlightField = StateField.define({
|
||||
haps
|
||||
.map((hap) =>
|
||||
(hap.context.locations || []).map(({ start, end }) => {
|
||||
const color = hap.context.color || e.value.color || '#FFCA28';
|
||||
const color = hap.context.color || e.value.color;
|
||||
let from = tr.newDoc.line(start.line).from + start.column;
|
||||
let to = tr.newDoc.line(end.line).from + end.column;
|
||||
const l = tr.newDoc.length;
|
||||
if (from > l || to > l) {
|
||||
return; // dont mark outside of range, as it will throw an error
|
||||
}
|
||||
// const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
|
||||
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
|
||||
let mark;
|
||||
if (color) {
|
||||
mark = Decoration.mark({ attributes: { style: `outline: 4px solid ${color};` } });
|
||||
} else {
|
||||
mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
|
||||
}
|
||||
return mark.range(from, to);
|
||||
}),
|
||||
)
|
||||
@@ -82,7 +88,7 @@ const highlightField = StateField.define({
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
const extensions = [
|
||||
const staticExtensions = [
|
||||
javascript(),
|
||||
highlightField,
|
||||
flashField,
|
||||
@@ -97,6 +103,9 @@ export default function CodeMirror({
|
||||
onViewChanged,
|
||||
onSelectionChange,
|
||||
theme,
|
||||
keybindings,
|
||||
fontSize = 18,
|
||||
fontFamily = 'monospace',
|
||||
options,
|
||||
editorDidMount,
|
||||
}) {
|
||||
@@ -120,8 +129,18 @@ export default function CodeMirror({
|
||||
},
|
||||
[onSelectionChange],
|
||||
);
|
||||
const extensions = useMemo(() => {
|
||||
let bindings = {
|
||||
vim,
|
||||
emacs,
|
||||
};
|
||||
if (bindings[keybindings]) {
|
||||
return [...staticExtensions, bindings[keybindings]()];
|
||||
}
|
||||
return staticExtensions;
|
||||
}, [keybindings]);
|
||||
return (
|
||||
<>
|
||||
<div style={{ fontSize, fontFamily }} className="w-full">
|
||||
<_CodeMirror
|
||||
value={value}
|
||||
theme={theme || strudelTheme}
|
||||
@@ -130,7 +149,7 @@ export default function CodeMirror({
|
||||
onUpdate={handleOnUpdate}
|
||||
extensions={extensions}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||