mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-23 13:42:56 -04:00
Compare commits
2 Commits
global-repl
..
docs
| Author | SHA1 | Date | |
|---|---|---|---|
| 193324b2b3 | |||
| f3a2cff5db |
+1
-4
@@ -17,7 +17,4 @@ vite.config.js
|
|||||||
**/*.ts
|
**/*.ts
|
||||||
**/*.json
|
**/*.json
|
||||||
**/dev-dist
|
**/dev-dist
|
||||||
**/dist
|
**/dist
|
||||||
/src-tauri/target/**/*
|
|
||||||
reverbGen.mjs
|
|
||||||
hydra.mjs
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
name: Tauri Builder
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
platform: [macos-latest, ubuntu-latest, windows-latest]
|
|
||||||
include:
|
|
||||||
- os: ubuntu-latest
|
|
||||||
rust_target: x86_64-unknown-linux-gnu
|
|
||||||
- os: macos-latest
|
|
||||||
rust_target: x86_64-apple-darwin
|
|
||||||
- os: macos-latest
|
|
||||||
rust_target: aarch64-apple-darwin
|
|
||||||
- os: windows-latest
|
|
||||||
rust_target: x86_64-pc-windows-msvc
|
|
||||||
runs-on: ${{ matrix.platform }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8.6.2
|
|
||||||
|
|
||||||
- name: Node.js setup
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: latest
|
|
||||||
# node-version-file: '.nvmrc'
|
|
||||||
|
|
||||||
- name: Install Rust (Stable)
|
|
||||||
run:
|
|
||||||
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
|
||||||
|
|
||||||
- name: Install dependencies (ubuntu only)
|
|
||||||
if: matrix.platform == 'ubuntu-latest'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf libasound2-dev
|
|
||||||
|
|
||||||
- name: Install app dependencies from lockfile and build web
|
|
||||||
run: pnpm install
|
|
||||||
|
|
||||||
- name: Build the app
|
|
||||||
uses: tauri-apps/tauri-action@v0
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
# tauri-action replaces \_\_VERSION\_\_ with the app version
|
|
||||||
tagName: ${{ github.ref_name }}
|
|
||||||
releaseName: "Strudel v__VERSION__"
|
|
||||||
releaseBody: |
|
|
||||||
See the assets to download this version and install.
|
|
||||||
releaseDraft: true
|
|
||||||
prerelease: false
|
|
||||||
+1
-5
@@ -39,8 +39,4 @@ server/samples/old
|
|||||||
repl/stats.html
|
repl/stats.html
|
||||||
coverage
|
coverage
|
||||||
public/icons/apple-splash-*
|
public/icons/apple-splash-*
|
||||||
dev-dist
|
dev-dist
|
||||||
Dirt-Samples
|
|
||||||
tidal-drum-machines
|
|
||||||
webaudiofontdata
|
|
||||||
src-tauri/target
|
|
||||||
+4
-11
@@ -13,7 +13,7 @@ To get in touch with the contributors, either
|
|||||||
## Ask a Question
|
## Ask a Question
|
||||||
|
|
||||||
If you have any questions about strudel, make sure you've glanced through the
|
If you have any questions about strudel, make sure you've glanced through the
|
||||||
[docs](https://strudel.cc/learn/) to find out if it answers your question.
|
[docs](https://strudel.tidalcycles.org/learn/) to find out if it answers your question.
|
||||||
If not, use one of the Communication Channels above!
|
If not, use one of the Communication Channels above!
|
||||||
|
|
||||||
Don't be afraid to ask! Your question might be of great value for other people too.
|
Don't be afraid to ask! Your question might be of great value for other people too.
|
||||||
@@ -31,7 +31,7 @@ Use one of the Communication Channels listed above.
|
|||||||
|
|
||||||
## Improve the Docs
|
## Improve the Docs
|
||||||
|
|
||||||
If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/),
|
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/learn/getting-started),
|
||||||
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
|
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
|
||||||
|
|
||||||
## Propose a Feature
|
## Propose a Feature
|
||||||
@@ -83,7 +83,7 @@ Please report any problems you've had with the setup instructions!
|
|||||||
|
|
||||||
To make sure the code changes only where it should, we are using prettier to unify the 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 codeformat` from the project root
|
- 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
|
- Run `pnpm format-check` from the project root to check if all files are well formatted
|
||||||
|
|
||||||
If you use VSCode, you can
|
If you use VSCode, you can
|
||||||
@@ -123,14 +123,7 @@ To publish all packages that have been changed since the last release, run:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm login
|
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`.
|
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is slowly stabilising, but please continue to tread carefully.
|
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is slowly stabilising, but please continue to tread carefully.
|
||||||
|
|
||||||
- Try it here: <https://strudel.cc>
|
- Try it here: <https://strudel.tidalcycles.org/>
|
||||||
- Docs: <https://strudel.cc/learn>
|
- Docs: <https://strudel.tidalcycles.org/learn/>
|
||||||
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
||||||
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
||||||
|
|
||||||
@@ -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:
|
After cloning the project, you can run the REPL locally:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run setup
|
npm run setup
|
||||||
pnpm run repl
|
npm run repl
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using Strudel In Your Project
|
## Using Strudel In Your Project
|
||||||
@@ -24,8 +24,8 @@ There are multiple npm packages you can use to use strudel, or only parts of it,
|
|||||||
|
|
||||||
- [`core`](./packages/core/): tidal pattern engine
|
- [`core`](./packages/core/): tidal pattern engine
|
||||||
- [`mini`](./packages/mini): mini notation parser + core binding
|
- [`mini`](./packages/mini): mini notation parser + core binding
|
||||||
- [`transpiler`](./packages/transpiler): user code transpiler
|
- [`eval`](./packages/eval): user code evaluator. syntax sugar + highlighting
|
||||||
- [`webaudio`](./packages/webaudio): webaudio output
|
- [`tone`](./packages/tone): bindings for Tone.js instruments and effects
|
||||||
- [`osc`](./packages/osc): bindings to communicate via OSC
|
- [`osc`](./packages/osc): bindings to communicate via OSC
|
||||||
- [`midi`](./packages/midi): webmidi bindings
|
- [`midi`](./packages/midi): webmidi bindings
|
||||||
- [`serial`](./packages/serial): webserial bindings
|
- [`serial`](./packages/serial): webserial bindings
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
export * from './packages/core/index.mjs';
|
export * from './packages/core/index.mjs';
|
||||||
export * from './packages/csound/index.mjs';
|
export * from './packages/csound/index.mjs';
|
||||||
export * from './packages/embed/index.mjs';
|
export * from './packages/embed/index.mjs';
|
||||||
export * from './packages/desktopbridge/index.mjs';
|
export * from './packages/eval/index.mjs';
|
||||||
export * from './packages/midi/index.mjs';
|
export * from './packages/midi/index.mjs';
|
||||||
export * from './packages/mini/index.mjs';
|
export * from './packages/mini/index.mjs';
|
||||||
export * from './packages/osc/index.mjs';
|
export * from './packages/osc/index.mjs';
|
||||||
@@ -10,6 +10,8 @@ export * from './packages/react/index.mjs';
|
|||||||
export * from './packages/serial/index.mjs';
|
export * from './packages/serial/index.mjs';
|
||||||
export * from './packages/soundfonts/index.mjs';
|
export * from './packages/soundfonts/index.mjs';
|
||||||
export * from './packages/tonal/index.mjs';
|
export * from './packages/tonal/index.mjs';
|
||||||
|
export * from './packages/tone/index.mjs';
|
||||||
export * from './packages/transpiler/index.mjs';
|
export * from './packages/transpiler/index.mjs';
|
||||||
export * from './packages/webaudio/index.mjs';
|
export * from './packages/webaudio/index.mjs';
|
||||||
|
export * from './packages/webdirt/index.mjs';
|
||||||
export * from './packages/xen/index.mjs';
|
export * from './packages/xen/index.mjs';
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ If you want to automatically deploy your site on push, go to `deploy.yml` and ch
|
|||||||
## running locally
|
## running locally
|
||||||
|
|
||||||
- install dependencies with `npm run setup`
|
- install dependencies with `npm run setup`
|
||||||
- run dev server with `npm run repl` and open `http://localhost:4321/strudel/swatch/`
|
- run dev server with `npm run repl` and open `http://localhost:3000/strudel/swatch/`
|
||||||
|
|
||||||
## tests fail?
|
## tests fail?
|
||||||
|
|
||||||
|
|||||||
+15
-14
@@ -18,6 +18,7 @@
|
|||||||
"build": "npm run prebuild && cd website && npm run build",
|
"build": "npm run prebuild && cd website && npm run build",
|
||||||
"preview": "cd website && npm run preview",
|
"preview": "cd website && npm run preview",
|
||||||
"osc": "cd packages/osc && npm run server",
|
"osc": "cd packages/osc && npm run server",
|
||||||
|
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
|
||||||
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
||||||
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
|
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
|
||||||
"lint": "eslint . --ext mjs,js --quiet",
|
"lint": "eslint . --ext mjs,js --quiet",
|
||||||
@@ -43,30 +44,30 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://strudel.cc",
|
"homepage": "https://strudel.tidalcycles.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"dependency-tree": "^9.0.0",
|
||||||
|
"vitest": "^0.25.7",
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
"@strudel.cycles/mini": "workspace:*",
|
||||||
"@strudel.cycles/tonal": "workspace:*",
|
"@strudel.cycles/tonal": "workspace:*",
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
"@strudel.cycles/transpiler": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
"@strudel.cycles/xen": "workspace:*",
|
"@strudel.cycles/xen": "workspace:*"
|
||||||
"acorn": "^8.8.1",
|
|
||||||
"dependency-tree": "^9.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.4.0",
|
"@vitest/ui": "^0.25.7",
|
||||||
"@vitest/ui": "^0.28.0",
|
"c8": "^7.12.0",
|
||||||
"canvas": "^2.11.2",
|
"canvas": "^2.11.0",
|
||||||
"eslint": "^8.39.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"jsdoc": "^4.0.2",
|
"gh-pages": "^4.0.0",
|
||||||
|
"jsdoc": "^3.6.10",
|
||||||
"jsdoc-json": "^2.0.2",
|
"jsdoc-json": "^2.0.2",
|
||||||
"jsdoc-to-markdown": "^8.0.0",
|
"jsdoc-to-markdown": "^7.1.1",
|
||||||
"lerna": "^6.6.1",
|
"lerna": "^4.0.0",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.1",
|
||||||
"rollup-plugin-visualizer": "^5.8.1",
|
"rollup-plugin-visualizer": "^5.8.1"
|
||||||
"vitest": "^0.33.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
import { createRoot } from 'react-dom/client';
|
|
||||||
import jsdoc from '../../doc.json';
|
|
||||||
// import { javascriptLanguage } from '@codemirror/lang-javascript';
|
|
||||||
import { autocompletion } from '@codemirror/autocomplete';
|
|
||||||
|
|
||||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
|
||||||
const getInnerText = (html) => {
|
|
||||||
var div = document.createElement('div');
|
|
||||||
div.innerHTML = html;
|
|
||||||
return div.textContent || div.innerText || '';
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Autocomplete({ doc }) {
|
|
||||||
return (
|
|
||||||
<div className="prose dark:prose-invert max-h-[400px] overflow-auto">
|
|
||||||
<h3 className="pt-0 mt-0">{getDocLabel(doc)}</h3>
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: doc.description }} />
|
|
||||||
<ul>
|
|
||||||
{doc.params?.map(({ name, type, description }, i) => (
|
|
||||||
<li key={i}>
|
|
||||||
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<div>
|
|
||||||
{doc.examples?.map((example, i) => (
|
|
||||||
<div key={i}>
|
|
||||||
<pre
|
|
||||||
className="cursor-pointer"
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
console.log('ola!');
|
|
||||||
navigator.clipboard.writeText(example);
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{example}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsdocCompletions = jsdoc.docs
|
|
||||||
.filter(
|
|
||||||
(doc) =>
|
|
||||||
getDocLabel(doc) &&
|
|
||||||
!getDocLabel(doc).startsWith('_') &&
|
|
||||||
!['package'].includes(doc.kind) &&
|
|
||||||
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
|
|
||||||
)
|
|
||||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
|
||||||
.map((doc) /*: Completion */ => ({
|
|
||||||
label: getDocLabel(doc),
|
|
||||||
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
|
||||||
info: () => {
|
|
||||||
const node = document.createElement('div');
|
|
||||||
// if Autocomplete is non-interactive, it could also be rendered at build time..
|
|
||||||
// .. using renderToStaticMarkup
|
|
||||||
createRoot(node).render(<Autocomplete doc={doc} />);
|
|
||||||
return node;
|
|
||||||
},
|
|
||||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const strudelAutocomplete = (context /* : CompletionContext */) => {
|
|
||||||
let word = context.matchBefore(/\w*/);
|
|
||||||
if (word.from == word.to && !context.explicit) return null;
|
|
||||||
return {
|
|
||||||
from: word.from,
|
|
||||||
options: jsdocCompletions,
|
|
||||||
/* options: [
|
|
||||||
{ label: 'match', type: 'keyword' },
|
|
||||||
{ label: 'hello', type: 'variable', info: '(World)' },
|
|
||||||
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
|
|
||||||
], */
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isAutoCompletionEnabled(on) {
|
|
||||||
return on
|
|
||||||
? [
|
|
||||||
autocompletion({ override: [strudelAutocomplete] }),
|
|
||||||
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
|
|
||||||
]
|
|
||||||
: []; // autocompletion({ override: [] })
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# @strudel/codemirror
|
|
||||||
|
|
||||||
This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it.
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
import { closeBrackets } from '@codemirror/autocomplete';
|
|
||||||
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
|
||||||
import { history } from '@codemirror/commands';
|
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
|
||||||
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
||||||
import { Compartment, EditorState } from '@codemirror/state';
|
|
||||||
import { EditorView, highlightActiveLineGutter, highlightActiveLine, keymap, lineNumbers } from '@codemirror/view';
|
|
||||||
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel.cycles/core';
|
|
||||||
import { isAutoCompletionEnabled } from './Autocomplete';
|
|
||||||
import { flash, isFlashEnabled } from './flash.mjs';
|
|
||||||
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
|
||||||
import { keybindings } from './keybindings.mjs';
|
|
||||||
import { theme } from './themes.mjs';
|
|
||||||
import { updateWidgets, sliderPlugin } from './slider.mjs';
|
|
||||||
|
|
||||||
const extensions = {
|
|
||||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
|
||||||
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
|
|
||||||
theme,
|
|
||||||
isAutoCompletionEnabled,
|
|
||||||
isPatternHighlightingEnabled,
|
|
||||||
isFlashEnabled,
|
|
||||||
keybindings,
|
|
||||||
};
|
|
||||||
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
|
||||||
|
|
||||||
// https://codemirror.net/docs/guide/
|
|
||||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, settings, root }) {
|
|
||||||
const initialSettings = Object.keys(compartments).map((key) =>
|
|
||||||
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
|
||||||
);
|
|
||||||
let state = EditorState.create({
|
|
||||||
doc: initialCode,
|
|
||||||
extensions: [
|
|
||||||
/* search(),
|
|
||||||
highlightSelectionMatches(), */
|
|
||||||
...initialSettings,
|
|
||||||
javascript(),
|
|
||||||
sliderPlugin,
|
|
||||||
// indentOnInput(), // works without. already brought with javascript extension?
|
|
||||||
// bracketMatching(), // does not do anything
|
|
||||||
closeBrackets(),
|
|
||||||
highlightActiveLineGutter(),
|
|
||||||
highlightActiveLine(),
|
|
||||||
syntaxHighlighting(defaultHighlightStyle),
|
|
||||||
history(),
|
|
||||||
EditorView.updateListener.of((v) => onChange(v)),
|
|
||||||
keymap.of([
|
|
||||||
{
|
|
||||||
key: 'Ctrl-Enter',
|
|
||||||
run: () => onEvaluate?.(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Alt-Enter',
|
|
||||||
run: () => onEvaluate?.(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Ctrl-.',
|
|
||||||
run: () => onStop?.(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Alt-.',
|
|
||||||
run: (_, e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onStop?.();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/* {
|
|
||||||
key: 'Ctrl-Shift-.',
|
|
||||||
run: () => (onPanic ? onPanic() : onStop?.()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Ctrl-Shift-Enter',
|
|
||||||
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
|
|
||||||
}, */
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
return new EditorView({
|
|
||||||
state,
|
|
||||||
parent: root,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export class StrudelMirror {
|
|
||||||
constructor(options) {
|
|
||||||
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, settings, ...replOptions } = options;
|
|
||||||
this.code = initialCode;
|
|
||||||
this.root = root;
|
|
||||||
this.miniLocations = [];
|
|
||||||
this.widgets = [];
|
|
||||||
this.painters = [];
|
|
||||||
this.onDraw = onDraw;
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
this.drawer = new Drawer((haps, time) => {
|
|
||||||
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
|
|
||||||
this.highlight(currentFrame, time);
|
|
||||||
this.onDraw?.(haps, time, currentFrame, this.painters);
|
|
||||||
}, drawTime);
|
|
||||||
|
|
||||||
// this approach might not work with multiple repls on screen..
|
|
||||||
Pattern.prototype.onPaint = function (onPaint) {
|
|
||||||
self.painters.push(onPaint);
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.prebaked = prebake();
|
|
||||||
// this.drawFirstFrame();
|
|
||||||
|
|
||||||
this.repl = repl({
|
|
||||||
...replOptions,
|
|
||||||
onToggle: (started) => {
|
|
||||||
replOptions?.onToggle?.(started);
|
|
||||||
if (started) {
|
|
||||||
this.drawer.start(this.repl.scheduler);
|
|
||||||
} else {
|
|
||||||
this.drawer.stop();
|
|
||||||
updateMiniLocations(this.editor, []);
|
|
||||||
cleanupDraw(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeEval: async () => {
|
|
||||||
cleanupDraw();
|
|
||||||
this.painters = [];
|
|
||||||
await this.prebaked;
|
|
||||||
await replOptions?.beforeEval?.();
|
|
||||||
},
|
|
||||||
afterEval: (options) => {
|
|
||||||
// remember for when highlighting is toggled on
|
|
||||||
this.miniLocations = options.meta?.miniLocations;
|
|
||||||
this.widgets = options.meta?.widgets;
|
|
||||||
updateWidgets(this.editor, this.widgets);
|
|
||||||
updateMiniLocations(this.editor, this.miniLocations);
|
|
||||||
replOptions?.afterEval?.(options);
|
|
||||||
this.drawer.invalidate();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.editor = initEditor({
|
|
||||||
root,
|
|
||||||
settings,
|
|
||||||
initialCode,
|
|
||||||
onChange: (v) => {
|
|
||||||
if (v.docChanged) {
|
|
||||||
this.code = v.state.doc.toString();
|
|
||||||
this.repl.setCode(this.code);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onEvaluate: () => this.evaluate(),
|
|
||||||
onStop: () => this.stop(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async drawFirstFrame() {
|
|
||||||
if (!this.onDraw) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// draw first frame instantly
|
|
||||||
await this.prebaked;
|
|
||||||
try {
|
|
||||||
await this.repl.evaluate(this.code, false);
|
|
||||||
this.drawer.invalidate(this.repl.scheduler);
|
|
||||||
this.onDraw?.(this.drawer.visibleHaps, 0, []);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('first frame could not be painted');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async evaluate() {
|
|
||||||
this.flash();
|
|
||||||
await this.repl.evaluate(this.code);
|
|
||||||
}
|
|
||||||
async stop() {
|
|
||||||
this.repl.scheduler.stop();
|
|
||||||
}
|
|
||||||
async toggle() {
|
|
||||||
if (this.repl.scheduler.started) {
|
|
||||||
this.repl.scheduler.stop();
|
|
||||||
} else {
|
|
||||||
this.evaluate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flash(ms) {
|
|
||||||
flash(this.editor, ms);
|
|
||||||
}
|
|
||||||
highlight(haps, time) {
|
|
||||||
highlightMiniLocations(this.editor, time, haps);
|
|
||||||
}
|
|
||||||
setFontSize(size) {
|
|
||||||
this.root.style.fontSize = size + 'px';
|
|
||||||
}
|
|
||||||
setFontFamily(family) {
|
|
||||||
this.root.style.fontFamily = family;
|
|
||||||
}
|
|
||||||
reconfigureExtension(key, value) {
|
|
||||||
if (!extensions[key]) {
|
|
||||||
console.warn(`extension ${key} is not known`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
value = parseBooleans(value);
|
|
||||||
const newValue = extensions[key](value, this);
|
|
||||||
this.editor.dispatch({
|
|
||||||
effects: compartments[key].reconfigure(newValue),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setLineWrappingEnabled(enabled) {
|
|
||||||
this.reconfigureExtension('isLineWrappingEnabled', enabled);
|
|
||||||
}
|
|
||||||
setLineNumbersDisplayed(enabled) {
|
|
||||||
this.reconfigureExtension('isLineNumbersDisplayed', enabled);
|
|
||||||
}
|
|
||||||
setTheme(theme) {
|
|
||||||
this.reconfigureExtension('theme', theme);
|
|
||||||
}
|
|
||||||
setAutocompletionEnabled(enabled) {
|
|
||||||
this.reconfigureExtension('isAutoCompletionEnabled', enabled);
|
|
||||||
}
|
|
||||||
updateSettings(settings) {
|
|
||||||
this.setFontSize(settings.fontSize);
|
|
||||||
this.setFontFamily(settings.fontFamily);
|
|
||||||
for (let key in extensions) {
|
|
||||||
this.reconfigureExtension(key, settings[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
changeSetting(key, value) {
|
|
||||||
if (extensions[key]) {
|
|
||||||
this.reconfigureExtension(key, value);
|
|
||||||
return;
|
|
||||||
} else if (key === 'fontFamily') {
|
|
||||||
this.setFontFamily(value);
|
|
||||||
} else if (key === 'fontSize') {
|
|
||||||
this.setFontSize(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setCode(code) {
|
|
||||||
const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
|
|
||||||
this.editor.dispatch({ changes });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseBooleans(value) {
|
|
||||||
return { true: true, false: false }[value] ?? value;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { StateEffect, StateField } from '@codemirror/state';
|
|
||||||
import { Decoration, EditorView } from '@codemirror/view';
|
|
||||||
|
|
||||||
export const setFlash = StateEffect.define();
|
|
||||||
export const flashField = StateField.define({
|
|
||||||
create() {
|
|
||||||
return Decoration.none;
|
|
||||||
},
|
|
||||||
update(flash, tr) {
|
|
||||||
try {
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(setFlash)) {
|
|
||||||
if (e.value && tr.newDoc.length > 0) {
|
|
||||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
|
||||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
|
||||||
} else {
|
|
||||||
flash = Decoration.set([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return flash;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('flash error', err);
|
|
||||||
return flash;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
provide: (f) => EditorView.decorations.from(f),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const flash = (view, ms = 200) => {
|
|
||||||
view.dispatch({ effects: setFlash.of(true) });
|
|
||||||
setTimeout(() => {
|
|
||||||
view.dispatch({ effects: setFlash.of(false) });
|
|
||||||
}, ms);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isFlashEnabled = (on) => (on ? flashField : []);
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
|
||||||
import { Decoration, EditorView } from '@codemirror/view';
|
|
||||||
|
|
||||||
export const setMiniLocations = StateEffect.define();
|
|
||||||
export const showMiniLocations = StateEffect.define();
|
|
||||||
export const updateMiniLocations = (view, locations) => {
|
|
||||||
view.dispatch({ effects: setMiniLocations.of(locations) });
|
|
||||||
};
|
|
||||||
export const highlightMiniLocations = (view, atTime, haps) => {
|
|
||||||
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
|
|
||||||
};
|
|
||||||
|
|
||||||
const miniLocations = StateField.define({
|
|
||||||
create() {
|
|
||||||
return Decoration.none;
|
|
||||||
},
|
|
||||||
update(locations, tr) {
|
|
||||||
if (tr.docChanged) {
|
|
||||||
locations = locations.map(tr.changes);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(setMiniLocations)) {
|
|
||||||
// this is called on eval, with the mini locations obtained from the transpiler
|
|
||||||
// codemirror will automatically remap the marks when the document is edited
|
|
||||||
// create a mark for each mini location, adding the range to the spec to find it later
|
|
||||||
const marks = e.value
|
|
||||||
.filter(([from]) => from < tr.newDoc.length)
|
|
||||||
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
|
|
||||||
.map(
|
|
||||||
(range) =>
|
|
||||||
Decoration.mark({
|
|
||||||
id: range.join(':'),
|
|
||||||
// this green is only to verify that the decoration moves when the document is edited
|
|
||||||
// it will be removed later, so the mark is not visible by default
|
|
||||||
attributes: { style: `background-color: #00CA2880` },
|
|
||||||
}).range(...range), // -> Decoration
|
|
||||||
);
|
|
||||||
|
|
||||||
locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<Decoration>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return locations;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const visibleMiniLocations = StateField.define({
|
|
||||||
create() {
|
|
||||||
return { atTime: 0, haps: new Map() };
|
|
||||||
},
|
|
||||||
update(visible, tr) {
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(showMiniLocations)) {
|
|
||||||
// this is called every frame to show the locations that are currently active
|
|
||||||
// we can NOT create new marks because the context.locations haven't changed since eval time
|
|
||||||
// this is why we need to find a way to update the existing decorations, showing the ones that have an active range
|
|
||||||
const haps = new Map();
|
|
||||||
for (let hap of e.value.haps) {
|
|
||||||
for (let { start, end } of hap.context.locations) {
|
|
||||||
let id = `${start}:${end}`;
|
|
||||||
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
|
|
||||||
haps.set(id, hap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visible = { atTime: e.value.atTime, haps };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return visible;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// // Derive the set of decorations from the miniLocations and visibleLocations
|
|
||||||
const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
|
|
||||||
const iterator = state.field(miniLocations).iter();
|
|
||||||
const { haps } = state.field(visibleMiniLocations);
|
|
||||||
const builder = new RangeSetBuilder();
|
|
||||||
|
|
||||||
while (iterator.value) {
|
|
||||||
const {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
value: {
|
|
||||||
spec: { id },
|
|
||||||
},
|
|
||||||
} = iterator;
|
|
||||||
|
|
||||||
if (haps.has(id)) {
|
|
||||||
const hap = haps.get(id);
|
|
||||||
const color = hap.context.color ?? 'var(--foreground)';
|
|
||||||
// Get explicit channels for color values
|
|
||||||
/*
|
|
||||||
const swatch = document.createElement('div');
|
|
||||||
swatch.style.color = color;
|
|
||||||
document.body.appendChild(swatch);
|
|
||||||
let channels = getComputedStyle(swatch)
|
|
||||||
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
|
|
||||||
.slice(1)
|
|
||||||
.map((c) => parseFloat(c || 1));
|
|
||||||
document.body.removeChild(swatch);
|
|
||||||
|
|
||||||
// Get percentage of event
|
|
||||||
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
|
|
||||||
channels[3] *= percent;
|
|
||||||
*/
|
|
||||||
|
|
||||||
builder.add(
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
Decoration.mark({
|
|
||||||
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
|
|
||||||
attributes: { style: `outline: solid 2px ${color}` },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
iterator.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.finish();
|
|
||||||
});
|
|
||||||
|
|
||||||
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
|
|
||||||
|
|
||||||
export const isPatternHighlightingEnabled = (on, config) => {
|
|
||||||
on &&
|
|
||||||
config &&
|
|
||||||
setTimeout(() => {
|
|
||||||
updateMiniLocations(config.editor, config.miniLocations);
|
|
||||||
}, 100);
|
|
||||||
return on ? highlightExtension : [];
|
|
||||||
};
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './codemirror.mjs';
|
|
||||||
export * from './highlight.mjs';
|
|
||||||
export * from './flash.mjs';
|
|
||||||
export * from './slider.mjs';
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { Prec } from '@codemirror/state';
|
|
||||||
import { keymap, ViewPlugin } from '@codemirror/view';
|
|
||||||
// import { searchKeymap } from '@codemirror/search';
|
|
||||||
import { emacs } from '@replit/codemirror-emacs';
|
|
||||||
import { vim } from '@replit/codemirror-vim';
|
|
||||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
|
||||||
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
|
|
||||||
|
|
||||||
const vscodePlugin = ViewPlugin.fromClass(
|
|
||||||
class {
|
|
||||||
constructor() {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: () => {
|
|
||||||
return Prec.highest(keymap.of([...vscodeKeymap]));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
|
||||||
|
|
||||||
const keymaps = {
|
|
||||||
vim,
|
|
||||||
emacs,
|
|
||||||
vscode: vscodeExtension,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function keybindings(name) {
|
|
||||||
const active = keymaps[name];
|
|
||||||
return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []];
|
|
||||||
// keymap.of(searchKeymap),
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@strudel/codemirror",
|
|
||||||
"version": "0.9.0",
|
|
||||||
"description": "Codemirror Extensions for Strudel",
|
|
||||||
"main": "index.mjs",
|
|
||||||
"publishConfig": {
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"module": "dist/index.mjs"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "vite build",
|
|
||||||
"prepublishOnly": "npm run build"
|
|
||||||
},
|
|
||||||
"type": "module",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"tidalcycles",
|
|
||||||
"strudel",
|
|
||||||
"pattern",
|
|
||||||
"livecoding",
|
|
||||||
"algorave"
|
|
||||||
],
|
|
||||||
"author": "Felix Roos <flix91@gmail.com>",
|
|
||||||
"contributors": [
|
|
||||||
"Alex McLean <alex@slab.org>"
|
|
||||||
],
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
|
||||||
"dependencies": {
|
|
||||||
"@codemirror/autocomplete": "^6.6.0",
|
|
||||||
"@codemirror/commands": "^6.2.4",
|
|
||||||
"@codemirror/lang-javascript": "^6.1.7",
|
|
||||||
"@codemirror/language": "^6.6.0",
|
|
||||||
"@codemirror/search": "^6.0.0",
|
|
||||||
"@codemirror/state": "^6.2.0",
|
|
||||||
"@codemirror/view": "^6.10.0",
|
|
||||||
"@lezer/highlight": "^1.1.4",
|
|
||||||
"@replit/codemirror-emacs": "^6.0.1",
|
|
||||||
"@replit/codemirror-vim": "^6.0.14",
|
|
||||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
|
||||||
"@strudel.cycles/core": "workspace:*",
|
|
||||||
"@uiw/codemirror-themes": "^4.19.16",
|
|
||||||
"@uiw/codemirror-themes-all": "^4.19.16",
|
|
||||||
"react-dom": "^18.2.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"vite": "^4.3.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import { ref, pure } from '@strudel.cycles/core';
|
|
||||||
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
|
|
||||||
import { StateEffect, StateField } from '@codemirror/state';
|
|
||||||
|
|
||||||
export let sliderValues = {};
|
|
||||||
const getSliderID = (from) => `slider_${from}`;
|
|
||||||
|
|
||||||
export class SliderWidget extends WidgetType {
|
|
||||||
constructor(value, min, max, from, to, step, view) {
|
|
||||||
super();
|
|
||||||
this.value = value;
|
|
||||||
this.min = min;
|
|
||||||
this.max = max;
|
|
||||||
this.from = from;
|
|
||||||
this.originalFrom = from;
|
|
||||||
this.to = to;
|
|
||||||
this.step = step;
|
|
||||||
this.view = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
eq() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
toDOM() {
|
|
||||||
let wrap = document.createElement('span');
|
|
||||||
wrap.setAttribute('aria-hidden', 'true');
|
|
||||||
wrap.className = 'cm-slider'; // inline-flex items-center
|
|
||||||
let slider = wrap.appendChild(document.createElement('input'));
|
|
||||||
slider.type = 'range';
|
|
||||||
slider.min = this.min;
|
|
||||||
slider.max = this.max;
|
|
||||||
slider.step = this.step ?? (this.max - this.min) / 1000;
|
|
||||||
slider.originalValue = this.value;
|
|
||||||
// to make sure the code stays in sync, let's save the original value
|
|
||||||
// becuase .value automatically clamps values so it'll desync with the code
|
|
||||||
slider.value = slider.originalValue;
|
|
||||||
slider.from = this.from;
|
|
||||||
slider.originalFrom = this.originalFrom;
|
|
||||||
slider.to = this.to;
|
|
||||||
slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
|
|
||||||
this.slider = slider;
|
|
||||||
slider.addEventListener('input', (e) => {
|
|
||||||
const next = e.target.value;
|
|
||||||
let insert = next;
|
|
||||||
//let insert = next.toFixed(2);
|
|
||||||
const to = slider.from + slider.originalValue.length;
|
|
||||||
let change = { from: slider.from, to, insert };
|
|
||||||
slider.originalValue = insert;
|
|
||||||
slider.value = insert;
|
|
||||||
this.view.dispatch({ changes: change });
|
|
||||||
const id = getSliderID(slider.originalFrom); // matches id generated in transpiler
|
|
||||||
window.postMessage({ type: 'cm-slider', value: Number(next), id });
|
|
||||||
});
|
|
||||||
return wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
ignoreEvent(e) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setWidgets = StateEffect.define();
|
|
||||||
|
|
||||||
export const updateWidgets = (view, widgets) => {
|
|
||||||
view.dispatch({ effects: setWidgets.of(widgets) });
|
|
||||||
};
|
|
||||||
|
|
||||||
function getWidgets(widgetConfigs, view) {
|
|
||||||
return widgetConfigs.map(({ from, to, value, min, max, step }) => {
|
|
||||||
return Decoration.widget({
|
|
||||||
widget: new SliderWidget(value, min, max, from, to, step, view),
|
|
||||||
side: 0,
|
|
||||||
}).range(from /* , to */);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sliderPlugin = ViewPlugin.fromClass(
|
|
||||||
class {
|
|
||||||
decorations; //: DecorationSet
|
|
||||||
|
|
||||||
constructor(view /* : EditorView */) {
|
|
||||||
this.decorations = Decoration.set([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(update /* : ViewUpdate */) {
|
|
||||||
update.transactions.forEach((tr) => {
|
|
||||||
if (tr.docChanged) {
|
|
||||||
this.decorations = this.decorations.map(tr.changes);
|
|
||||||
const iterator = this.decorations.iter();
|
|
||||||
while (iterator.value) {
|
|
||||||
// when the widgets are moved, we need to tell the dom node the current position
|
|
||||||
// this is important because the updateSliderValue function has to work with the dom node
|
|
||||||
if (iterator.value?.widget?.slider) {
|
|
||||||
iterator.value.widget.slider.from = iterator.from;
|
|
||||||
iterator.value.widget.slider.to = iterator.to;
|
|
||||||
}
|
|
||||||
iterator.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(setWidgets)) {
|
|
||||||
this.decorations = Decoration.set(getWidgets(e.value, update.view));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
decorations: (v) => v.decorations,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export let slider = (value) => {
|
|
||||||
console.warn('slider will only work when the transpiler is used... passing value as is');
|
|
||||||
return pure(value);
|
|
||||||
};
|
|
||||||
// function transpiled from slider = (value, min, max)
|
|
||||||
export let sliderWithID = (id, value, min, max) => {
|
|
||||||
sliderValues[id] = value; // sync state at eval time (code -> state)
|
|
||||||
return ref(() => sliderValues[id]); // use state at query time
|
|
||||||
};
|
|
||||||
// update state when sliders are moved
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.addEventListener('message', (e) => {
|
|
||||||
if (e.data.type === 'cm-slider') {
|
|
||||||
if (sliderValues[e.data.id] !== undefined) {
|
|
||||||
// update state when slider is moved
|
|
||||||
sliderValues[e.data.id] = e.data.value;
|
|
||||||
} else {
|
|
||||||
console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Vendored
-484
@@ -1,484 +0,0 @@
|
|||||||
import {
|
|
||||||
abcdef,
|
|
||||||
androidstudio,
|
|
||||||
atomone,
|
|
||||||
aura,
|
|
||||||
bespin,
|
|
||||||
darcula,
|
|
||||||
dracula,
|
|
||||||
duotoneDark,
|
|
||||||
eclipse,
|
|
||||||
githubDark,
|
|
||||||
gruvboxDark,
|
|
||||||
materialDark,
|
|
||||||
nord,
|
|
||||||
okaidia,
|
|
||||||
solarizedDark,
|
|
||||||
sublime,
|
|
||||||
tokyoNight,
|
|
||||||
tokyoNightStorm,
|
|
||||||
vscodeDark,
|
|
||||||
xcodeDark,
|
|
||||||
bbedit,
|
|
||||||
duotoneLight,
|
|
||||||
githubLight,
|
|
||||||
gruvboxLight,
|
|
||||||
materialLight,
|
|
||||||
noctisLilac,
|
|
||||||
solarizedLight,
|
|
||||||
tokyoNightDay,
|
|
||||||
xcodeLight,
|
|
||||||
} from '@uiw/codemirror-themes-all';
|
|
||||||
|
|
||||||
import strudelTheme from './themes/strudel-theme';
|
|
||||||
import bluescreen, { settings as bluescreenSettings } from './themes/bluescreen';
|
|
||||||
import blackscreen, { settings as blackscreenSettings } from './themes/blackscreen';
|
|
||||||
import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen';
|
|
||||||
import teletext, { settings as teletextSettings } from './themes/teletext';
|
|
||||||
import algoboy, { settings as algoboySettings } from './themes/algoboy';
|
|
||||||
import terminal, { settings as terminalSettings } from './themes/terminal';
|
|
||||||
|
|
||||||
export const themes = {
|
|
||||||
strudelTheme,
|
|
||||||
bluescreen,
|
|
||||||
blackscreen,
|
|
||||||
whitescreen,
|
|
||||||
teletext,
|
|
||||||
algoboy,
|
|
||||||
terminal,
|
|
||||||
abcdef,
|
|
||||||
androidstudio,
|
|
||||||
atomone,
|
|
||||||
aura,
|
|
||||||
bespin,
|
|
||||||
darcula,
|
|
||||||
dracula,
|
|
||||||
duotoneDark,
|
|
||||||
eclipse,
|
|
||||||
githubDark,
|
|
||||||
gruvboxDark,
|
|
||||||
materialDark,
|
|
||||||
nord,
|
|
||||||
okaidia,
|
|
||||||
solarizedDark,
|
|
||||||
sublime,
|
|
||||||
tokyoNight,
|
|
||||||
tokyoNightStorm,
|
|
||||||
vscodeDark,
|
|
||||||
xcodeDark,
|
|
||||||
bbedit,
|
|
||||||
duotoneLight,
|
|
||||||
githubLight,
|
|
||||||
gruvboxLight,
|
|
||||||
materialLight,
|
|
||||||
noctisLilac,
|
|
||||||
solarizedLight,
|
|
||||||
tokyoNightDay,
|
|
||||||
xcodeLight,
|
|
||||||
};
|
|
||||||
|
|
||||||
// lineBackground is background with 50% opacity, to make sure the selection below is visible
|
|
||||||
|
|
||||||
export const settings = {
|
|
||||||
strudelTheme: {
|
|
||||||
background: '#222',
|
|
||||||
lineBackground: '#22222299',
|
|
||||||
foreground: '#fff',
|
|
||||||
// foreground: '#75baff',
|
|
||||||
caret: '#ffcc00',
|
|
||||||
selection: 'rgba(128, 203, 196, 0.5)',
|
|
||||||
selectionMatch: '#036dd626',
|
|
||||||
// lineHighlight: '#8a91991a', // original
|
|
||||||
lineHighlight: '#00000050',
|
|
||||||
gutterBackground: 'transparent',
|
|
||||||
// gutterForeground: '#8a919966',
|
|
||||||
gutterForeground: '#8a919966',
|
|
||||||
},
|
|
||||||
bluescreen: bluescreenSettings,
|
|
||||||
blackscreen: blackscreenSettings,
|
|
||||||
whitescreen: whitescreenSettings,
|
|
||||||
teletext: teletextSettings,
|
|
||||||
algoboy: algoboySettings,
|
|
||||||
terminal: terminalSettings,
|
|
||||||
abcdef: {
|
|
||||||
background: '#0f0f0f',
|
|
||||||
lineBackground: '#0f0f0f99',
|
|
||||||
foreground: '#defdef',
|
|
||||||
caret: '#00FF00',
|
|
||||||
selection: '#515151',
|
|
||||||
selectionMatch: '#515151',
|
|
||||||
gutterBackground: '#555',
|
|
||||||
gutterForeground: '#FFFFFF',
|
|
||||||
lineHighlight: '#314151',
|
|
||||||
},
|
|
||||||
androidstudio: {
|
|
||||||
background: '#282b2e',
|
|
||||||
lineBackground: '#282b2e99',
|
|
||||||
foreground: '#a9b7c6',
|
|
||||||
caret: '#00FF00',
|
|
||||||
selection: '#343739',
|
|
||||||
selectionMatch: '#343739',
|
|
||||||
lineHighlight: '#343739',
|
|
||||||
},
|
|
||||||
atomone: {
|
|
||||||
background: '#272C35',
|
|
||||||
lineBackground: '#272C3599',
|
|
||||||
foreground: '#9d9b97',
|
|
||||||
caret: '#797977',
|
|
||||||
selection: '#ffffff30',
|
|
||||||
selectionMatch: '#2B323D',
|
|
||||||
gutterBackground: '#272C35',
|
|
||||||
gutterForeground: '#465063',
|
|
||||||
gutterBorder: 'transparent',
|
|
||||||
lineHighlight: '#2B323D',
|
|
||||||
},
|
|
||||||
aura: {
|
|
||||||
background: '#21202e',
|
|
||||||
lineBackground: '#21202e99',
|
|
||||||
foreground: '#edecee',
|
|
||||||
caret: '#a277ff',
|
|
||||||
selection: '#3d375e7f',
|
|
||||||
selectionMatch: '#3d375e7f',
|
|
||||||
gutterBackground: '#21202e',
|
|
||||||
gutterForeground: '#edecee',
|
|
||||||
gutterBorder: 'transparent',
|
|
||||||
lineHighlight: '#a394f033',
|
|
||||||
},
|
|
||||||
bbedit: {
|
|
||||||
light: true,
|
|
||||||
background: '#FFFFFF',
|
|
||||||
lineBackground: '#FFFFFF99',
|
|
||||||
foreground: '#000000',
|
|
||||||
caret: '#FBAC52',
|
|
||||||
selection: '#FFD420',
|
|
||||||
selectionMatch: '#FFD420',
|
|
||||||
gutterBackground: '#f5f5f5',
|
|
||||||
gutterForeground: '#4D4D4C',
|
|
||||||
gutterBorder: 'transparent',
|
|
||||||
lineHighlight: '#00000012',
|
|
||||||
},
|
|
||||||
bespin: {
|
|
||||||
background: '#28211c',
|
|
||||||
lineBackground: '#28211c99',
|
|
||||||
foreground: '#9d9b97',
|
|
||||||
caret: '#797977',
|
|
||||||
selection: '#36312e',
|
|
||||||
selectionMatch: '#4f382b',
|
|
||||||
gutterBackground: '#28211c',
|
|
||||||
gutterForeground: '#666666',
|
|
||||||
lineHighlight: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
},
|
|
||||||
darcula: {
|
|
||||||
background: '#2B2B2B',
|
|
||||||
lineBackground: '#2B2B2B99',
|
|
||||||
foreground: '#f8f8f2',
|
|
||||||
caret: '#FFFFFF',
|
|
||||||
selection: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
selectionMatch: 'rgba(255, 255, 255, 0.2)',
|
|
||||||
gutterBackground: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
gutterForeground: '#999',
|
|
||||||
gutterBorder: 'transparent',
|
|
||||||
lineHighlight: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
},
|
|
||||||
dracula: {
|
|
||||||
background: '#282a36',
|
|
||||||
lineBackground: '#282a3699',
|
|
||||||
foreground: '#f8f8f2',
|
|
||||||
caret: '#f8f8f0',
|
|
||||||
selection: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
selectionMatch: 'rgba(255, 255, 255, 0.2)',
|
|
||||||
gutterBackground: '#282a36',
|
|
||||||
gutterForeground: '#6D8A88',
|
|
||||||
gutterBorder: 'transparent',
|
|
||||||
lineHighlight: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
},
|
|
||||||
duotoneLight: {
|
|
||||||
light: true,
|
|
||||||
background: '#faf8f5',
|
|
||||||
lineBackground: '#faf8f599',
|
|
||||||
foreground: '#b29762',
|
|
||||||
caret: '#93abdc',
|
|
||||||
selection: '#e3dcce',
|
|
||||||
selectionMatch: '#e3dcce',
|
|
||||||
gutterBackground: '#faf8f5',
|
|
||||||
gutterForeground: '#cdc4b1',
|
|
||||||
gutterBorder: 'transparent',
|
|
||||||
lineHighlight: '#EFEFEF',
|
|
||||||
},
|
|
||||||
duotoneDark: {
|
|
||||||
background: '#2a2734',
|
|
||||||
lineBackground: '#2a273499',
|
|
||||||
foreground: '#6c6783',
|
|
||||||
caret: '#ffad5c',
|
|
||||||
selection: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
gutterBackground: '#2a2734',
|
|
||||||
gutterForeground: '#545167',
|
|
||||||
lineHighlight: '#36334280',
|
|
||||||
},
|
|
||||||
eclipse: {
|
|
||||||
light: true,
|
|
||||||
background: '#fff',
|
|
||||||
lineBackground: '#ffffff99',
|
|
||||||
foreground: '#000',
|
|
||||||
caret: '#FFFFFF',
|
|
||||||
selection: '#d7d4f0',
|
|
||||||
selectionMatch: '#d7d4f0',
|
|
||||||
gutterBackground: '#f7f7f7',
|
|
||||||
gutterForeground: '#999',
|
|
||||||
lineHighlight: '#e8f2ff',
|
|
||||||
gutterBorder: 'transparent',
|
|
||||||
},
|
|
||||||
githubLight: {
|
|
||||||
light: true,
|
|
||||||
background: '#fff',
|
|
||||||
lineBackground: '#ffffff99',
|
|
||||||
foreground: '#24292e',
|
|
||||||
selection: '#BBDFFF',
|
|
||||||
selectionMatch: '#BBDFFF',
|
|
||||||
gutterBackground: '#fff',
|
|
||||||
gutterForeground: '#6e7781',
|
|
||||||
},
|
|
||||||
githubDark: {
|
|
||||||
background: '#0d1117',
|
|
||||||
lineBackground: '#0d111799',
|
|
||||||
foreground: '#c9d1d9',
|
|
||||||
caret: '#c9d1d9',
|
|
||||||
selection: '#003d73',
|
|
||||||
selectionMatch: '#003d73',
|
|
||||||
lineHighlight: '#36334280',
|
|
||||||
},
|
|
||||||
gruvboxDark: {
|
|
||||||
background: '#282828',
|
|
||||||
lineBackground: '#28282899',
|
|
||||||
foreground: '#ebdbb2',
|
|
||||||
caret: '#ebdbb2',
|
|
||||||
selection: '#bdae93',
|
|
||||||
selectionMatch: '#bdae93',
|
|
||||||
lineHighlight: '#3c3836',
|
|
||||||
gutterBackground: '#282828',
|
|
||||||
gutterForeground: '#7c6f64',
|
|
||||||
},
|
|
||||||
gruvboxLight: {
|
|
||||||
light: true,
|
|
||||||
background: '#fbf1c7',
|
|
||||||
lineBackground: '#fbf1c799',
|
|
||||||
foreground: '#3c3836',
|
|
||||||
caret: '#af3a03',
|
|
||||||
selection: '#ebdbb2',
|
|
||||||
selectionMatch: '#bdae93',
|
|
||||||
lineHighlight: '#ebdbb2',
|
|
||||||
gutterBackground: '#ebdbb2',
|
|
||||||
gutterForeground: '#665c54',
|
|
||||||
gutterBorder: 'transparent',
|
|
||||||
},
|
|
||||||
materialDark: {
|
|
||||||
background: '#2e3235',
|
|
||||||
lineBackground: '#2e323599',
|
|
||||||
foreground: '#bdbdbd',
|
|
||||||
caret: '#a0a4ae',
|
|
||||||
selection: '#d7d4f0',
|
|
||||||
selectionMatch: '#d7d4f0',
|
|
||||||
gutterBackground: '#2e3235',
|
|
||||||
gutterForeground: '#999',
|
|
||||||
gutterActiveForeground: '#4f5b66',
|
|
||||||
lineHighlight: '#545b61',
|
|
||||||
},
|
|
||||||
materialLight: {
|
|
||||||
light: true,
|
|
||||||
background: '#FAFAFA',
|
|
||||||
lineBackground: '#FAFAFA99',
|
|
||||||
foreground: '#90A4AE',
|
|
||||||
caret: '#272727',
|
|
||||||
selection: '#80CBC440',
|
|
||||||
selectionMatch: '#FAFAFA',
|
|
||||||
gutterBackground: '#FAFAFA',
|
|
||||||
gutterForeground: '#90A4AE',
|
|
||||||
gutterBorder: 'transparent',
|
|
||||||
lineHighlight: '#CCD7DA50',
|
|
||||||
},
|
|
||||||
noctisLilac: {
|
|
||||||
light: true,
|
|
||||||
background: '#f2f1f8',
|
|
||||||
lineBackground: '#f2f1f899',
|
|
||||||
foreground: '#0c006b',
|
|
||||||
caret: '#5c49e9',
|
|
||||||
selection: '#d5d1f2',
|
|
||||||
selectionMatch: '#d5d1f2',
|
|
||||||
gutterBackground: '#f2f1f8',
|
|
||||||
gutterForeground: '#0c006b70',
|
|
||||||
lineHighlight: '#e1def3',
|
|
||||||
},
|
|
||||||
nord: {
|
|
||||||
background: '#2e3440',
|
|
||||||
lineBackground: '#2e344099',
|
|
||||||
foreground: '#FFFFFF',
|
|
||||||
caret: '#FFFFFF',
|
|
||||||
selection: '#3b4252',
|
|
||||||
selectionMatch: '#e5e9f0',
|
|
||||||
gutterBackground: '#2e3440',
|
|
||||||
gutterForeground: '#4c566a',
|
|
||||||
gutterActiveForeground: '#d8dee9',
|
|
||||||
lineHighlight: '#4c566a',
|
|
||||||
},
|
|
||||||
okaidia: {
|
|
||||||
background: '#272822',
|
|
||||||
lineBackground: '#27282299',
|
|
||||||
foreground: '#FFFFFF',
|
|
||||||
caret: '#FFFFFF',
|
|
||||||
selection: '#49483E',
|
|
||||||
selectionMatch: '#49483E',
|
|
||||||
gutterBackground: '#272822',
|
|
||||||
gutterForeground: '#FFFFFF70',
|
|
||||||
lineHighlight: '#00000059',
|
|
||||||
},
|
|
||||||
solarizedLight: {
|
|
||||||
light: true,
|
|
||||||
background: '#fdf6e3',
|
|
||||||
lineBackground: '#fdf6e399',
|
|
||||||
foreground: '#657b83',
|
|
||||||
caret: '#586e75',
|
|
||||||
selection: '#dfd9c8',
|
|
||||||
selectionMatch: '#dfd9c8',
|
|
||||||
gutterBackground: '#00000010',
|
|
||||||
gutterForeground: '#657b83',
|
|
||||||
lineHighlight: '#dfd9c8',
|
|
||||||
},
|
|
||||||
solarizedDark: {
|
|
||||||
background: '#002b36',
|
|
||||||
lineBackground: '#002b3699',
|
|
||||||
foreground: '#93a1a1',
|
|
||||||
caret: '#839496',
|
|
||||||
selection: '#173541',
|
|
||||||
selectionMatch: '#aafe661a',
|
|
||||||
gutterBackground: '#00252f',
|
|
||||||
gutterForeground: '#839496',
|
|
||||||
lineHighlight: '#173541',
|
|
||||||
},
|
|
||||||
sublime: {
|
|
||||||
background: '#303841',
|
|
||||||
lineBackground: '#30384199',
|
|
||||||
foreground: '#FFFFFF',
|
|
||||||
caret: '#FBAC52',
|
|
||||||
selection: '#4C5964',
|
|
||||||
selectionMatch: '#3A546E',
|
|
||||||
gutterBackground: '#303841',
|
|
||||||
gutterForeground: '#FFFFFF70',
|
|
||||||
lineHighlight: '#00000059',
|
|
||||||
},
|
|
||||||
tokyoNightDay: {
|
|
||||||
light: true,
|
|
||||||
background: '#e1e2e7',
|
|
||||||
lineBackground: '#e1e2e799',
|
|
||||||
foreground: '#3760bf',
|
|
||||||
caret: '#3760bf',
|
|
||||||
selection: '#99a7df',
|
|
||||||
selectionMatch: '#99a7df',
|
|
||||||
gutterBackground: '#e1e2e7',
|
|
||||||
gutterForeground: '#3760bf',
|
|
||||||
gutterBorder: 'transparent',
|
|
||||||
lineHighlight: '#5f5faf11',
|
|
||||||
},
|
|
||||||
tokyoNightStorm: {
|
|
||||||
background: '#24283b',
|
|
||||||
lineBackground: '#24283b99',
|
|
||||||
foreground: '#7982a9',
|
|
||||||
caret: '#c0caf5',
|
|
||||||
selection: '#6f7bb630',
|
|
||||||
selectionMatch: '#1f2335',
|
|
||||||
gutterBackground: '#24283b',
|
|
||||||
gutterForeground: '#7982a9',
|
|
||||||
gutterBorder: 'transparent',
|
|
||||||
lineHighlight: '#292e42',
|
|
||||||
},
|
|
||||||
tokyoNight: {
|
|
||||||
background: '#1a1b26',
|
|
||||||
lineBackground: '#1a1b2699',
|
|
||||||
foreground: '#787c99',
|
|
||||||
caret: '#c0caf5',
|
|
||||||
selection: '#515c7e40',
|
|
||||||
selectionMatch: '#16161e',
|
|
||||||
gutterBackground: '#1a1b26',
|
|
||||||
gutterForeground: '#787c99',
|
|
||||||
gutterBorder: 'transparent',
|
|
||||||
lineHighlight: '#1e202e',
|
|
||||||
},
|
|
||||||
vscodeDark: {
|
|
||||||
background: '#1e1e1e',
|
|
||||||
lineBackground: '#1e1e1e99',
|
|
||||||
foreground: '#9cdcfe',
|
|
||||||
caret: '#c6c6c6',
|
|
||||||
selection: '#6199ff2f',
|
|
||||||
selectionMatch: '#72a1ff59',
|
|
||||||
lineHighlight: '#ffffff0f',
|
|
||||||
gutterBackground: '#1e1e1e',
|
|
||||||
gutterForeground: '#838383',
|
|
||||||
gutterActiveForeground: '#fff',
|
|
||||||
},
|
|
||||||
xcodeLight: {
|
|
||||||
light: true,
|
|
||||||
background: '#fff',
|
|
||||||
lineBackground: '#ffffff99',
|
|
||||||
foreground: '#3D3D3D',
|
|
||||||
selection: '#BBDFFF',
|
|
||||||
selectionMatch: '#BBDFFF',
|
|
||||||
gutterBackground: '#fff',
|
|
||||||
gutterForeground: '#AFAFAF',
|
|
||||||
lineHighlight: '#EDF4FF',
|
|
||||||
},
|
|
||||||
xcodeDark: {
|
|
||||||
background: '#292A30',
|
|
||||||
lineBackground: '#292A3099',
|
|
||||||
foreground: '#CECFD0',
|
|
||||||
caret: '#fff',
|
|
||||||
selection: '#727377',
|
|
||||||
selectionMatch: '#727377',
|
|
||||||
lineHighlight: '#2F3239',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function getColors(str) {
|
|
||||||
const colorRegex = /#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})/g;
|
|
||||||
const colors = [];
|
|
||||||
|
|
||||||
let match;
|
|
||||||
while ((match = colorRegex.exec(str)) !== null) {
|
|
||||||
const color = match[0];
|
|
||||||
if (!colors.includes(color)) {
|
|
||||||
colors.push(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: remove
|
|
||||||
export function themeColors(theme) {
|
|
||||||
return getColors(stringifySafe(theme));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCircularReplacer() {
|
|
||||||
const seen = new WeakSet();
|
|
||||||
return (key, value) => {
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
|
||||||
if (seen.has(value)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
seen.add(value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringifySafe(json) {
|
|
||||||
return JSON.stringify(json, getCircularReplacer());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function injectStyle(rule) {
|
|
||||||
const newStyle = document.createElement('style');
|
|
||||||
document.head.appendChild(newStyle);
|
|
||||||
const styleSheet = newStyle.sheet;
|
|
||||||
const ruleIndex = styleSheet.insertRule(rule, 0);
|
|
||||||
return () => styleSheet.deleteRule(ruleIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const theme = (theme) => themes[theme] || themes.strudelTheme;
|
|
||||||
-41
@@ -1,41 +0,0 @@
|
|||||||
import { tags as t } from '@lezer/highlight';
|
|
||||||
import { createTheme } from '@uiw/codemirror-themes';
|
|
||||||
export const settings = {
|
|
||||||
background: '#9bbc0f',
|
|
||||||
foreground: '#0f380f', // whats that?
|
|
||||||
caret: '#0f380f',
|
|
||||||
selection: '#306230',
|
|
||||||
selectionMatch: '#ffffff26',
|
|
||||||
lineHighlight: '#8bac0f',
|
|
||||||
lineBackground: '#9bbc0f50',
|
|
||||||
//lineBackground: 'transparent',
|
|
||||||
gutterBackground: 'transparent',
|
|
||||||
gutterForeground: '#0f380f',
|
|
||||||
light: true,
|
|
||||||
customStyle: '.cm-line { line-height: 1 }',
|
|
||||||
};
|
|
||||||
export default createTheme({
|
|
||||||
theme: 'light',
|
|
||||||
settings,
|
|
||||||
styles: [
|
|
||||||
{ tag: t.keyword, color: '#0f380f' },
|
|
||||||
{ tag: t.operator, color: '#0f380f' },
|
|
||||||
{ tag: t.special(t.variableName), color: '#0f380f' },
|
|
||||||
{ tag: t.typeName, color: '#0f380f' },
|
|
||||||
{ tag: t.atom, color: '#0f380f' },
|
|
||||||
{ tag: t.number, color: '#0f380f' },
|
|
||||||
{ tag: t.definition(t.variableName), color: '#0f380f' },
|
|
||||||
{ tag: t.string, color: '#0f380f' },
|
|
||||||
{ tag: t.special(t.string), color: '#0f380f' },
|
|
||||||
{ tag: t.comment, color: '#0f380f' },
|
|
||||||
{ tag: t.variableName, color: '#0f380f' },
|
|
||||||
{ tag: t.tagName, color: '#0f380f' },
|
|
||||||
{ tag: t.bracket, color: '#0f380f' },
|
|
||||||
{ tag: t.meta, color: '#0f380f' },
|
|
||||||
{ tag: t.attributeName, color: '#0f380f' },
|
|
||||||
{ tag: t.propertyName, color: '#0f380f' },
|
|
||||||
{ tag: t.className, color: '#0f380f' },
|
|
||||||
{ tag: t.invalid, color: '#0f380f' },
|
|
||||||
{ tag: [t.unit, t.punctuation], color: '#0f380f' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
-38
@@ -1,38 +0,0 @@
|
|||||||
import { tags as t } from '@lezer/highlight';
|
|
||||||
import { createTheme } from '@uiw/codemirror-themes';
|
|
||||||
export const settings = {
|
|
||||||
background: 'black',
|
|
||||||
foreground: 'white', // whats that?
|
|
||||||
caret: 'white',
|
|
||||||
selection: '#ffffff20',
|
|
||||||
selectionMatch: '#036dd626',
|
|
||||||
lineHighlight: '#ffffff10',
|
|
||||||
lineBackground: '#00000050',
|
|
||||||
gutterBackground: 'transparent',
|
|
||||||
gutterForeground: '#8a919966',
|
|
||||||
};
|
|
||||||
export default createTheme({
|
|
||||||
theme: 'dark',
|
|
||||||
settings,
|
|
||||||
styles: [
|
|
||||||
{ tag: t.keyword, color: 'white' },
|
|
||||||
{ tag: t.operator, color: 'white' },
|
|
||||||
{ tag: t.special(t.variableName), color: 'white' },
|
|
||||||
{ tag: t.typeName, color: 'white' },
|
|
||||||
{ tag: t.atom, color: 'white' },
|
|
||||||
{ tag: t.number, color: 'white' },
|
|
||||||
{ tag: t.definition(t.variableName), color: 'white' },
|
|
||||||
{ tag: t.string, color: 'white' },
|
|
||||||
{ tag: t.special(t.string), color: 'white' },
|
|
||||||
{ tag: t.comment, color: 'white' },
|
|
||||||
{ tag: t.variableName, color: 'white' },
|
|
||||||
{ tag: t.tagName, color: 'white' },
|
|
||||||
{ tag: t.bracket, color: 'white' },
|
|
||||||
{ tag: t.meta, color: 'white' },
|
|
||||||
{ tag: t.attributeName, color: 'white' },
|
|
||||||
{ tag: t.propertyName, color: 'white' },
|
|
||||||
{ tag: t.className, color: 'white' },
|
|
||||||
{ tag: t.invalid, color: 'white' },
|
|
||||||
{ tag: [t.unit, t.punctuation], color: 'white' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
-41
@@ -1,41 +0,0 @@
|
|||||||
import { tags as t } from '@lezer/highlight';
|
|
||||||
import { createTheme } from '@uiw/codemirror-themes';
|
|
||||||
export const settings = {
|
|
||||||
background: '#051DB5',
|
|
||||||
lineBackground: '#051DB550',
|
|
||||||
foreground: 'white', // whats that?
|
|
||||||
caret: 'white',
|
|
||||||
selection: 'rgba(128, 203, 196, 0.5)',
|
|
||||||
selectionMatch: '#036dd626',
|
|
||||||
// lineHighlight: '#8a91991a', // original
|
|
||||||
lineHighlight: '#00000050',
|
|
||||||
gutterBackground: 'transparent',
|
|
||||||
// gutterForeground: '#8a919966',
|
|
||||||
gutterForeground: '#8a919966',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createTheme({
|
|
||||||
theme: 'dark',
|
|
||||||
settings,
|
|
||||||
styles: [
|
|
||||||
{ tag: t.keyword, color: 'white' },
|
|
||||||
{ tag: t.operator, color: 'white' },
|
|
||||||
{ tag: t.special(t.variableName), color: 'white' },
|
|
||||||
{ tag: t.typeName, color: 'white' },
|
|
||||||
{ tag: t.atom, color: 'white' },
|
|
||||||
{ tag: t.number, color: 'white' },
|
|
||||||
{ tag: t.definition(t.variableName), color: 'white' },
|
|
||||||
{ tag: t.string, color: 'white' },
|
|
||||||
{ tag: t.special(t.string), color: 'white' },
|
|
||||||
{ tag: t.comment, color: 'white' },
|
|
||||||
{ tag: t.variableName, color: 'white' },
|
|
||||||
{ tag: t.tagName, color: 'white' },
|
|
||||||
{ tag: t.bracket, color: 'white' },
|
|
||||||
{ tag: t.meta, color: 'white' },
|
|
||||||
{ tag: t.attributeName, color: 'white' },
|
|
||||||
{ tag: t.propertyName, color: 'white' },
|
|
||||||
{ tag: t.className, color: 'white' },
|
|
||||||
{ tag: t.invalid, color: 'white' },
|
|
||||||
{ tag: [t.unit, t.punctuation], color: 'white' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
-45
@@ -1,45 +0,0 @@
|
|||||||
import { tags as t } from '@lezer/highlight';
|
|
||||||
import { createTheme } from '@uiw/codemirror-themes';
|
|
||||||
export default createTheme({
|
|
||||||
theme: 'dark',
|
|
||||||
settings: {
|
|
||||||
background: '#222',
|
|
||||||
foreground: '#75baff', // whats that?
|
|
||||||
caret: '#ffcc00',
|
|
||||||
selection: 'rgba(128, 203, 196, 0.5)',
|
|
||||||
selectionMatch: '#036dd626',
|
|
||||||
// lineHighlight: '#8a91991a', // original
|
|
||||||
lineHighlight: '#00000050',
|
|
||||||
gutterBackground: 'transparent',
|
|
||||||
// gutterForeground: '#8a919966',
|
|
||||||
gutterForeground: '#8a919966',
|
|
||||||
},
|
|
||||||
styles: [
|
|
||||||
{ tag: t.keyword, color: '#c792ea' },
|
|
||||||
{ tag: t.operator, color: '#89ddff' },
|
|
||||||
{ tag: t.special(t.variableName), color: '#eeffff' },
|
|
||||||
// { tag: t.typeName, color: '#f07178' }, // original
|
|
||||||
{ tag: t.typeName, color: '#c3e88d' },
|
|
||||||
{ tag: t.atom, color: '#f78c6c' },
|
|
||||||
// { tag: t.number, color: '#ff5370' }, // original
|
|
||||||
{ tag: t.number, color: '#c3e88d' },
|
|
||||||
{ tag: t.definition(t.variableName), color: '#82aaff' },
|
|
||||||
{ tag: t.string, color: '#c3e88d' },
|
|
||||||
// { tag: t.special(t.string), color: '#f07178' }, // original
|
|
||||||
{ tag: t.special(t.string), color: '#c3e88d' },
|
|
||||||
{ tag: t.comment, color: '#7d8799' },
|
|
||||||
// { tag: t.variableName, color: '#f07178' }, // original
|
|
||||||
{ tag: t.variableName, color: '#c792ea' },
|
|
||||||
// { tag: t.tagName, color: '#ff5370' }, // original
|
|
||||||
{ tag: t.tagName, color: '#c3e88d' },
|
|
||||||
{ tag: t.bracket, color: '#525154' },
|
|
||||||
// { tag: t.bracket, color: '#a2a1a4' }, // original
|
|
||||||
{ tag: t.meta, color: '#ffcb6b' },
|
|
||||||
{ tag: t.attributeName, color: '#c792ea' },
|
|
||||||
{ tag: t.propertyName, color: '#c792ea' },
|
|
||||||
|
|
||||||
{ tag: t.className, color: '#decb6b' },
|
|
||||||
{ tag: t.invalid, color: '#ffffff' },
|
|
||||||
{ tag: [t.unit, t.punctuation], color: '#82aaff' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
-50
@@ -1,50 +0,0 @@
|
|||||||
import { tags as t } from '@lezer/highlight';
|
|
||||||
import { createTheme } from '@uiw/codemirror-themes';
|
|
||||||
|
|
||||||
let colorA = '#6edee4';
|
|
||||||
//let colorB = 'magenta';
|
|
||||||
let colorB = 'white';
|
|
||||||
let colorC = 'red';
|
|
||||||
let colorD = '#f8fc55';
|
|
||||||
|
|
||||||
export const settings = {
|
|
||||||
background: '#000000',
|
|
||||||
foreground: colorA, // whats that?
|
|
||||||
caret: colorC,
|
|
||||||
selection: colorD,
|
|
||||||
selectionMatch: colorA,
|
|
||||||
lineHighlight: '#6edee440', // panel bg
|
|
||||||
lineBackground: '#00000040',
|
|
||||||
gutterBackground: 'transparent',
|
|
||||||
gutterForeground: '#8a919966',
|
|
||||||
customStyle: '.cm-line { line-height: 1 }',
|
|
||||||
};
|
|
||||||
|
|
||||||
let punctuation = colorD;
|
|
||||||
let mini = colorB;
|
|
||||||
|
|
||||||
export default createTheme({
|
|
||||||
theme: 'dark',
|
|
||||||
settings,
|
|
||||||
styles: [
|
|
||||||
{ tag: t.keyword, color: colorA },
|
|
||||||
{ tag: t.operator, color: mini },
|
|
||||||
{ tag: t.special(t.variableName), color: colorA },
|
|
||||||
{ tag: t.typeName, color: colorA },
|
|
||||||
{ tag: t.atom, color: colorA },
|
|
||||||
{ tag: t.number, color: mini },
|
|
||||||
{ tag: t.definition(t.variableName), color: colorA },
|
|
||||||
{ tag: t.string, color: mini },
|
|
||||||
{ tag: t.special(t.string), color: mini },
|
|
||||||
{ tag: t.comment, color: punctuation },
|
|
||||||
{ tag: t.variableName, color: colorA },
|
|
||||||
{ tag: t.tagName, color: colorA },
|
|
||||||
{ tag: t.bracket, color: punctuation },
|
|
||||||
{ tag: t.meta, color: colorA },
|
|
||||||
{ tag: t.attributeName, color: colorA },
|
|
||||||
{ tag: t.propertyName, color: colorA }, // methods
|
|
||||||
{ tag: t.className, color: colorA },
|
|
||||||
{ tag: t.invalid, color: colorC },
|
|
||||||
{ tag: [t.unit, t.punctuation], color: punctuation },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
import { tags as t } from '@lezer/highlight';
|
|
||||||
import { createTheme } from '@uiw/codemirror-themes';
|
|
||||||
export const settings = {
|
|
||||||
background: 'black',
|
|
||||||
foreground: '#41FF00', // whats that?
|
|
||||||
caret: '#41FF00',
|
|
||||||
selection: '#ffffff20',
|
|
||||||
selectionMatch: '#036dd626',
|
|
||||||
lineHighlight: '#ffffff10',
|
|
||||||
gutterBackground: 'transparent',
|
|
||||||
gutterForeground: '#8a919966',
|
|
||||||
};
|
|
||||||
export default createTheme({
|
|
||||||
theme: 'dark',
|
|
||||||
settings,
|
|
||||||
styles: [
|
|
||||||
{ tag: t.keyword, color: '#41FF00' },
|
|
||||||
{ tag: t.operator, color: '#41FF00' },
|
|
||||||
{ tag: t.special(t.variableName), color: '#41FF00' },
|
|
||||||
{ tag: t.typeName, color: '#41FF00' },
|
|
||||||
{ tag: t.atom, color: '#41FF00' },
|
|
||||||
{ tag: t.number, color: '#41FF00' },
|
|
||||||
{ tag: t.definition(t.variableName), color: '#41FF00' },
|
|
||||||
{ tag: t.string, color: '#41FF00' },
|
|
||||||
{ tag: t.special(t.string), color: '#41FF00' },
|
|
||||||
{ tag: t.comment, color: '#41FF00' },
|
|
||||||
{ tag: t.variableName, color: '#41FF00' },
|
|
||||||
{ tag: t.tagName, color: '#41FF00' },
|
|
||||||
{ tag: t.bracket, color: '#41FF00' },
|
|
||||||
{ tag: t.meta, color: '#41FF00' },
|
|
||||||
{ tag: t.attributeName, color: '#41FF00' },
|
|
||||||
{ tag: t.propertyName, color: '#41FF00' },
|
|
||||||
{ tag: t.className, color: '#41FF00' },
|
|
||||||
{ tag: t.invalid, color: '#41FF00' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
-38
@@ -1,38 +0,0 @@
|
|||||||
import { tags as t } from '@lezer/highlight';
|
|
||||||
import { createTheme } from '@uiw/codemirror-themes';
|
|
||||||
export const settings = {
|
|
||||||
background: 'white',
|
|
||||||
foreground: 'black', // whats that?
|
|
||||||
caret: 'black',
|
|
||||||
selection: 'rgba(128, 203, 196, 0.5)',
|
|
||||||
selectionMatch: '#ffffff26',
|
|
||||||
lineHighlight: '#cccccc50',
|
|
||||||
lineBackground: '#ffffff50',
|
|
||||||
gutterBackground: 'transparent',
|
|
||||||
gutterForeground: 'black',
|
|
||||||
light: true,
|
|
||||||
};
|
|
||||||
export default createTheme({
|
|
||||||
theme: 'light',
|
|
||||||
settings,
|
|
||||||
styles: [
|
|
||||||
{ tag: t.keyword, color: 'black' },
|
|
||||||
{ tag: t.operator, color: 'black' },
|
|
||||||
{ tag: t.special(t.variableName), color: 'black' },
|
|
||||||
{ tag: t.typeName, color: 'black' },
|
|
||||||
{ tag: t.atom, color: 'black' },
|
|
||||||
{ tag: t.number, color: 'black' },
|
|
||||||
{ tag: t.definition(t.variableName), color: 'black' },
|
|
||||||
{ tag: t.string, color: 'black' },
|
|
||||||
{ tag: t.special(t.string), color: 'black' },
|
|
||||||
{ tag: t.comment, color: 'black' },
|
|
||||||
{ tag: t.variableName, color: 'black' },
|
|
||||||
{ tag: t.tagName, color: 'black' },
|
|
||||||
{ tag: t.bracket, color: 'black' },
|
|
||||||
{ tag: t.meta, color: 'black' },
|
|
||||||
{ tag: t.attributeName, color: 'black' },
|
|
||||||
{ tag: t.propertyName, color: 'black' },
|
|
||||||
{ tag: t.className, color: 'black' },
|
|
||||||
{ tag: t.invalid, color: 'black' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
||||||
import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
|
|
||||||
const { createParams } = controls;
|
const { createParams } = controls;
|
||||||
|
|
||||||
let clearColor = '#22222210';
|
let clearColor = '#22222210';
|
||||||
|
|||||||
+307
-831
File diff suppressed because it is too large
Load Diff
+17
-23
@@ -10,49 +10,44 @@ import { logger } from './logger.mjs';
|
|||||||
export class Cyclist {
|
export class Cyclist {
|
||||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||||
this.started = false;
|
this.started = false;
|
||||||
this.cps = 1;
|
this.cps = 1; // TODO
|
||||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
this.phase = 0;
|
||||||
this.lastBegin = 0; // query begin of last tick
|
this.getTime = getTime;
|
||||||
this.lastEnd = 0; // query end of last tick
|
|
||||||
this.getTime = getTime; // get absolute time
|
|
||||||
this.onToggle = onToggle;
|
this.onToggle = onToggle;
|
||||||
this.latency = latency; // fixed trigger time offset
|
this.latency = latency;
|
||||||
const round = (x) => Math.round(x * 1000) / 1000;
|
const round = (x) => Math.round(x * 1000) / 1000;
|
||||||
this.clock = createClock(
|
this.clock = createClock(
|
||||||
getTime,
|
getTime,
|
||||||
// called slightly before each cycle
|
|
||||||
(phase, duration, tick) => {
|
(phase, duration, tick) => {
|
||||||
if (tick === 0) {
|
if (tick === 0) {
|
||||||
this.origin = phase;
|
this.origin = phase;
|
||||||
}
|
}
|
||||||
|
const begin = round(phase - this.origin);
|
||||||
|
this.phase = begin - latency;
|
||||||
|
const end = round(begin + duration);
|
||||||
|
const time = getTime();
|
||||||
try {
|
try {
|
||||||
const time = getTime();
|
const haps = this.pattern.queryArc(begin, end); // get Haps
|
||||||
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) => {
|
haps.forEach((hap) => {
|
||||||
if (hap.part.begin.equals(hap.whole.begin)) {
|
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||||
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
|
const deadline = hap.whole.begin + this.origin - time + latency;
|
||||||
const duration = hap.duration / this.cps;
|
const duration = hap.duration * 1;
|
||||||
onTrigger?.(hap, deadline, duration, this.cps);
|
onTrigger?.(hap, deadline, duration);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger(`[cyclist] error: ${e.message}`);
|
logger(`[cyclist] error: ${e.message}`);
|
||||||
onError?.(e);
|
onError?.(e);
|
||||||
}
|
}
|
||||||
},
|
}, // called slightly before each cycle
|
||||||
interval, // duration of each cycle
|
interval, // duration of each cycle
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
getPhase() {
|
||||||
|
return this.getTime() - this.origin - this.latency;
|
||||||
|
}
|
||||||
now() {
|
now() {
|
||||||
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
|
return this.getTime() - this.origin + this.clock.minLatency;
|
||||||
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
|
|
||||||
}
|
}
|
||||||
setStarted(v) {
|
setStarted(v) {
|
||||||
this.started = v;
|
this.started = v;
|
||||||
@@ -74,7 +69,6 @@ export class Cyclist {
|
|||||||
stop() {
|
stop() {
|
||||||
logger('[cyclist] stop');
|
logger('[cyclist] stop');
|
||||||
this.clock.stop();
|
this.clock.stop();
|
||||||
this.lastEnd = 0;
|
|
||||||
this.setStarted(false);
|
this.setStarted(false);
|
||||||
}
|
}
|
||||||
setPattern(pat, autostart = false) {
|
setPattern(pat, autostart = false) {
|
||||||
|
|||||||
+6
-112
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
draw.mjs - <short description TODO>
|
draw.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/draw.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
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/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -9,29 +9,17 @@ import { Pattern, getTime, State, TimeSpan } from './index.mjs';
|
|||||||
export const getDrawContext = (id = 'test-canvas') => {
|
export const getDrawContext = (id = 'test-canvas') => {
|
||||||
let canvas = document.querySelector('#' + id);
|
let canvas = document.querySelector('#' + id);
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
const scale = 2; // 2 = crisp on retina screens
|
|
||||||
canvas = document.createElement('canvas');
|
canvas = document.createElement('canvas');
|
||||||
canvas.id = id;
|
canvas.id = id;
|
||||||
canvas.width = window.innerWidth * scale;
|
canvas.width = window.innerWidth;
|
||||||
canvas.height = window.innerHeight * scale;
|
canvas.height = window.innerHeight;
|
||||||
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
|
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0;z-index:5';
|
||||||
document.body.prepend(canvas);
|
document.body.prepend(canvas);
|
||||||
let timeout;
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
timeout && clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
canvas.width = window.innerWidth * scale;
|
|
||||||
canvas.height = window.innerHeight * scale;
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return canvas.getContext('2d');
|
return canvas.getContext('2d');
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
|
Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (window.strudelAnimation) {
|
if (window.strudelAnimation) {
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
}
|
}
|
||||||
@@ -63,7 +51,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
|
|||||||
|
|
||||||
export const cleanupDraw = (clearScreen = true) => {
|
export const cleanupDraw = (clearScreen = true) => {
|
||||||
const ctx = getDrawContext();
|
const ctx = getDrawContext();
|
||||||
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
|
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||||
if (window.strudelAnimation) {
|
if (window.strudelAnimation) {
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
}
|
}
|
||||||
@@ -77,97 +65,3 @@ Pattern.prototype.onPaint = function (onPaint) {
|
|||||||
this.context = { onPaint };
|
this.context = { onPaint };
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
// const round = (x) => Math.round(x * 1000) / 1000;
|
|
||||||
|
|
||||||
// encapsulates starting and stopping animation frames
|
|
||||||
export class Framer {
|
|
||||||
constructor(onFrame, onError) {
|
|
||||||
this.onFrame = onFrame;
|
|
||||||
this.onError = onError;
|
|
||||||
}
|
|
||||||
start() {
|
|
||||||
const self = this;
|
|
||||||
let frame = requestAnimationFrame(function updateHighlights(time) {
|
|
||||||
try {
|
|
||||||
self.onFrame(time);
|
|
||||||
} catch (err) {
|
|
||||||
self.onError(err);
|
|
||||||
}
|
|
||||||
frame = requestAnimationFrame(updateHighlights);
|
|
||||||
});
|
|
||||||
self.cancel = () => {
|
|
||||||
cancelAnimationFrame(frame);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
stop() {
|
|
||||||
if (this.cancel) {
|
|
||||||
this.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// syncs animation frames to a cyclist scheduler
|
|
||||||
// see vite-vanilla-repl-cm6 for an example
|
|
||||||
export class Drawer {
|
|
||||||
constructor(onDraw, drawTime) {
|
|
||||||
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
|
|
||||||
lookbehind = Math.abs(lookbehind);
|
|
||||||
this.visibleHaps = [];
|
|
||||||
this.lastFrame = null;
|
|
||||||
this.drawTime = drawTime;
|
|
||||||
this.framer = new Framer(
|
|
||||||
() => {
|
|
||||||
if (!this.scheduler) {
|
|
||||||
console.warn('Drawer: no scheduler');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// calculate current frame time (think right side of screen for pianoroll)
|
|
||||||
const phase = this.scheduler.now() + lookahead;
|
|
||||||
// first frame just captures the phase
|
|
||||||
if (this.lastFrame === null) {
|
|
||||||
this.lastFrame = phase;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// query haps from last frame till now. take last 100ms max
|
|
||||||
const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase);
|
|
||||||
this.lastFrame = phase;
|
|
||||||
this.visibleHaps = (this.visibleHaps || [])
|
|
||||||
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
|
|
||||||
.filter((h) => h.whole.end >= phase - lookbehind - lookahead)
|
|
||||||
// add new haps with onset (think right edge bars scrolling in)
|
|
||||||
.concat(haps.filter((h) => h.hasOnset()));
|
|
||||||
const time = phase - lookahead;
|
|
||||||
onDraw(this.visibleHaps, time, this);
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
console.warn('draw error', err);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
invalidate(scheduler = this.scheduler) {
|
|
||||||
if (!scheduler || !scheduler.pattern) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.scheduler = scheduler;
|
|
||||||
const t = scheduler.now();
|
|
||||||
let [_, lookahead] = this.drawTime;
|
|
||||||
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
|
|
||||||
// remove all future haps
|
|
||||||
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
|
|
||||||
// query future haps
|
|
||||||
const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query..
|
|
||||||
// append future haps
|
|
||||||
this.visibleHaps = this.visibleHaps.concat(futureHaps);
|
|
||||||
}
|
|
||||||
start(scheduler) {
|
|
||||||
this.scheduler = scheduler;
|
|
||||||
this.invalidate();
|
|
||||||
this.framer.start();
|
|
||||||
}
|
|
||||||
stop() {
|
|
||||||
if (this.framer) {
|
|
||||||
this.framer.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,9 +11,15 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, timeCat, register, silence } from './pattern.mjs';
|
import { Pattern, timeCat, register, silence } from './pattern.mjs';
|
||||||
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
|
import { rotate, flatten } from './util.mjs';
|
||||||
import Fraction from './fraction.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 left = function (n, x) {
|
||||||
const [ons, offs] = n;
|
const [ons, offs] = n;
|
||||||
const [xs, ys] = x;
|
const [xs, ys] = x;
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
evaluate.mjs - <short description TODO>
|
evaluate.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/evaluate.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.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/>.
|
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 { isPattern } from './index.mjs';
|
import { isPattern } from './index.mjs';
|
||||||
|
|
||||||
|
let scoped = false;
|
||||||
export const evalScope = async (...args) => {
|
export const evalScope = async (...args) => {
|
||||||
|
if (scoped) {
|
||||||
|
console.warn('evalScope was called more than once.');
|
||||||
|
}
|
||||||
|
scoped = true;
|
||||||
const results = await Promise.allSettled(args);
|
const results = await Promise.allSettled(args);
|
||||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||||
results.forEach((result, i) => {
|
results.forEach((result, i) => {
|
||||||
@@ -37,12 +42,11 @@ function safeEval(str, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const evaluate = async (code, transpiler) => {
|
export const evaluate = async (code, transpiler) => {
|
||||||
let meta = {};
|
if (!scoped) {
|
||||||
|
await evalScope(); // at least scope Pattern.prototype.boostrap
|
||||||
|
}
|
||||||
if (transpiler) {
|
if (transpiler) {
|
||||||
// transform syntactically correct js code to semantically usable code
|
code = transpiler(code); // transform syntactically correct js code to semantically usable code
|
||||||
const transpiled = transpiler(code);
|
|
||||||
code = transpiled.output;
|
|
||||||
meta = transpiled;
|
|
||||||
}
|
}
|
||||||
// if no transpiler is given, we expect a single instruction (!wrapExpression)
|
// if no transpiler is given, we expect a single instruction (!wrapExpression)
|
||||||
const options = { wrapExpression: !!transpiler };
|
const options = { wrapExpression: !!transpiler };
|
||||||
@@ -52,5 +56,5 @@ export const evaluate = async (code, transpiler) => {
|
|||||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||||
}
|
}
|
||||||
return { mode: 'javascript', pattern: evaluated, meta };
|
return { mode: 'javascript', pattern: evaluated };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# 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?
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# vite-vanilla-repl-cm6
|
|
||||||
|
|
||||||
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pnpm i
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Vite Vanilla Strudel REPL</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<nav>
|
|
||||||
<button id="play">eval</button>
|
|
||||||
<button id="stop">stop</button>
|
|
||||||
</nav>
|
|
||||||
<div class="container">
|
|
||||||
<div id="editor"></div>
|
|
||||||
<div id="output"></div>
|
|
||||||
</div>
|
|
||||||
<canvas id="roll"></canvas>
|
|
||||||
</main>
|
|
||||||
<script type="module" src="./main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { StrudelMirror } from '@strudel/codemirror';
|
|
||||||
import { funk42 } from './tunes';
|
|
||||||
import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
|
|
||||||
import './style.css';
|
|
||||||
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
|
||||||
import { transpiler } from '@strudel.cycles/transpiler';
|
|
||||||
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
|
|
||||||
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
|
|
||||||
|
|
||||||
// init canvas
|
|
||||||
const canvas = document.getElementById('roll');
|
|
||||||
canvas.width = canvas.width * 2;
|
|
||||||
canvas.height = canvas.height * 2;
|
|
||||||
const drawContext = canvas.getContext('2d');
|
|
||||||
const drawTime = [-2, 2]; // time window of drawn haps
|
|
||||||
|
|
||||||
const editor = new StrudelMirror({
|
|
||||||
defaultOutput: webaudioOutput,
|
|
||||||
getTime: () => getAudioContext().currentTime,
|
|
||||||
transpiler,
|
|
||||||
root: document.getElementById('editor'),
|
|
||||||
initialCode: funk42,
|
|
||||||
drawTime,
|
|
||||||
onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }),
|
|
||||||
prebake: async () => {
|
|
||||||
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
|
|
||||||
const loadModules = evalScope(
|
|
||||||
controls,
|
|
||||||
import('@strudel.cycles/core'),
|
|
||||||
import('@strudel.cycles/mini'),
|
|
||||||
import('@strudel.cycles/tonal'),
|
|
||||||
import('@strudel.cycles/webaudio'),
|
|
||||||
);
|
|
||||||
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('play').addEventListener('click', () => editor.evaluate());
|
|
||||||
document.getElementById('stop').addEventListener('click', () => editor.stop());
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "vite-vanilla-repl-cm6",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"vite": "^4.3.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@strudel/codemirror": "workspace:*",
|
|
||||||
"@strudel.cycles/core": "workspace:*",
|
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
|
||||||
"@strudel.cycles/soundfonts": "workspace:*",
|
|
||||||
"@strudel.cycles/tonal": "workspace:*",
|
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
|
||||||
"@strudel.cycles/webaudio": "workspace:*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
body,
|
|
||||||
html {
|
|
||||||
margin: 0;
|
|
||||||
height: 100%;
|
|
||||||
background: #282c34;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
flex-grow: 1;
|
|
||||||
max-height: 100%;
|
|
||||||
position: relative;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#editor {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-editor {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#roll {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
|
|
||||||
await samples('github:felixroos/samples/main')
|
|
||||||
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
|
||||||
|
|
||||||
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
|
|
||||||
// make it 22 edo
|
|
||||||
.fmap(v => Math.pow(2,v/22))
|
|
||||||
// mess with the base frequency
|
|
||||||
.mul("<300 [300@3 200]>/8").freq()
|
|
||||||
.layer(
|
|
||||||
// chords
|
|
||||||
x=>x.div(freq(2)).s("flute").euclidLegato("<3 2>",8)
|
|
||||||
.shape(.4).lpf(sine.range(800,4000).slow(8)),
|
|
||||||
// adlibs
|
|
||||||
x=>x.arp("{0 3 2 [1 3]}%1.5")
|
|
||||||
.s('xylo').mul(freq(2))
|
|
||||||
.delay(.5).delayfeedback(.4).juxBy(.5, rev)
|
|
||||||
.hpf(sine.range(200,3000).slow(8)),
|
|
||||||
// bass
|
|
||||||
x=>x.arp("[0 [2 1?]](5,8)").s('sawtooth').div(freq(4))
|
|
||||||
.lpf(sine.range(400,2000).slow(8)).lpq(8).shape(.4)
|
|
||||||
.off(1/8, x=>x.mul(freq(2)).degradeBy(.5)).gain(.3)
|
|
||||||
).clip(1).release(.2)
|
|
||||||
.stack(
|
|
||||||
// drums
|
|
||||||
s("bd sd:<2 1>, [~ hh]*2, [~ rim]").bank('RolandTR909')
|
|
||||||
.off(1/8, x=>x.speed(2).gain(.4)).sometimes(ply(2)).gain(.8)
|
|
||||||
.mask("<0@4 1@12>/4")
|
|
||||||
.reset("<x@15 [x(3,8) x*[4 8]]>")
|
|
||||||
// wait for it...
|
|
||||||
).fast(2/3)
|
|
||||||
//.crush(6) // remove "//" if you dare`;
|
|
||||||
|
|
||||||
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
|
|
||||||
|
|
||||||
await samples('github:felixroos/samples/main')
|
|
||||||
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
|
||||||
|
|
||||||
addVoicings('hip', {
|
|
||||||
m11: ['2M 3m 4P 7m'],
|
|
||||||
'^7#11': ['3M 4A 5P 7M'],
|
|
||||||
}, ['C4', 'C6'])
|
|
||||||
|
|
||||||
stack(
|
|
||||||
stack(
|
|
||||||
"<Bbm11 A^7#11>/2".voicings('hip').note()
|
|
||||||
.s("gm_epiano1:2")
|
|
||||||
.arp("[<[0 1 2 3] [3 2 1 0]> ~@5]/2")
|
|
||||||
.release(2).late(.25).lpf(2000),
|
|
||||||
"<Bb1 A1>/2".note().s('gm_acoustic_bass'),
|
|
||||||
n("<0 2 3>(3,8)".off(1/8, add(4)))
|
|
||||||
.scale("<Bb4:minor A4:lydian>/2")
|
|
||||||
.s('gm_electric_guitar_jazz')
|
|
||||||
.decay(sine.range(.05, .2).slow(32)).sustain(0)
|
|
||||||
.delay(.5).lpf(sine.range(100,5000).slow(64))
|
|
||||||
.gain(.7).room(.5).pan(sine.range(0,1).slow(11))
|
|
||||||
).add(perlin.range(0,.25).note()),
|
|
||||||
stack(
|
|
||||||
s("bd:1(3,8) rim").bank('RolandTR707').slow(2).room("<0 <.1 .6>>")
|
|
||||||
.when("<0@7 1>",x=>x.echoWith(3, .0625, (x,i) => x.speed(1+i*.24))),
|
|
||||||
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.late("[0 .05]*2").late(12)
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const funk42 = `// froos - how to funk in 42 lines of code
|
|
||||||
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
|
|
||||||
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
|
|
||||||
|
|
||||||
await samples('github:felixroos/samples/main')
|
|
||||||
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
|
||||||
|
|
||||||
setcps(.5)
|
|
||||||
|
|
||||||
let drums = stack(
|
|
||||||
s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"),
|
|
||||||
s("hh*4").begin(.2).release(.02).end(.25).release(.02)
|
|
||||||
.gain(.3).bank('RolandTR707').late(.02).room(.5),
|
|
||||||
//s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02)
|
|
||||||
).fast(2)
|
|
||||||
|
|
||||||
let wurli = note(\`<
|
|
||||||
[[a2,g3,[b3 c4],e4] ~ [g3,c4,e4](3,8)@4 ~@2]!3
|
|
||||||
[[e2,e3,a3,b3,e4]@3 [e2,e3,ab3,b3,e4]@5]>\`)
|
|
||||||
.s("gm_epiano1:5").decay(.2).sustain("<[1 0@7]!3 1>")
|
|
||||||
.gain("<[.8@2 .4@14]!3 .7>").room(.3)
|
|
||||||
|
|
||||||
let organ = note("<[~@3 [a3,d4,f#4]@2 [[a3,c4,e4]@2 ~] ~@2]!3 ~>".add(12))
|
|
||||||
.s("gm_percussive_organ:2").gain(.6).lpf(1800).pan(.2).room(.3);
|
|
||||||
|
|
||||||
let clav = note(\`<
|
|
||||||
[~@3 a2 [g3,[b3 c4],e4]@2 ~ a2 [g3,b3,e4] ~@2 [g3,c4,e4] ~@4]!3
|
|
||||||
[~@3 e3 [[a3 b3],c3,e3]@2 ~ e2 [e3,a3]@3 [b3,e3] ~@2 [b3,e3]@2]>\`)
|
|
||||||
.s("gm_clavinet:1").decay("<.25!3 [.25 .4]>").sustain(0)
|
|
||||||
.gain(.7).pan(.8).room(.2);
|
|
||||||
|
|
||||||
let bass = note(\`<
|
|
||||||
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
|
|
||||||
[a1 [~ [g2 a2]] [e3 d3] [c3 [g3 a3]]]
|
|
||||||
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
|
|
||||||
[e2@6 e1@5 e1 [[d2 e3] g1]@4]
|
|
||||||
>\`).s("gm_electric_bass_pick:1").release(.1)
|
|
||||||
|
|
||||||
stack(
|
|
||||||
drums
|
|
||||||
,wurli
|
|
||||||
,organ
|
|
||||||
,clav
|
|
||||||
,bass
|
|
||||||
)`;
|
|
||||||
@@ -6,3 +6,5 @@ This folder demonstrates how to set up a strudel repl using vite and vanilla JS.
|
|||||||
npm i
|
npm i
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
or view it [live on githack](https://rawcdn.githack.com/tidalcycles/strudel/5fb36acb046ead7cd6ad3cd10f532e7f585f536a/packages/core/examples/vite-vanilla-repl/dist/index.html)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.3.3"
|
"vite": "^3.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
|
|||||||
+1
-18
@@ -32,11 +32,7 @@ export class Hap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get duration() {
|
get duration() {
|
||||||
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
|
return this.whole.end.sub(this.whole.begin);
|
||||||
}
|
|
||||||
|
|
||||||
get endClipped() {
|
|
||||||
return this.whole.begin.add(this.duration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wholeOrPart() {
|
wholeOrPart() {
|
||||||
@@ -130,19 +126,6 @@ export class Hap {
|
|||||||
setContext(context) {
|
setContext(context) {
|
||||||
return new Hap(this.whole, this.part, this.value, 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;
|
export default Hap;
|
||||||
|
|||||||
@@ -18,13 +18,11 @@ export * from './util.mjs';
|
|||||||
export * from './speak.mjs';
|
export * from './speak.mjs';
|
||||||
export * from './evaluate.mjs';
|
export * from './evaluate.mjs';
|
||||||
export * from './repl.mjs';
|
export * from './repl.mjs';
|
||||||
export * from './cyclist.mjs';
|
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './time.mjs';
|
export * from './time.mjs';
|
||||||
export * from './draw.mjs';
|
export * from './draw.mjs';
|
||||||
export * from './animate.mjs';
|
export * from './animate.mjs';
|
||||||
export * from './pianoroll.mjs';
|
export * from './pianoroll.mjs';
|
||||||
export * from './spiral.mjs';
|
|
||||||
export * from './ui.mjs';
|
export * from './ui.mjs';
|
||||||
export { default as drawLine } from './drawLine.mjs';
|
export { default as drawLine } from './drawLine.mjs';
|
||||||
export { default as gist } from './gist.js';
|
export { default as gist } from './gist.js';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/core",
|
"name": "@strudel.cycles/core",
|
||||||
"version": "0.9.0",
|
"version": "0.6.8",
|
||||||
"description": "Port of Tidal Cycles to JavaScript",
|
"description": "Port of Tidal Cycles to JavaScript",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -29,14 +29,13 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://strudel.cc",
|
"homepage": "https://strudel.tidalcycles.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fraction.js": "^4.2.0",
|
"fraction.js": "^4.2.0"
|
||||||
"nanostores": "^0.8.1"
|
|
||||||
},
|
},
|
||||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.3.3",
|
"vite": "^3.2.2",
|
||||||
"vitest": "^0.33.0"
|
"vitest": "^0.25.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+329
-340
@@ -21,6 +21,141 @@ let stringParser;
|
|||||||
// intended to use with mini to automatically interpret all strings as mini notation
|
// intended to use with mini to automatically interpret all strings as mini notation
|
||||||
export const setStringParser = (parser) => (stringParser = parser);
|
export const setStringParser = (parser) => (stringParser = parser);
|
||||||
|
|
||||||
|
const alignments = ['in', 'out', 'mix', 'squeeze', 'squeezeout', 'trig', 'trigzero'];
|
||||||
|
|
||||||
|
const methodRegistry = [];
|
||||||
|
const getterRegistry = [];
|
||||||
|
const controlRegistry = [];
|
||||||
|
const controlSubscribers = [];
|
||||||
|
const composifiedRegistry = [];
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
// Magic for supporting higher order composition of method chains
|
||||||
|
|
||||||
|
// Dresses the given (unary) function with methods for composition chaining, so e.g.
|
||||||
|
// `fast(2).iter(4)` composes to pattern functions into a new one.
|
||||||
|
function composify(func) {
|
||||||
|
if (!func.__composified) {
|
||||||
|
for (const [name, method] of methodRegistry) {
|
||||||
|
func[name] = method;
|
||||||
|
}
|
||||||
|
for (const [name, getter] of getterRegistry) {
|
||||||
|
Object.defineProperty(func, name, getter);
|
||||||
|
}
|
||||||
|
func.__composified = true;
|
||||||
|
composifiedRegistry.push(func);
|
||||||
|
} else {
|
||||||
|
console.log('Warning: attempt at composifying a function more than once');
|
||||||
|
}
|
||||||
|
return func;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerMethod(name, addAlignments = false, addControls = false) {
|
||||||
|
if (addAlignments || addControls) {
|
||||||
|
// This method needs to make its 'this' object available to chained alignments and/or
|
||||||
|
// control parameters, so it has to be implemented as a getter
|
||||||
|
const getter = {
|
||||||
|
get: function () {
|
||||||
|
const func = this;
|
||||||
|
const wrapped = function (...args) {
|
||||||
|
const composed = (pat) => func(pat)[name](...args);
|
||||||
|
return composify(composed);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (addAlignments) {
|
||||||
|
for (const alignment of alignments) {
|
||||||
|
wrapped[alignment] = function (...args) {
|
||||||
|
const composed = (pat) => func(pat)[name][alignment](...args);
|
||||||
|
return composify(composed);
|
||||||
|
};
|
||||||
|
for (const [controlname, controlfunc] of controlRegistry) {
|
||||||
|
wrapped[alignment][controlname] = function (...args) {
|
||||||
|
const composed = (pat) => func(pat)[name][alignment](controlfunc(...args));
|
||||||
|
return composify(composed);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (addControls) {
|
||||||
|
for (const [controlname, controlfunc] of controlRegistry) {
|
||||||
|
wrapped[controlname] = function (...args) {
|
||||||
|
const composed = (pat) => func(pat)[name](controlfunc(...args));
|
||||||
|
return composify(composed);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wrapped;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
getterRegistry.push([name, getter]);
|
||||||
|
|
||||||
|
// Add to functions already 'composified'
|
||||||
|
for (const composified of composifiedRegistry) {
|
||||||
|
Object.defineProperty(composified, name, getter);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No chained alignments/controls needed, so we can just add as a plain method,
|
||||||
|
// probably more efficient this way?
|
||||||
|
const method = function (...args) {
|
||||||
|
const func = this;
|
||||||
|
const composed = (pat) => func(pat)[name](...args);
|
||||||
|
return composify(composed);
|
||||||
|
};
|
||||||
|
|
||||||
|
methodRegistry.push([name, method]);
|
||||||
|
|
||||||
|
// Add to functions already 'composified'
|
||||||
|
for (const composified of composifiedRegistry) {
|
||||||
|
composified[name] = method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerControl(controlname, controlfunc) {
|
||||||
|
registerMethod(controlname);
|
||||||
|
controlRegistry.push([controlname, controlfunc]);
|
||||||
|
for (const subscriber of controlSubscribers) {
|
||||||
|
subscriber(controlname, controlfunc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withControls(func) {
|
||||||
|
for (const [controlname, controlfunc] of controlRegistry) {
|
||||||
|
func(controlname, controlfunc);
|
||||||
|
}
|
||||||
|
controlSubscribers.push(func);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addToPrototype(name, func) {
|
||||||
|
Pattern.prototype[name] = func;
|
||||||
|
registerMethod(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function curryPattern(func, arity = func.length) {
|
||||||
|
const fn = function curried(...args) {
|
||||||
|
if (args.length >= arity) {
|
||||||
|
return func.apply(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
const partial = function (...args2) {
|
||||||
|
return curried.apply(this, args.concat(args2));
|
||||||
|
};
|
||||||
|
if (args.length == arity - 1) {
|
||||||
|
return composify(partial);
|
||||||
|
}
|
||||||
|
|
||||||
|
return partial;
|
||||||
|
};
|
||||||
|
if (arity == 1) {
|
||||||
|
composify(fn);
|
||||||
|
}
|
||||||
|
return fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
// The core Pattern class
|
||||||
|
|
||||||
/** @class Class representing a pattern. */
|
/** @class Class representing a pattern. */
|
||||||
export class Pattern {
|
export class Pattern {
|
||||||
/**
|
/**
|
||||||
@@ -472,15 +607,15 @@ export class Pattern {
|
|||||||
/**
|
/**
|
||||||
* Returns a new pattern with the given location information added to the
|
* Returns a new pattern with the given location information added to the
|
||||||
* context of every hap.
|
* context of every hap.
|
||||||
* @param {Number} start start offset
|
* @param {Number} start
|
||||||
* @param {Number} end end offset
|
* @param {Number} end
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
withLoc(start, end) {
|
withLocation(start, end) {
|
||||||
const location = {
|
const location = {
|
||||||
start,
|
start: { line: start[0], column: start[1], offset: start[2] },
|
||||||
end,
|
end: { line: end[0], column: end[1], offset: end[2] },
|
||||||
};
|
};
|
||||||
return this.withContext((context) => {
|
return this.withContext((context) => {
|
||||||
const locations = (context.locations || []).concat([location]);
|
const locations = (context.locations || []).concat([location]);
|
||||||
@@ -488,6 +623,32 @@ export class Pattern {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withMiniLocation(start, end) {
|
||||||
|
const offset = {
|
||||||
|
start: { line: start[0], column: start[1], offset: start[2] },
|
||||||
|
end: { line: end[0], column: end[1], offset: end[2] },
|
||||||
|
};
|
||||||
|
return this.withContext((context) => {
|
||||||
|
let locations = context.locations || [];
|
||||||
|
locations = locations.map(({ start, end }) => {
|
||||||
|
const colOffset = start.line === 1 ? offset.start.column : 0;
|
||||||
|
return {
|
||||||
|
start: {
|
||||||
|
...start,
|
||||||
|
line: start.line - 1 + (offset.start.line - 1) + 1,
|
||||||
|
column: start.column - 1 + colOffset,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
...end,
|
||||||
|
line: end.line - 1 + (offset.start.line - 1) + 1,
|
||||||
|
column: end.column - 1 + colOffset,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return { ...context, locations };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new Pattern, which only returns haps that meet the given test.
|
* Returns a new Pattern, which only returns haps that meet the given test.
|
||||||
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
|
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
|
||||||
@@ -617,7 +778,9 @@ export class Pattern {
|
|||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
get firstCycleValues() {
|
get firstCycleValues() {
|
||||||
return this.firstCycle().map((hap) => hap.value);
|
return this.sortHapsByPart()
|
||||||
|
.firstCycle()
|
||||||
|
.map((hap) => hap.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -667,7 +830,7 @@ export class Pattern {
|
|||||||
const otherPat = reify(other);
|
const otherPat = reify(other);
|
||||||
return this.fmap((a) => otherPat.fmap((b) => func(a)(b))).squeezeJoin();
|
return this.fmap((a) => otherPat.fmap((b) => func(a)(b))).squeezeJoin();
|
||||||
}
|
}
|
||||||
_opSqueezeOut(other, func) {
|
_opSqueezeout(other, func) {
|
||||||
const thisPat = this;
|
const thisPat = this;
|
||||||
const otherPat = reify(other);
|
const otherPat = reify(other);
|
||||||
return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a))).squeezeJoin();
|
return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a))).squeezeJoin();
|
||||||
@@ -783,14 +946,13 @@ export class Pattern {
|
|||||||
hap.setContext({
|
hap.setContext({
|
||||||
...hap.context,
|
...hap.context,
|
||||||
onTrigger: (...args) => {
|
onTrigger: (...args) => {
|
||||||
// run previously set trigger, if it exists
|
if (!dominant && hap.context.onTrigger) {
|
||||||
hap.context.onTrigger?.(...args);
|
hap.context.onTrigger(...args);
|
||||||
|
}
|
||||||
onTrigger(...args);
|
onTrigger(...args);
|
||||||
},
|
},
|
||||||
// if dominantTrigger is set to true, the default output (webaudio) will be disabled
|
// we need this to know later if the default trigger should still fire
|
||||||
// when using multiple triggers, you cannot flip this flag to false again!
|
dominantTrigger: dominant,
|
||||||
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
|
|
||||||
dominantTrigger: hap.context.dominantTrigger || dominant,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -815,7 +977,7 @@ export class Pattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// functions relating to chords/patterns of lists/lists of patterns
|
// functions relating to chords/patterns of lists
|
||||||
|
|
||||||
// returns Array<Hap[]> where each list of haps satisfies eq
|
// returns Array<Hap[]> where each list of haps satisfies eq
|
||||||
function groupHapsBy(eq, haps) {
|
function groupHapsBy(eq, haps) {
|
||||||
@@ -835,11 +997,11 @@ function groupHapsBy(eq, haps) {
|
|||||||
const congruent = (a, b) => a.spanEquals(b);
|
const congruent = (a, b) => a.spanEquals(b);
|
||||||
// Pattern<Hap<T>> -> Pattern<Hap<T[]>>
|
// Pattern<Hap<T>> -> Pattern<Hap<T[]>>
|
||||||
// returned pattern contains arrays of congruent haps
|
// returned pattern contains arrays of congruent haps
|
||||||
Pattern.prototype.collect = function () {
|
addToPrototype('collect', function () {
|
||||||
return this.withHaps((haps) =>
|
return this.withHaps((haps) =>
|
||||||
groupHapsBy(congruent, haps).map((_haps) => new Hap(_haps[0].whole, _haps[0].part, _haps, {})),
|
groupHapsBy(congruent, haps).map((_haps) => new Hap(_haps[0].whole, _haps[0].part, _haps, {})),
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects indices in in stacked notes.
|
* Selects indices in in stacked notes.
|
||||||
@@ -847,12 +1009,12 @@ Pattern.prototype.collect = function () {
|
|||||||
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
|
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
|
||||||
* .arpWith(haps => haps[2])
|
* .arpWith(haps => haps[2])
|
||||||
* */
|
* */
|
||||||
Pattern.prototype.arpWith = function (func) {
|
addToPrototype('arpWith', function (func) {
|
||||||
return this.collect()
|
return this.collect()
|
||||||
.fmap((v) => reify(func(v)))
|
.fmap((v) => reify(func(v)))
|
||||||
.innerJoin()
|
.innerJoin()
|
||||||
.withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value)));
|
.withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value)));
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects indices in in stacked notes.
|
* Selects indices in in stacked notes.
|
||||||
@@ -860,39 +1022,10 @@ Pattern.prototype.arpWith = function (func) {
|
|||||||
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
|
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
|
||||||
* .arp("0 [0,2] 1 [0,2]").slow(2)
|
* .arp("0 [0,2] 1 [0,2]").slow(2)
|
||||||
* */
|
* */
|
||||||
Pattern.prototype.arp = function (pat) {
|
addToPrototype('arp', function (pat) {
|
||||||
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
|
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
|
// compose matrix functions
|
||||||
|
|
||||||
@@ -989,15 +1122,15 @@ function _composeOp(a, b, func) {
|
|||||||
func: [(a, b) => b(a)],
|
func: [(a, b) => b(a)],
|
||||||
};
|
};
|
||||||
|
|
||||||
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Trig', 'Trigzero'];
|
const hows = alignments.map((x) => x.charAt(0).toUpperCase() + x.slice(1));
|
||||||
|
|
||||||
// generate methods to do what and how
|
// generate methods to do what and how
|
||||||
for (const [what, [op, preprocess]] of Object.entries(composers)) {
|
for (const [what, [op, preprocess]] of Object.entries(composers)) {
|
||||||
// make plain version, e.g. pat._add(value) adds that plain value
|
// make plain version, e.g. pat._add(value) adds that plain value
|
||||||
// to all the values in pat
|
// to all the values in pat
|
||||||
Pattern.prototype['_' + what] = function (value) {
|
addToPrototype('_' + what, function (value) {
|
||||||
return this.fmap((x) => op(x, value));
|
return this.fmap((x) => op(x, value));
|
||||||
};
|
});
|
||||||
|
|
||||||
// make patternified monster version
|
// make patternified monster version
|
||||||
Object.defineProperty(Pattern.prototype, what, {
|
Object.defineProperty(Pattern.prototype, what, {
|
||||||
@@ -1011,7 +1144,7 @@ function _composeOp(a, b, func) {
|
|||||||
|
|
||||||
// add methods to that function for each behaviour
|
// add methods to that function for each behaviour
|
||||||
for (const how of hows) {
|
for (const how of hows) {
|
||||||
wrapper[how.toLowerCase()] = function (...other) {
|
const howfunc = function (...other) {
|
||||||
var howpat = pat;
|
var howpat = pat;
|
||||||
other = sequence(other);
|
other = sequence(other);
|
||||||
if (preprocess) {
|
if (preprocess) {
|
||||||
@@ -1029,19 +1162,41 @@ function _composeOp(a, b, func) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const [controlname, controlfunc] of controlRegistry) {
|
||||||
|
howfunc[controlname] = (...args) => howfunc(controlfunc(...args));
|
||||||
|
}
|
||||||
|
wrapper[how.toLowerCase()] = howfunc;
|
||||||
}
|
}
|
||||||
wrapper.squeezein = wrapper.squeeze;
|
wrapper.squeezein = wrapper.squeeze;
|
||||||
|
|
||||||
|
for (const [controlname, controlfunc] of controlRegistry) {
|
||||||
|
wrapper[controlname] = (...args) => wrapper.in(controlfunc(...args));
|
||||||
|
}
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
|
registerMethod(what, true, true);
|
||||||
for (const how of hows) {
|
}
|
||||||
Pattern.prototype[how.toLowerCase()] = function (...args) {
|
|
||||||
return this.set[how.toLowerCase()](args);
|
// Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
|
||||||
};
|
for (const howLower of alignments) {
|
||||||
}
|
// Using a 'get'ted function so that all the controls are added
|
||||||
|
Object.defineProperty(Pattern.prototype, howLower, {
|
||||||
|
get: function () {
|
||||||
|
const pat = this;
|
||||||
|
const howfunc = function (...args) {
|
||||||
|
return pat.set[howLower](args);
|
||||||
|
};
|
||||||
|
for (const [controlname, controlfunc] of controlRegistry) {
|
||||||
|
howfunc[controlname] = (...args) => howfunc(controlfunc(...args));
|
||||||
|
}
|
||||||
|
return howfunc;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
registerMethod(howLower, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// binary composers
|
// binary composers
|
||||||
@@ -1053,36 +1208,36 @@ function _composeOp(a, b, func) {
|
|||||||
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
|
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
|
||||||
* .slow(4)
|
* .slow(4)
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.struct = function (...args) {
|
addToPrototype('struct', function (...args) {
|
||||||
return this.keepif.out(...args);
|
return this.keepif.out(...args);
|
||||||
};
|
});
|
||||||
Pattern.prototype.structAll = function (...args) {
|
addToPrototype('structAll', function (...args) {
|
||||||
return this.keep.out(...args);
|
return this.keep.out(...args);
|
||||||
};
|
});
|
||||||
/**
|
/**
|
||||||
* Returns silence when mask is 0 or "~"
|
* Returns silence when mask is 0 or "~"
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>").slow(2)
|
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>").slow(2)
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.mask = function (...args) {
|
addToPrototype('mask', function (...args) {
|
||||||
return this.keepif.in(...args);
|
return this.keepif.in(...args);
|
||||||
};
|
});
|
||||||
Pattern.prototype.maskAll = function (...args) {
|
addToPrototype('maskAll', function (...args) {
|
||||||
return this.keep.in(...args);
|
return this.keep.in(...args);
|
||||||
};
|
});
|
||||||
/**
|
/**
|
||||||
* Resets the pattern to the start of the cycle for each onset of the reset pattern.
|
* Resets the pattern to the start of the cycle for each onset of the reset pattern.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* s("<bd lt> sd, hh*4").reset("<x@3 x(3,8)>")
|
* s("<bd lt> sd, hh*4").reset("<x@3 x(3,8)>")
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.reset = function (...args) {
|
addToPrototype('reset', function (...args) {
|
||||||
return this.keepif.trig(...args);
|
return this.keepif.trig(...args);
|
||||||
};
|
});
|
||||||
Pattern.prototype.resetAll = function (...args) {
|
addToPrototype('resetAll', function (...args) {
|
||||||
return this.keep.trig(...args);
|
return this.keep.trig(...args);
|
||||||
};
|
});
|
||||||
/**
|
/**
|
||||||
* Restarts the pattern for each onset of the restart pattern.
|
* Restarts the pattern for each onset of the restart pattern.
|
||||||
* While reset will only reset the current cycle, restart will start from cycle 0.
|
* While reset will only reset the current cycle, restart will start from cycle 0.
|
||||||
@@ -1090,20 +1245,18 @@ function _composeOp(a, b, func) {
|
|||||||
* @example
|
* @example
|
||||||
* s("<bd lt> sd, hh*4").restart("<x@3 x(3,8)>")
|
* s("<bd lt> sd, hh*4").restart("<x@3 x(3,8)>")
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.restart = function (...args) {
|
addToPrototype('restart', function (...args) {
|
||||||
return this.keepif.trigzero(...args);
|
return this.keepif.trigzero(...args);
|
||||||
};
|
});
|
||||||
Pattern.prototype.restartAll = function (...args) {
|
addToPrototype('restartAll', function (...args) {
|
||||||
return this.keep.trigzero(...args);
|
return this.keep.trigzero(...args);
|
||||||
};
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// aliases
|
// aliases
|
||||||
export const polyrhythm = stack;
|
export const polyrhythm = stack;
|
||||||
export const pr = stack;
|
export const pr = stack;
|
||||||
|
|
||||||
export const pm = polymeter;
|
|
||||||
|
|
||||||
// methods that create patterns, which are added to patternified Pattern methods
|
// methods that create patterns, which are added to patternified Pattern methods
|
||||||
// TODO: remove? this is only used in old transpiler (shapeshifter)
|
// TODO: remove? this is only used in old transpiler (shapeshifter)
|
||||||
Pattern.prototype.factories = {
|
Pattern.prototype.factories = {
|
||||||
@@ -1261,20 +1414,6 @@ export function timeCat(...timepats) {
|
|||||||
return stack(...pats);
|
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) {
|
export function fastcat(...pats) {
|
||||||
return slowcat(...pats)._fast(pats.length);
|
return slowcat(...pats)._fast(pats.length);
|
||||||
}
|
}
|
||||||
@@ -1351,36 +1490,73 @@ export function polymeter(...args) {
|
|||||||
return polymeterSteps(0, ...args);
|
return polymeterSteps(0, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mask = curry((a, b) => reify(b).mask(a));
|
// alias
|
||||||
export const struct = curry((a, b) => reify(b).struct(a));
|
export function pm(...args) {
|
||||||
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
|
polymeter(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mask = curryPattern((a, b) => reify(b).mask(a));
|
||||||
|
export const struct = curryPattern((a, b) => reify(b).struct(a));
|
||||||
|
export const superimpose = curryPattern((a, b) => reify(b).superimpose(...a));
|
||||||
|
|
||||||
|
const methodToFunction = function (name, addAlignments = false) {
|
||||||
|
const func = curryPattern((a, b) => reify(b)[name](a));
|
||||||
|
|
||||||
|
withControls((controlname, controlfunc) => {
|
||||||
|
func[controlname] = function (...pats) {
|
||||||
|
return func(controlfunc(...pats));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (addAlignments) {
|
||||||
|
for (const alignment of alignments) {
|
||||||
|
func[alignment] = curryPattern((a, b) => reify(b)[name][alignment](a));
|
||||||
|
withControls((controlname, controlfunc) => {
|
||||||
|
func[alignment][controlname] = function (...pats) {
|
||||||
|
return func[alignment](controlfunc(...pats));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return func;
|
||||||
|
};
|
||||||
|
|
||||||
// operators
|
// operators
|
||||||
export const set = curry((a, b) => reify(b).set(a));
|
export const set = methodToFunction('set', true);
|
||||||
export const keep = curry((a, b) => reify(b).keep(a));
|
export const keep = methodToFunction('keep', true);
|
||||||
export const keepif = curry((a, b) => reify(b).keepif(a));
|
export const keepif = methodToFunction('keepif', true);
|
||||||
export const add = curry((a, b) => reify(b).add(a));
|
export const add = methodToFunction('add', true);
|
||||||
export const sub = curry((a, b) => reify(b).sub(a));
|
export const sub = methodToFunction('sub', true);
|
||||||
export const mul = curry((a, b) => reify(b).mul(a));
|
export const mul = methodToFunction('mul', true);
|
||||||
export const div = curry((a, b) => reify(b).div(a));
|
export const div = methodToFunction('div', true);
|
||||||
export const mod = curry((a, b) => reify(b).mod(a));
|
export const mod = methodToFunction('mod', true);
|
||||||
export const pow = curry((a, b) => reify(b).pow(a));
|
export const pow = methodToFunction('pow', true);
|
||||||
export const band = curry((a, b) => reify(b).band(a));
|
export const band = methodToFunction('band', true);
|
||||||
export const bor = curry((a, b) => reify(b).bor(a));
|
export const bor = methodToFunction('bor', true);
|
||||||
export const bxor = curry((a, b) => reify(b).bxor(a));
|
export const bxor = methodToFunction('bxor', true);
|
||||||
export const blshift = curry((a, b) => reify(b).blshift(a));
|
export const blshift = methodToFunction('blshift', true);
|
||||||
export const brshift = curry((a, b) => reify(b).brshift(a));
|
export const brshift = methodToFunction('brshift', true);
|
||||||
export const lt = curry((a, b) => reify(b).lt(a));
|
export const lt = methodToFunction('lt', true);
|
||||||
export const gt = curry((a, b) => reify(b).gt(a));
|
export const gt = methodToFunction('gt', true);
|
||||||
export const lte = curry((a, b) => reify(b).lte(a));
|
export const lte = methodToFunction('lte', true);
|
||||||
export const gte = curry((a, b) => reify(b).gte(a));
|
export const gte = methodToFunction('gte', true);
|
||||||
export const eq = curry((a, b) => reify(b).eq(a));
|
export const eq = methodToFunction('eq', true);
|
||||||
export const eqt = curry((a, b) => reify(b).eqt(a));
|
export const eqt = methodToFunction('eqt', true);
|
||||||
export const ne = curry((a, b) => reify(b).ne(a));
|
export const ne = methodToFunction('ne', true);
|
||||||
export const net = curry((a, b) => reify(b).net(a));
|
export const net = methodToFunction('net', true);
|
||||||
export const and = curry((a, b) => reify(b).and(a));
|
export const and = methodToFunction('and', true);
|
||||||
export const or = curry((a, b) => reify(b).or(a));
|
export const or = methodToFunction('or', true);
|
||||||
export const func = curry((a, b) => reify(b).func(a));
|
export const func = methodToFunction('func', true);
|
||||||
|
|
||||||
|
// alignments
|
||||||
|
// export const in = methodToFunction('in'); // reserved word :(
|
||||||
|
export const out = methodToFunction('out');
|
||||||
|
export const mix = methodToFunction('mix');
|
||||||
|
export const squeeze = methodToFunction('squeeze');
|
||||||
|
export const squeezeout = methodToFunction('squeezeout');
|
||||||
|
export const trig = methodToFunction('trig');
|
||||||
|
export const trigzero = methodToFunction('trigzero');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
|
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
|
||||||
@@ -1390,45 +1566,44 @@ export const func = curry((a, b) => reify(b).func(a));
|
|||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function register(name, func, patternify = true) {
|
export function register(name, func) {
|
||||||
if (Array.isArray(name)) {
|
if (Array.isArray(name)) {
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const name_item of name) {
|
for (const name_item of name) {
|
||||||
result[name_item] = register(name_item, func, patternify);
|
result[name_item] = register(name_item, func);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
const arity = func.length;
|
const arity = func.length;
|
||||||
var pfunc; // the patternified function
|
|
||||||
|
|
||||||
if (patternify) {
|
registerMethod(name);
|
||||||
pfunc = function (...args) {
|
|
||||||
args = args.map(reify);
|
const pfunc = function (...args) {
|
||||||
const pat = args[args.length - 1];
|
args = args.map(reify);
|
||||||
if (arity === 1) {
|
const pat = args[args.length - 1];
|
||||||
return func(pat);
|
if (arity === 1) {
|
||||||
}
|
return func(pat);
|
||||||
const [left, ...right] = args.slice(0, -1);
|
}
|
||||||
let mapFn = (...args) => {
|
const [left, ...right] = args.slice(0, -1);
|
||||||
// make sure to call func with the correct argument count
|
let mapFn = (...args) => {
|
||||||
// args.length is expected to be <= arity-1
|
// make sure to call func with the correct argument count
|
||||||
// so we set undefined args explicitly undefined
|
// args.length is expected to be <= arity-1
|
||||||
Array(arity - 1)
|
// so we set undefined args explicitly undefined
|
||||||
.fill()
|
Array(arity - 1)
|
||||||
.map((_, i) => args[i] ?? undefined);
|
.fill()
|
||||||
return func(...args, pat);
|
.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();
|
|
||||||
};
|
};
|
||||||
} else {
|
mapFn = curryPattern(mapFn, arity - 1);
|
||||||
pfunc = function (...args) {
|
|
||||||
args = args.map(reify);
|
const app = (acc, p) => acc.appLeft(p);
|
||||||
return func(...args);
|
const start = left.fmap(mapFn);
|
||||||
};
|
|
||||||
}
|
return right.reduce(app, start).innerJoin();
|
||||||
|
};
|
||||||
|
|
||||||
Pattern.prototype[name] = function (...args) {
|
Pattern.prototype[name] = function (...args) {
|
||||||
|
args = args.map(reify);
|
||||||
// For methods that take a single argument (plus 'this'), allow
|
// For methods that take a single argument (plus 'this'), allow
|
||||||
// multiple arguments but sequence them
|
// multiple arguments but sequence them
|
||||||
if (arity === 2 && args.length !== 1) {
|
if (arity === 2 && args.length !== 1) {
|
||||||
@@ -1436,7 +1611,6 @@ export function register(name, func, patternify = true) {
|
|||||||
} else if (arity !== args.length + 1) {
|
} else if (arity !== args.length + 1) {
|
||||||
throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`);
|
throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`);
|
||||||
}
|
}
|
||||||
args = args.map(reify);
|
|
||||||
return pfunc(...args, this);
|
return pfunc(...args, this);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1450,7 +1624,7 @@ export function register(name, func, patternify = true) {
|
|||||||
|
|
||||||
// toplevel functions get curried as well as patternified
|
// toplevel functions get curried as well as patternified
|
||||||
// because pfunc uses spread args, we need to state the arity explicitly!
|
// because pfunc uses spread args, we need to state the arity explicitly!
|
||||||
return curry(pfunc, null, arity);
|
return curryPattern(pfunc, arity);
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
@@ -1557,24 +1731,6 @@ export const range2 = register('range2', function (min, max, pat) {
|
|||||||
return pat.fromBipolar()._range(min, max);
|
return pat.fromBipolar()._range(min, max);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows dividing numbers via list notation using ":".
|
|
||||||
* Returns a new pattern with just numbers.
|
|
||||||
* @name ratio
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* ratio("1, 5:4, 3:2").mul(110).freq().s("piano").slow(2)
|
|
||||||
*/
|
|
||||||
export const ratio = register('ratio', (pat) =>
|
|
||||||
pat.fmap((v) => {
|
|
||||||
if (!Array.isArray(v)) {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
return v.slice(1).reduce((acc, n) => acc / n, v[0]);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Structural and temporal transformations
|
// Structural and temporal transformations
|
||||||
|
|
||||||
@@ -1670,9 +1826,6 @@ export const ply = register('ply', function (factor, pat) {
|
|||||||
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
|
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
|
||||||
*/
|
*/
|
||||||
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
|
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
|
||||||
if (factor === 0) {
|
|
||||||
return silence;
|
|
||||||
}
|
|
||||||
factor = Fraction(factor);
|
factor = Fraction(factor);
|
||||||
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
||||||
return fastQuery.withHapTime((t) => t.div(factor));
|
return fastQuery.withHapTime((t) => t.div(factor));
|
||||||
@@ -1699,9 +1852,6 @@ export const hurry = register('hurry', function (r, pat) {
|
|||||||
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
|
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
|
||||||
*/
|
*/
|
||||||
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
|
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
|
||||||
if (factor === 0) {
|
|
||||||
return silence;
|
|
||||||
}
|
|
||||||
return pat._fast(Fraction(1).div(factor));
|
return pat._fast(Fraction(1).div(factor));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2028,7 +2178,7 @@ export const jux = register('jux', function (func, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* "<0 [2 4]>"
|
* "<0 [2 4]>"
|
||||||
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
|
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
|
||||||
* .scale('C minor').note().clip(.2)
|
* .scale('C minor').note().legato(.2)
|
||||||
*/
|
*/
|
||||||
export const { echoWith, echowith, stutWith, stutwith } = register(
|
export const { echoWith, echowith, stutWith, stutwith } = register(
|
||||||
['echoWith', 'echowith', 'stutWith', 'stutwith'],
|
['echoWith', 'echowith', 'stutWith', 'stutwith'],
|
||||||
@@ -2100,43 +2250,23 @@ export const { iterBack, iterback } = register(['iterBack', 'iterback'], functio
|
|||||||
return _iter(times, pat, true);
|
return _iter(times, pat, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Repeats each cycle the given number of times.
|
|
||||||
* @name repeatCycles
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* note(irand(12).add(34)).segment(4).repeatCycles(2).s("gm_acoustic_guitar_nylon")
|
|
||||||
*/
|
|
||||||
const _repeatCycles = function (n, pat) {
|
|
||||||
return slowcat(...Array(n).fill(pat));
|
|
||||||
};
|
|
||||||
|
|
||||||
const { repeatCycles } = register('repeatCycles', _repeatCycles);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle).
|
* Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle).
|
||||||
* @name chunk
|
* @name chunk
|
||||||
* @synonyms slowChunk, slowchunk
|
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* "0 1 2 3".chunk(4, x=>x.add(7)).scale('A minor').note()
|
* "0 1 2 3".chunk(4, x=>x.add(7)).scale('A minor').note()
|
||||||
*/
|
*/
|
||||||
const _chunk = function (n, func, pat, back = false, fast = false) {
|
const _chunk = function (n, func, pat, back = false) {
|
||||||
const binary = Array(n - 1).fill(false);
|
const binary = Array(n - 1).fill(false);
|
||||||
binary.unshift(true);
|
binary.unshift(true);
|
||||||
// Invert the 'back' because we want to shift the pattern forwards,
|
const binary_pat = _iter(n, sequence(...binary), back);
|
||||||
// and so time backwards
|
|
||||||
const binary_pat = _iter(n, sequence(...binary), !back);
|
|
||||||
if (!fast) {
|
|
||||||
pat = pat.repeatCycles(n);
|
|
||||||
}
|
|
||||||
return pat.when(binary_pat, func);
|
return pat.when(binary_pat, func);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) {
|
export const chunk = register('chunk', function (n, func, pat) {
|
||||||
return _chunk(n, func, pat, false, false);
|
return _chunk(n, func, pat, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2152,21 +2282,6 @@ export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], fun
|
|||||||
return _chunk(n, func, pat, true);
|
return _chunk(n, func, pat, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Like `chunk`, but the cycles of the source pattern aren't repeated
|
|
||||||
* for each set of chunks.
|
|
||||||
* @name fastChunk
|
|
||||||
* @synonyms fastchunk
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* "<0 8> 1 2 3 4 5 6 7".fastChunk(4, x => x.color('red')).slow(4).scale("C2:major").note()
|
|
||||||
.s("folkharp")
|
|
||||||
*/
|
|
||||||
const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], function (n, func, pat) {
|
|
||||||
return _chunk(n, func, pat, false, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO - redefine elsewhere in terms of mask
|
// TODO - redefine elsewhere in terms of mask
|
||||||
export const bypass = register('bypass', function (on, pat) {
|
export const bypass = register('bypass', function (on, pat) {
|
||||||
on = Boolean(parseInt(on));
|
on = Boolean(parseInt(on));
|
||||||
@@ -2191,9 +2306,6 @@ export const duration = register('duration', function (value, pat) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the color of the hap in visualizations like pianoroll or highlighting.
|
* Sets the color of the hap in visualizations like pianoroll or highlighting.
|
||||||
* @name color
|
|
||||||
* @synonyms colour
|
|
||||||
* @param {string} color Hexadecimal or CSS color name
|
|
||||||
*/
|
*/
|
||||||
// TODO: move this to controls https://github.com/tidalcycles/strudel/issues/288
|
// TODO: move this to controls https://github.com/tidalcycles/strudel/issues/288
|
||||||
export const { color, colour } = register(['color', 'colour'], function (color, pat) {
|
export const { color, colour } = register(['color', 'colour'], function (color, pat) {
|
||||||
@@ -2216,7 +2328,6 @@ export const velocity = register('velocity', function (velocity, pat) {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Multiplies the hap duration with the given factor.
|
* Multiplies the hap duration with the given factor.
|
||||||
* With samples, `clip` might be a better function to use ([more info](https://github.com/tidalcycles/strudel/pull/598))
|
|
||||||
* @name legato
|
* @name legato
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @example
|
* @example
|
||||||
@@ -2255,14 +2366,6 @@ export const chop = register('chop', function (n, pat) {
|
|||||||
return pat.squeezeBind(func);
|
return pat.squeezeBind(func);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Cuts each sample into the given number of parts, triggering progressive portions of each sample at each loop.
|
|
||||||
* @name striate
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("numbers:0 numbers:1 numbers:2").striate(6).slow(6)
|
|
||||||
*/
|
|
||||||
export const striate = register('striate', function (n, pat) {
|
export const striate = register('striate', function (n, pat) {
|
||||||
const slices = Array.from({ length: n }, (x, i) => i);
|
const slices = Array.from({ length: n }, (x, i) => i);
|
||||||
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
|
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
|
||||||
@@ -2287,92 +2390,10 @@ const _loopAt = function (factor, pat, cps = 1) {
|
|||||||
.slow(factor);
|
.slow(factor);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
|
||||||
* Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers.
|
|
||||||
* Instead of a number, it also accepts a list of numbers from 0 to 1 to slice at specific points.
|
|
||||||
* @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)
|
|
||||||
* @example
|
|
||||||
* await samples('github:tidalcycles/Dirt-Samples/master')
|
|
||||||
* s("breaks125/2").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
|
|
||||||
*/
|
|
||||||
|
|
||||||
export 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 };
|
|
||||||
const begin = Array.isArray(n) ? n[i] : i / n;
|
|
||||||
const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n;
|
|
||||||
return pure({ begin, end, _slices: n, ...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
|
|
||||||
* @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)
|
|
||||||
*/
|
|
||||||
|
|
||||||
export 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
|
|
||||||
);
|
|
||||||
|
|
||||||
// this function will be redefined in repl.mjs to use the correct cps value.
|
|
||||||
// It is still here to work in cases where repl.mjs is not used
|
|
||||||
|
|
||||||
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
|
|
||||||
return _loopAt(factor, pat, 1);
|
return _loopAt(factor, pat, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// the fit function will be redefined in repl.mjs to use the correct cps value.
|
|
||||||
// It is still here to work in cases where repl.mjs is not used
|
|
||||||
/**
|
|
||||||
* Makes the sample fit its event duration. Good for rhythmical loops like drum breaks.
|
|
||||||
* Similar to loopAt.
|
|
||||||
* @name fit
|
|
||||||
* @example
|
|
||||||
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
|
||||||
* s("rhodes/4").fit()
|
|
||||||
*/
|
|
||||||
export const fit = register('fit', (pat) =>
|
|
||||||
pat.withHap((hap) =>
|
|
||||||
hap.withValue((v) => ({
|
|
||||||
...v,
|
|
||||||
speed: 1 / hap.whole.duration,
|
|
||||||
unit: 'c',
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes the sample fit the given number of cycles and cps value, by
|
* Makes the sample fit the given number of cycles and cps value, by
|
||||||
* changing the speed. Please note that at some point cps will be
|
* changing the speed. Please note that at some point cps will be
|
||||||
@@ -2386,38 +2407,6 @@ export const fit = register('fit', (pat) =>
|
|||||||
* s("rhodes").loopAtCps(4,1.5).cps(1.5)
|
* s("rhodes").loopAtCps(4,1.5).cps(1.5)
|
||||||
*/
|
*/
|
||||||
// TODO - global cps clock
|
// TODO - global cps clock
|
||||||
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
|
const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
|
||||||
return _loopAt(factor, pat, cps);
|
return _loopAt(factor, pat, cps);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** exposes a custom value at query time. basically allows mutating state without evaluation */
|
|
||||||
export const ref = (accessor) =>
|
|
||||||
pure(1)
|
|
||||||
.withValue(() => reify(accessor()))
|
|
||||||
.innerJoin();
|
|
||||||
|
|
||||||
let fadeGain = (p) => (p < 0.5 ? 1 : 1 - (p - 0.5) / 0.5);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cross-fades between left and right from 0 to 1:
|
|
||||||
* - 0 = (full left, no right)
|
|
||||||
* - .5 = (both equal)
|
|
||||||
* - 1 = (no left, full right)
|
|
||||||
*
|
|
||||||
* @name xfade
|
|
||||||
* @example
|
|
||||||
* xfade(s("bd*2"), "<0 .25 .5 .75 1>", s("hh*8"))
|
|
||||||
*/
|
|
||||||
export let xfade = (a, pos, b) => {
|
|
||||||
pos = reify(pos);
|
|
||||||
a = reify(a);
|
|
||||||
b = reify(b);
|
|
||||||
let gaina = pos.fmap((v) => ({ gain: fadeGain(v) }));
|
|
||||||
let gainb = pos.fmap((v) => ({ gain: fadeGain(1 - v) }));
|
|
||||||
return stack(a.mul(gaina), b.mul(gainb));
|
|
||||||
};
|
|
||||||
|
|
||||||
// the prototype version is actually flipped so left/right makes sense
|
|
||||||
Pattern.prototype.xfade = function (pos, b) {
|
|
||||||
return xfade(this, pos, b);
|
|
||||||
};
|
|
||||||
|
|||||||
+157
-139
@@ -1,10 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
pianoroll.mjs - <short description TODO>
|
pianoroll.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pianoroll.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/pianoroll.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/>.
|
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, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
import { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
||||||
|
|
||||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||||
const getValue = (e) => {
|
const getValue = (e) => {
|
||||||
@@ -18,7 +18,7 @@ const getValue = (e) => {
|
|||||||
}
|
}
|
||||||
note = note ?? n;
|
note = note ?? n;
|
||||||
if (typeof note === 'string') {
|
if (typeof note === 'string') {
|
||||||
return noteToMidi(note);
|
return toMidi(note);
|
||||||
}
|
}
|
||||||
if (typeof note === 'number') {
|
if (typeof note === 'number') {
|
||||||
return note;
|
return note;
|
||||||
@@ -29,26 +29,129 @@ const getValue = (e) => {
|
|||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.pianoroll = function (options = {}) {
|
Pattern.prototype.pianoroll = function ({
|
||||||
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
|
cycles = 4,
|
||||||
|
playhead = 0.5,
|
||||||
|
overscan = 1,
|
||||||
|
flipTime = 0,
|
||||||
|
flipValues = 0,
|
||||||
|
hideNegative = false,
|
||||||
|
// inactive = '#C9E597',
|
||||||
|
// inactive = '#FFCA28',
|
||||||
|
inactive = '#7491D2',
|
||||||
|
active = '#FFCA28',
|
||||||
|
// background = '#2A3236',
|
||||||
|
background = 'transparent',
|
||||||
|
smear = 0,
|
||||||
|
playheadColor = 'white',
|
||||||
|
minMidi = 10,
|
||||||
|
maxMidi = 90,
|
||||||
|
autorange = 0,
|
||||||
|
timeframe: timeframeProp,
|
||||||
|
fold = 0,
|
||||||
|
vertical = 0,
|
||||||
|
} = {}) {
|
||||||
|
const ctx = getDrawContext();
|
||||||
|
const w = ctx.canvas.width;
|
||||||
|
const h = ctx.canvas.height;
|
||||||
let from = -cycles * playhead;
|
let from = -cycles * playhead;
|
||||||
let to = cycles * (1 - playhead);
|
let to = cycles * (1 - playhead);
|
||||||
|
|
||||||
|
if (timeframeProp) {
|
||||||
|
console.warn('timeframe is deprecated! use from/to instead');
|
||||||
|
from = 0;
|
||||||
|
to = timeframeProp;
|
||||||
|
}
|
||||||
|
const timeAxis = vertical ? h : w;
|
||||||
|
const valueAxis = vertical ? w : h;
|
||||||
|
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
|
||||||
|
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
|
||||||
|
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
|
||||||
|
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
|
||||||
|
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
|
||||||
|
let foldValues = [];
|
||||||
|
flipTime && timeRange.reverse();
|
||||||
|
flipValues && valueRange.reverse();
|
||||||
|
|
||||||
this.draw(
|
this.draw(
|
||||||
(ctx, haps, t) => {
|
(ctx, events, t) => {
|
||||||
|
ctx.fillStyle = background;
|
||||||
|
ctx.globalAlpha = 1; // reset!
|
||||||
|
if (!smear) {
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
}
|
||||||
const inFrame = (event) =>
|
const inFrame = (event) =>
|
||||||
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
|
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
|
||||||
pianoroll({
|
events.filter(inFrame).forEach((event) => {
|
||||||
...options,
|
const isActive = event.whole.begin <= t && event.whole.end > t;
|
||||||
time: t,
|
ctx.fillStyle = event.context?.color || inactive;
|
||||||
ctx,
|
ctx.strokeStyle = event.context?.color || active;
|
||||||
haps: haps.filter(inFrame),
|
ctx.globalAlpha = event.context.velocity ?? 1;
|
||||||
|
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
||||||
|
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||||
|
const value = getValue(event);
|
||||||
|
const valuePx = scale(
|
||||||
|
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
|
||||||
|
...valueRange,
|
||||||
|
);
|
||||||
|
let margin = 0;
|
||||||
|
const offset = scale(t / timeExtent, ...timeRange);
|
||||||
|
let coords;
|
||||||
|
if (vertical) {
|
||||||
|
coords = [
|
||||||
|
valuePx + 1 - (flipValues ? barThickness : 0), // x
|
||||||
|
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
|
||||||
|
barThickness - 2, // width
|
||||||
|
durationPx - 2, // height
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
coords = [
|
||||||
|
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
|
||||||
|
valuePx + 1 - (flipValues ? 0 : barThickness), // y
|
||||||
|
durationPx - 2, // widith
|
||||||
|
barThickness - 2, // height
|
||||||
|
];
|
||||||
|
}
|
||||||
|
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
||||||
});
|
});
|
||||||
|
ctx.globalAlpha = 1; // reset!
|
||||||
|
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||||
|
// draw playhead
|
||||||
|
ctx.strokeStyle = playheadColor;
|
||||||
|
ctx.beginPath();
|
||||||
|
if (vertical) {
|
||||||
|
ctx.moveTo(0, playheadPosition);
|
||||||
|
ctx.lineTo(valueAxis, playheadPosition);
|
||||||
|
} else {
|
||||||
|
ctx.moveTo(playheadPosition, 0);
|
||||||
|
ctx.lineTo(playheadPosition, valueAxis);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: from - overscan,
|
from: from - overscan,
|
||||||
to: to + overscan,
|
to: to + overscan,
|
||||||
|
onQuery: (events) => {
|
||||||
|
const { min, max, values } = events.reduce(
|
||||||
|
({ min, max, values }, e) => {
|
||||||
|
const v = getValue(e);
|
||||||
|
return {
|
||||||
|
min: v < min ? v : min,
|
||||||
|
max: v > max ? v : max,
|
||||||
|
values: values.includes(v) ? values : [...values, v],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ min: Infinity, max: -Infinity, values: [] },
|
||||||
|
);
|
||||||
|
if (autorange) {
|
||||||
|
minMidi = min;
|
||||||
|
maxMidi = max;
|
||||||
|
valueExtent = maxMidi - minMidi + 1;
|
||||||
|
}
|
||||||
|
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
|
||||||
|
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return this;
|
return this;
|
||||||
@@ -56,40 +159,6 @@ Pattern.prototype.pianoroll = function (options = {}) {
|
|||||||
|
|
||||||
// this function allows drawing a pianoroll without ties to Pattern.prototype
|
// this function allows drawing a pianoroll without ties to Pattern.prototype
|
||||||
// it will probably replace the above in the future
|
// it will probably replace the above in the future
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays a midi-style piano roll
|
|
||||||
*
|
|
||||||
* @name pianoroll
|
|
||||||
* @param {Object} options Object containing all the optional following parameters as key value pairs:
|
|
||||||
* @param {integer} cycles number of cycles to be displayed at the same time - defaults to 4
|
|
||||||
* @param {number} playhead location of the active notes on the time axis - 0 to 1, defaults to 0.5
|
|
||||||
* @param {boolean} vertical displays the roll vertically - 0 by default
|
|
||||||
* @param {boolean} labels displays labels on individual notes (see the label function) - 0 by default
|
|
||||||
* @param {boolean} flipTime reverse the direction of the roll - 0 by default
|
|
||||||
* @param {boolean} flipValues reverse the relative location of notes on the value axis - 0 by default
|
|
||||||
* @param {number} overscan lookup X cycles outside of the cycles window to display notes in advance - 1 by default
|
|
||||||
* @param {boolean} hideNegative hide notes with negative time (before starting playing the pattern) - 0 by default
|
|
||||||
* @param {boolean} smear notes leave a solid trace - 0 by default
|
|
||||||
* @param {boolean} fold notes takes the full value axis width - 0 by default
|
|
||||||
* @param {string} active hexadecimal or CSS color of the active notes - defaults to #FFCA28
|
|
||||||
* @param {string} inactive hexadecimal or CSS color of the inactive notes - defaults to #7491D2
|
|
||||||
* @param {string} background hexadecimal or CSS color of the background - defaults to transparent
|
|
||||||
* @param {string} playheadColor hexadecimal or CSS color of the line representing the play head - defaults to white
|
|
||||||
* @param {boolean} fill notes are filled with color (otherwise only the label is displayed) - 0 by default
|
|
||||||
* @param {boolean} fillActive active notes are filled with color - 0 by default
|
|
||||||
* @param {boolean} stroke notes are shown with colored borders - 0 by default
|
|
||||||
* @param {boolean} strokeActive active notes are shown with colored borders - 0 by default
|
|
||||||
* @param {boolean} hideInactive only active notes are shown - 0 by default
|
|
||||||
* @param {boolean} colorizeInactive use note color for inactive notes - 1 by default
|
|
||||||
* @param {string} fontFamily define the font used by notes labels - defaults to 'monospace'
|
|
||||||
* @param {integer} minMidi minimum note value to display on the value axis - defaults to 10
|
|
||||||
* @param {integer} maxMidi maximum note value to display on the value axis - defaults to 90
|
|
||||||
* @param {boolean} autorange automatically calculate the minMidi and maxMidi parameters - 0 by default
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* note("C2 A2 G2").euclid(5,8).s('piano').clip(1).color('salmon').pianoroll({vertical:1, labels:1})
|
|
||||||
*/
|
|
||||||
export function pianoroll({
|
export function pianoroll({
|
||||||
time,
|
time,
|
||||||
haps,
|
haps,
|
||||||
@@ -112,15 +181,6 @@ export function pianoroll({
|
|||||||
timeframe: timeframeProp,
|
timeframe: timeframeProp,
|
||||||
fold = 0,
|
fold = 0,
|
||||||
vertical = 0,
|
vertical = 0,
|
||||||
labels = false,
|
|
||||||
fill = 1,
|
|
||||||
fillActive = false,
|
|
||||||
strokeActive = true,
|
|
||||||
stroke,
|
|
||||||
hideInactive = 0,
|
|
||||||
colorizeInactive = 1,
|
|
||||||
fontFamily,
|
|
||||||
clear = true,
|
|
||||||
ctx,
|
ctx,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const w = ctx.canvas.width;
|
const w = ctx.canvas.width;
|
||||||
@@ -164,75 +224,50 @@ export function pianoroll({
|
|||||||
// foldValues = values.sort((a, b) => a - b);
|
// foldValues = values.sort((a, b) => a - b);
|
||||||
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
|
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
|
||||||
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
||||||
|
|
||||||
ctx.fillStyle = background;
|
ctx.fillStyle = background;
|
||||||
ctx.globalAlpha = 1; // reset!
|
ctx.globalAlpha = 1; // reset!
|
||||||
if (clear && !smear) {
|
if (!smear) {
|
||||||
ctx.clearRect(0, 0, w, h);
|
ctx.clearRect(0, 0, w, h);
|
||||||
ctx.fillRect(0, 0, w, h);
|
ctx.fillRect(0, 0, w, h);
|
||||||
}
|
}
|
||||||
haps.forEach((event) => {
|
/* const inFrame = (event) =>
|
||||||
const isActive = event.whole.begin <= time && event.endClipped > time;
|
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
|
||||||
let strokeCurrent = stroke ?? (strokeActive && isActive);
|
haps
|
||||||
let fillCurrent = (!isActive && fill) || (isActive && fillActive);
|
// .filter(inFrame)
|
||||||
if (hideInactive && !isActive) {
|
.forEach((event) => {
|
||||||
return;
|
const isActive = event.whole.begin <= time && event.whole.end > time;
|
||||||
}
|
const color = event.value?.color || event.context?.color;
|
||||||
let color = event.value?.color || event.context?.color;
|
ctx.fillStyle = color || inactive;
|
||||||
active = color || active;
|
ctx.strokeStyle = color || active;
|
||||||
inactive = colorizeInactive ? color || inactive : inactive;
|
ctx.globalAlpha = event.context.velocity ?? 1;
|
||||||
color = isActive ? active : inactive;
|
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
||||||
ctx.fillStyle = fillCurrent ? color : 'transparent';
|
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||||
ctx.strokeStyle = color;
|
const value = getValue(event);
|
||||||
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
|
const valuePx = scale(
|
||||||
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
|
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
|
||||||
const timePx = scale(timeProgress, ...timeRange);
|
...valueRange,
|
||||||
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
);
|
||||||
const value = getValue(event);
|
let margin = 0;
|
||||||
const valueProgress = fold
|
const offset = scale(time / timeExtent, ...timeRange);
|
||||||
? foldValues.indexOf(value) / foldValues.length
|
let coords;
|
||||||
: (Number(value) - minMidi) / valueExtent;
|
if (vertical) {
|
||||||
const valuePx = scale(valueProgress, ...valueRange);
|
coords = [
|
||||||
let margin = 0;
|
valuePx + 1 - (flipValues ? barThickness : 0), // x
|
||||||
const offset = scale(time / timeExtent, ...timeRange);
|
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
|
||||||
let coords;
|
barThickness - 2, // width
|
||||||
if (vertical) {
|
durationPx - 2, // height
|
||||||
coords = [
|
];
|
||||||
valuePx + 1 - (flipValues ? barThickness : 0), // x
|
} else {
|
||||||
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
|
coords = [
|
||||||
barThickness - 2, // width
|
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
|
||||||
durationPx - 2, // height
|
valuePx + 1 - (flipValues ? 0 : barThickness), // y
|
||||||
];
|
durationPx - 2, // widith
|
||||||
} else {
|
barThickness - 2, // height
|
||||||
coords = [
|
];
|
||||||
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
|
}
|
||||||
valuePx + 1 - (flipValues ? 0 : barThickness), // y
|
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
||||||
durationPx - 2, // widith
|
});
|
||||||
barThickness - 2, // height
|
|
||||||
];
|
|
||||||
}
|
|
||||||
/* const xFactor = Math.sin(performance.now() / 500) + 1;
|
|
||||||
coords[0] *= xFactor; */
|
|
||||||
|
|
||||||
if (strokeCurrent) {
|
|
||||||
ctx.strokeRect(...coords);
|
|
||||||
}
|
|
||||||
if (fillCurrent) {
|
|
||||||
ctx.fillRect(...coords);
|
|
||||||
}
|
|
||||||
//ctx.ellipse(...ellipseFromRect(...coords))
|
|
||||||
if (labels) {
|
|
||||||
const defaultLabel = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
|
|
||||||
const { label: inactiveLabel, activeLabel } = event.value;
|
|
||||||
const customLabel = isActive ? activeLabel || inactiveLabel : inactiveLabel;
|
|
||||||
const label = customLabel ?? defaultLabel;
|
|
||||||
let measure = vertical ? durationPx : barThickness * 0.75;
|
|
||||||
ctx.font = `${measure}px ${fontFamily || 'monospace'}`;
|
|
||||||
// font color
|
|
||||||
ctx.fillStyle = /* isActive && */ !fillCurrent ? color : 'black';
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
ctx.fillText(label, ...coords);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ctx.globalAlpha = 1; // reset!
|
ctx.globalAlpha = 1; // reset!
|
||||||
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||||
// draw playhead
|
// draw playhead
|
||||||
@@ -249,7 +284,7 @@ export function pianoroll({
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDrawOptions(drawTime, options = {}) {
|
function getOptions(drawTime, options = {}) {
|
||||||
let [lookbehind, lookahead] = drawTime;
|
let [lookbehind, lookahead] = drawTime;
|
||||||
lookbehind = Math.abs(lookbehind);
|
lookbehind = Math.abs(lookbehind);
|
||||||
const cycles = lookahead + lookbehind;
|
const cycles = lookahead + lookbehind;
|
||||||
@@ -258,22 +293,5 @@ export function getDrawOptions(drawTime, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.punchcard = function (options) {
|
Pattern.prototype.punchcard = function (options) {
|
||||||
return this.onPaint((ctx, time, haps, drawTime, paintOptions = {}) =>
|
return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
|
||||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) }),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.wordfall = function (options) {
|
|
||||||
return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options });
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Pattern.prototype.pianoroll = function (options) {
|
|
||||||
return this.onPaint((ctx, time, haps, drawTime) =>
|
|
||||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),
|
|
||||||
);
|
|
||||||
}; */
|
|
||||||
|
|
||||||
export function drawPianoroll(options) {
|
|
||||||
const { drawTime, ...rest } = options;
|
|
||||||
pianoroll({ ...getDrawOptions(drawTime), ...rest });
|
|
||||||
}
|
|
||||||
|
|||||||
+22
-99
@@ -2,24 +2,6 @@ import { Cyclist } from './cyclist.mjs';
|
|||||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { setTime } from './time.mjs';
|
import { setTime } from './time.mjs';
|
||||||
import { evalScope } from './evaluate.mjs';
|
|
||||||
import { register } from './pattern.mjs';
|
|
||||||
import { atom, computed } from 'nanostores';
|
|
||||||
|
|
||||||
export const $replstate = atom({
|
|
||||||
schedulerError: undefined,
|
|
||||||
evalError: undefined,
|
|
||||||
code: '// LOADING',
|
|
||||||
activeCode: '// LOADING',
|
|
||||||
pattern: undefined,
|
|
||||||
miniLocations: [],
|
|
||||||
widgets: [],
|
|
||||||
pending: true,
|
|
||||||
started: false,
|
|
||||||
});
|
|
||||||
export const $repldirty = computed($replstate, (s) => s.code !== s.activeCode);
|
|
||||||
|
|
||||||
export const setReplState = (key, value) => $replstate.set({ ...$replstate.get(), [key]: value });
|
|
||||||
|
|
||||||
export function repl({
|
export function repl({
|
||||||
interval,
|
interval,
|
||||||
@@ -35,105 +17,46 @@ export function repl({
|
|||||||
}) {
|
}) {
|
||||||
const scheduler = new Cyclist({
|
const scheduler = new Cyclist({
|
||||||
interval,
|
interval,
|
||||||
onTrigger: getTrigger({ defaultOutput, getTime }),
|
onTrigger: async (hap, deadline, duration) => {
|
||||||
|
try {
|
||||||
|
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||||
|
await defaultOutput(hap, deadline, duration);
|
||||||
|
}
|
||||||
|
if (hap.context.onTrigger) {
|
||||||
|
const cps = 1;
|
||||||
|
// call signature of output / onTrigger is different...
|
||||||
|
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
onError: onSchedulerError,
|
onError: onSchedulerError,
|
||||||
getTime,
|
getTime,
|
||||||
onToggle: (started) => {
|
onToggle,
|
||||||
setReplState('started', started);
|
|
||||||
onToggle?.(started);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
let playPatterns = [];
|
|
||||||
const setPattern = (pattern, autostart = true) => {
|
|
||||||
pattern = editPattern?.(pattern) || pattern;
|
|
||||||
scheduler.setPattern(pattern, autostart);
|
|
||||||
};
|
|
||||||
setTime(() => scheduler.now()); // TODO: refactor?
|
setTime(() => scheduler.now()); // TODO: refactor?
|
||||||
const evaluate = async (code, autostart = true) => {
|
const evaluate = async (code, autostart = true) => {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error('no code to evaluate');
|
throw new Error('no code to evaluate');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
setReplState('code', code);
|
beforeEval?.({ code });
|
||||||
setReplState('pending', true);
|
let { pattern } = await _evaluate(code, transpiler);
|
||||||
await beforeEval?.({ code });
|
|
||||||
playPatterns = [];
|
|
||||||
let { pattern, meta } = await _evaluate(code, transpiler);
|
|
||||||
if (playPatterns.length) {
|
|
||||||
pattern = pattern.stack(...playPatterns);
|
|
||||||
}
|
|
||||||
logger(`[eval] code updated`);
|
logger(`[eval] code updated`);
|
||||||
setPattern(pattern, autostart);
|
pattern = editPattern?.(pattern) || pattern;
|
||||||
setReplState('miniLocations', meta?.miniLocations || []);
|
scheduler.setPattern(pattern, autostart);
|
||||||
setReplState('widgets', meta?.widgets || []);
|
afterEval?.({ code, pattern });
|
||||||
setReplState('activeCode', code);
|
|
||||||
setReplState('pattern', pattern);
|
|
||||||
setReplState('evalError', undefined);
|
|
||||||
setReplState('schedulerError', undefined);
|
|
||||||
setReplState('pending', false);
|
|
||||||
afterEval?.({ code, pattern, meta });
|
|
||||||
return pattern;
|
return pattern;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.warn(`[repl] eval error: ${err.message}`);
|
// console.warn(`[repl] eval error: ${err.message}`);
|
||||||
logger(`[eval] error: ${err.message}`, 'error');
|
logger(`[eval] error: ${err.message}`, 'error');
|
||||||
setReplState('evalError', err);
|
|
||||||
setReplState('pending', false);
|
|
||||||
onEvalError?.(err);
|
onEvalError?.(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const stop = () => scheduler.stop();
|
const stop = () => scheduler.stop();
|
||||||
const start = () => scheduler.start();
|
const start = () => scheduler.start();
|
||||||
const pause = () => scheduler.pause();
|
const pause = () => scheduler.pause();
|
||||||
const toggle = () => scheduler.toggle();
|
return { scheduler, evaluate, start, stop, pause };
|
||||||
const setCps = (cps) => scheduler.setCps(cps);
|
|
||||||
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
|
||||||
|
|
||||||
// the following functions use the cps value, which is why they are defined here..
|
|
||||||
const loopAt = register('loopAt', (cycles, pat) => {
|
|
||||||
return pat.loopAtCps(cycles, scheduler.cps);
|
|
||||||
});
|
|
||||||
|
|
||||||
const play = register('play', (pat) => {
|
|
||||||
playPatterns.push(pat);
|
|
||||||
return pat;
|
|
||||||
});
|
|
||||||
|
|
||||||
const fit = register('fit', (pat) =>
|
|
||||||
pat.withHap((hap) =>
|
|
||||||
hap.withValue((v) => ({
|
|
||||||
...v,
|
|
||||||
speed: scheduler.cps / hap.whole.duration, // overwrite speed completely?
|
|
||||||
unit: 'c',
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
evalScope({
|
|
||||||
loopAt,
|
|
||||||
fit,
|
|
||||||
play,
|
|
||||||
setCps,
|
|
||||||
setcps: setCps,
|
|
||||||
setCpm,
|
|
||||||
setcpm: setCpm,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setCode = (c) => setReplState('code', c);
|
|
||||||
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTrigger =
|
|
||||||
({ getTime, defaultOutput }) =>
|
|
||||||
async (hap, deadline, duration, cps) => {
|
|
||||||
try {
|
|
||||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
|
||||||
await defaultOutput(hap, deadline, duration, cps);
|
|
||||||
}
|
|
||||||
if (hap.context.onTrigger) {
|
|
||||||
// call signature of output / onTrigger is different...
|
|
||||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger(`[cyclist] error: ${err.message}`, 'error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import { Hap } from './hap.mjs';
|
import { Hap } from './hap.mjs';
|
||||||
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
|
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
import { id, _mod, clamp } from './util.mjs';
|
import { id } from './util.mjs';
|
||||||
|
|
||||||
export function steady(value) {
|
export function steady(value) {
|
||||||
// A continuous value
|
// A continuous value
|
||||||
@@ -27,7 +27,7 @@ export const isaw2 = isaw.toBipolar();
|
|||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
|
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
|
||||||
* @example
|
* @example
|
||||||
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
||||||
*
|
*
|
||||||
@@ -155,62 +155,12 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
|||||||
*/
|
*/
|
||||||
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
||||||
|
|
||||||
/**
|
|
||||||
* pick from the list of values (or patterns of values) via the index using the given
|
|
||||||
* pattern of integers
|
|
||||||
* @param {Pattern} pat
|
|
||||||
* @param {*} xs
|
|
||||||
* @returns {Pattern}
|
|
||||||
* @example
|
|
||||||
* note(pick("<0 1 [2!2] 3>", ["g a", "e f", "f g f g" , "g a c d"]))
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const pick = (pat, xs) => {
|
|
||||||
xs = xs.map(reify);
|
|
||||||
if (xs.length == 0) {
|
|
||||||
return silence;
|
|
||||||
}
|
|
||||||
return pat
|
|
||||||
.fmap((i) => {
|
|
||||||
const key = clamp(Math.round(i), 0, xs.length - 1);
|
|
||||||
return xs[key];
|
|
||||||
})
|
|
||||||
.innerJoin();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* pick from the list of values (or patterns of values) via the index using the given
|
|
||||||
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
|
|
||||||
* @param {Pattern} pat
|
|
||||||
* @param {*} xs
|
|
||||||
* @returns {Pattern}
|
|
||||||
* @example
|
|
||||||
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const squeeze = (pat, xs) => {
|
|
||||||
xs = xs.map(reify);
|
|
||||||
if (xs.length == 0) {
|
|
||||||
return silence;
|
|
||||||
}
|
|
||||||
return pat
|
|
||||||
.fmap((i) => {
|
|
||||||
const key = _mod(Math.round(i), xs.length);
|
|
||||||
return xs[key];
|
|
||||||
})
|
|
||||||
.squeezeJoin();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const __chooseWith = (pat, xs) => {
|
export const __chooseWith = (pat, xs) => {
|
||||||
xs = xs.map(reify);
|
xs = xs.map(reify);
|
||||||
if (xs.length == 0) {
|
if (xs.length == 0) {
|
||||||
return silence;
|
return silence;
|
||||||
}
|
}
|
||||||
|
return pat.range(0, xs.length).fmap((i) => xs[Math.floor(i)]);
|
||||||
return pat.range(0, xs.length).fmap((i) => {
|
|
||||||
const key = Math.min(Math.max(Math.floor(i), 0), xs.length - 1);
|
|
||||||
return xs[key];
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Choose from the list of values (or patterns of values) using the given
|
* Choose from the list of values (or patterns of values) using the given
|
||||||
@@ -218,8 +168,6 @@ export const __chooseWith = (pat, xs) => {
|
|||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
|
||||||
* note("c2 g2!2 d2 f1").s(chooseWith(sine.fast(2), ["sawtooth", "triangle", "bd:6"]))
|
|
||||||
*/
|
*/
|
||||||
export const chooseWith = (pat, xs) => {
|
export const chooseWith = (pat, xs) => {
|
||||||
return __chooseWith(pat, xs).outerJoin();
|
return __chooseWith(pat, xs).outerJoin();
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
import { Pattern } from './index.mjs';
|
|
||||||
|
|
||||||
// polar coords -> xy
|
|
||||||
function fromPolar(angle, radius, cx, cy) {
|
|
||||||
const radians = ((angle - 90) * Math.PI) / 180;
|
|
||||||
return [cx + Math.cos(radians) * radius, cy + Math.sin(radians) * radius];
|
|
||||||
}
|
|
||||||
|
|
||||||
const xyOnSpiral = (angle, margin, cx, cy, rotate = 0) => fromPolar((angle + rotate) * 360, margin * angle, cx, cy); // TODO: logSpiral
|
|
||||||
|
|
||||||
// draw spiral / segment of spiral
|
|
||||||
function spiralSegment(options) {
|
|
||||||
let {
|
|
||||||
ctx,
|
|
||||||
from = 0,
|
|
||||||
to = 3,
|
|
||||||
margin = 50,
|
|
||||||
cx = 100,
|
|
||||||
cy = 100,
|
|
||||||
rotate = 0,
|
|
||||||
thickness = margin / 2,
|
|
||||||
color = '#0000ff30',
|
|
||||||
cap = 'round',
|
|
||||||
stretch = 1,
|
|
||||||
fromOpacity = 1,
|
|
||||||
toOpacity = 1,
|
|
||||||
} = options;
|
|
||||||
from *= stretch;
|
|
||||||
to *= stretch;
|
|
||||||
rotate *= stretch;
|
|
||||||
ctx.lineWidth = thickness;
|
|
||||||
ctx.lineCap = cap;
|
|
||||||
ctx.strokeStyle = color;
|
|
||||||
ctx.globalAlpha = fromOpacity;
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
let [sx, sy] = xyOnSpiral(from, margin, cx, cy, rotate);
|
|
||||||
ctx.moveTo(sx, sy);
|
|
||||||
|
|
||||||
const increment = 1 / 60;
|
|
||||||
let angle = from;
|
|
||||||
while (angle <= to) {
|
|
||||||
const [x, y] = xyOnSpiral(angle, margin, cx, cy, rotate);
|
|
||||||
//ctx.lineWidth = angle*thickness;
|
|
||||||
ctx.globalAlpha = ((angle - from) / (to - from)) * toOpacity;
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
angle += increment;
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
Pattern.prototype.spiral = function (options = {}) {
|
|
||||||
const {
|
|
||||||
stretch = 1,
|
|
||||||
size = 80,
|
|
||||||
thickness = size / 2,
|
|
||||||
cap = 'butt', // round butt squar,
|
|
||||||
inset = 3, // start angl,
|
|
||||||
playheadColor = '#ffffff90',
|
|
||||||
playheadLength = 0.02,
|
|
||||||
playheadThickness = thickness,
|
|
||||||
padding = 0,
|
|
||||||
steady = 1,
|
|
||||||
inactiveColor = '#ffffff20',
|
|
||||||
colorizeInactive = 0,
|
|
||||||
fade = true,
|
|
||||||
// logSpiral = true,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
function spiral({ ctx, time, haps, drawTime, clear }) {
|
|
||||||
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
|
|
||||||
clear && ctx.clearRect(0, 0, w * 2, h * 2);
|
|
||||||
const [cx, cy] = [w / 2, h / 2];
|
|
||||||
const settings = {
|
|
||||||
margin: size / stretch,
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
stretch,
|
|
||||||
cap,
|
|
||||||
thickness,
|
|
||||||
};
|
|
||||||
|
|
||||||
const playhead = {
|
|
||||||
...settings,
|
|
||||||
thickness: playheadThickness,
|
|
||||||
from: inset - playheadLength,
|
|
||||||
to: inset,
|
|
||||||
color: playheadColor,
|
|
||||||
};
|
|
||||||
|
|
||||||
const [min] = drawTime;
|
|
||||||
const rotate = steady * time;
|
|
||||||
haps.forEach((hap) => {
|
|
||||||
const isActive = hap.whole.begin <= time && hap.endClipped > time;
|
|
||||||
const from = hap.whole.begin - time + inset;
|
|
||||||
const to = hap.endClipped - time + inset - padding;
|
|
||||||
const { color } = hap.context;
|
|
||||||
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
|
|
||||||
spiralSegment({
|
|
||||||
ctx,
|
|
||||||
...settings,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
rotate,
|
|
||||||
color: colorizeInactive || isActive ? color : inactiveColor,
|
|
||||||
fromOpacity: opacity,
|
|
||||||
toOpacity: opacity,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
spiralSegment({
|
|
||||||
ctx,
|
|
||||||
...playhead,
|
|
||||||
rotate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.onPaint((ctx, time, haps, drawTime, options) => spiral({ ctx, time, haps, drawTime, ...options }));
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
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 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -45,6 +45,8 @@ import {
|
|||||||
rev,
|
rev,
|
||||||
time,
|
time,
|
||||||
run,
|
run,
|
||||||
|
hitch,
|
||||||
|
set,
|
||||||
} from '../index.mjs';
|
} from '../index.mjs';
|
||||||
|
|
||||||
import { steady } from '../signal.mjs';
|
import { steady } from '../signal.mjs';
|
||||||
@@ -204,7 +206,7 @@ describe('Pattern', () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can SqueezeOut() structure', () => {
|
it('can squeezeout() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
sequence(1, [2, 3]).add.squeezeout(10, 20, 30),
|
sequence(1, [2, 3]).add.squeezeout(10, 20, 30),
|
||||||
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
|
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
|
||||||
@@ -252,7 +254,7 @@ describe('Pattern', () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can SqueezeOut() structure', () => {
|
it('can squeezeout() structure', () => {
|
||||||
sameFirst(sequence(1, [2, 3]).keep.squeezeout(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
|
sameFirst(sequence(1, [2, 3]).keep.squeezeout(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -294,7 +296,7 @@ describe('Pattern', () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can SqueezeOut() structure', () => {
|
it('can squeezeout() structure', () => {
|
||||||
sameFirst(sequence(1, [2, 3]).keepif.squeezeout(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
|
sameFirst(sequence(1, [2, 3]).keepif.squeezeout(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -929,6 +931,14 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('alignments', () => {
|
describe('alignments', () => {
|
||||||
|
it('Can combine controls', () => {
|
||||||
|
sameFirst(s('bd').set.in.n(3), s('bd').n(3));
|
||||||
|
sameFirst(s('bd').set.squeeze.n(3, 4), sequence(s('bd').n(3), s('bd').n(4)));
|
||||||
|
});
|
||||||
|
it('Can combine functions with alignmed controls', () => {
|
||||||
|
sameFirst(s('bd').apply(fast(2).set(n(3))), s('bd').fast(2).set.in.n(3));
|
||||||
|
sameFirst(s('bd').apply(fast(2).set.in.n(3)), s('bd').fast(2).set.in.n(3));
|
||||||
|
});
|
||||||
it('Can squeeze arguments', () => {
|
it('Can squeeze arguments', () => {
|
||||||
expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle());
|
expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle());
|
||||||
});
|
});
|
||||||
@@ -959,7 +969,7 @@ describe('Pattern', () => {
|
|||||||
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
|
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
/*describe('composable functions', () => {
|
describe('composable functions', () => {
|
||||||
it('Can compose functions', () => {
|
it('Can compose functions', () => {
|
||||||
sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
|
sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
|
||||||
});
|
});
|
||||||
@@ -971,55 +981,4 @@ describe('Pattern', () => {
|
|||||||
// sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.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 },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('chunk', () => {
|
|
||||||
it('Processes each cycle of the source pattern multiple times, once for each chunk', () => {
|
|
||||||
expect(sequence(0, 1, 2, 3).slow(2).chunk(2, add(10)).fast(4).firstCycleValues).toStrictEqual([
|
|
||||||
10, 1, 0, 11, 12, 3, 2, 13,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('fastChunk', () => {
|
|
||||||
it('Unlike chunk, cycles of the source pattern proceed cycle-by-cycle', () => {
|
|
||||||
expect(sequence(0, 1, 2, 3).slow(2).fastChunk(2, add(10)).fast(4).firstCycleValues).toStrictEqual([
|
|
||||||
10, 1, 2, 13, 10, 1, 2, 13,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('repeatCycles', () => {
|
|
||||||
it('Repeats each cycle of the source pattern the given number of times', () => {
|
|
||||||
expect(slowcat(0, 1).repeatCycles(2).fast(6).firstCycleValues).toStrictEqual([0, 0, 1, 1, 0, 0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
/*test for issue 302 support alternative solmization types */
|
|
||||||
import { sol2note } from '../util.mjs';
|
|
||||||
import { test } from 'vitest';
|
|
||||||
import assert from 'assert';
|
|
||||||
|
|
||||||
test('solmization - letters', () => {
|
|
||||||
const result = sol2note(60, 'letters');
|
|
||||||
const expected = 'C4';
|
|
||||||
assert.equal(result, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('solmization - solfeggio', () => {
|
|
||||||
const result = sol2note(60, 'solfeggio');
|
|
||||||
const expected = 'Do4';
|
|
||||||
assert.equal(result, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('solmization - indian', () => {
|
|
||||||
const result = sol2note(60, 'indian');
|
|
||||||
const expected = 'Sa4';
|
|
||||||
assert.equal(result, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('solmization - german', () => {
|
|
||||||
const result = sol2note(60, 'german');
|
|
||||||
const expected = 'C4';
|
|
||||||
assert.equal(result, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('solmization - byzantine', () => {
|
|
||||||
const result = sol2note(60, 'byzantine');
|
|
||||||
const expected = 'Ni4';
|
|
||||||
assert.equal(result, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('solmization - japanese', () => {
|
|
||||||
const result = sol2note(60, 'japanese');
|
|
||||||
const expected = 'I4';
|
|
||||||
assert.equal(result, expected);
|
|
||||||
});
|
|
||||||
@@ -8,8 +8,8 @@ import { pure } from '../pattern.mjs';
|
|||||||
import {
|
import {
|
||||||
isNote,
|
isNote,
|
||||||
tokenizeNote,
|
tokenizeNote,
|
||||||
noteToMidi,
|
toMidi,
|
||||||
midiToFreq,
|
fromMidi,
|
||||||
freqToMidi,
|
freqToMidi,
|
||||||
_mod,
|
_mod,
|
||||||
compose,
|
compose,
|
||||||
@@ -75,27 +75,27 @@ describe('isNote', () => {
|
|||||||
expect(tokenizeNote(123)).toStrictEqual([]);
|
expect(tokenizeNote(123)).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('noteToMidi', () => {
|
describe('toMidi', () => {
|
||||||
it('should turn notes into midi', () => {
|
it('should turn notes into midi', () => {
|
||||||
expect(noteToMidi('A4')).toEqual(69);
|
expect(toMidi('A4')).toEqual(69);
|
||||||
expect(noteToMidi('C4')).toEqual(60);
|
expect(toMidi('C4')).toEqual(60);
|
||||||
expect(noteToMidi('Db4')).toEqual(61);
|
expect(toMidi('Db4')).toEqual(61);
|
||||||
expect(noteToMidi('C3')).toEqual(48);
|
expect(toMidi('C3')).toEqual(48);
|
||||||
expect(noteToMidi('Cb3')).toEqual(47);
|
expect(toMidi('Cb3')).toEqual(47);
|
||||||
expect(noteToMidi('Cbb3')).toEqual(46);
|
expect(toMidi('Cbb3')).toEqual(46);
|
||||||
expect(noteToMidi('C#3')).toEqual(49);
|
expect(toMidi('C#3')).toEqual(49);
|
||||||
expect(noteToMidi('C#3')).toEqual(49);
|
expect(toMidi('C#3')).toEqual(49);
|
||||||
expect(noteToMidi('C##3')).toEqual(50);
|
expect(toMidi('C##3')).toEqual(50);
|
||||||
});
|
});
|
||||||
it('should throw an error when given a non-note', () => {
|
it('should throw an error when given a non-note', () => {
|
||||||
expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
|
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||||
expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
|
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('midiToFreq', () => {
|
describe('fromMidi', () => {
|
||||||
it('should turn midi into frequency', () => {
|
it('should turn midi into frequency', () => {
|
||||||
expect(midiToFreq(69)).toEqual(440);
|
expect(fromMidi(69)).toEqual(440);
|
||||||
expect(midiToFreq(57)).toEqual(220);
|
expect(fromMidi(57)).toEqual(220);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('freqToMidi', () => {
|
describe('freqToMidi', () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
ui.mjs - <short description TODO>
|
ui.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/ui.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/ui.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/>.
|
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/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -46,5 +46,7 @@ export const cleanupUi = () => {
|
|||||||
const container = document.getElementById('code');
|
const container = document.getElementById('code');
|
||||||
if (container) {
|
if (container) {
|
||||||
container.style = '';
|
container.style = '';
|
||||||
|
// TODO: find a way to remove that duplication..
|
||||||
|
container.className = 'grow flex text-gray-100 relative overflow-auto cursor-text pb-0'; // has to match App.tsx
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+17
-85
@@ -6,32 +6,29 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
// returns true if the given string is a note
|
// returns true if the given string is a note
|
||||||
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
||||||
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
|
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]?$/.test(name);
|
||||||
export const tokenizeNote = (note) => {
|
export const tokenizeNote = (note) => {
|
||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
|
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [pc, acc, oct ? Number(oct) : undefined];
|
return [pc, acc, oct ? Number(oct) : undefined];
|
||||||
};
|
};
|
||||||
|
|
||||||
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
|
||||||
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
|
||||||
|
|
||||||
// turns the given note into its midi number representation
|
// turns the given note into its midi number representation
|
||||||
export const noteToMidi = (note, defaultOctave = 3) => {
|
export const toMidi = (note) => {
|
||||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
const [pc, acc, oct = 3] = tokenizeNote(note);
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
throw new Error('not a note: "' + note + '"');
|
throw new Error('not a note: "' + note + '"');
|
||||||
}
|
}
|
||||||
const chroma = chromas[pc.toLowerCase()];
|
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
|
||||||
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
|
||||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
export const midiToFreq = (n) => {
|
export const fromMidi = (n) => {
|
||||||
return Math.pow(2, (n - 69) / 12) * 440;
|
return Math.pow(2, (n - 69) / 12) * 440;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,7 +45,7 @@ export const valueToMidi = (value, fallbackValue) => {
|
|||||||
return freqToMidi(freq);
|
return freqToMidi(freq);
|
||||||
}
|
}
|
||||||
if (typeof note === 'string') {
|
if (typeof note === 'string') {
|
||||||
return noteToMidi(note);
|
return toMidi(note);
|
||||||
}
|
}
|
||||||
if (typeof note === 'number') {
|
if (typeof note === 'number') {
|
||||||
return note;
|
return note;
|
||||||
@@ -65,19 +62,18 @@ export const valueToMidi = (value, fallbackValue) => {
|
|||||||
*/
|
*/
|
||||||
export const getFreq = (noteOrMidi) => {
|
export const getFreq = (noteOrMidi) => {
|
||||||
if (typeof noteOrMidi === 'number') {
|
if (typeof noteOrMidi === 'number') {
|
||||||
return midiToFreq(noteOrMidi);
|
return fromMidi(noteOrMidi);
|
||||||
}
|
}
|
||||||
return midiToFreq(noteToMidi(noteOrMidi));
|
return fromMidi(toMidi(noteOrMidi));
|
||||||
};
|
};
|
||||||
|
|
||||||
const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
|
||||||
/**
|
/**
|
||||||
* @deprecated only used in workshop (first-notes)
|
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
export const midi2note = (n) => {
|
export const midi2note = (n) => {
|
||||||
const oct = Math.floor(n / 12) - 1;
|
const oct = Math.floor(n / 12) - 1;
|
||||||
const pc = pcs[n % 12];
|
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
|
||||||
return pc + oct;
|
return pc + oct;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,7 +91,7 @@ export const getPlayableNoteValue = (hap) => {
|
|||||||
}
|
}
|
||||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||||
note = midiToFreq(hap.value);
|
note = fromMidi(hap.value);
|
||||||
} else if (typeof note === 'number' && context.type === 'frequency') {
|
} else if (typeof note === 'number' && context.type === 'frequency') {
|
||||||
note = hap.value; // legacy workaround.. will be removed in the future
|
note = hap.value; // legacy workaround.. will be removed in the future
|
||||||
} else if (typeof note !== 'string' || !isNote(note)) {
|
} else if (typeof note !== 'string' || !isNote(note)) {
|
||||||
@@ -114,9 +110,9 @@ export const getFrequency = (hap) => {
|
|||||||
return getFreq(value.note || value.n || value.value);
|
return getFreq(value.note || value.n || value.value);
|
||||||
}
|
}
|
||||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||||
value = midiToFreq(hap.value);
|
value = fromMidi(hap.value);
|
||||||
} else if (typeof value === 'string' && isNote(value)) {
|
} else if (typeof value === 'string' && isNote(value)) {
|
||||||
value = midiToFreq(noteToMidi(hap.value));
|
value = fromMidi(toMidi(hap.value));
|
||||||
} else if (typeof value !== 'number') {
|
} else if (typeof value !== 'number') {
|
||||||
throw new Error('not a note or frequency: ' + value);
|
throw new Error('not a note or frequency: ' + value);
|
||||||
}
|
}
|
||||||
@@ -143,7 +139,7 @@ export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
|
|||||||
export const flatten = (arr) => [].concat(...arr);
|
export const flatten = (arr) => [].concat(...arr);
|
||||||
|
|
||||||
export const id = (a) => a;
|
export const id = (a) => a;
|
||||||
export const constant = (a, b) => a;
|
export const constant = curry((a, b) => a);
|
||||||
|
|
||||||
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
||||||
|
|
||||||
@@ -174,7 +170,7 @@ export function parseNumeral(numOrString) {
|
|||||||
return asNumber;
|
return asNumber;
|
||||||
}
|
}
|
||||||
if (isNote(numOrString)) {
|
if (isNote(numOrString)) {
|
||||||
return noteToMidi(numOrString);
|
return toMidi(numOrString);
|
||||||
}
|
}
|
||||||
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
||||||
}
|
}
|
||||||
@@ -210,67 +206,3 @@ export function parseFractional(numOrString) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
|
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]));
|
|
||||||
|
|
||||||
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
|
||||||
|
|
||||||
/* solmization, not used yet */
|
|
||||||
const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/
|
|
||||||
const indian = [
|
|
||||||
'Sa',
|
|
||||||
'Re',
|
|
||||||
'Ga',
|
|
||||||
'Ma',
|
|
||||||
'Pa',
|
|
||||||
'Dha',
|
|
||||||
'Ni',
|
|
||||||
]; /*indian musical notes, seems like they do not use flats or sharps*/
|
|
||||||
const german = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Hb', 'H']; /*german & dutch musical notes*/
|
|
||||||
const byzantine = [
|
|
||||||
'Ni',
|
|
||||||
'Pab',
|
|
||||||
'Pa',
|
|
||||||
'Voub',
|
|
||||||
'Vou',
|
|
||||||
'Ga',
|
|
||||||
'Dib',
|
|
||||||
'Di',
|
|
||||||
'Keb',
|
|
||||||
'Ke',
|
|
||||||
'Zob',
|
|
||||||
'Zo',
|
|
||||||
]; /*byzantine musical notes*/
|
|
||||||
const japanese = [
|
|
||||||
'I',
|
|
||||||
'Ro',
|
|
||||||
'Ha',
|
|
||||||
'Ni',
|
|
||||||
'Ho',
|
|
||||||
'He',
|
|
||||||
'To',
|
|
||||||
]; /*traditional japanese musical notes, seems like they do not use falts or sharps*/
|
|
||||||
|
|
||||||
const english = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
|
||||||
|
|
||||||
export const sol2note = (n, notation = 'letters') => {
|
|
||||||
const pc =
|
|
||||||
notation === 'solfeggio'
|
|
||||||
? solfeggio /*check if its is any of the following*/
|
|
||||||
: notation === 'indian'
|
|
||||||
? indian
|
|
||||||
: notation === 'german'
|
|
||||||
? german
|
|
||||||
: notation === 'byzantine'
|
|
||||||
? byzantine
|
|
||||||
: notation === 'japanese'
|
|
||||||
? japanese
|
|
||||||
: english; /*if not use standard version*/
|
|
||||||
const note = pc[n % 12]; /*calculating the midi value to the note*/
|
|
||||||
const oct = Math.floor(n / 12) - 1;
|
|
||||||
return note + oct;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -44,6 +44,6 @@ function createClock(
|
|||||||
};
|
};
|
||||||
const getPhase = () => phase;
|
const getPhase = () => phase;
|
||||||
// setCallback
|
// setCallback
|
||||||
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
|
return { setDuration, start, stop, pause, duration, getPhase, minLatency };
|
||||||
}
|
}
|
||||||
export default createClock;
|
export default createClock;
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
logger('[csound] not loaded yet', 'warning');
|
logger('[csound] not loaded yet', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hap.ensureObjectValue();
|
if (typeof hap.value !== 'object') {
|
||||||
|
throw new Error('csound only support objects as hap values');
|
||||||
|
}
|
||||||
let { gain = 0.8 } = hap.value;
|
let { gain = 0.8 } = hap.value;
|
||||||
gain *= 0.2;
|
gain *= 0.2;
|
||||||
|
|
||||||
@@ -137,7 +139,7 @@ export async function loadOrc(url) {
|
|||||||
export const csoundm = register('csoundm', (instrument, pat) => {
|
export const csoundm = register('csoundm', (instrument, pat) => {
|
||||||
let p1 = instrument;
|
let p1 = instrument;
|
||||||
if (typeof instrument === 'string') {
|
if (typeof instrument === 'string') {
|
||||||
p1 = `"${instrument}"`;
|
p1 = `"{instrument}"`;
|
||||||
}
|
}
|
||||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||||
return pat.onTrigger((tidal_time, hap) => {
|
return pat.onTrigger((tidal_time, hap) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/csound",
|
"name": "@strudel.cycles/csound",
|
||||||
"version": "0.9.0",
|
"version": "0.6.2",
|
||||||
"description": "csound bindings for strudel",
|
"description": "csound bindings for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -32,11 +32,11 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@csound/browser": "6.18.7",
|
"@csound/browser": "6.18.5",
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*"
|
"@strudel.cycles/webaudio": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.3.3"
|
"vite": "^3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# @strudel/desktopbridge
|
|
||||||
|
|
||||||
This package contains utilities used to communicate with the Tauri backend
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { listen } from '@tauri-apps/api/event';
|
|
||||||
import { logger } from '../core/logger.mjs';
|
|
||||||
|
|
||||||
// listen for log events from the Tauri backend and log in the UI
|
|
||||||
await listen('log-event', (e) => {
|
|
||||||
if (e.payload == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { message, message_type } = e.payload;
|
|
||||||
logger(message, message_type);
|
|
||||||
});
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { Invoke } from './utils.mjs';
|
|
||||||
import { Pattern, noteToMidi } from '@strudel.cycles/core';
|
|
||||||
|
|
||||||
const ON_MESSAGE = 0x90;
|
|
||||||
const OFF_MESSAGE = 0x80;
|
|
||||||
const CC_MESSAGE = 0xb0;
|
|
||||||
|
|
||||||
Pattern.prototype.midi = function (output) {
|
|
||||||
return this.onTrigger((time, hap, currentTime) => {
|
|
||||||
const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
|
|
||||||
const offset = (time - currentTime) * 1000;
|
|
||||||
const velocity = Math.floor((hap.context?.velocity ?? 0.9) * 100); // TODO: refactor velocity
|
|
||||||
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
|
|
||||||
const roundedOffset = Math.round(offset);
|
|
||||||
const midichan = (hap.value.midichan ?? 1) - 1;
|
|
||||||
const requestedport = output ?? 'IAC';
|
|
||||||
const messagesfromjs = [];
|
|
||||||
if (note != null) {
|
|
||||||
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
|
||||||
messagesfromjs.push({
|
|
||||||
requestedport,
|
|
||||||
message: [ON_MESSAGE + midichan, midiNumber, velocity],
|
|
||||||
offset: roundedOffset,
|
|
||||||
});
|
|
||||||
messagesfromjs.push({
|
|
||||||
requestedport,
|
|
||||||
message: [OFF_MESSAGE + midichan, midiNumber, velocity],
|
|
||||||
offset: roundedOffset + duration,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
messagesfromjs.push({
|
|
||||||
requestedport,
|
|
||||||
message: [CC_MESSAGE + midichan, ccn, scaled],
|
|
||||||
offset: roundedOffset,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// invoke is temporarily blocking, run in an async process
|
|
||||||
if (messagesfromjs.length) {
|
|
||||||
setTimeout(() => {
|
|
||||||
Invoke('sendmidi', { messagesfromjs });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { parseNumeral, Pattern } from '@strudel.cycles/core';
|
|
||||||
import { Invoke } from './utils.mjs';
|
|
||||||
|
|
||||||
Pattern.prototype.osc = function () {
|
|
||||||
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
|
|
||||||
hap.ensureObjectValue();
|
|
||||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
|
||||||
const delta = hap.duration.valueOf();
|
|
||||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
|
||||||
// make sure n and note are numbers
|
|
||||||
controls.n && (controls.n = parseNumeral(controls.n));
|
|
||||||
controls.note && (controls.note = parseNumeral(controls.note));
|
|
||||||
|
|
||||||
const params = [];
|
|
||||||
|
|
||||||
const timestamp = Math.round(Date.now() + (time - currentTime) * 1000);
|
|
||||||
|
|
||||||
Object.keys(controls).forEach((key) => {
|
|
||||||
const val = controls[key];
|
|
||||||
const value = typeof val === 'number' ? val.toString() : val;
|
|
||||||
|
|
||||||
if (value == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
params.push({
|
|
||||||
name: key,
|
|
||||||
value,
|
|
||||||
valueisnumber: typeof val === 'number',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const messagesfromjs = [];
|
|
||||||
if (params.length) {
|
|
||||||
messagesfromjs.push({ target: '/dirt/play', timestamp, params });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messagesfromjs.length) {
|
|
||||||
setTimeout(() => {
|
|
||||||
Invoke('sendosc', { messagesfromjs });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@strudel/desktopbridge",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"description": "tools/shims for communicating between the JS and Tauri (Rust) sides of the Studel desktop app",
|
|
||||||
"main": "index.mjs",
|
|
||||||
"type": "module",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"tidalcycles",
|
|
||||||
"strudel",
|
|
||||||
"pattern",
|
|
||||||
"livecoding",
|
|
||||||
"algorave"
|
|
||||||
],
|
|
||||||
"author": "Jade Rowland <jaderowlanddev@gmail.com>",
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@strudel.cycles/core": "workspace:*",
|
|
||||||
"@tauri-apps/api": "^1.4.0"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { invoke } from '@tauri-apps/api/tauri';
|
|
||||||
|
|
||||||
export const Invoke = invoke;
|
|
||||||
export const isTauri = () => window.__TAURI_IPC__ != null;
|
|
||||||
@@ -30,4 +30,5 @@ note(`[[e5 [b4 c5] d5 [c5 b4]]
|
|||||||
</strudel-repl>
|
</strudel-repl>
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that the Code is placed inside HTML comments to prevent the browser from treating it as HTML.
|
- Note that the Code is placed inside HTML comments to prevent the browser from treating it as HTML.
|
||||||
|
- [Play with this example on stackblitz](https://stackblitz.com/edit/js-75cvww?file=index.html)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class Strudel extends HTMLElement {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const code = (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
|
const code = (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
const src = `https://strudel.cc/#${encodeURIComponent(btoa(code))}`;
|
const src = `https://strudel.tidalcycles.org/#${encodeURIComponent(btoa(code))}`;
|
||||||
// const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`;
|
// const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`;
|
||||||
iframe.setAttribute('src', src);
|
iframe.setAttribute('src', src);
|
||||||
iframe.setAttribute('width', '600');
|
iframe.setAttribute('width', '600');
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
shift-parser
|
||||||
|
shift-reducer
|
||||||
|
!shift-traverser
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# @strudel.cycles/eval
|
||||||
|
|
||||||
|
This package contains the strudel code transformer and evaluator.
|
||||||
|
It allows creating strudel patterns from input code that is optimized for minimal keystrokes and human readability.
|
||||||
|
|
||||||
|
## Deprecation Note
|
||||||
|
|
||||||
|
This package will not be developed further. Consider using `@strudel.cycles/transpiler` as a replacement.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i @strudel.cycles/eval --save
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { evalScope } from '@strudel.cycles/core';
|
||||||
|
import { evaluate } from '@strudel.cycles/eval';
|
||||||
|
|
||||||
|
evalScope(
|
||||||
|
import('@strudel.cycles/core'),
|
||||||
|
// import other strudel packages here
|
||||||
|
); // add strudel to eval scope
|
||||||
|
|
||||||
|
async function run(code) {
|
||||||
|
const { pattern } = await evaluate(code);
|
||||||
|
const events = pattern.firstCycle();
|
||||||
|
console.log(events.map((e) => e.show()).join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
run('sequence([a3, [b3, c4]])');
|
||||||
|
```
|
||||||
|
|
||||||
|
yields:
|
||||||
|
|
||||||
|
```js
|
||||||
|
(0/1 -> 1/2, 0/1 -> 1/2, a3)
|
||||||
|
(1/2 -> 3/4, 1/2 -> 3/4, b3)
|
||||||
|
(3/4 -> 1/1, 3/4 -> 1/1, c4)
|
||||||
|
```
|
||||||
|
|
||||||
|
[play with @strudel.cycles/eval on codesandbox](https://codesandbox.io/s/strudel-eval-example-ndz1d8?file=/src/index.js)
|
||||||
|
|
||||||
|
## Dev Notes
|
||||||
|
|
||||||
|
shift-traverser is currently monkey patched because its package.json uses estraverse@^4.2.0,
|
||||||
|
which does not support the spread operator (Error: Unknown node type SpreadProperty.).
|
||||||
|
By monkey patched, I mean I copied the source of shift-traverser to a subfolder and installed the dependencies (shift-spec + estraverse@^5.3.0)
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
index.mjs - <short description TODO>
|
evaluate.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/desktopbridge/index.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.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/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './midibridge.mjs';
|
import { evaluate as _evaluate } from '@strudel.cycles/core';
|
||||||
export * from './utils.mjs';
|
import shapeshifter from './shapeshifter.mjs';
|
||||||
export * from './oscbridge.mjs';
|
|
||||||
|
export const evaluate = async (code) => {
|
||||||
|
return _evaluate(code, shapeshifter);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './evaluate.mjs';
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/web",
|
"name": "@strudel.cycles/eval",
|
||||||
"version": "0.9.0",
|
"version": "0.6.2",
|
||||||
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
"description": "Code evaluator for strudel",
|
||||||
"main": "web.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.mjs"
|
"module": "dist/index.mjs"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"test": "vitest run",
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"directories": {
|
||||||
|
"test": "test"
|
||||||
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
@@ -24,9 +28,6 @@
|
|||||||
"algorave"
|
"algorave"
|
||||||
],
|
],
|
||||||
"author": "Felix Roos <flix91@gmail.com>",
|
"author": "Felix Roos <flix91@gmail.com>",
|
||||||
"contributors": [
|
|
||||||
"Alex McLean <alex@slab.org>"
|
|
||||||
],
|
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
@@ -34,12 +35,16 @@
|
|||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
"estraverse": "^5.3.0",
|
||||||
"@strudel.cycles/tonal": "workspace:*",
|
"shift-ast": "^7.0.0",
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
"shift-codegen": "^8.1.0",
|
||||||
"@strudel.cycles/webaudio": "workspace:*"
|
"shift-parser": "^8.0.0",
|
||||||
|
"shift-spec": "^2019.0.0",
|
||||||
|
"shift-traverser": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.3.3"
|
"@strudel.cycles/mini": "workspace:*",
|
||||||
|
"vite": "^3.2.2",
|
||||||
|
"vitest": "^0.25.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
/*
|
||||||
|
shapeshifter.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/shapeshifter.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 { parseScriptWithLocation } from './shift-parser/index.js'; // npm module does not work in the browser
|
||||||
|
import traverser from './shift-traverser/index.js'; // npm module does not work in the browser */
|
||||||
|
import { parseScriptWithLocation } from 'shift-parser';
|
||||||
|
import traverser from './shift-traverser/index.js';
|
||||||
|
const { replace } = traverser;
|
||||||
|
import {
|
||||||
|
LiteralStringExpression,
|
||||||
|
IdentifierExpression,
|
||||||
|
CallExpression,
|
||||||
|
StaticMemberExpression,
|
||||||
|
ReturnStatement,
|
||||||
|
ArrayExpression,
|
||||||
|
LiteralNumericExpression,
|
||||||
|
} from 'shift-ast';
|
||||||
|
import shiftCodegen from 'shift-codegen';
|
||||||
|
const codegen = shiftCodegen.default || shiftCodegen; // parcel module resolution fuckup
|
||||||
|
|
||||||
|
import * as strudel from '@strudel.cycles/core';
|
||||||
|
|
||||||
|
const { Pattern } = strudel;
|
||||||
|
|
||||||
|
const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
|
||||||
|
|
||||||
|
const addLocations = true;
|
||||||
|
export const addMiniLocations = true;
|
||||||
|
export const minifyStrings = true;
|
||||||
|
export const wrappedAsync = false; // this is now handled by core evaluate by default
|
||||||
|
export const shouldAddReturn = true;
|
||||||
|
|
||||||
|
export default (_code) => {
|
||||||
|
const { code, addReturn } = wrapAsync(_code);
|
||||||
|
const ast = parseScriptWithLocation(disguiseImports(code));
|
||||||
|
const artificialNodes = [];
|
||||||
|
const parents = [];
|
||||||
|
const shifted = replace(ast.tree, {
|
||||||
|
enter(node, parent) {
|
||||||
|
parents.push(parent);
|
||||||
|
const isSynthetic = parents.some((p) => artificialNodes.includes(p));
|
||||||
|
if (isSynthetic) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace template string `xxx` with mini(`xxx`)
|
||||||
|
if (minifyStrings && isBackTickString(node)) {
|
||||||
|
return minifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||||
|
}
|
||||||
|
// allows to use top level strings, which are normally directives... but we don't need directives
|
||||||
|
if (minifyStrings && node.directives?.length === 1 && !node.statements?.length) {
|
||||||
|
const str = new LiteralStringExpression({ value: node.directives[0].rawValue });
|
||||||
|
const wrapped = minifyWithLocation(str, node.directives[0], ast.locations, artificialNodes);
|
||||||
|
return { ...node, directives: [], statements: [wrapped] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace double quote string "xxx" with mini('xxx')
|
||||||
|
if (minifyStrings && isStringWithDoubleQuotes(node, ast.locations, code)) {
|
||||||
|
return minifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// operator overloading => still not done
|
||||||
|
const operators = {
|
||||||
|
'*': 'fast',
|
||||||
|
'/': 'slow',
|
||||||
|
'&': 'stack',
|
||||||
|
'&&': 'append',
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
node.type === 'BinaryExpression' &&
|
||||||
|
operators[node.operator] &&
|
||||||
|
['LiteralNumericExpression', 'LiteralStringExpression', 'IdentifierExpression'].includes(node.right?.type) &&
|
||||||
|
canBeOverloaded(node.left)
|
||||||
|
) {
|
||||||
|
let arg = node.left;
|
||||||
|
if (node.left.type === 'IdentifierExpression') {
|
||||||
|
arg = wrapFunction('reify', node.left);
|
||||||
|
}
|
||||||
|
return new CallExpression({
|
||||||
|
callee: new StaticMemberExpression({
|
||||||
|
property: operators[node.operator],
|
||||||
|
object: wrapFunction('reify', arg),
|
||||||
|
}),
|
||||||
|
arguments: [node.right],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMarkable = isPatternArg(parents) || hasModifierCall(parent);
|
||||||
|
// add to location to pure(x) calls
|
||||||
|
if (node.type === 'CallExpression' && node.callee.name === 'pure') {
|
||||||
|
const literal = node.arguments[0];
|
||||||
|
// const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
|
||||||
|
return reifyWithLocation(literal, node.arguments[0], ast.locations, artificialNodes);
|
||||||
|
}
|
||||||
|
// replace pseudo note variables
|
||||||
|
if (node.type === 'IdentifierExpression') {
|
||||||
|
if (isNote(node.name)) {
|
||||||
|
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
|
||||||
|
if (addLocations && isMarkable) {
|
||||||
|
return reifyWithLocation(new LiteralStringExpression({ value }), node, ast.locations, artificialNodes);
|
||||||
|
}
|
||||||
|
return new LiteralStringExpression({ value });
|
||||||
|
}
|
||||||
|
if (node.name === 'r') {
|
||||||
|
return new IdentifierExpression({ name: 'silence' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
addLocations &&
|
||||||
|
['LiteralStringExpression' /* , 'LiteralNumericExpression' */].includes(node.type) &&
|
||||||
|
isMarkable
|
||||||
|
) {
|
||||||
|
// TODO: to make LiteralNumericExpression work, we need to make sure we're not inside timeCat...
|
||||||
|
return reifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||||
|
}
|
||||||
|
if (addMiniLocations) {
|
||||||
|
return addMiniNotationLocations(node, ast.locations, artificialNodes);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
},
|
||||||
|
leave() {
|
||||||
|
parents.pop();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// add return to last statement (because it's wrapped in an async function artificially)
|
||||||
|
if (shouldAddReturn) {
|
||||||
|
addReturn(shifted);
|
||||||
|
}
|
||||||
|
const generated = undisguiseImports(codegen(shifted));
|
||||||
|
return generated;
|
||||||
|
};
|
||||||
|
|
||||||
|
// renames all import statements to "_mport" as Shift doesn't support dynamic import.
|
||||||
|
// there shouldn't be any side-effects from this as this change does not affect
|
||||||
|
// the syntax & will be undone by the equivalent replace in "undisguiseImports".
|
||||||
|
function disguiseImports(code) {
|
||||||
|
return code.replaceAll('import', '_mport'); // Must be the same length!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename the renamed import statements back to "import"
|
||||||
|
function undisguiseImports(code) {
|
||||||
|
return code.replaceAll('_mport', 'import');
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapAsync(code) {
|
||||||
|
// wrap code in async to make await work on top level => this will create 1 line offset to locations
|
||||||
|
// this is why line offset is -1 in getLocationObject calls below
|
||||||
|
if (wrappedAsync) {
|
||||||
|
code = `(async () => {
|
||||||
|
${code}
|
||||||
|
})()`;
|
||||||
|
}
|
||||||
|
const addReturn = (ast) => {
|
||||||
|
const body = wrappedAsync ? ast.statements[0].expression.callee.body : ast;
|
||||||
|
body.statements = body.statements
|
||||||
|
.slice(0, -1)
|
||||||
|
.concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]);
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
addReturn,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMiniNotationLocations(node, locations, artificialNodes) {
|
||||||
|
const miniFunctions = ['mini', 'm'];
|
||||||
|
// const isAlreadyWrapped = parent?.type === 'CallExpression' && parent.callee.name === 'withLocationOffset';
|
||||||
|
if (node.type === 'CallExpression' && miniFunctions.includes(node.callee.name)) {
|
||||||
|
// mini('c3')
|
||||||
|
if (node.arguments.length > 1) {
|
||||||
|
// TODO: transform mini(...args) to cat(...args.map(mini)) ?
|
||||||
|
console.warn('multi arg mini locations not supported yet...');
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
const str = node.arguments[0];
|
||||||
|
return minifyWithLocation(str, str, locations, artificialNodes);
|
||||||
|
}
|
||||||
|
if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property)) {
|
||||||
|
// 'c3'.mini or 'c3'.m
|
||||||
|
return minifyWithLocation(node.object, node, locations, artificialNodes);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapFunction(name, ...args) {
|
||||||
|
return new CallExpression({
|
||||||
|
callee: new IdentifierExpression({ name }),
|
||||||
|
arguments: args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBackTickString(node) {
|
||||||
|
return node.type === 'TemplateExpression' && node.elements.length === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStringWithDoubleQuotes(node, locations, code) {
|
||||||
|
if (node.type !== 'LiteralStringExpression') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const loc = locations.get(node);
|
||||||
|
const snippet = code.slice(loc.start.offset, loc.end.offset);
|
||||||
|
return snippet[0] === '"'; // we can trust the end is also ", as the parsing did not fail
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if the given parents belong to a pattern argument node
|
||||||
|
// this is used to check if a node should receive a location for highlighting
|
||||||
|
function isPatternArg(parents) {
|
||||||
|
if (!parents.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const ancestors = parents.slice(0, -1);
|
||||||
|
const parent = parents[parents.length - 1];
|
||||||
|
if (isPatternFactory(parent)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (parent?.type === 'ArrayExpression') {
|
||||||
|
return isPatternArg(ancestors);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasModifierCall(parent) {
|
||||||
|
// TODO: modifiers are more than composables, for example every is not composable but should be seen as modifier..
|
||||||
|
// need all prototypes of Pattern
|
||||||
|
return parent?.type === 'StaticMemberExpression';
|
||||||
|
// && Object.keys(Pattern.prototype.composable).includes(parent.property)
|
||||||
|
}
|
||||||
|
const factories = Object.keys(Pattern.prototype.factories).concat(['mini']);
|
||||||
|
|
||||||
|
function isPatternFactory(node) {
|
||||||
|
return node?.type === 'CallExpression' && factories.includes(node.callee.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function canBeOverloaded(node) {
|
||||||
|
return (node.type === 'IdentifierExpression' && isNote(node.name)) || isPatternFactory(node);
|
||||||
|
// TODO: support sequence(c3).transpose(3).x.y.z
|
||||||
|
}
|
||||||
|
|
||||||
|
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
|
||||||
|
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
|
||||||
|
function reifyWithLocation(literalNode, node, locations, artificialNodes) {
|
||||||
|
const args = getLocationArguments(node, locations);
|
||||||
|
const withLocation = new CallExpression({
|
||||||
|
callee: new StaticMemberExpression({
|
||||||
|
object: wrapFunction('reify', literalNode),
|
||||||
|
property: 'withLocation',
|
||||||
|
}),
|
||||||
|
arguments: args,
|
||||||
|
});
|
||||||
|
artificialNodes.push(withLocation);
|
||||||
|
return withLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
|
||||||
|
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
|
||||||
|
function minifyWithLocation(literalNode, node, locations, artificialNodes) {
|
||||||
|
const args = getLocationArguments(node, locations);
|
||||||
|
const wrapped = wrapFunction('mini', literalNode);
|
||||||
|
if (!addMiniLocations) {
|
||||||
|
artificialNodes.push(wrapped);
|
||||||
|
return wrapped;
|
||||||
|
}
|
||||||
|
const withLocation = new CallExpression({
|
||||||
|
callee: new StaticMemberExpression({
|
||||||
|
object: wrapped,
|
||||||
|
property: 'withMiniLocation',
|
||||||
|
}),
|
||||||
|
arguments: args,
|
||||||
|
});
|
||||||
|
artificialNodes.push(withLocation);
|
||||||
|
return withLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocationArguments(node, locations) {
|
||||||
|
const loc = locations.get(node);
|
||||||
|
const lineOffset = wrappedAsync ? -1 : 0;
|
||||||
|
return [
|
||||||
|
new ArrayExpression({
|
||||||
|
elements: [
|
||||||
|
new LiteralNumericExpression({ value: loc.start.line + lineOffset }), // the minus 1 assumes the code has been wrapped in async iife
|
||||||
|
new LiteralNumericExpression({ value: loc.start.column }),
|
||||||
|
new LiteralNumericExpression({ value: loc.start.offset }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
new ArrayExpression({
|
||||||
|
elements: [
|
||||||
|
new LiteralNumericExpression({ value: loc.end.line + lineOffset }), // the minus 1 assumes the code has been wrapped in async iife
|
||||||
|
new LiteralNumericExpression({ value: loc.end.column }),
|
||||||
|
new LiteralNumericExpression({ value: loc.end.offset }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
index.js - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/shift-traverser/index.js>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright (C) 2014 Yusuke Suzuki <utatane.tea@gmail.com>
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||||
|
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import _Spec from 'shift-spec';
|
||||||
|
const Spec = _Spec.default || _Spec; // parcel module resolution fuckup
|
||||||
|
// import { version } from '../package.json'
|
||||||
|
|
||||||
|
// Loading uncached estraverse for changing estraverse.Syntax.
|
||||||
|
import _estraverse from 'estraverse';
|
||||||
|
|
||||||
|
const estraverse = _estraverse.cloneEnvironment();
|
||||||
|
|
||||||
|
// Adjust estraverse members.
|
||||||
|
|
||||||
|
Object.keys(estraverse.Syntax)
|
||||||
|
.filter((key) => key !== 'Property')
|
||||||
|
.forEach((key) => {
|
||||||
|
delete estraverse.Syntax[key];
|
||||||
|
delete estraverse.VisitorKeys[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
estraverse.Syntax,
|
||||||
|
Object.keys(Spec).reduce((result, key) => {
|
||||||
|
result[key] = key;
|
||||||
|
return result;
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
estraverse.VisitorKeys,
|
||||||
|
Object.keys(Spec).reduce((result, key) => {
|
||||||
|
result[key] = Spec[key].fields.map((field) => field.name);
|
||||||
|
return result;
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// estraverse.version = version;
|
||||||
|
export default estraverse;
|
||||||
|
|
||||||
|
/* vim: set sw=4 ts=4 et tw=80 : */
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
evaluate.test.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/test/evaluate.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 { expect, describe, it } from 'vitest';
|
||||||
|
|
||||||
|
import { evaluate } from '../evaluate.mjs';
|
||||||
|
import { mini } from '@strudel.cycles/mini';
|
||||||
|
import * as strudel from '@strudel.cycles/core';
|
||||||
|
const { fastcat, evalScope } = strudel;
|
||||||
|
|
||||||
|
describe('evaluate', async () => {
|
||||||
|
await evalScope({ mini }, strudel);
|
||||||
|
const ev = async (code) => (await evaluate(code)).pattern.firstCycleValues;
|
||||||
|
it('Should evaluate strudel functions', async () => {
|
||||||
|
expect(await ev('pure("c3")')).toEqual(['c3']);
|
||||||
|
expect(await ev('cat("c3")')).toEqual(['c3']);
|
||||||
|
expect(await ev('fastcat("c3", "d3")')).toEqual(['c3', 'd3']);
|
||||||
|
expect(await ev('slowcat("c3", "d3")')).toEqual(['c3']);
|
||||||
|
});
|
||||||
|
it('Scope should be extendable', async () => {
|
||||||
|
await evalScope({ myFunction: (...x) => fastcat(...x) });
|
||||||
|
expect(await ev('myFunction("c3", "d3")')).toEqual(['c3', 'd3']);
|
||||||
|
});
|
||||||
|
it('Should evaluate simple double quoted mini notation', async () => {
|
||||||
|
expect(await ev('"c3"')).toEqual(['c3']);
|
||||||
|
expect(await ev('"c3 d3"')).toEqual(['c3', 'd3']);
|
||||||
|
expect(await ev('"<c3 d3>"')).toEqual(['c3']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
shapeshifter.test.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/test/shapeshifter.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 { describe, it, expect } from 'vitest';
|
||||||
|
import shapeshifter, { wrappedAsync } from '../shapeshifter.mjs';
|
||||||
|
|
||||||
|
describe('shapeshifter', () => {
|
||||||
|
it('Should shift simple double quote string', () => {
|
||||||
|
if (wrappedAsync) {
|
||||||
|
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
|
||||||
|
} else {
|
||||||
|
expect(shapeshifter('"c3"')).toEqual('return mini("c3").withMiniLocation([1,0,0],[1,4,4])');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (wrappedAsync) {
|
||||||
|
it('Should handle dynamic imports', () => {
|
||||||
|
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
|
||||||
|
'const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# @strudel/hydra
|
|
||||||
|
|
||||||
This package integrates [hydra-synth](https://www.npmjs.com/package/hydra-synth) into strudel.
|
|
||||||
|
|
||||||
## Usage in Strudel
|
|
||||||
|
|
||||||
This package is imported into strudel by default. To activate Hydra, place this code at the top of your code:
|
|
||||||
|
|
||||||
```js
|
|
||||||
await initHydra();
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can use hydra below!
|
|
||||||
|
|
||||||
## Usage via npm
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm i @strudel/hydra
|
|
||||||
```
|
|
||||||
|
|
||||||
Then add the import to your evalScope:
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { evalScope } from '@strudel.cycles/core';
|
|
||||||
|
|
||||||
evalScope(
|
|
||||||
import('@strudel/hydra')
|
|
||||||
)
|
|
||||||
```
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { getDrawContext } from '@strudel.cycles/core';
|
|
||||||
|
|
||||||
export async function initHydra() {
|
|
||||||
if (!document.getElementById('hydra-canvas')) {
|
|
||||||
const { canvas: testCanvas } = getDrawContext();
|
|
||||||
await import('https://unpkg.com/hydra-synth');
|
|
||||||
const hydraCanvas = testCanvas.cloneNode(true);
|
|
||||||
hydraCanvas.id = 'hydra-canvas';
|
|
||||||
testCanvas.after(hydraCanvas);
|
|
||||||
new Hydra({ canvas: hydraCanvas, detectAudio: false });
|
|
||||||
s0.init({ src: testCanvas });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const H = (p) => () => p.queryArc(getTime(), getTime())[0].value;
|
|
||||||
+66
-138
@@ -5,9 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _WebMidi from 'webmidi';
|
import * as _WebMidi from 'webmidi';
|
||||||
import { Pattern, isPattern, logger, ref } from '@strudel.cycles/core';
|
import { Pattern, isPattern, isNote, getPlayableNoteValue, logger } from '@strudel.cycles/core';
|
||||||
import { noteToMidi } from '@strudel.cycles/core';
|
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||||
import { Note } from 'webmidi';
|
|
||||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
export const { WebMidi } = _WebMidi;
|
export const { WebMidi } = _WebMidi;
|
||||||
|
|
||||||
@@ -15,28 +15,12 @@ function supportsMidi() {
|
|||||||
return typeof navigator.requestMIDIAccess === 'function';
|
return typeof navigator.requestMIDIAccess === 'function';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMidiDeviceNamesString(devices) {
|
|
||||||
return devices.map((o) => `'${o.name}'`).join(' | ');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function enableWebMidi(options = {}) {
|
export function enableWebMidi(options = {}) {
|
||||||
const { onReady, onConnected, onDisconnected, onEnabled } = options;
|
const { onReady, onConnected, onDisconnected } = options;
|
||||||
if (WebMidi.enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!supportsMidi()) {
|
if (!supportsMidi()) {
|
||||||
throw new Error('Your Browser does not support WebMIDI.');
|
throw new Error('Your Browser does not support WebMIDI.');
|
||||||
}
|
}
|
||||||
WebMidi.addListener('connected', () => {
|
|
||||||
onConnected?.(WebMidi);
|
|
||||||
});
|
|
||||||
WebMidi.addListener('enabled', () => {
|
|
||||||
onEnabled?.(WebMidi);
|
|
||||||
});
|
|
||||||
// Reacting when a device becomes unavailable
|
|
||||||
WebMidi.addListener('disconnected', (e) => {
|
|
||||||
onDisconnected?.(WebMidi, e);
|
|
||||||
});
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (WebMidi.enabled) {
|
if (WebMidi.enabled) {
|
||||||
// if already enabled, just resolve WebMidi
|
// if already enabled, just resolve WebMidi
|
||||||
@@ -47,49 +31,57 @@ export function enableWebMidi(options = {}) {
|
|||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
|
WebMidi.addListener('connected', (e) => {
|
||||||
|
onConnected?.(WebMidi);
|
||||||
|
});
|
||||||
|
// Reacting when a device becomes unavailable
|
||||||
|
WebMidi.addListener('disconnected', (e) => {
|
||||||
|
onDisconnected?.(WebMidi, e);
|
||||||
|
});
|
||||||
onReady?.(WebMidi);
|
onReady?.(WebMidi);
|
||||||
resolve(WebMidi);
|
resolve(WebMidi);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
|
||||||
|
const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||||
|
|
||||||
function getDevice(indexOrName, devices) {
|
let midiReady;
|
||||||
if (!devices.length) {
|
|
||||||
|
// output?: string | number, outputs: typeof WebMidi.outputs
|
||||||
|
function getDevice(output, outputs) {
|
||||||
|
if (!outputs.length) {
|
||||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||||
}
|
}
|
||||||
if (typeof indexOrName === 'number') {
|
if (typeof output === 'number') {
|
||||||
return devices[indexOrName];
|
return outputs[output];
|
||||||
}
|
}
|
||||||
const byName = (name) => devices.find((output) => output.name.includes(name));
|
if (typeof output === 'string') {
|
||||||
if (typeof indexOrName === 'string') {
|
return outputByName(output);
|
||||||
return byName(indexOrName);
|
|
||||||
}
|
}
|
||||||
// attempt to default to first IAC device if none is specified
|
return outputs[0];
|
||||||
const IACOutput = byName('IAC');
|
|
||||||
const device = IACOutput ?? devices[0];
|
|
||||||
if (!device) {
|
|
||||||
throw new Error(
|
|
||||||
`🔌 MIDI device '${device ? device : ''}' not found. Use one of ${getMidiDeviceNamesString(devices)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return IACOutput ?? devices[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// send start/stop messages to outputs when repl starts/stops
|
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||||
if (typeof window !== 'undefined') {
|
Pattern.prototype.midi = function (output, channel = 1) {
|
||||||
window.addEventListener('message', (e) => {
|
if (!supportsMidi()) {
|
||||||
if (!WebMidi?.enabled) {
|
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||||
return;
|
}
|
||||||
}
|
/* await */ enableWebMidi({
|
||||||
if (e.data === 'strudel-stop') {
|
onConnected: ({ outputs }) =>
|
||||||
WebMidi.outputs.forEach((output) => output.sendStop());
|
logger(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
||||||
}
|
onDisconnected: ({ outputs }) =>
|
||||||
// cannot start here, since we have no timing info, see sendStart below
|
logger(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
||||||
|
onReady: ({ outputs }) => {
|
||||||
|
const device = getDevice(output, outputs);
|
||||||
|
const otherOutputs = outputs
|
||||||
|
.filter((o) => o.name !== device.name)
|
||||||
|
.map((o) => `'${o.name}'`)
|
||||||
|
.join(' | ');
|
||||||
|
midiReady = true;
|
||||||
|
logger(`Midi connected! Using "${device.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
Pattern.prototype.midi = function (output) {
|
|
||||||
if (isPattern(output)) {
|
if (isPattern(output)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
||||||
@@ -97,96 +89,32 @@ Pattern.prototype.midi = function (output) {
|
|||||||
}')`,
|
}')`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return this.onTrigger((time, hap) => {
|
||||||
enableWebMidi({
|
let note = getPlayableNoteValue(hap);
|
||||||
onEnabled: ({ outputs }) => {
|
const velocity = hap.context?.velocity ?? 0.9;
|
||||||
const device = getDevice(output, outputs);
|
if (!isNote(note)) {
|
||||||
const otherOutputs = outputs.filter((o) => o.name !== device.name);
|
throw new Error('not a note: ' + note);
|
||||||
logger(
|
}
|
||||||
`Midi enabled! Using "${device.name}". ${
|
if (!midiReady) {
|
||||||
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onDisconnected: ({ outputs }) =>
|
|
||||||
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.onTrigger((time, hap, currentTime, cps) => {
|
|
||||||
if (!WebMidi.enabled) {
|
|
||||||
console.log('not enabled');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const device = getDevice(output, WebMidi.outputs);
|
const device = getDevice(output, WebMidi.outputs);
|
||||||
hap.ensureObjectValue();
|
if (!device) {
|
||||||
|
throw new Error(
|
||||||
const offset = (time - currentTime) * 1000;
|
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||||
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
|
.map((o) => `'${o.name}'`)
|
||||||
const timeOffsetString = `+${offset}`;
|
.join(' | ')}`,
|
||||||
|
);
|
||||||
// destructure value
|
|
||||||
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd } = hap.value;
|
|
||||||
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
|
|
||||||
|
|
||||||
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
|
|
||||||
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
|
|
||||||
if (note != null) {
|
|
||||||
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
|
||||||
const midiNote = new Note(midiNumber, { attack: velocity, duration });
|
|
||||||
device.playNote(midiNote, midichan, {
|
|
||||||
time: timeOffsetString,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (ccv !== undefined && ccn !== undefined) {
|
|
||||||
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: timeOffsetString });
|
|
||||||
}
|
|
||||||
if (hap.whole.begin + 0 === 0) {
|
|
||||||
// we need to start here because we have the timing info
|
|
||||||
device.sendStart({ time: timeOffsetString });
|
|
||||||
}
|
|
||||||
if (['clock', 'midiClock'].includes(midicmd)) {
|
|
||||||
device.sendClock({ time: timeOffsetString });
|
|
||||||
} else if (['start'].includes(midicmd)) {
|
|
||||||
device.sendStart({ time: timeOffsetString });
|
|
||||||
} else if (['stop'].includes(midicmd)) {
|
|
||||||
device.sendStop({ time: timeOffsetString });
|
|
||||||
} else if (['continue'].includes(midicmd)) {
|
|
||||||
device.sendContinue({ time: timeOffsetString });
|
|
||||||
}
|
}
|
||||||
|
// console.log('midi', value, output);
|
||||||
|
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,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let listeners = {};
|
|
||||||
const refs = {};
|
|
||||||
|
|
||||||
export async function midin(input) {
|
|
||||||
const initial = await enableWebMidi(); // only returns on first init
|
|
||||||
const device = getDevice(input, WebMidi.inputs);
|
|
||||||
|
|
||||||
if (initial) {
|
|
||||||
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
|
||||||
logger(
|
|
||||||
`Midi enabled! Using "${device.name}". ${
|
|
||||||
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
refs[input] = {};
|
|
||||||
}
|
|
||||||
const cc = (cc) => ref(() => refs[input][cc] || 0);
|
|
||||||
|
|
||||||
listeners[input] && device.removeListener('midimessage', listeners[input]);
|
|
||||||
listeners[input] = (e) => {
|
|
||||||
const cc = e.dataBytes[0];
|
|
||||||
const v = e.dataBytes[1];
|
|
||||||
refs[input] && (refs[input][cc] = v / 127);
|
|
||||||
};
|
|
||||||
device.addListener('midimessage', listeners[input]);
|
|
||||||
return cc;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/midi",
|
"name": "@strudel.cycles/midi",
|
||||||
"version": "0.9.0",
|
"version": "0.6.0",
|
||||||
"description": "Midi API for strudel",
|
"description": "Midi API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -31,9 +31,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
"webmidi": "^3.1.5"
|
"webmidi": "^3.0.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.3.3"
|
"vite": "^3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ yields:
|
|||||||
|
|
||||||
## Mini Notation API
|
## Mini Notation API
|
||||||
|
|
||||||
See "Mini Notation" in the [Strudel Tutorial](https://strudel.cc/learn/mini-notation)
|
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/learn/mini-notation)
|
||||||
|
|
||||||
## Building the Parser
|
## Building the Parser
|
||||||
|
|
||||||
@@ -40,5 +40,5 @@ The parser [krill-parser.js] is generated from [krill.pegjs](./krill.pegjs) usin
|
|||||||
To generate the parser, run
|
To generate the parser, run
|
||||||
|
|
||||||
```js
|
```js
|
||||||
npm run build:parser
|
npm build:parser
|
||||||
```
|
```
|
||||||
|
|||||||
+338
-559
File diff suppressed because one or more lines are too long
@@ -19,13 +19,10 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
this.location_ = location();
|
this.location_ = location();
|
||||||
}
|
}
|
||||||
|
|
||||||
var PatternStub = function(source, alignment, seed)
|
var PatternStub = function(source, alignment)
|
||||||
{
|
{
|
||||||
this.type_ = "pattern";
|
this.type_ = "pattern";
|
||||||
this.arguments_ = { alignment: alignment };
|
this.arguments_ = { alignment : alignment};
|
||||||
if (seed !== undefined) {
|
|
||||||
this.arguments_.seed = seed;
|
|
||||||
}
|
|
||||||
this.source_ = source;
|
this.source_ = source;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +48,6 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
this.options_ = options;
|
this.options_ = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
var seed = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start = statement
|
start = statement
|
||||||
@@ -79,9 +75,6 @@ frac
|
|||||||
int
|
int
|
||||||
= zero / (digit1_9 DIGIT*)
|
= zero / (digit1_9 DIGIT*)
|
||||||
|
|
||||||
intneg
|
|
||||||
= minus? int { return parseInt(text()); }
|
|
||||||
|
|
||||||
minus
|
minus
|
||||||
= "-"
|
= "-"
|
||||||
|
|
||||||
@@ -103,8 +96,7 @@ quote = '"' / "'"
|
|||||||
// ------------------ steps and cycles ---------------------------
|
// ------------------ steps and cycles ---------------------------
|
||||||
|
|
||||||
// single step definition (e.g bd)
|
// single step definition (e.g bd)
|
||||||
step_char "a letter, a number, \"-\", \"#\", \".\", \"^\", \"_\"" =
|
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
|
||||||
unicode_letter / [0-9~] / "-" / "#" / "." / "^" / "_"
|
|
||||||
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
|
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
|
||||||
|
|
||||||
// define a sub cycle e.g. [1 2, 3 [4]]
|
// define a sub cycle e.g. [1 2, 3 [4]]
|
||||||
@@ -127,7 +119,7 @@ slice = step / sub_cycle / polymeter / slow_sequence
|
|||||||
|
|
||||||
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
// 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
|
// 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 / op_tail / op_range
|
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade
|
||||||
|
|
||||||
op_weight = "@" a:number
|
op_weight = "@" a:number
|
||||||
{ return x => x.options_['weight'] = a }
|
{ return x => x.options_['weight'] = a }
|
||||||
@@ -145,13 +137,7 @@ op_fast = "*"a:slice
|
|||||||
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
|
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
|
||||||
|
|
||||||
op_degrade = "?"a:number?
|
op_degrade = "?"a:number?
|
||||||
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) }
|
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
|
||||||
|
|
||||||
op_tail = ":" s:slice
|
|
||||||
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
|
|
||||||
|
|
||||||
op_range = ".." s:slice
|
|
||||||
{ return x => x.options_['ops'].push({ type_: "range", arguments_ :{ element:s } }) }
|
|
||||||
|
|
||||||
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
||||||
slice_with_ops = s:slice ops:slice_op*
|
slice_with_ops = s:slice ops:slice_op*
|
||||||
@@ -173,12 +159,12 @@ stack_tail = tail:(comma @sequence)+
|
|||||||
// a choose is a series of pipe-separated sequence, one of which is
|
// a choose is a series of pipe-separated sequence, one of which is
|
||||||
// chosen at random, each cycle
|
// chosen at random, each cycle
|
||||||
choose_tail = tail:(pipe @sequence)+
|
choose_tail = tail:(pipe @sequence)+
|
||||||
{ return { alignment: 'rand', list: tail, seed: seed++ }; }
|
{ return { alignment: 'rand', list: tail }; }
|
||||||
|
|
||||||
// if the stack contains only one element, we don't create a stack but return the
|
// if the stack contains only one element, we don't create a stack but return the
|
||||||
// underlying element
|
// underlying element
|
||||||
stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
|
stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
|
||||||
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
|
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
|
||||||
|
|
||||||
polymeter_stack = head:sequence tail:stack_tail?
|
polymeter_stack = head:sequence tail:stack_tail?
|
||||||
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
|
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
|
||||||
@@ -189,7 +175,7 @@ polymeter_stack = head:sequence tail:stack_tail?
|
|||||||
// Experimental haskellish parser begins
|
// Experimental haskellish parser begins
|
||||||
|
|
||||||
// mini-notation = a quoted stack
|
// mini-notation = a quoted stack
|
||||||
mini = ws quote ws sc:stack_or_choose ws quote
|
mini = ws quote sc:stack_or_choose quote
|
||||||
{ return sc; }
|
{ return sc; }
|
||||||
|
|
||||||
// ------------------ operators ---------------------------
|
// ------------------ operators ---------------------------
|
||||||
@@ -266,25 +252,3 @@ hush = "hush"
|
|||||||
// ---------------------- statements ----------------------------
|
// ---------------------- statements ----------------------------
|
||||||
|
|
||||||
statement = mini_definition / command
|
statement = mini_definition / command
|
||||||
|
|
||||||
// ---------------------- unicode ----------------------------
|
|
||||||
|
|
||||||
unicode_letter = Lu / Ll / Lt / Lm / Lo / Nl
|
|
||||||
|
|
||||||
// Letter, Lowercase
|
|
||||||
Ll = [\u0061-\u007A\u00B5\u00DF-\u00F6\u00F8-\u00FF\u0101\u0103\u0105\u0107\u0109\u010B\u010D\u010F\u0111\u0113\u0115\u0117\u0119\u011B\u011D\u011F\u0121\u0123\u0125\u0127\u0129\u012B\u012D\u012F\u0131\u0133\u0135\u0137-\u0138\u013A\u013C\u013E\u0140\u0142\u0144\u0146\u0148-\u0149\u014B\u014D\u014F\u0151\u0153\u0155\u0157\u0159\u015B\u015D\u015F\u0161\u0163\u0165\u0167\u0169\u016B\u016D\u016F\u0171\u0173\u0175\u0177\u017A\u017C\u017E-\u0180\u0183\u0185\u0188\u018C-\u018D\u0192\u0195\u0199-\u019B\u019E\u01A1\u01A3\u01A5\u01A8\u01AA-\u01AB\u01AD\u01B0\u01B4\u01B6\u01B9-\u01BA\u01BD-\u01BF\u01C6\u01C9\u01CC\u01CE\u01D0\u01D2\u01D4\u01D6\u01D8\u01DA\u01DC-\u01DD\u01DF\u01E1\u01E3\u01E5\u01E7\u01E9\u01EB\u01ED\u01EF-\u01F0\u01F3\u01F5\u01F9\u01FB\u01FD\u01FF\u0201\u0203\u0205\u0207\u0209\u020B\u020D\u020F\u0211\u0213\u0215\u0217\u0219\u021B\u021D\u021F\u0221\u0223\u0225\u0227\u0229\u022B\u022D\u022F\u0231\u0233-\u0239\u023C\u023F-\u0240\u0242\u0247\u0249\u024B\u024D\u024F-\u0293\u0295-\u02AF\u0371\u0373\u0377\u037B-\u037D\u0390\u03AC-\u03CE\u03D0-\u03D1\u03D5-\u03D7\u03D9\u03DB\u03DD\u03DF\u03E1\u03E3\u03E5\u03E7\u03E9\u03EB\u03ED\u03EF-\u03F3\u03F5\u03F8\u03FB-\u03FC\u0430-\u045F\u0461\u0463\u0465\u0467\u0469\u046B\u046D\u046F\u0471\u0473\u0475\u0477\u0479\u047B\u047D\u047F\u0481\u048B\u048D\u048F\u0491\u0493\u0495\u0497\u0499\u049B\u049D\u049F\u04A1\u04A3\u04A5\u04A7\u04A9\u04AB\u04AD\u04AF\u04B1\u04B3\u04B5\u04B7\u04B9\u04BB\u04BD\u04BF\u04C2\u04C4\u04C6\u04C8\u04CA\u04CC\u04CE-\u04CF\u04D1\u04D3\u04D5\u04D7\u04D9\u04DB\u04DD\u04DF\u04E1\u04E3\u04E5\u04E7\u04E9\u04EB\u04ED\u04EF\u04F1\u04F3\u04F5\u04F7\u04F9\u04FB\u04FD\u04FF\u0501\u0503\u0505\u0507\u0509\u050B\u050D\u050F\u0511\u0513\u0515\u0517\u0519\u051B\u051D\u051F\u0521\u0523\u0525\u0527\u0529\u052B\u052D\u052F\u0560-\u0588\u10D0-\u10FA\u10FD-\u10FF\u13F8-\u13FD\u1C80-\u1C88\u1D00-\u1D2B\u1D6B-\u1D77\u1D79-\u1D9A\u1E01\u1E03\u1E05\u1E07\u1E09\u1E0B\u1E0D\u1E0F\u1E11\u1E13\u1E15\u1E17\u1E19\u1E1B\u1E1D\u1E1F\u1E21\u1E23\u1E25\u1E27\u1E29\u1E2B\u1E2D\u1E2F\u1E31\u1E33\u1E35\u1E37\u1E39\u1E3B\u1E3D\u1E3F\u1E41\u1E43\u1E45\u1E47\u1E49\u1E4B\u1E4D\u1E4F\u1E51\u1E53\u1E55\u1E57\u1E59\u1E5B\u1E5D\u1E5F\u1E61\u1E63\u1E65\u1E67\u1E69\u1E6B\u1E6D\u1E6F\u1E71\u1E73\u1E75\u1E77\u1E79\u1E7B\u1E7D\u1E7F\u1E81\u1E83\u1E85\u1E87\u1E89\u1E8B\u1E8D\u1E8F\u1E91\u1E93\u1E95-\u1E9D\u1E9F\u1EA1\u1EA3\u1EA5\u1EA7\u1EA9\u1EAB\u1EAD\u1EAF\u1EB1\u1EB3\u1EB5\u1EB7\u1EB9\u1EBB\u1EBD\u1EBF\u1EC1\u1EC3\u1EC5\u1EC7\u1EC9\u1ECB\u1ECD\u1ECF\u1ED1\u1ED3\u1ED5\u1ED7\u1ED9\u1EDB\u1EDD\u1EDF\u1EE1\u1EE3\u1EE5\u1EE7\u1EE9\u1EEB\u1EED\u1EEF\u1EF1\u1EF3\u1EF5\u1EF7\u1EF9\u1EFB\u1EFD\u1EFF-\u1F07\u1F10-\u1F15\u1F20-\u1F27\u1F30-\u1F37\u1F40-\u1F45\u1F50-\u1F57\u1F60-\u1F67\u1F70-\u1F7D\u1F80-\u1F87\u1F90-\u1F97\u1FA0-\u1FA7\u1FB0-\u1FB4\u1FB6-\u1FB7\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FC7\u1FD0-\u1FD3\u1FD6-\u1FD7\u1FE0-\u1FE7\u1FF2-\u1FF4\u1FF6-\u1FF7\u210A\u210E-\u210F\u2113\u212F\u2134\u2139\u213C-\u213D\u2146-\u2149\u214E\u2184\u2C30-\u2C5E\u2C61\u2C65-\u2C66\u2C68\u2C6A\u2C6C\u2C71\u2C73-\u2C74\u2C76-\u2C7B\u2C81\u2C83\u2C85\u2C87\u2C89\u2C8B\u2C8D\u2C8F\u2C91\u2C93\u2C95\u2C97\u2C99\u2C9B\u2C9D\u2C9F\u2CA1\u2CA3\u2CA5\u2CA7\u2CA9\u2CAB\u2CAD\u2CAF\u2CB1\u2CB3\u2CB5\u2CB7\u2CB9\u2CBB\u2CBD\u2CBF\u2CC1\u2CC3\u2CC5\u2CC7\u2CC9\u2CCB\u2CCD\u2CCF\u2CD1\u2CD3\u2CD5\u2CD7\u2CD9\u2CDB\u2CDD\u2CDF\u2CE1\u2CE3-\u2CE4\u2CEC\u2CEE\u2CF3\u2D00-\u2D25\u2D27\u2D2D\uA641\uA643\uA645\uA647\uA649\uA64B\uA64D\uA64F\uA651\uA653\uA655\uA657\uA659\uA65B\uA65D\uA65F\uA661\uA663\uA665\uA667\uA669\uA66B\uA66D\uA681\uA683\uA685\uA687\uA689\uA68B\uA68D\uA68F\uA691\uA693\uA695\uA697\uA699\uA69B\uA723\uA725\uA727\uA729\uA72B\uA72D\uA72F-\uA731\uA733\uA735\uA737\uA739\uA73B\uA73D\uA73F\uA741\uA743\uA745\uA747\uA749\uA74B\uA74D\uA74F\uA751\uA753\uA755\uA757\uA759\uA75B\uA75D\uA75F\uA761\uA763\uA765\uA767\uA769\uA76B\uA76D\uA76F\uA771-\uA778\uA77A\uA77C\uA77F\uA781\uA783\uA785\uA787\uA78C\uA78E\uA791\uA793-\uA795\uA797\uA799\uA79B\uA79D\uA79F\uA7A1\uA7A3\uA7A5\uA7A7\uA7A9\uA7AF\uA7B5\uA7B7\uA7B9\uA7FA\uAB30-\uAB5A\uAB60-\uAB65\uAB70-\uABBF\uFB00-\uFB06\uFB13-\uFB17\uFF41-\uFF5A]
|
|
||||||
|
|
||||||
// Letter, Modifier
|
|
||||||
Lm = [\u02B0-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0374\u037A\u0559\u0640\u06E5-\u06E6\u07F4-\u07F5\u07FA\u081A\u0824\u0828\u0971\u0E46\u0EC6\u10FC\u17D7\u1843\u1AA7\u1C78-\u1C7D\u1D2C-\u1D6A\u1D78\u1D9B-\u1DBF\u2071\u207F\u2090-\u209C\u2C7C-\u2C7D\u2D6F\u2E2F\u3005\u3031-\u3035\u303B\u309D-\u309E\u30FC-\u30FE\uA015\uA4F8-\uA4FD\uA60C\uA67F\uA69C-\uA69D\uA717-\uA71F\uA770\uA788\uA7F8-\uA7F9\uA9CF\uA9E6\uAA70\uAADD\uAAF3-\uAAF4\uAB5C-\uAB5F\uFF70\uFF9E-\uFF9F]
|
|
||||||
|
|
||||||
// Letter, Other
|
|
||||||
Lo = [\u00AA\u00BA\u01BB\u01C0-\u01C3\u0294\u05D0-\u05EA\u05EF-\u05F2\u0620-\u063F\u0641-\u064A\u066E-\u066F\u0671-\u06D3\u06D5\u06EE-\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u0800-\u0815\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0972-\u0980\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC-\u09DD\u09DF-\u09E1\u09F0-\u09F1\u09FC\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33\u0A35-\u0A36\u0A38-\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0-\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32-\u0B33\u0B35-\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60-\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0-\u0CE1\u0CF1-\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32-\u0E33\u0E40-\u0E45\u0E81-\u0E82\u0E84\u0E87-\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA-\u0EAB\u0EAD-\u0EB0\u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065-\u1066\u106E-\u1070\u1075-\u1081\u108E\u1100-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17DC\u1820-\u1842\u1844-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE-\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C77\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5-\u1CF6\u2135-\u2138\u2D30-\u2D67\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3006\u303C\u3041-\u3096\u309F\u30A1-\u30FA\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEF\uA000-\uA014\uA016-\uA48C\uA4D0-\uA4F7\uA500-\uA60B\uA610-\uA61F\uA62A-\uA62B\uA66E\uA6A0-\uA6E5\uA78F\uA7F7\uA7FB-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD-\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9E0-\uA9E4\uA9E7-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA6F\uAA71-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5-\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADC\uAAE0-\uAAEA\uAAF2\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF66-\uFF6F\uFF71-\uFF9D\uFFA0-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]
|
|
||||||
|
|
||||||
// Letter, Titlecase
|
|
||||||
Lt = [\u01C5\u01C8\u01CB\u01F2\u1F88-\u1F8F\u1F98-\u1F9F\u1FA8-\u1FAF\u1FBC\u1FCC\u1FFC]
|
|
||||||
|
|
||||||
// Letter, Uppercase
|
|
||||||
Lu = [\u0041-\u005A\u00C0-\u00D6\u00D8-\u00DE\u0100\u0102\u0104\u0106\u0108\u010A\u010C\u010E\u0110\u0112\u0114\u0116\u0118\u011A\u011C\u011E\u0120\u0122\u0124\u0126\u0128\u012A\u012C\u012E\u0130\u0132\u0134\u0136\u0139\u013B\u013D\u013F\u0141\u0143\u0145\u0147\u014A\u014C\u014E\u0150\u0152\u0154\u0156\u0158\u015A\u015C\u015E\u0160\u0162\u0164\u0166\u0168\u016A\u016C\u016E\u0170\u0172\u0174\u0176\u0178-\u0179\u017B\u017D\u0181-\u0182\u0184\u0186-\u0187\u0189-\u018B\u018E-\u0191\u0193-\u0194\u0196-\u0198\u019C-\u019D\u019F-\u01A0\u01A2\u01A4\u01A6-\u01A7\u01A9\u01AC\u01AE-\u01AF\u01B1-\u01B3\u01B5\u01B7-\u01B8\u01BC\u01C4\u01C7\u01CA\u01CD\u01CF\u01D1\u01D3\u01D5\u01D7\u01D9\u01DB\u01DE\u01E0\u01E2\u01E4\u01E6\u01E8\u01EA\u01EC\u01EE\u01F1\u01F4\u01F6-\u01F8\u01FA\u01FC\u01FE\u0200\u0202\u0204\u0206\u0208\u020A\u020C\u020E\u0210\u0212\u0214\u0216\u0218\u021A\u021C\u021E\u0220\u0222\u0224\u0226\u0228\u022A\u022C\u022E\u0230\u0232\u023A-\u023B\u023D-\u023E\u0241\u0243-\u0246\u0248\u024A\u024C\u024E\u0370\u0372\u0376\u037F\u0386\u0388-\u038A\u038C\u038E-\u038F\u0391-\u03A1\u03A3-\u03AB\u03CF\u03D2-\u03D4\u03D8\u03DA\u03DC\u03DE\u03E0\u03E2\u03E4\u03E6\u03E8\u03EA\u03EC\u03EE\u03F4\u03F7\u03F9-\u03FA\u03FD-\u042F\u0460\u0462\u0464\u0466\u0468\u046A\u046C\u046E\u0470\u0472\u0474\u0476\u0478\u047A\u047C\u047E\u0480\u048A\u048C\u048E\u0490\u0492\u0494\u0496\u0498\u049A\u049C\u049E\u04A0\u04A2\u04A4\u04A6\u04A8\u04AA\u04AC\u04AE\u04B0\u04B2\u04B4\u04B6\u04B8\u04BA\u04BC\u04BE\u04C0-\u04C1\u04C3\u04C5\u04C7\u04C9\u04CB\u04CD\u04D0\u04D2\u04D4\u04D6\u04D8\u04DA\u04DC\u04DE\u04E0\u04E2\u04E4\u04E6\u04E8\u04EA\u04EC\u04EE\u04F0\u04F2\u04F4\u04F6\u04F8\u04FA\u04FC\u04FE\u0500\u0502\u0504\u0506\u0508\u050A\u050C\u050E\u0510\u0512\u0514\u0516\u0518\u051A\u051C\u051E\u0520\u0522\u0524\u0526\u0528\u052A\u052C\u052E\u0531-\u0556\u10A0-\u10C5\u10C7\u10CD\u13A0-\u13F5\u1C90-\u1CBA\u1CBD-\u1CBF\u1E00\u1E02\u1E04\u1E06\u1E08\u1E0A\u1E0C\u1E0E\u1E10\u1E12\u1E14\u1E16\u1E18\u1E1A\u1E1C\u1E1E\u1E20\u1E22\u1E24\u1E26\u1E28\u1E2A\u1E2C\u1E2E\u1E30\u1E32\u1E34\u1E36\u1E38\u1E3A\u1E3C\u1E3E\u1E40\u1E42\u1E44\u1E46\u1E48\u1E4A\u1E4C\u1E4E\u1E50\u1E52\u1E54\u1E56\u1E58\u1E5A\u1E5C\u1E5E\u1E60\u1E62\u1E64\u1E66\u1E68\u1E6A\u1E6C\u1E6E\u1E70\u1E72\u1E74\u1E76\u1E78\u1E7A\u1E7C\u1E7E\u1E80\u1E82\u1E84\u1E86\u1E88\u1E8A\u1E8C\u1E8E\u1E90\u1E92\u1E94\u1E9E\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAA\u1EAC\u1EAE\u1EB0\u1EB2\u1EB4\u1EB6\u1EB8\u1EBA\u1EBC\u1EBE\u1EC0\u1EC2\u1EC4\u1EC6\u1EC8\u1ECA\u1ECC\u1ECE\u1ED0\u1ED2\u1ED4\u1ED6\u1ED8\u1EDA\u1EDC\u1EDE\u1EE0\u1EE2\u1EE4\u1EE6\u1EE8\u1EEA\u1EEC\u1EEE\u1EF0\u1EF2\u1EF4\u1EF6\u1EF8\u1EFA\u1EFC\u1EFE\u1F08-\u1F0F\u1F18-\u1F1D\u1F28-\u1F2F\u1F38-\u1F3F\u1F48-\u1F4D\u1F59\u1F5B\u1F5D\u1F5F\u1F68-\u1F6F\u1FB8-\u1FBB\u1FC8-\u1FCB\u1FD8-\u1FDB\u1FE8-\u1FEC\u1FF8-\u1FFB\u2102\u2107\u210B-\u210D\u2110-\u2112\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u2130-\u2133\u213E-\u213F\u2145\u2183\u2C00-\u2C2E\u2C60\u2C62-\u2C64\u2C67\u2C69\u2C6B\u2C6D-\u2C70\u2C72\u2C75\u2C7E-\u2C80\u2C82\u2C84\u2C86\u2C88\u2C8A\u2C8C\u2C8E\u2C90\u2C92\u2C94\u2C96\u2C98\u2C9A\u2C9C\u2C9E\u2CA0\u2CA2\u2CA4\u2CA6\u2CA8\u2CAA\u2CAC\u2CAE\u2CB0\u2CB2\u2CB4\u2CB6\u2CB8\u2CBA\u2CBC\u2CBE\u2CC0\u2CC2\u2CC4\u2CC6\u2CC8\u2CCA\u2CCC\u2CCE\u2CD0\u2CD2\u2CD4\u2CD6\u2CD8\u2CDA\u2CDC\u2CDE\u2CE0\u2CE2\u2CEB\u2CED\u2CF2\uA640\uA642\uA644\uA646\uA648\uA64A\uA64C\uA64E\uA650\uA652\uA654\uA656\uA658\uA65A\uA65C\uA65E\uA660\uA662\uA664\uA666\uA668\uA66A\uA66C\uA680\uA682\uA684\uA686\uA688\uA68A\uA68C\uA68E\uA690\uA692\uA694\uA696\uA698\uA69A\uA722\uA724\uA726\uA728\uA72A\uA72C\uA72E\uA732\uA734\uA736\uA738\uA73A\uA73C\uA73E\uA740\uA742\uA744\uA746\uA748\uA74A\uA74C\uA74E\uA750\uA752\uA754\uA756\uA758\uA75A\uA75C\uA75E\uA760\uA762\uA764\uA766\uA768\uA76A\uA76C\uA76E\uA779\uA77B\uA77D-\uA77E\uA780\uA782\uA784\uA786\uA78B\uA78D\uA790\uA792\uA796\uA798\uA79A\uA79C\uA79E\uA7A0\uA7A2\uA7A4\uA7A6\uA7A8\uA7AA-\uA7AE\uA7B0-\uA7B4\uA7B6\uA7B8\uFF21-\uFF3A]
|
|
||||||
|
|
||||||
// Number, Letter
|
|
||||||
Nl = [\u16EE-\u16F0\u2160-\u2182\u2185-\u2188\u3007\u3021-\u3029\u3038-\u303A\uA6E6-\uA6EF]
|
|
||||||
+82
-94
@@ -7,13 +7,17 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import * as krill from './krill-parser.js';
|
import * as krill from './krill-parser.js';
|
||||||
import * as strudel from '@strudel.cycles/core';
|
import * as strudel from '@strudel.cycles/core';
|
||||||
|
|
||||||
const randOffset = 0.0003;
|
/* var _seedState = 0;
|
||||||
|
const randOffset = 0.0002;
|
||||||
|
|
||||||
const applyOptions = (parent, enter) => (pat, i) => {
|
function _nextSeed() {
|
||||||
|
return _seedState++;
|
||||||
|
} */
|
||||||
|
|
||||||
|
const applyOptions = (parent, code) => (pat, i) => {
|
||||||
const ast = parent.source_[i];
|
const ast = parent.source_[i];
|
||||||
const options = ast.options_;
|
const options = ast.options_;
|
||||||
const ops = options?.ops;
|
const ops = options?.ops;
|
||||||
|
|
||||||
if (ops) {
|
if (ops) {
|
||||||
for (const op of ops) {
|
for (const op of ops) {
|
||||||
switch (op.type_) {
|
switch (op.type_) {
|
||||||
@@ -23,37 +27,43 @@ const applyOptions = (parent, enter) => (pat, i) => {
|
|||||||
if (!legalTypes.includes(type)) {
|
if (!legalTypes.includes(type)) {
|
||||||
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
|
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
|
||||||
}
|
}
|
||||||
pat = strudel.reify(pat)[type](enter(amount));
|
pat = strudel.reify(pat)[type](patternifyAST(amount, code));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'bjorklund': {
|
case 'bjorklund': {
|
||||||
if (op.arguments_.rotation) {
|
if (op.arguments_.rotation) {
|
||||||
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
|
pat = pat.euclidRot(
|
||||||
|
patternifyAST(op.arguments_.pulse, code),
|
||||||
|
patternifyAST(op.arguments_.step, code),
|
||||||
|
patternifyAST(op.arguments_.rotation, code),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
pat = pat.euclid(enter(op.arguments_.pulse), enter(op.arguments_.step));
|
pat = pat.euclid(patternifyAST(op.arguments_.pulse, code), patternifyAST(op.arguments_.step, code));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'degradeBy': {
|
case 'degradeBy': {
|
||||||
pat = strudel
|
// TODO: find out what is right here
|
||||||
.reify(pat)
|
// example:
|
||||||
._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5);
|
/*
|
||||||
break;
|
stack(
|
||||||
}
|
s("hh*8").degrade(),
|
||||||
case 'tail': {
|
s("[ht*8]?")
|
||||||
const friend = enter(op.arguments_.element);
|
)
|
||||||
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
|
*/
|
||||||
break;
|
// above example will only be in sync when _degradeBy is used...
|
||||||
}
|
// it also seems that the nextSeed will create undeterministic behaviour
|
||||||
case 'range': {
|
// as it uses a global _seedState. This is probably the reason for
|
||||||
const friend = enter(op.arguments_.element);
|
// https://github.com/tidalcycles/strudel/issues/245
|
||||||
pat = strudel.reify(pat);
|
|
||||||
const arrayRange = (start, stop, step = 1) =>
|
// this is how it was:
|
||||||
Array.from({ length: Math.abs(stop - start) / step + 1 }, (value, index) =>
|
/*
|
||||||
start < stop ? start + index * step : start - index * step,
|
return strudel.reify(pat)._degradeByWith(
|
||||||
);
|
strudel.rand.early(randOffset * _nextSeed()).segment(1),
|
||||||
let range = (apat, bpat) => apat.squeezeBind((a) => bpat.bind((b) => strudel.fastcat(...arrayRange(a, b))));
|
op.arguments_.amount ?? 0.5,
|
||||||
pat = range(pat, friend);
|
);
|
||||||
|
*/
|
||||||
|
pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -79,14 +89,11 @@ function resolveReplications(ast) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
|
export function patternifyAST(ast, code) {
|
||||||
export function patternifyAST(ast, code, onEnter, offset = 0) {
|
|
||||||
onEnter?.(ast);
|
|
||||||
const enter = (node) => patternifyAST(node, code, onEnter, offset);
|
|
||||||
switch (ast.type_) {
|
switch (ast.type_) {
|
||||||
case 'pattern': {
|
case 'pattern': {
|
||||||
resolveReplications(ast);
|
resolveReplications(ast);
|
||||||
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
|
const children = ast.source_.map((child) => patternifyAST(child, code)).map(applyOptions(ast, code));
|
||||||
const alignment = ast.arguments_.alignment;
|
const alignment = ast.arguments_.alignment;
|
||||||
if (alignment === 'stack') {
|
if (alignment === 'stack') {
|
||||||
return strudel.stack(...children);
|
return strudel.stack(...children);
|
||||||
@@ -94,14 +101,16 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
|
|||||||
if (alignment === 'polymeter') {
|
if (alignment === 'polymeter') {
|
||||||
// polymeter
|
// polymeter
|
||||||
const stepsPerCycle = ast.arguments_.stepsPerCycle
|
const stepsPerCycle = ast.arguments_.stepsPerCycle
|
||||||
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
|
? patternifyAST(ast.arguments_.stepsPerCycle, code).fmap((x) => strudel.Fraction(x))
|
||||||
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
|
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
|
||||||
|
|
||||||
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
|
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
|
||||||
return strudel.stack(...aligned);
|
return strudel.stack(...aligned);
|
||||||
}
|
}
|
||||||
if (alignment === 'rand') {
|
if (alignment === 'rand') {
|
||||||
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
|
// https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422
|
||||||
|
// return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
|
||||||
|
return strudel.chooseCycles(...children);
|
||||||
}
|
}
|
||||||
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
||||||
if (!weightedChildren && alignment === 'slowcat') {
|
if (!weightedChildren && alignment === 'slowcat') {
|
||||||
@@ -121,7 +130,7 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
|
|||||||
return pat;
|
return pat;
|
||||||
}
|
}
|
||||||
case 'element': {
|
case 'element': {
|
||||||
return enter(ast.source_);
|
return patternifyAST(ast.source_, code);
|
||||||
}
|
}
|
||||||
case 'atom': {
|
case 'atom': {
|
||||||
if (ast.source_ === '~') {
|
if (ast.source_ === '~') {
|
||||||
@@ -131,82 +140,66 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
|
|||||||
console.warn('no location for', ast);
|
console.warn('no location for', ast);
|
||||||
return ast.source_;
|
return ast.source_;
|
||||||
}
|
}
|
||||||
|
const { start, end } = ast.location_;
|
||||||
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
|
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
|
||||||
if (offset === -1) {
|
// the following line expects the shapeshifter append .withMiniLocation
|
||||||
// skip location handling (used when getting leaves to avoid confusion)
|
// because location_ is only relative to the mini string, but we need it relative to whole code
|
||||||
return strudel.pure(value);
|
// make sure whitespaces are not part of the highlight:
|
||||||
}
|
const actual = code?.split('').slice(start.offset, end.offset).join('');
|
||||||
const [from, to] = getLeafLocation(code, ast, offset);
|
const [offsetStart = 0, offsetEnd = 0] = actual
|
||||||
return strudel.pure(value).withLoc(from, to);
|
? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length)
|
||||||
|
: [];
|
||||||
|
return strudel
|
||||||
|
.pure(value)
|
||||||
|
.withLocation(
|
||||||
|
[start.line, start.column + offsetStart, start.offset + offsetStart],
|
||||||
|
[start.line, end.column - offsetEnd, end.offset - offsetEnd],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
case 'stretch':
|
case 'stretch':
|
||||||
return enter(ast.source_).slow(enter(ast.arguments_.amount));
|
return patternifyAST(ast.source_, code).slow(patternifyAST(ast.arguments_.amount, code));
|
||||||
|
/* case 'scale':
|
||||||
|
let [tonic, scale] = Scale.tokenize(ast.arguments_.scale);
|
||||||
|
const intervals = Scale.get(scale).intervals;
|
||||||
|
const pattern = patternifyAST(ast.source_);
|
||||||
|
tonic = tonic || 'C4';
|
||||||
|
// console.log('scale', ast, pattern, tonic, scale);
|
||||||
|
console.log('tonic', tonic);
|
||||||
|
return pattern.fmap((step: any) => {
|
||||||
|
step = Number(step);
|
||||||
|
if (isNaN(step)) {
|
||||||
|
console.warn(`scale step "${step}" not a number`);
|
||||||
|
return step;
|
||||||
|
}
|
||||||
|
const octaves = Math.floor(step / intervals.length);
|
||||||
|
const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
|
||||||
|
const index = mod(step, intervals.length); // % with negative numbers. e.g. -1 % 3 = 2
|
||||||
|
const interval = Interval.add(intervals[index], Interval.fromSemitones(octaves * 12));
|
||||||
|
return Note.transpose(tonic, interval || '1P');
|
||||||
|
}); */
|
||||||
|
/* case 'struct':
|
||||||
|
// TODO:
|
||||||
|
return strudel.silence; */
|
||||||
default:
|
default:
|
||||||
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
|
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
|
||||||
return strudel.silence;
|
return strudel.silence;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// takes quoted mini string + leaf node within, returns source location of node (whitespace corrected)
|
|
||||||
export const getLeafLocation = (code, leaf, globalOffset = 0) => {
|
|
||||||
// value is expected without quotes!
|
|
||||||
const { start, end } = leaf.location_;
|
|
||||||
const actual = code?.split('').slice(start.offset, end.offset).join('');
|
|
||||||
// make sure whitespaces are not part of the highlight
|
|
||||||
const [offsetStart = 0, offsetEnd = 0] = actual
|
|
||||||
? actual.split(leaf.source_).map((p) => p.split('').filter((c) => c === ' ').length)
|
|
||||||
: [];
|
|
||||||
return [start.offset + offsetStart + globalOffset, end.offset - offsetEnd + globalOffset];
|
|
||||||
};
|
|
||||||
|
|
||||||
// takes quoted mini string, returns ast
|
|
||||||
export const mini2ast = (code) => krill.parse(code);
|
|
||||||
|
|
||||||
// takes quoted mini string, returns all nodes that are leaves
|
|
||||||
export const getLeaves = (code) => {
|
|
||||||
const ast = mini2ast(code);
|
|
||||||
let leaves = [];
|
|
||||||
patternifyAST(
|
|
||||||
ast,
|
|
||||||
code,
|
|
||||||
(node) => {
|
|
||||||
if (node.type_ === 'atom') {
|
|
||||||
leaves.push(node);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
return leaves;
|
|
||||||
};
|
|
||||||
|
|
||||||
// takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes
|
|
||||||
export const getLeafLocations = (code, offset = 0) => {
|
|
||||||
return getLeaves(code).map((l) => getLeafLocation(code, l, offset));
|
|
||||||
};
|
|
||||||
|
|
||||||
// mini notation only (wraps in "")
|
// mini notation only (wraps in "")
|
||||||
export const mini = (...strings) => {
|
export const mini = (...strings) => {
|
||||||
const pats = strings.map((str) => {
|
const pats = strings.map((str) => {
|
||||||
const code = `"${str}"`;
|
const code = `"${str}"`;
|
||||||
const ast = mini2ast(code);
|
const ast = krill.parse(code);
|
||||||
return patternifyAST(ast, code);
|
return patternifyAST(ast, code);
|
||||||
});
|
});
|
||||||
return strudel.sequence(...pats);
|
return strudel.sequence(...pats);
|
||||||
};
|
};
|
||||||
|
|
||||||
// turns str mini string (without quotes) into pattern
|
|
||||||
// offset is the position of the mini string in the JS code
|
|
||||||
// each leaf node will get .withLoc added
|
|
||||||
// this function is used by the transpiler for double quoted strings
|
|
||||||
export const m = (str, offset) => {
|
|
||||||
const code = `"${str}"`;
|
|
||||||
const ast = mini2ast(code);
|
|
||||||
return patternifyAST(ast, code, null, offset);
|
|
||||||
};
|
|
||||||
|
|
||||||
// includes haskell style (raw krill parsing)
|
// includes haskell style (raw krill parsing)
|
||||||
export const h = (string) => {
|
export const h = (string) => {
|
||||||
const ast = mini2ast(string);
|
const ast = krill.parse(string);
|
||||||
|
// console.log('ast', ast);
|
||||||
return patternifyAST(ast, string);
|
return patternifyAST(ast, string);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -216,8 +209,3 @@ export function minify(thing) {
|
|||||||
}
|
}
|
||||||
return strudel.reify(thing);
|
return strudel.reify(thing);
|
||||||
}
|
}
|
||||||
|
|
||||||
// calling this function will cause patterns to parse strings as mini notation by default
|
|
||||||
export function miniAllStrings() {
|
|
||||||
strudel.setStringParser(mini);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/mini",
|
"name": "@strudel.cycles/mini",
|
||||||
"version": "0.9.0",
|
"version": "0.6.0",
|
||||||
"description": "Mini notation for strudel",
|
"description": "Mini notation for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
"@strudel.cycles/core": "workspace:*"
|
"@strudel.cycles/core": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"peggy": "^3.0.2",
|
"peggy": "^2.0.1",
|
||||||
"vite": "^4.3.3",
|
"vite": "^3.2.2",
|
||||||
"vitest": "^0.33.0"
|
"vitest": "^0.25.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/>.
|
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 { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs';
|
import { mini } from '../mini.mjs';
|
||||||
import '@strudel.cycles/core/euclid.mjs';
|
import '@strudel.cycles/core/euclid.mjs';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@@ -140,22 +140,7 @@ describe('mini', () => {
|
|||||||
expect(haps.length < 230).toBe(true);
|
expect(haps.length < 230).toBe(true);
|
||||||
// 'Had too many cycles remaining after degradeBy 0.8');
|
// 'Had too many cycles remaining after degradeBy 0.8');
|
||||||
});
|
});
|
||||||
it('supports multiple independent uses of the random choice operator ("|")', () => {
|
/*it('supports the random choice operator ("|") with nesting', () => {
|
||||||
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 numCycles = 900;
|
||||||
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
|
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.
|
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
|
||||||
@@ -180,47 +165,6 @@ describe('mini', () => {
|
|||||||
// 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
|
// 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
|
||||||
// PRNG, this test should succeed
|
// PRNG, this test should succeed
|
||||||
expect(chisq <= 15.086).toBe(true);
|
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']);
|
|
||||||
});
|
|
||||||
it('supports ranges', () => {
|
|
||||||
expect(minV('0 .. 4')).toEqual([0, 1, 2, 3, 4]);
|
|
||||||
});
|
|
||||||
it('supports patterned ranges', () => {
|
|
||||||
expect(minS('[<0 1> .. <2 4>]*2')).toEqual(minS('[0 1 2] [1 2 3 4]'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getLeafLocation', () => {
|
|
||||||
it('gets location of leaf nodes', () => {
|
|
||||||
const code = '"bd sd"';
|
|
||||||
const ast = mini2ast(code);
|
|
||||||
|
|
||||||
const bd = ast.source_[0].source_;
|
|
||||||
expect(getLeafLocation(code, bd)).toEqual([1, 3]);
|
|
||||||
|
|
||||||
const sd = ast.source_[1].source_;
|
|
||||||
expect(getLeafLocation(code, sd)).toEqual([4, 6]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getLeafLocations', () => {
|
|
||||||
it('gets locations of leaf nodes', () => {
|
|
||||||
expect(getLeafLocations('"bd sd"')).toEqual([
|
|
||||||
[1, 3], // bd columns
|
|
||||||
[4, 6], // sd columns
|
|
||||||
]);
|
|
||||||
expect(getLeafLocations('"bd*2 [sd cp]"')).toEqual([
|
|
||||||
[1, 3], // bd columns
|
|
||||||
[7, 9], // sd columns
|
|
||||||
[10, 12], // cp columns
|
|
||||||
[4, 5], // "2" columns
|
|
||||||
]);
|
|
||||||
expect(getLeafLocations('"bd*<2 3>"')).toEqual([
|
|
||||||
[1, 3], // bd columns
|
|
||||||
[5, 6], // "2" columns
|
|
||||||
[7, 8], // "3" columns
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,6 +34,6 @@ Now open the REPL and type:
|
|||||||
s("<bd sd> hh").osc()
|
s("<bd sd> hh").osc()
|
||||||
```
|
```
|
||||||
|
|
||||||
or just [click here](https://strudel.cc/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
|
or just [click here](https://strudel.tidalcycles.org/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
|
||||||
|
|
||||||
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
|
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.tidalcycles.org/learn/input-output/#superdirt-api)
|
||||||
|
|||||||
+13
-11
@@ -5,7 +5,6 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import OSC from 'osc-js';
|
import OSC from 'osc-js';
|
||||||
|
|
||||||
import { logger, parseNumeral, Pattern } from '@strudel.cycles/core';
|
import { logger, parseNumeral, Pattern } from '@strudel.cycles/core';
|
||||||
|
|
||||||
let connection; // Promise<OSC>
|
let connection; // Promise<OSC>
|
||||||
@@ -34,31 +33,34 @@ function connect() {
|
|||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const latency = 0.1;
|
||||||
|
let startedAt = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
|
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
|
||||||
* For more info, read [MIDI & OSC in the docs](https://strudel.cc/learn/input-output)
|
* For more info, read [MIDI & OSC in the docs](https://strudel.tidalcycles.org/learn/input-output)
|
||||||
*
|
*
|
||||||
* @name osc
|
* @name osc
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.osc = function () {
|
Pattern.prototype.osc = async function () {
|
||||||
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
|
const osc = await connect();
|
||||||
hap.ensureObjectValue();
|
return this.onTrigger((time, hap, currentTime, cps = 1) => {
|
||||||
const osc = await connect();
|
|
||||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||||
const delta = hap.duration.valueOf();
|
const delta = hap.duration.valueOf();
|
||||||
|
// time should be audio time of onset
|
||||||
|
// currentTime should be current time of audio context (slightly before time)
|
||||||
|
if (startedAt < 0) {
|
||||||
|
startedAt = Date.now() - currentTime * 1000;
|
||||||
|
}
|
||||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||||
// make sure n and note are numbers
|
// make sure n and note are numbers
|
||||||
controls.n && (controls.n = parseNumeral(controls.n));
|
controls.n && (controls.n = parseNumeral(controls.n));
|
||||||
controls.note && (controls.note = parseNumeral(controls.note));
|
controls.note && (controls.note = parseNumeral(controls.note));
|
||||||
const keyvals = Object.entries(controls).flat();
|
const keyvals = Object.entries(controls).flat();
|
||||||
// time should be audio time of onset
|
const ts = Math.floor(startedAt + (time + latency) * 1000);
|
||||||
// currentTime should be current time of audio context (slightly before time)
|
|
||||||
const offset = (time - currentTime) * 1000;
|
|
||||||
// timestamp in milliseconds used to trigger the osc bundle at a precise moment
|
|
||||||
const ts = Math.floor(Date.now() + offset);
|
|
||||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||||
const bundle = new OSC.Bundle([message], ts);
|
const bundle = new OSC.Bundle([message], ts);
|
||||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/osc",
|
"name": "@strudel.cycles/osc",
|
||||||
"version": "0.9.0",
|
"version": "0.6.0",
|
||||||
"description": "OSC messaging for strudel",
|
"description": "OSC messaging for strudel",
|
||||||
"main": "osc.mjs",
|
"main": "osc.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"osc-js": "^2.4.0"
|
"osc-js": "^2.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pkg": "^5.8.1",
|
"pkg": "^5.7.0",
|
||||||
"vite": "^4.3.3"
|
"vite": "^3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-33
@@ -13,43 +13,24 @@ npm i @strudel.cycles/react
|
|||||||
Here is a minimal example of how to set up a MiniRepl:
|
Here is a minimal example of how to set up a MiniRepl:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
import * as React from 'react';
|
|
||||||
import '@strudel.cycles/react/dist/style.css';
|
|
||||||
import { MiniRepl } from '@strudel.cycles/react';
|
|
||||||
import { evalScope, controls } from '@strudel.cycles/core';
|
import { evalScope, controls } from '@strudel.cycles/core';
|
||||||
import { samples, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
import { MiniRepl } from '@strudel.cycles/react';
|
||||||
|
import { prebake } from '../repl/src/prebake.mjs';
|
||||||
|
|
||||||
async function prebake() {
|
evalScope(
|
||||||
await samples(
|
controls,
|
||||||
'https://strudel.cc/tidal-drum-machines.json',
|
import('@strudel.cycles/core'),
|
||||||
'github:ritchse/tidal-drum-machines/main/machines/'
|
import('@strudel.cycles/tonal'),
|
||||||
);
|
import('@strudel.cycles/mini'),
|
||||||
await samples(
|
import('@strudel.cycles/webaudio'),
|
||||||
'https://strudel.cc/EmuSP12.json',
|
/* probably import other strudel packages */
|
||||||
'https://strudel.cc/EmuSP12/'
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init() {
|
prebake();
|
||||||
await evalScope(
|
|
||||||
controls,
|
|
||||||
import('@strudel.cycles/core'),
|
|
||||||
import('@strudel.cycles/mini'),
|
|
||||||
import('@strudel.cycles/webaudio'),
|
|
||||||
import('@strudel.cycles/tonal')
|
|
||||||
);
|
|
||||||
await prebake();
|
|
||||||
initAudioOnFirstClick();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
export function Repl({ tune }) {
|
||||||
init();
|
return <MiniRepl tune={tune} hideOutsideView={true} />;
|
||||||
}
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return <MiniRepl tune={`s("bd sd,hh*4")`} />;
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- Open [example on stackblitz](https://stackblitz.com/edit/react-ts-saaair?file=tune.tsx,App.tsx)
|
For a more sophisticated example, check out the [nano-repl](./examples/nano-repl/)!
|
||||||
- Also check out the [nano-repl](./examples/nano-repl/) for a more sophisticated example
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
# 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/`
|
|
||||||
@@ -15,18 +15,17 @@
|
|||||||
"@strudel.cycles/osc": "workspace:*",
|
"@strudel.cycles/osc": "workspace:*",
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
"@strudel.cycles/mini": "workspace:*",
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
"@strudel.cycles/transpiler": "workspace:*",
|
||||||
"@strudel.cycles/soundfonts": "workspace:*",
|
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
"@strudel.cycles/tonal": "workspace:*",
|
"@strudel.cycles/tonal": "workspace:*",
|
||||||
"@strudel.cycles/react": "workspace:*"
|
"@strudel.cycles/react": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.0.17",
|
||||||
"@types/react-dom": "^18.2.1",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^2.0.1",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.8",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.16",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.1.8",
|
||||||
"vite": "^4.3.3"
|
"vite": "^3.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,22 @@
|
|||||||
import { controls, evalScope } from '@strudel.cycles/core';
|
import { controls, evalScope } from '@strudel.cycles/core';
|
||||||
import { CodeMirror, useHighlighting, useStrudel, flash } from '@strudel.cycles/react';
|
import { CodeMirror, useHighlighting, useKeydown, useStrudel, flash } from '@strudel.cycles/react';
|
||||||
import {
|
import { getAudioContext, initAudioOnFirstClick, panic, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||||
getAudioContext,
|
|
||||||
initAudioOnFirstClick,
|
|
||||||
panic,
|
|
||||||
webaudioOutput,
|
|
||||||
registerSynthSounds,
|
|
||||||
} from '@strudel.cycles/webaudio';
|
|
||||||
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
// import { prebake } from '../../../../../repl/src/prebake.mjs';
|
// import { prebake } from '../../../../../repl/src/prebake.mjs';
|
||||||
|
|
||||||
initAudioOnFirstClick();
|
initAudioOnFirstClick();
|
||||||
|
|
||||||
async function init() {
|
// TODO: only import stuff when play is pressed?
|
||||||
// TODO: only import stuff when play is pressed?
|
evalScope(
|
||||||
const loadModules = evalScope(
|
controls,
|
||||||
controls,
|
import('@strudel.cycles/core'),
|
||||||
import('@strudel.cycles/core'),
|
import('@strudel.cycles/tonal'),
|
||||||
import('@strudel.cycles/tonal'),
|
import('@strudel.cycles/mini'),
|
||||||
import('@strudel.cycles/mini'),
|
import('@strudel.cycles/xen'),
|
||||||
import('@strudel.cycles/xen'),
|
import('@strudel.cycles/webaudio'),
|
||||||
import('@strudel.cycles/webaudio'),
|
import('@strudel.cycles/osc'),
|
||||||
import('@strudel.cycles/osc'),
|
);
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
|
|
||||||
}
|
|
||||||
init();
|
|
||||||
|
|
||||||
const defaultTune = `samples({
|
const defaultTune = `samples({
|
||||||
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
|
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
|
||||||
@@ -43,7 +31,7 @@ stack(
|
|||||||
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
|
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
|
||||||
.add(perlin.range(0,.5)) // random pitch variation
|
.add(perlin.range(0,.5)) // random pitch variation
|
||||||
.superimpose(add(.05)) // add second, slightly detuned voice
|
.superimpose(add(.05)) // add second, slightly detuned voice
|
||||||
.note() // wrap in "note"
|
.n() // wrap in "n"
|
||||||
.decay(.15).sustain(0) // make each note of equal length
|
.decay(.15).sustain(0) // make each note of equal length
|
||||||
.s('sawtooth') // waveform
|
.s('sawtooth') // waveform
|
||||||
.gain(.4) // turn down
|
.gain(.4) // turn down
|
||||||
@@ -52,7 +40,7 @@ stack(
|
|||||||
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
|
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
|
||||||
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
||||||
.add(perlin.range(0,.5)) // random pitch variation
|
.add(perlin.range(0,.5)) // random pitch variation
|
||||||
.note() // wrap in "n"
|
.n() // wrap in "n"
|
||||||
.s('square') // waveform
|
.s('square') // waveform
|
||||||
.gain(.16) // turn down
|
.gain(.16) // turn down
|
||||||
.cutoff(500) // fixed cutoff
|
.cutoff(500) // fixed cutoff
|
||||||
@@ -61,7 +49,7 @@ stack(
|
|||||||
,"a4 c5 <e6 a6>".struct("x(5,8)")
|
,"a4 c5 <e6 a6>".struct("x(5,8)")
|
||||||
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
||||||
.add(perlin.range(0,.5)) // random pitch variation
|
.add(perlin.range(0,.5)) // random pitch variation
|
||||||
.note() // wrap in "note"
|
.n() // wrap in "n"
|
||||||
.decay(.1).sustain(0) // make notes short
|
.decay(.1).sustain(0) // make notes short
|
||||||
.s('triangle') // waveform
|
.s('triangle') // waveform
|
||||||
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
|
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
|
||||||
@@ -78,21 +66,46 @@ function App() {
|
|||||||
const [code, setCode] = useState(defaultTune);
|
const [code, setCode] = useState(defaultTune);
|
||||||
const [view, setView] = useState();
|
const [view, setView] = useState();
|
||||||
// const [code, setCode] = useState(`"c3".note().slow(2)`);
|
// const [code, setCode] = useState(`"c3".note().slow(2)`);
|
||||||
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern, started } = useStrudel({
|
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern } = useStrudel({
|
||||||
code,
|
code,
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: webaudioOutput,
|
||||||
getTime,
|
getTime,
|
||||||
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { setMiniLocations } = useHighlighting({
|
useHighlighting({
|
||||||
view,
|
view,
|
||||||
pattern,
|
pattern,
|
||||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
active: !activeCode?.includes('strudel disable-highlighting'),
|
||||||
getTime: () => scheduler.now(),
|
getTime: () => scheduler.getPhase(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const error = evalError || schedulerError;
|
const error = evalError || schedulerError;
|
||||||
|
useKeydown(
|
||||||
|
useCallback(
|
||||||
|
async (e) => {
|
||||||
|
if (e.ctrlKey || e.altKey) {
|
||||||
|
if (e.code === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
flash(view);
|
||||||
|
await evaluate(code);
|
||||||
|
if (e.shiftKey) {
|
||||||
|
panic();
|
||||||
|
scheduler.stop();
|
||||||
|
scheduler.start();
|
||||||
|
}
|
||||||
|
if (!scheduler.started) {
|
||||||
|
scheduler.start();
|
||||||
|
}
|
||||||
|
} else if (e.code === 'Period') {
|
||||||
|
scheduler.stop();
|
||||||
|
panic();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scheduler, evaluate, view],
|
||||||
|
),
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<nav className="z-[12] w-full flex justify-center fixed bottom-0">
|
<nav className="z-[12] w-full flex justify-center fixed bottom-0">
|
||||||
@@ -110,28 +123,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
{error && <p>error {error.message}</p>}
|
{error && <p>error {error.message}</p>}
|
||||||
</nav>
|
</nav>
|
||||||
<CodeMirror
|
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
|
||||||
value={code}
|
|
||||||
onChange={setCode}
|
|
||||||
onViewChanged={setView}
|
|
||||||
onEvaluate={async () => {
|
|
||||||
flash(view);
|
|
||||||
await evaluate(code);
|
|
||||||
if (!scheduler.started) {
|
|
||||||
scheduler.start();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onReEvaluate={async () => {
|
|
||||||
await evaluate(code);
|
|
||||||
panic();
|
|
||||||
scheduler.stop();
|
|
||||||
scheduler.start();
|
|
||||||
}}
|
|
||||||
onStop={() => {
|
|
||||||
scheduler.stop();
|
|
||||||
panic();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,6 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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 {
|
body {
|
||||||
background: #123;
|
background: #123;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,9 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// TODO: find out if leaving out tutorial path works now
|
// TODO: find out if leaving out tutorial path works now
|
||||||
content: ['./src/**/*.{js,jsx,ts,tsx}', '../../src/**/*.{html,js,jsx,md,mdx,ts,tsx}'],
|
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||||
theme: {
|
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: [],
|
plugins: [],
|
||||||
};
|
};
|
||||||
|
|||||||
+18
-26
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/react",
|
"name": "@strudel.cycles/react",
|
||||||
"version": "0.9.0",
|
"version": "0.6.0",
|
||||||
"description": "React components for strudel",
|
"description": "React components for strudel",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -32,39 +32,31 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.6.0",
|
"@codemirror/autocomplete": "^6.4.0",
|
||||||
"@codemirror/commands": "^6.0.0",
|
"@codemirror/lang-javascript": "^6.1.1",
|
||||||
"@codemirror/lang-javascript": "^6.1.7",
|
|
||||||
"@codemirror/language": "^6.0.0",
|
|
||||||
"@codemirror/lint": "^6.0.0",
|
|
||||||
"@codemirror/search": "^6.0.0",
|
|
||||||
"@codemirror/state": "^6.2.0",
|
"@codemirror/state": "^6.2.0",
|
||||||
"@codemirror/view": "^6.10.0",
|
"@codemirror/view": "^6.7.3",
|
||||||
"@lezer/highlight": "^1.1.4",
|
"@lezer/highlight": "^1.1.3",
|
||||||
"@replit/codemirror-emacs": "^6.0.1",
|
|
||||||
"@replit/codemirror-vim": "^6.0.14",
|
|
||||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
"@strudel.cycles/transpiler": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
"@strudel/codemirror": "workspace:*",
|
"@uiw/codemirror-themes": "^4.12.4",
|
||||||
"@uiw/codemirror-themes": "^4.19.16",
|
"@uiw/react-codemirror": "^4.12.4",
|
||||||
"@uiw/react-codemirror": "^4.19.16",
|
|
||||||
"react-hook-inview": "^4.5.0"
|
"react-hook-inview": "^4.5.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^17.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^17.0.2",
|
||||||
"@types/react-dom": "^18.2.1",
|
"@types/react-dom": "^17.0.2",
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^2.2.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.7",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.18",
|
||||||
"react": "^18.2.0",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^17.0.2",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.0.24",
|
||||||
"vite": "^4.3.3"
|
"vite": "^3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ evalScope(
|
|||||||
import('@strudel.cycles/core'),
|
import('@strudel.cycles/core'),
|
||||||
import('@strudel.cycles/tonal'),
|
import('@strudel.cycles/tonal'),
|
||||||
import('@strudel.cycles/mini'),
|
import('@strudel.cycles/mini'),
|
||||||
|
import('@strudel.cycles/midi'),
|
||||||
|
import('@strudel.cycles/xen'),
|
||||||
import('@strudel.cycles/webaudio'),
|
import('@strudel.cycles/webaudio'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function Autocomplete({ doc }) {
|
|||||||
<pre
|
<pre
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
|
console.log('ola!');
|
||||||
navigator.clipboard.writeText(example);
|
navigator.clipboard.writeText(example);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,47 +1,104 @@
|
|||||||
import { autocompletion } from '@codemirror/autocomplete';
|
import React from 'react';
|
||||||
import { Prec } from '@codemirror/state';
|
|
||||||
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
|
||||||
import { ViewPlugin, EditorView, keymap } from '@codemirror/view';
|
|
||||||
import { emacs } from '@replit/codemirror-emacs';
|
|
||||||
import { vim } from '@replit/codemirror-vim';
|
|
||||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
|
||||||
import _CodeMirror from '@uiw/react-codemirror';
|
import _CodeMirror from '@uiw/react-codemirror';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import { EditorView, Decoration } from '@codemirror/view';
|
||||||
|
import { StateField, StateEffect } from '@codemirror/state';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import strudelTheme from '../themes/strudel-theme';
|
import strudelTheme from '../themes/strudel-theme';
|
||||||
import { strudelAutocomplete } from './Autocomplete';
|
|
||||||
import { strudelTooltip } from './Tooltip';
|
|
||||||
import {
|
|
||||||
highlightExtension,
|
|
||||||
flashField,
|
|
||||||
flash,
|
|
||||||
highlightMiniLocations,
|
|
||||||
updateMiniLocations,
|
|
||||||
} from '@strudel/codemirror';
|
|
||||||
import './style.css';
|
import './style.css';
|
||||||
import { sliderPlugin } from '@strudel/codemirror/slider.mjs';
|
import { useCallback } from 'react';
|
||||||
|
import { autocompletion } from '@codemirror/autocomplete';
|
||||||
|
import { strudelAutocomplete } from './Autocomplete';
|
||||||
|
|
||||||
export { flash, highlightMiniLocations, updateMiniLocations };
|
export const setFlash = StateEffect.define();
|
||||||
|
const flashField = StateField.define({
|
||||||
|
create() {
|
||||||
|
return Decoration.none;
|
||||||
|
},
|
||||||
|
update(flash, tr) {
|
||||||
|
try {
|
||||||
|
for (let e of tr.effects) {
|
||||||
|
if (e.is(setFlash)) {
|
||||||
|
if (e.value) {
|
||||||
|
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||||
|
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||||
|
} else {
|
||||||
|
flash = Decoration.set([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flash;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('flash error', err);
|
||||||
|
return flash;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
|
});
|
||||||
|
|
||||||
const staticExtensions = [javascript(), flashField, highlightExtension, sliderPlugin];
|
export const flash = (view) => {
|
||||||
|
view.dispatch({ effects: setFlash.of(true) });
|
||||||
|
setTimeout(() => {
|
||||||
|
view.dispatch({ effects: setFlash.of(false) });
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setHighlights = StateEffect.define();
|
||||||
|
const highlightField = StateField.define({
|
||||||
|
create() {
|
||||||
|
return Decoration.none;
|
||||||
|
},
|
||||||
|
update(highlights, tr) {
|
||||||
|
try {
|
||||||
|
for (let e of tr.effects) {
|
||||||
|
if (e.is(setHighlights)) {
|
||||||
|
const { haps } = e.value;
|
||||||
|
const marks =
|
||||||
|
haps
|
||||||
|
.map((hap) =>
|
||||||
|
(hap.context.locations || []).map(({ start, end }) => {
|
||||||
|
const color = hap.context.color || e.value.color || '#FFCA28';
|
||||||
|
let from = tr.newDoc.line(start.line).from + start.column;
|
||||||
|
let to = tr.newDoc.line(end.line).from + end.column;
|
||||||
|
const l = tr.newDoc.length;
|
||||||
|
if (from > l || to > l) {
|
||||||
|
return; // dont mark outside of range, as it will throw an error
|
||||||
|
}
|
||||||
|
// const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
|
||||||
|
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
|
||||||
|
return mark.range(from, to);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.flat()
|
||||||
|
.filter(Boolean) || [];
|
||||||
|
highlights = Decoration.set(marks, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return highlights;
|
||||||
|
} catch (err) {
|
||||||
|
// console.warn('highlighting error', err);
|
||||||
|
return Decoration.set([]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
|
});
|
||||||
|
|
||||||
|
const extensions = [
|
||||||
|
javascript(),
|
||||||
|
highlightField,
|
||||||
|
flashField,
|
||||||
|
// javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
|
||||||
|
// autocompletion({ override: [strudelAutocomplete] }),
|
||||||
|
autocompletion({ override: [] }), // wait for https://github.com/uiwjs/react-codemirror/pull/458
|
||||||
|
];
|
||||||
|
|
||||||
export default function CodeMirror({
|
export default function CodeMirror({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
onViewChanged,
|
onViewChanged,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
onDocChange,
|
|
||||||
onEvaluate,
|
|
||||||
onReEvaluate,
|
|
||||||
onPanic,
|
|
||||||
onStop,
|
|
||||||
theme,
|
theme,
|
||||||
keybindings,
|
options,
|
||||||
isLineNumbersDisplayed,
|
editorDidMount,
|
||||||
isAutoCompletionEnabled,
|
|
||||||
isTooltipEnabled,
|
|
||||||
isLineWrappingEnabled,
|
|
||||||
fontSize = 18,
|
|
||||||
fontFamily = 'monospace',
|
|
||||||
}) {
|
}) {
|
||||||
const handleOnChange = useCallback(
|
const handleOnChange = useCallback(
|
||||||
(value) => {
|
(value) => {
|
||||||
@@ -49,113 +106,22 @@ export default function CodeMirror({
|
|||||||
},
|
},
|
||||||
[onChange],
|
[onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnCreateEditor = useCallback(
|
const handleOnCreateEditor = useCallback(
|
||||||
(view) => {
|
(view) => {
|
||||||
onViewChanged?.(view);
|
onViewChanged?.(view);
|
||||||
},
|
},
|
||||||
[onViewChanged],
|
[onViewChanged],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnUpdate = useCallback(
|
const handleOnUpdate = useCallback(
|
||||||
(viewUpdate) => {
|
(viewUpdate) => {
|
||||||
if (viewUpdate.docChanged && onDocChange) {
|
|
||||||
onDocChange?.(viewUpdate);
|
|
||||||
}
|
|
||||||
if (viewUpdate.selectionSet && onSelectionChange) {
|
if (viewUpdate.selectionSet && onSelectionChange) {
|
||||||
onSelectionChange?.(viewUpdate.state.selection);
|
onSelectionChange?.(viewUpdate.state.selection);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onSelectionChange],
|
[onSelectionChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const vscodePlugin = ViewPlugin.fromClass(
|
|
||||||
class {
|
|
||||||
constructor(view) {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: (plugin) => {
|
|
||||||
return Prec.highest(keymap.of([...vscodeKeymap]));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
|
||||||
|
|
||||||
const extensions = useMemo(() => {
|
|
||||||
let _extensions = [...staticExtensions];
|
|
||||||
let bindings = {
|
|
||||||
vim,
|
|
||||||
emacs,
|
|
||||||
vscode: vscodeExtension,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (bindings[keybindings]) {
|
|
||||||
_extensions.push(bindings[keybindings]());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAutoCompletionEnabled) {
|
|
||||||
_extensions.push(javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }));
|
|
||||||
} else {
|
|
||||||
_extensions.push(autocompletion({ override: [] }));
|
|
||||||
}
|
|
||||||
if (isTooltipEnabled) {
|
|
||||||
_extensions.push(strudelTooltip);
|
|
||||||
}
|
|
||||||
//_extensions.push([keymap.of({})]);
|
|
||||||
|
|
||||||
if (isLineWrappingEnabled) {
|
|
||||||
_extensions.push(EditorView.lineWrapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
_extensions.push(
|
|
||||||
keymap.of([
|
|
||||||
{
|
|
||||||
key: 'Ctrl-Enter',
|
|
||||||
run: () => onEvaluate?.(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Alt-Enter',
|
|
||||||
run: () => onEvaluate?.(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Ctrl-.',
|
|
||||||
run: () => onStop?.(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Alt-.',
|
|
||||||
run: (_, e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onStop?.();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Ctrl-Shift-.',
|
|
||||||
run: () => (onPanic ? onPanic() : onStop?.()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Ctrl-Shift-Enter',
|
|
||||||
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
return _extensions;
|
|
||||||
}, [
|
|
||||||
keybindings,
|
|
||||||
isAutoCompletionEnabled,
|
|
||||||
isTooltipEnabled,
|
|
||||||
isLineWrappingEnabled,
|
|
||||||
onEvaluate,
|
|
||||||
onReEvaluate,
|
|
||||||
onStop,
|
|
||||||
onPanic,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const basicSetup = useMemo(() => ({ lineNumbers: isLineNumbersDisplayed }), [isLineNumbersDisplayed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ fontSize, fontFamily }} className="w-full">
|
<>
|
||||||
<_CodeMirror
|
<_CodeMirror
|
||||||
value={value}
|
value={value}
|
||||||
theme={theme || strudelTheme}
|
theme={theme || strudelTheme}
|
||||||
@@ -163,8 +129,107 @@ export default function CodeMirror({
|
|||||||
onCreateEditor={handleOnCreateEditor}
|
onCreateEditor={handleOnCreateEditor}
|
||||||
onUpdate={handleOnUpdate}
|
onUpdate={handleOnUpdate}
|
||||||
extensions={extensions}
|
extensions={extensions}
|
||||||
basicSetup={basicSetup}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let parenMark;
|
||||||
|
export const markParens = (editor, data) => {
|
||||||
|
const v = editor.getDoc().getValue();
|
||||||
|
const marked = getCurrentParenArea(v, data);
|
||||||
|
parenMark?.clear();
|
||||||
|
parenMark = editor.getDoc().markText(...marked, { css: 'background-color: #00007720' }); //
|
||||||
|
};
|
||||||
|
|
||||||
|
// returns { line, ch } from absolute character offset
|
||||||
|
export function offsetToPosition(offset, code) {
|
||||||
|
const lines = code.split('\n');
|
||||||
|
let line = 0;
|
||||||
|
let ch = 0;
|
||||||
|
for (let i = 0; i < offset; i++) {
|
||||||
|
if (ch === lines[line].length) {
|
||||||
|
line++;
|
||||||
|
ch = 0;
|
||||||
|
} else {
|
||||||
|
ch++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { line, ch };
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns absolute character offset from { line, ch }
|
||||||
|
export function positionToOffset(position, code) {
|
||||||
|
const lines = code.split('\n');
|
||||||
|
if (position.line > lines.length) {
|
||||||
|
// throw new Error('positionToOffset: position.line > lines.length');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let offset = 0;
|
||||||
|
for (let i = 0; i < position.line; i++) {
|
||||||
|
offset += lines[i].length + 1;
|
||||||
|
}
|
||||||
|
offset += position.ch;
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// given code and caret position, the functions returns the indices of the parens we are in
|
||||||
|
export function getCurrentParenArea(code, caretPosition) {
|
||||||
|
const caret = positionToOffset(caretPosition, code);
|
||||||
|
let open, i, begin, end;
|
||||||
|
// walk left
|
||||||
|
i = caret;
|
||||||
|
open = 0;
|
||||||
|
while (i > 0) {
|
||||||
|
if (code[i - 1] === '(') {
|
||||||
|
open--;
|
||||||
|
} else if (code[i - 1] === ')') {
|
||||||
|
open++;
|
||||||
|
}
|
||||||
|
if (open === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
begin = i;
|
||||||
|
// walk right
|
||||||
|
i = caret;
|
||||||
|
open = 0;
|
||||||
|
while (i < code.length) {
|
||||||
|
if (code[i] === '(') {
|
||||||
|
open--;
|
||||||
|
} else if (code[i] === ')') {
|
||||||
|
open++;
|
||||||
|
}
|
||||||
|
if (open === 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
end = i;
|
||||||
|
return [begin, end].map((o) => offsetToPosition(o, code));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export const markEvent = (editor) => (time, event) => {
|
||||||
|
const locs = event.context.locations;
|
||||||
|
if (!locs || !editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const col = event.context?.color || '#FFCA28';
|
||||||
|
// mark active event
|
||||||
|
const marks = locs.map(({ start, end }) =>
|
||||||
|
editor.getDoc().markText(
|
||||||
|
{ line: start.line - 1, ch: start.column },
|
||||||
|
{ line: end.line - 1, ch: end.column },
|
||||||
|
//{ css: 'background-color: #FFCA28; color: black' } // background-color is now used by parent marking
|
||||||
|
{ css: 'outline: 1px solid ' + col + '; box-sizing:border-box' },
|
||||||
|
//{ css: `background-color: ${col};border-radius:5px` },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
//Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler...
|
||||||
|
setTimeout(() => {
|
||||||
|
marks.forEach((mark) => mark.clear());
|
||||||
|
// }, '+' + event.duration * 0.5);
|
||||||
|
}, event.duration * 1000);
|
||||||
|
}; */
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import useHighlighting from '../hooks/useHighlighting.mjs';
|
|||||||
import useStrudel from '../hooks/useStrudel.mjs';
|
import useStrudel from '../hooks/useStrudel.mjs';
|
||||||
import CodeMirror6, { flash } from './CodeMirror6';
|
import CodeMirror6, { flash } from './CodeMirror6';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
import styles from './MiniRepl.module.css';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
import { logger } from '@strudel.cycles/core';
|
import { logger } from '@strudel.cycles/core';
|
||||||
import useEvent from '../hooks/useEvent.mjs';
|
import useEvent from '../hooks/useEvent.mjs';
|
||||||
|
import useKeydown from '../hooks/useKeydown.mjs';
|
||||||
|
|
||||||
const getTime = () => getAudioContext().currentTime;
|
const getTime = () => getAudioContext().currentTime;
|
||||||
|
|
||||||
@@ -17,24 +19,17 @@ export function MiniRepl({
|
|||||||
tune,
|
tune,
|
||||||
hideOutsideView = false,
|
hideOutsideView = false,
|
||||||
enableKeyboard,
|
enableKeyboard,
|
||||||
onTrigger,
|
|
||||||
drawTime,
|
drawTime,
|
||||||
punchcard,
|
punchcard,
|
||||||
punchcardLabels,
|
|
||||||
onPaint,
|
|
||||||
canvasHeight = 200,
|
canvasHeight = 200,
|
||||||
fontSize = 18,
|
|
||||||
fontFamily,
|
|
||||||
hideHeader = false,
|
|
||||||
theme,
|
theme,
|
||||||
keybindings,
|
highlightColor,
|
||||||
isLineNumbersDisplayed,
|
|
||||||
}) {
|
}) {
|
||||||
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
|
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
|
||||||
const evalOnMount = !!drawTime;
|
const evalOnMount = !!drawTime;
|
||||||
const drawContext = useCallback(
|
const drawContext = useCallback(
|
||||||
punchcard ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
|
!!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
|
||||||
[punchcard],
|
[drawTime],
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
code,
|
code,
|
||||||
@@ -54,23 +49,11 @@ export function MiniRepl({
|
|||||||
} = useStrudel({
|
} = useStrudel({
|
||||||
initialCode: tune,
|
initialCode: tune,
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: webaudioOutput,
|
||||||
editPattern: (pat, id) => {
|
editPattern: (pat) => (punchcard ? pat.punchcard() : pat),
|
||||||
//pat = pat.withContext((ctx) => ({ ...ctx, id }));
|
|
||||||
if (onTrigger) {
|
|
||||||
pat = pat.onTrigger(onTrigger, false);
|
|
||||||
}
|
|
||||||
if (onPaint) {
|
|
||||||
pat = pat.onPaint(onPaint);
|
|
||||||
} else if (punchcard) {
|
|
||||||
pat = pat.punchcard({ labels: punchcardLabels });
|
|
||||||
}
|
|
||||||
return pat;
|
|
||||||
},
|
|
||||||
getTime,
|
getTime,
|
||||||
evalOnMount,
|
evalOnMount,
|
||||||
drawContext,
|
drawContext,
|
||||||
drawTime,
|
drawTime,
|
||||||
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [view, setView] = useState();
|
const [view, setView] = useState();
|
||||||
@@ -84,13 +67,55 @@ export function MiniRepl({
|
|||||||
}
|
}
|
||||||
return isVisible || wasVisible.current;
|
return isVisible || wasVisible.current;
|
||||||
}, [isVisible, hideOutsideView]);
|
}, [isVisible, hideOutsideView]);
|
||||||
const { setMiniLocations } = useHighlighting({
|
useHighlighting({
|
||||||
view,
|
view,
|
||||||
pattern,
|
pattern,
|
||||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||||
getTime: () => scheduler.now(),
|
getTime: () => scheduler.now(),
|
||||||
|
color: highlightColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// keyboard shortcuts
|
||||||
|
useKeydown(
|
||||||
|
useCallback(
|
||||||
|
async (e) => {
|
||||||
|
if (view?.hasFocus) {
|
||||||
|
if (e.ctrlKey || e.altKey) {
|
||||||
|
if (e.code === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
flash(view);
|
||||||
|
await activateCode();
|
||||||
|
} else if (e.code === 'Period') {
|
||||||
|
stop();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[activateCode, stop, view],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// set active pattern on ctrl+enter
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (enableKeyboard) {
|
||||||
|
const handleKeyPress = async (e) => {
|
||||||
|
if (e.ctrlKey || e.altKey) {
|
||||||
|
if (e.code === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
flash(view);
|
||||||
|
await activateCode();
|
||||||
|
} else if (e.code === 'Period') {
|
||||||
|
stop();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeyPress, true);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
||||||
|
}
|
||||||
|
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
|
||||||
|
|
||||||
const [log, setLog] = useState([]);
|
const [log, setLog] = useState([]);
|
||||||
useLogger(
|
useLogger(
|
||||||
useCallback((e) => {
|
useCallback((e) => {
|
||||||
@@ -99,59 +124,29 @@ export function MiniRepl({
|
|||||||
// const logId = data?.pattern?.meta?.id;
|
// const logId = data?.pattern?.meta?.id;
|
||||||
if (logId === replId) {
|
if (logId === replId) {
|
||||||
setLog((l) => {
|
setLog((l) => {
|
||||||
return l.concat([e.detail]).slice(-8);
|
return l.concat([e.detail]).slice(-10);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []),
|
}, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight" ref={ref}>
|
<div className={styles.container} ref={ref}>
|
||||||
{!hideHeader && (
|
<div className={styles.header}>
|
||||||
<div className="flex justify-between bg-lineHighlight">
|
<div className={styles.buttons}>
|
||||||
<div className="flex">
|
<button className={cx(styles.button, started ? 'animate-pulse' : '')} onClick={() => togglePlay()}>
|
||||||
<button
|
<Icon type={started ? 'stop' : 'play'} />
|
||||||
className={cx(
|
</button>
|
||||||
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
|
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
||||||
started ? 'animate-pulse' : '',
|
<Icon type="refresh" />
|
||||||
)}
|
</button>
|
||||||
onClick={() => togglePlay()}
|
|
||||||
>
|
|
||||||
<Icon type={started ? 'stop' : 'play'} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={cx(
|
|
||||||
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
|
|
||||||
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
|
|
||||||
)}
|
|
||||||
onClick={() => activateCode()}
|
|
||||||
>
|
|
||||||
<Icon type="refresh" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{error && <div className={styles.error}>{error.message}</div>}
|
||||||
<div className="overflow-auto relative">
|
|
||||||
{show && (
|
|
||||||
<CodeMirror6
|
|
||||||
value={code}
|
|
||||||
onChange={setCode}
|
|
||||||
onViewChanged={setView}
|
|
||||||
theme={theme}
|
|
||||||
fontFamily={fontFamily}
|
|
||||||
fontSize={fontSize}
|
|
||||||
keybindings={keybindings}
|
|
||||||
isLineNumbersDisplayed={isLineNumbersDisplayed}
|
|
||||||
onEvaluate={() => {
|
|
||||||
flash(view);
|
|
||||||
activateCode();
|
|
||||||
}}
|
|
||||||
onStop={() => stop()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
{punchcard && (
|
<div className={styles.body}>
|
||||||
|
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} theme={theme} />}
|
||||||
|
</div>
|
||||||
|
{drawTime && (
|
||||||
<canvas
|
<canvas
|
||||||
id={canvasId}
|
id={canvasId}
|
||||||
className="w-full pointer-events-none"
|
className="w-full pointer-events-none"
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
.container {
|
||||||
|
@apply overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
@apply flex justify-between bg-lineHighlight border-t border-l border-r border-lineHighlight rounded-t-md overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
@apply flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
@apply cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground hover:bg-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonDisabled {
|
||||||
|
@apply w-16 flex items-center justify-center p-1 opacity-50 cursor-not-allowed border-r border-lineHighlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
@apply text-right p-1 text-sm text-red-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
@apply overflow-auto relative;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user