Compare commits

..

21 Commits

Author SHA1 Message Date
Jade Rowland 1bae027aaa Merge branch 'tidalcycles:main' into ableton_link_integration 2023-10-05 23:07:41 -04:00
Felix Roos 3a69a2561c half working cps link 2023-10-02 21:21:11 +02:00
Felix Roos 30a0059ef3 remove todo 2023-10-02 20:59:35 +02:00
Felix Roos b1fcf6ca34 cleanup 2023-10-02 20:59:10 +02:00
Felix Roos f2534310e8 fix: default cyclist 2023-10-02 20:57:25 +02:00
Felix Roos 25be8fca0d encapsulate cyclist 2023-10-02 20:56:45 +02:00
Felix Roos f089037f18 add cmake instructions 2023-10-02 08:35:41 +02:00
Jade Rowland e818ca9f03 Merge branch 'tidalcycles:main' into ableton_link_integration 2023-10-01 20:44:09 -04:00
Jade Rowland 0efcf84c46 cleaning up 2023-10-01 20:30:47 -04:00
Jade Rowland 5db39b8e9e fix clock being set to 0 2023-10-01 19:45:44 -04:00
Jade Rowland 2127a8f642 cleaning things up 2023-10-01 19:19:49 -04:00
Jade Rowland 1d85d60b11 cleaning up 2023-10-01 00:46:40 -04:00
Jade Rowland d5c6cb85ea use cps instead of bpm 2023-09-30 22:18:31 -04:00
Jade Rowland fe8c4e2bcd cyclist bridge 2023-09-29 23:27:43 -04:00
Jade Rowland 6eda7af14b play sync now accurate 2023-09-29 01:01:01 -04:00
Jade Rowland 6719c6127b figuring out backend integration 2023-09-27 15:28:23 -04:00
Jade Rowland 172006a3b3 improving listener 2023-09-25 22:32:31 -04:00
Jade Rowland 209122b8f7 start offset 2023-09-25 19:29:14 -04:00
Jade Rowland d7fe491c84 play stop working 2023-09-22 14:27:03 -04:00
Jade Rowland db36636ab2 play working kinda 2023-09-21 23:08:30 -04:00
Jade Rowland 789cad3e97 figuring things out 2023-09-21 22:57:05 -04:00
331 changed files with 13637 additions and 15539 deletions
+1 -3
View File
@@ -19,6 +19,4 @@ vite.config.js
**/dev-dist **/dev-dist
**/dist **/dist
/src-tauri/target/**/* /src-tauri/target/**/*
reverbGen.mjs reverbGen.mjs
hydra.mjs
jsdoc-synonyms.js
+3 -3
View File
@@ -21,13 +21,13 @@ jobs:
name: github-pages name: github-pages
url: ${{ steps.deployment.outputs.page_url }} url: ${{ steps.deployment.outputs.page_url }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 8.11.0 version: 7
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 20 node-version: 18
cache: "pnpm" cache: "pnpm"
- name: Install Dependencies - name: Install Dependencies
run: pnpm install run: pnpm install
+3 -3
View File
@@ -7,13 +7,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [20] node-version: [18]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 8.11.0 version: 7
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
+1 -84
View File
@@ -43,87 +43,4 @@ dev-dist
Dirt-Samples Dirt-Samples
tidal-drum-machines tidal-drum-machines
webaudiofontdata webaudiofontdata
src-tauri/target src-tauri/target
# BEGIN JetBrains -> END JetBrains
# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# END JetBrains -> BEGIN JetBrains
+1 -2
View File
@@ -9,5 +9,4 @@ packages/xen/tunejs.js
paper paper
pnpm-lock.yaml pnpm-lock.yaml
pnpm-workspace.yaml pnpm-workspace.yaml
**/dev-dist **/dev-dist
website/.astro
+4 -4
View File
@@ -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/workshop/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
@@ -66,7 +66,7 @@ To get the project up and running for development, make sure you have installed:
- [git](https://git-scm.com/) - [git](https://git-scm.com/)
- [node](https://nodejs.org/en/) >= 18 - [node](https://nodejs.org/en/) >= 18
- [pnpm](https://pnpm.io/) (`curl -fsSL https://get.pnpm.io/install.sh | env PNPM_VERSION=8.11.0 sh -`) - [pnpm](https://pnpm.io/) (`npm i pnpm -g`)
then, do the following: then, do the following:
@@ -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
+3 -2
View File
@@ -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>
@@ -30,6 +30,7 @@ There are multiple npm packages you can use to use strudel, or only parts of it,
- [`midi`](./packages/midi): webmidi bindings - [`midi`](./packages/midi): webmidi bindings
- [`serial`](./packages/serial): webserial bindings - [`serial`](./packages/serial): webserial bindings
- [`tonal`](./packages/tonal): tonal functions - [`tonal`](./packages/tonal): tonal functions
- [`xen`](./packages/xen): microtonal / xenharmonic functions
- ... [and there are more](./packages/) - ... [and there are more](./packages/)
Click on the package names to find out more about each one. Click on the package names to find out more about each one.
+1806
View File
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 150 KiB

-3
View File
@@ -1,3 +0,0 @@
# examples
This folder contains usage examples for different scenarios.
-5
View File
@@ -1,5 +0,0 @@
# buildless examples
These examples show you how strudel can be used in a regular html file, without the need for a build tool.
Most examples are using [skypack](https://www.skypack.dev/)
@@ -1,31 +0,0 @@
<script src="https://unpkg.com/@strudel/repl@0.9.4"></script>
<strudel-editor>
<!--
// @date 23-08-15
// "golf rolf" @by froos @license CC BY-NC-SA 4.0
setcps(1)
stack(
s("bd*2, ~ rim*<1!3 2>, hh*4").bank('RolandTR909')
.off(-1/8, set(speed("1.5").gain(.25)))
.mask("<0!16 1!64>")
,
note("g1(3,8)")
.s("gm_synth_bass_2:<0 2>")
.delay(".8:.25:.25")
.clip("<.5!16 2!32>")
.off(1/8, add(note("12?0.7")))
.lpf(sine.range(500,2000).slow(32)).lpq(8)
.add(note("0,.05"))
.mask("<0!8 1!32>")
,
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
).reset("<x@15 x(5,8)>")
-->
</strudel-editor>
-8
View File
@@ -1,8 +0,0 @@
# codemirror-repl example
This folder demonstrates how to set up a full strudel repl with the `@strudel/codemirror` package. Run it using:
```sh
pnpm i
pnpm dev
```
-13
View File
@@ -1,13 +0,0 @@
# headless-repl demo
This demo shows how to use strudel in "headless mode".
Buttons A / B / C will switch between different patterns.
It showcases the usage of the `@strudel/web` package, using [vite](https://vitejs.dev/) as the dev server.
## Running
```sh
pnpm i && cd examples/headless-repl
pnpm dev
# open http://localhost:5173/
```
-1
View File
@@ -1 +0,0 @@
dist
-10
View File
@@ -1,10 +0,0 @@
# minimal repl
This folder demonstrates how to set up a minimal strudel repl using vite and vanilla JS. Run it using:
```sh
npm i
npm run dev
```
If you're looking for a more feature rich alternative, have a look at the [../codemirror-repl](codemirror-repl example)
-11
View File
@@ -1,11 +0,0 @@
# superdough demo
This demo shows how to use [superdough](https://www.npmjs.com/package/superdough) with [vite](https://vitejs.dev/).
## Running
```sh
pnpm i && cd examples/headless-repl
pnpm dev
# open http://localhost:5173/
```
-38
View File
@@ -1,38 +0,0 @@
<!doctype html>
<html>
<head>
<title>Superdough Example</title>
<meta charset="UTF-8" />
</head>
<body>
<button id="play">PLAAAAAAAY</button>
<script type="module">
import { superdough, samples, initAudioOnFirstClick, registerSynthSounds } from 'superdough';
const init = Promise.all([
initAudioOnFirstClick(),
samples('github:tidalcycles/Dirt-Samples/master'),
registerSynthSounds(),
]);
const loop = (t = 0) => {
// superdough(value, time, duration)
superdough({ s: 'bd', delay: 0.5 }, t);
superdough({ note: 'g1', s: 'sawtooth', cutoff: 600, resonance: 8 }, t, 0.125);
superdough({ note: 'g2', s: 'sawtooth', cutoff: 600, resonance: 8 }, t + 0.25, 0.125);
superdough({ s: 'hh' }, t + 0.25);
superdough({ s: 'sd', room: 0.5 }, t + 0.5);
superdough({ s: 'hh' }, t + 0.75);
};
document.getElementById('play').addEventListener('click', async () => {
await init;
let t = 0.1;
while (t < 16) {
loop(t++);
}
});
</script>
</body>
</html>
@@ -3,7 +3,7 @@
"includePattern": ".+\\.(js(doc|x)?|mjs)$", "includePattern": ".+\\.(js(doc|x)?|mjs)$",
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist" "excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
}, },
"plugins": ["plugins/markdown", "jsdoc/jsdoc-synonyms"], "plugins": ["plugins/markdown"],
"opts": { "opts": {
"destination": "./out/", "destination": "./out/",
"recurse": true "recurse": true
+4 -2
View File
@@ -1,6 +1,8 @@
{ {
"packages": ["packages/*"], "packages": [
"packages/*"
],
"version": "independent", "version": "independent",
"npmClient": "pnpm", "npmClient": "pnpm",
"$schema": "node_modules/lerna/schemas/lerna-schema.json" "useWorkspaces": true
} }
+1 -1
View File
@@ -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
View File
@@ -18,12 +18,12 @@
"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",
"jsdoc": "jsdoc packages/ -c jsdoc/jsdoc.config.json", "jsdoc": "jsdoc packages/ -c jsdoc.config.json",
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc/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",
"codeformat": "prettier --write .", "codeformat": "prettier --write .",
"format-check": "prettier --check .", "format-check": "prettier --check .",
"report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json", "report-undocumented": "npm run jsdoc-json && node undocumented.mjs > undocumented.json",
"check": "npm run format-check && npm run lint && npm run test", "check": "npm run format-check && npm run lint && npm run test",
"iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf" "iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf"
}, },
@@ -43,29 +43,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": {
"@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": {
"dependency-tree": "^10.0.9", "@tauri-apps/cli": "^1.4.0",
"@tauri-apps/cli": "^1.5.9", "@vitest/ui": "^0.28.0",
"@vitest/ui": "^1.1.0",
"canvas": "^2.11.2", "canvas": "^2.11.2",
"eslint": "^8.56.0", "eslint": "^8.39.0",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.27.5",
"events": "^3.3.0", "events": "^3.3.0",
"jsdoc": "^4.0.2", "jsdoc": "^4.0.2",
"jsdoc-json": "^2.0.2", "jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^8.0.0", "jsdoc-to-markdown": "^8.0.0",
"lerna": "^8.0.1", "lerna": "^6.6.1",
"prettier": "^3.1.1", "prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.8.1",
"vitest": "^1.1.0" "vitest": "^0.33.0"
} }
} }
+10
View File
@@ -3,3 +3,13 @@
Each folder represents one of the @strudel.cycles/* packages [published to npm](https://www.npmjs.com/org/strudel.cycles). Each folder represents one of the @strudel.cycles/* packages [published to npm](https://www.npmjs.com/org/strudel.cycles).
To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs. To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs.
This is a graphical view of all the packages: [full screen](https://raw.githubusercontent.com/tidalcycles/strudel/main/dependencies.svg)
![dependencies](https://raw.githubusercontent.com/tidalcycles/strudel/main/dependencies.svg)
Generated with
```sh
npx depcruise --include-only "^packages" -X "node_modules" --output-type dot packages | dot -T svg > dependencygraph.svg
```
+47 -273
View File
@@ -1,112 +1,37 @@
import { closeBrackets } from '@codemirror/autocomplete'; import { defaultKeymap } from '@codemirror/commands';
// import { search, highlightSelectionMatches } from '@codemirror/search';
import { history } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language'; import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state'; import { EditorState } from '@codemirror/state';
import { import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from '@codemirror/view';
EditorView, import { Drawer, repl } from '@strudel.cycles/core';
highlightActiveLineGutter, import { flashField, flash } from './flash.mjs';
highlightActiveLine, import { highlightExtension, highlightMiniLocations } from './highlight.mjs';
keymap, import { oneDark } from './themes/one-dark';
lineNumbers,
drawSelection,
} from '@codemirror/view';
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel.cycles/core';
import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs';
import { initTheme, activateTheme, theme } from './themes.mjs';
import { updateWidgets, sliderPlugin } from './slider.mjs';
import { persistentAtom } from '@nanostores/persistent';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
theme,
isAutoCompletionEnabled,
isTooltipEnabled,
isPatternHighlightingEnabled,
isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
isFlashEnabled,
keybindings,
};
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
export const defaultSettings = {
keybindings: 'codemirror',
isLineNumbersDisplayed: true,
isActiveLineHighlighted: false,
isAutoCompletionEnabled: false,
isPatternHighlightingEnabled: true,
isFlashEnabled: true,
isTooltipEnabled: false,
isLineWrappingEnabled: false,
theme: 'strudelTheme',
fontFamily: 'monospace',
fontSize: 18,
};
export const codemirrorSettings = persistentAtom('codemirror-settings', defaultSettings, {
encode: JSON.stringify,
decode: JSON.parse,
});
// https://codemirror.net/docs/guide/ // https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root }) { export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
const settings = codemirrorSettings.get();
const initialSettings = Object.keys(compartments).map((key) =>
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
);
initTheme(settings.theme);
let state = EditorState.create({ let state = EditorState.create({
doc: initialCode, doc: initialCode,
extensions: [ extensions: [
/* search(), theme,
highlightSelectionMatches(), */
...initialSettings,
javascript(), javascript(),
sliderPlugin, lineNumbers(),
// indentOnInput(), // works without. already brought with javascript extension? highlightExtension,
// bracketMatching(), // does not do anything highlightActiveLineGutter(),
closeBrackets(),
syntaxHighlighting(defaultHighlightStyle), syntaxHighlighting(defaultHighlightStyle),
history(), keymap.of(defaultKeymap),
flashField,
EditorView.updateListener.of((v) => onChange(v)), EditorView.updateListener.of((v) => onChange(v)),
drawSelection({ cursorBlinkRate: 0 }), keymap.of([
Prec.highest( {
keymap.of([ key: 'Ctrl-Enter',
{ run: () => onEvaluate(),
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', key: 'Ctrl-.',
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()), run: () => onStop(),
}, */ },
]), ]),
),
], ],
}); });
@@ -118,76 +43,45 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
export class StrudelMirror { export class StrudelMirror {
constructor(options) { constructor(options) {
const { const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
root,
id,
initialCode = '',
onDraw,
drawTime = [0, 0],
autodraw,
prebake,
bgFill = true,
...replOptions
} = options;
this.code = initialCode; this.code = initialCode;
this.root = root;
this.miniLocations = [];
this.widgets = [];
this.painters = [];
this.drawTime = drawTime;
this.onDraw = onDraw;
const self = this;
this.id = id || s4();
this.drawer = new Drawer((haps, time) => { this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped); const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
this.highlight(currentFrame, time); this.highlight(currentFrame, time);
this.onDraw?.(haps, time, currentFrame, this.painters); onDraw?.(haps, time, currentFrame);
}, drawTime); }, drawTime);
// this approach does not work with multiple repls on screen const prebaked = prebake();
// TODO: refactor onPaint usages + find fix, maybe remove painters here? prebaked.then(async () => {
Pattern.prototype.onPaint = function (onPaint) { if (!onDraw) {
self.painters.push(onPaint); return;
return this; }
}; const { scheduler, evaluate } = await this.repl;
// draw first frame instantly
this.prebaked = prebake(); prebaked.then(async () => {
autodraw && this.drawFirstFrame(); await evaluate(this.code, false);
this.drawer.invalidate(scheduler);
onDraw?.(this.drawer.visibleHaps, 0, []);
});
});
this.repl = repl({ this.repl = repl({
...replOptions, ...replOptions,
onToggle: (started) => { onToggle: async (started) => {
replOptions?.onToggle?.(started); replOptions?.onToggle?.(started);
const { scheduler } = await this.repl;
if (started) { if (started) {
this.adjustDrawTime(); this.drawer.start(scheduler);
this.drawer.start(this.repl.scheduler);
// stop other repls when this one is started
document.dispatchEvent(
new CustomEvent('start-repl', {
detail: this.id,
}),
);
} else { } else {
this.drawer.stop(); this.drawer.stop();
updateMiniLocations(this.editor, []);
cleanupDraw(false);
} }
}, },
beforeEval: async () => { beforeEval: async () => {
cleanupDraw(); await prebaked;
this.painters = [];
await this.prebaked;
await replOptions?.beforeEval?.();
}, },
afterEval: (options) => { 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); replOptions?.afterEval?.(options);
this.adjustDrawTime();
this.drawer.invalidate(); this.drawer.invalidate();
}, },
}); });
@@ -195,145 +89,25 @@ export class StrudelMirror {
root, root,
initialCode, initialCode,
onChange: (v) => { onChange: (v) => {
if (v.docChanged) { this.code = v.state.doc.toString();
this.code = v.state.doc.toString();
this.repl.setCode?.(this.code);
}
}, },
onEvaluate: () => this.evaluate(), onEvaluate: () => this.evaluate(),
onStop: () => this.stop(), onStop: () => this.stop(),
}); });
const cmEditor = this.root.querySelector('.cm-editor');
if (cmEditor) {
this.root.style.display = 'block';
if (bgFill) {
this.root.style.backgroundColor = 'var(--background)';
}
cmEditor.style.backgroundColor = 'transparent';
}
const settings = codemirrorSettings.get();
this.setFontSize(settings.fontSize);
this.setFontFamily(settings.fontFamily);
// stop this repl when another repl is started
this.onStartRepl = (e) => {
if (e.detail !== this.id) {
this.stop();
}
};
document.addEventListener('start-repl', this.onStartRepl);
}
// adjusts draw time depending on if there are painters
adjustDrawTime() {
// when no painters are set, [0,0] is enough (just highlighting)
this.drawer.setDrawTime(this.painters.length ? this.drawTime : [0, 0]);
}
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, -0.001);
// draw at -0.001 to avoid haps at 0 to be visualized as active
this.onDraw?.(this.drawer.visibleHaps, -0.001, [], this.painters);
} catch (err) {
console.warn('first frame could not be painted');
}
} }
async evaluate() { async evaluate() {
const { evaluate } = await this.repl;
this.flash(); this.flash();
await this.repl.evaluate(this.code); await evaluate(this.code);
} }
async stop() { async stop() {
this.repl.scheduler.stop(); const { scheduler } = await this.repl;
} scheduler.stop();
async toggle() {
if (this.repl.scheduler.started) {
this.repl.stop();
} else {
this.evaluate();
}
} }
flash(ms) { flash(ms) {
flash(this.editor, ms); flash(this.editor, ms);
} }
highlight(haps, time) { highlight(haps, time) {
highlightMiniLocations(this.editor, time, haps); highlightMiniLocations(this.editor.view, time, haps);
}
setFontSize(size) {
this.root.style.fontSize = size + 'px';
}
setFontFamily(family) {
this.root.style.fontFamily = family;
const scroller = this.root.querySelector('.cm-scroller');
if (scroller) {
scroller.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),
});
if (key === 'theme') {
activateTheme(value);
}
}
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]);
}
const updated = { ...codemirrorSettings.get(), ...settings };
codemirrorSettings.set(updated);
}
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 });
}
clear() {
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
} }
} }
function parseBooleans(value) {
return { true: true, false: false }[value] ?? value;
}
// helper function to generate repl ids
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
-2
View File
@@ -33,5 +33,3 @@ export const flash = (view, ms = 200) => {
view.dispatch({ effects: setFlash.of(false) }); view.dispatch({ effects: setFlash.of(false) });
}, ms); }, ms);
}; };
export const isFlashEnabled = (on) => (on ? flashField : []);
-9
View File
@@ -124,12 +124,3 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
}); });
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights]; export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
export const isPatternHighlightingEnabled = (on, config) => {
on &&
config &&
setTimeout(() => {
updateMiniLocations(config.editor, config.miniLocations);
}, 100);
return on ? highlightExtension : [];
};
-17
View File
@@ -1,17 +0,0 @@
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
export let html = (string) => {
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
};
let parseChunk = (chunk) => {
if (Array.isArray(chunk)) return chunk.flat().join('');
if (chunk === undefined) return '';
return chunk;
};
export let h = (strings, ...vars) => {
let string = '';
for (let i in strings) {
string += parseChunk(strings[i]);
string += parseChunk(vars[i]);
}
return html(string);
};
-1
View File
@@ -2,4 +2,3 @@ export * from './codemirror.mjs';
export * from './highlight.mjs'; export * from './highlight.mjs';
export * from './flash.mjs'; export * from './flash.mjs';
export * from './slider.mjs'; export * from './slider.mjs';
export * from './themes.mjs';
-31
View File
@@ -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),
}
+9 -18
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/codemirror", "name": "@strudel/codemirror",
"version": "0.10.0", "version": "0.9.0",
"description": "Codemirror Extensions for Strudel", "description": "Codemirror Extensions for Strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
@@ -33,24 +33,15 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.11.1", "@codemirror/commands": "^6.2.4",
"@codemirror/commands": "^6.3.3", "@codemirror/lang-javascript": "^6.1.7",
"@codemirror/lang-javascript": "^6.2.1", "@codemirror/language": "^6.6.0",
"@codemirror/language": "^6.10.0", "@codemirror/state": "^6.2.0",
"@codemirror/search": "^6.5.5", "@codemirror/view": "^6.10.0",
"@codemirror/state": "^6.4.0", "@lezer/highlight": "^1.1.4",
"@codemirror/view": "^6.23.0", "@strudel.cycles/core": "workspace:*"
"@lezer/highlight": "^1.2.0",
"@nanostores/persistent": "^0.9.1",
"@replit/codemirror-emacs": "^6.0.1",
"@replit/codemirror-vim": "^6.1.0",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel.cycles/core": "workspace:*",
"@uiw/codemirror-themes": "^4.21.21",
"@uiw/codemirror-themes-all": "^4.21.21",
"nanostores": "^0.9.5"
}, },
"devDependencies": { "devDependencies": {
"vite": "^5.0.10" "vite": "^4.3.3"
} }
} }
-9
View File
@@ -111,15 +111,6 @@ export const sliderPlugin = ViewPlugin.fromClass(
}, },
); );
/**
* Displays a slider widget to allow the user manipulate a value
*
* @name slider
* @param {number} value Initial value
* @param {number} min Minimum value - optional, defaults to 0
* @param {number} max Maximum value - optional, defaults to 1
* @param {number} step Step size - optional
*/
export let slider = (value) => { export let slider = (value) => {
console.warn('slider will only work when the transpiler is used... passing value as is'); console.warn('slider will only work when the transpiler is used... passing value as is');
return pure(value); return pure(value);
+139
View File
@@ -0,0 +1,139 @@
import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags as t } from '@lezer/highlight';
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
const chalky = '#e5c07b',
coral = '#e06c75',
cyan = '#56b6c2',
invalid = '#ffffff',
ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
malibu = '#61afef',
sage = '#98c379',
whiskey = '#d19a66',
violet = '#c678dd',
darkBackground = '#21252b',
highlightBackground = '#2c313a',
background = '#282c34',
tooltipBackground = '#353a42',
selection = '#3E4451',
cursor = '#528bff';
/// The colors used in the theme, as CSS color strings.
export const color = {
chalky,
coral,
cyan,
invalid,
ivory,
stone,
malibu,
sage,
whiskey,
violet,
darkBackground,
highlightBackground,
background,
tooltipBackground,
selection,
cursor,
};
/// The editor theme styles for One Dark.
export const oneDarkTheme = EditorView.theme(
{
'&': {
color: ivory,
backgroundColor: background,
},
'.cm-content': {
caretColor: cursor,
},
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
{ backgroundColor: selection },
'.cm-panels': { backgroundColor: darkBackground, color: ivory },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
},
'.cm-gutters': {
backgroundColor: background,
color: stone,
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: true },
);
/// The highlighting style for code in the One Dark theme.
export const oneDarkHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: violet },
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
{ tag: [t.definition(t.name), t.separator], color: ivory },
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
{ tag: [t.meta, t.comment], color: stone },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.link, color: stone, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: coral },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
{ tag: t.invalid, color: invalid },
]);
/// Extension to enable the One Dark theme (both the editor theme and
/// the highlight style).
export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
-78
View File
@@ -1,78 +0,0 @@
import { hoverTooltip } from '@codemirror/view';
import jsdoc from '../../doc.json';
import { Autocomplete } from './autocomplete.mjs';
const getDocLabel = (doc) => doc.name || doc.longname;
let ctrlDown = false;
if (typeof window !== 'undefined') {
// Record Control key event to trigger or block the tooltip depending on the state
window.addEventListener(
'keyup',
function (e) {
if (e.key == 'Control') {
ctrlDown = false;
}
},
true,
);
window.addEventListener(
'keydown',
function (e) {
if (e.key == 'Control') {
ctrlDown = true;
}
},
true,
);
}
export const strudelTooltip = hoverTooltip(
(view, pos, side) => {
// Word selection from CodeMirror Hover Tooltip example https://codemirror.net/examples/tooltip/#hover-tooltips
if (!ctrlDown) {
return null;
}
let { from, to, text } = view.state.doc.lineAt(pos);
let start = pos,
end = pos;
while (start > from && /\w/.test(text[start - from - 1])) {
start--;
}
while (end < to && /\w/.test(text[end - from])) {
end++;
}
if ((start == pos && side < 0) || (end == pos && side > 0)) {
return null;
}
let word = text.slice(start - from, end - from);
// Get entry from Strudel documentation
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
if (!entry) {
// Try for synonyms
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
if (!entry) {
return null;
}
}
return {
pos: start,
end,
above: false,
arrow: true,
create(view) {
let dom = document.createElement('div');
dom.className = 'strudel-tooltip';
const ac = Autocomplete({ doc: entry, label: word });
dom.appendChild(ac);
return { dom };
},
};
},
{ hoverTime: 10 },
);
export const isTooltipEnabled = (on) => (on ? strudelTooltip : []);
+1 -1
View File
@@ -9,7 +9,7 @@ export default defineConfig({
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'], formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext], fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
}, },
rollupOptions: { rollupOptions: {
external: [...Object.keys(dependencies)], external: [...Object.keys(dependencies)],
-2
View File
@@ -1,7 +1,5 @@
# @strudel.cycles/core # @strudel.cycles/core
**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/core](https://www.npmjs.com/package/@strudel/core).
This package contains the bare essence of strudel. This package contains the bare essence of strudel.
## Install ## Install
+12 -225
View File
@@ -86,16 +86,6 @@ const generic_params = [
* *
*/ */
['gain'], ['gain'],
/**
* Gain applied after all effects have been processed.
*
* @name postgain
* @example
* s("bd sd,hh*4")
* .compressor("-20:20:10:.002:.02").postgain(1.5)
*
*/
['postgain'],
/** /**
* Like {@link gain}, but linear. * Like {@link gain}, but linear.
* *
@@ -234,7 +224,7 @@ const generic_params = [
* note("c3 e3").decay("<.1 .2 .3 .4>").sustain(0) * note("c3 e3").decay("<.1 .2 .3 .4>").sustain(0)
* *
*/ */
['decay', 'dec'], ['decay'],
/** /**
* Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset. * Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset.
* *
@@ -270,7 +260,7 @@ const generic_params = [
* s("bd sd,hh*3").bpf("<1000 2000 4000 8000>") * s("bd sd,hh*3").bpf("<1000 2000 4000 8000>")
* *
*/ */
[['bandf', 'bandq', 'bpenv'], 'bpf', 'bp'], [['bandf', 'bandq'], 'bpf', 'bp'],
// TODO: in tidal, it seems to be normalized // TODO: in tidal, it seems to be normalized
/** /**
* Sets the **b**and-**p**ass **q**-factor (resonance). * Sets the **b**and-**p**ass **q**-factor (resonance).
@@ -380,75 +370,6 @@ const generic_params = [
* *
*/ */
['coarse'], ['coarse'],
/**
* Allows you to set the output channels on the interface
*
* @name channels
* @synonyms ch
*
* @param {number | Pattern} channels pattern the output channels
* @example
* note("e a d b g").channels("3:4")
*
*/
['channels', 'ch'],
['phaserrate', 'phasr'], // superdirt only
/**
* Phaser audio effect that approximates popular guitar pedals.
*
* @name phaser
* @synonyms ph
* @param {number | Pattern} speed speed of modulation
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
* .phaser("<1 2 4 8>")
*
*/
[['phaser', 'phaserdepth', 'phasercenter', 'phasersweep'], 'ph'],
/**
* The frequency sweep range of the lfo for the phaser effect. Defaults to 2000
*
* @name phasersweep
* @synonyms phs
* @param {number | Pattern} phasersweep most useful values are between 0 and 4000
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
* .phaser(2).phasersweep("<800 2000 4000>")
*
*/
['phasersweep', 'phs'],
/**
* The center frequency of the phaser in HZ. Defaults to 1000
*
* @name phasercenter
* @synonyms phc
* @param {number | Pattern} centerfrequency in HZ
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
* .phaser(2).phasercenter("<800 2000 4000>")
*
*/
['phasercenter', 'phc'],
/**
* The amount the signal is affected by the phaser effect. Defaults to 0.75
*
* @name phaserdepth
* @synonyms phd
* @param {number | Pattern} depth number between 0 and 1
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
* .phaser(2).phaserdepth("<0 .5 .75 1>")
*
*/
['phaserdepth', 'phd', 'phasdp'], // also a superdirt control
/** /**
* choose the channel the pattern is sent to in superdirt * choose the channel the pattern is sent to in superdirt
* *
@@ -481,7 +402,7 @@ const generic_params = [
* s("bd*8").lpf("1000:0 1000:10 1000:20 1000:30") * s("bd*8").lpf("1000:0 1000:10 1000:20 1000:30")
* *
*/ */
[['cutoff', 'resonance', 'lpenv'], 'ctf', 'lpf', 'lp'], [['cutoff', 'resonance'], 'ctf', 'lpf', 'lp'],
/** /**
* Sets the lowpass filter envelope modulation depth. * Sets the lowpass filter envelope modulation depth.
@@ -758,7 +679,7 @@ const generic_params = [
* .vibmod("<.25 .5 1 2 12>:8") * .vibmod("<.25 .5 1 2 12>:8")
*/ */
[['vibmod', 'vib'], 'vmod'], [['vibmod', 'vib'], 'vmod'],
[['hcutoff', 'hresonance', 'hpenv'], 'hpf', 'hp'], [['hcutoff', 'hresonance'], 'hpf', 'hp'],
/** /**
* Controls the **h**igh-**p**ass **q**-value. * Controls the **h**igh-**p**ass **q**-value.
* *
@@ -891,82 +812,6 @@ const generic_params = [
* *
*/ */
['freq'], ['freq'],
// pitch envelope
/**
* Attack time of pitch envelope.
*
* @name pattack
* @synonyms patt
* @param {number | Pattern} time time in seconds
* @example
* note("<c eb g bb>").pattack("<0 .1 .25 .5>")
*
*/
['pattack', 'patt'],
/**
* Decay time of pitch envelope.
*
* @name pdecay
* @synonyms pdec
* @param {number | Pattern} time time in seconds
* @example
* note("<c eb g bb>").pdecay("<0 .1 .25 .5>")
*
*/
['pdecay', 'pdec'],
// TODO: how to use psustain?!
['psustain', 'psus'],
/**
* Release time of pitch envelope
*
* @name prelease
* @synonyms prel
* @param {number | Pattern} time time in seconds
* @example
* note("<c eb g bb> ~")
* .release(.5) // to hear the pitch release
* .prelease("<0 .1 .25 .5>")
*
*/
['prelease', 'prel'],
/**
* Amount of pitch envelope. Negative values will flip the envelope.
* If you don't set other pitch envelope controls, `pattack:.2` will be the default.
*
* @name penv
* @param {number | Pattern} semitones change in semitones
* @example
* note("c")
* .penv("<12 7 1 .5 0 -1 -7 -12>")
*
*/
['penv'],
/**
* Curve of envelope. Defaults to linear. exponential is good for kicks
*
* @name pcurve
* @param {number | Pattern} type 0 = linear, 1 = exponential
* @example
* note("g1*2")
* .s("sine").pdec(.5)
* .penv(32)
* .pcurve("<0 1>")
*
*/
['pcurve'],
/**
* Sets the range anchor of the envelope:
* - anchor 0: range = [note, note + penv]
* - anchor 1: range = [note - penv, note]
* If you don't set an anchor, the value will default to the psustain value.
*
* @name panchor
* @param {number | Pattern} anchor anchor offset
* @example
* note("c").penv(12).panchor("<0 .5 1 .5>")
*
*/
['panchor'],
// TODO: https://tidalcycles.org/docs/configuration/MIDIOSC/control-voltage/#gate // TODO: https://tidalcycles.org/docs/configuration/MIDIOSC/control-voltage/#gate
['gate', 'gat'], ['gate', 'gat'],
// ['hatgrain'], // ['hatgrain'],
@@ -1012,12 +857,7 @@ const generic_params = [
* *
*/ */
['lsize'], ['lsize'],
/** // label for pianoroll
* Sets the displayed text for an event on the pianoroll
*
* @name label
* @param {string} label text to display
*/
['activeLabel'], ['activeLabel'],
[['label', 'activeLabel']], [['label', 'activeLabel']],
// ['lfo'], // ['lfo'],
@@ -1180,16 +1020,6 @@ const generic_params = [
* *
*/ */
['roomfade', 'rfade'], ['roomfade', 'rfade'],
/**
* Sets the sample to use as an impulse response for the reverb.
* @name iresponse
* @param {string | Pattern} sample to use as an impulse response
* @synonyms ir
* @example
* s("bd sd").room(.8).ir("<shaker_large:0 shaker_large:2>")
*
*/
[['ir', 'i'], 'iresponse'],
/** /**
* Sets the room size of the reverb, see {@link room}. * Sets the room size of the reverb, see {@link room}.
* When this property is changed, the reverb will be recaculated, so only change this sparsely.. * When this property is changed, the reverb will be recaculated, so only change this sparsely..
@@ -1221,21 +1051,6 @@ const generic_params = [
* *
*/ */
['shape'], ['shape'],
/**
* Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")`
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
*
* @name compressor
* @example
* s("bd sd,hh*4")
* .compressor("-20:20:10:.002:.02")
*
*/
[['compressor', 'compressorRatio', 'compressorKnee', 'compressorAttack', 'compressorRelease']],
['compressorKnee'],
['compressorRatio'],
['compressorAttack'],
['compressorRelease'],
/** /**
* Changes the speed of sample playback, i.e. a cheap way of changing pitch. * Changes the speed of sample playback, i.e. a cheap way of changing pitch.
* *
@@ -1285,7 +1100,7 @@ const generic_params = [
* Formant filter to make things sound like vowels. * Formant filter to make things sound like vowels.
* *
* @name vowel * @name vowel
* @param {string | Pattern} vowel You can use a e i o u ae aa oe ue y uh un en an on, corresponding to [a] [e] [i] [o] [u] [æ] [ɑ] [ø] [y] [ɯ] [ʌ] [œ̃] [ɛ̃] [ɑ̃] [ɔ̃]. Aliases: aa = å = ɑ, oe = ø = ö, y = ı, ae = æ. * @param {string | Pattern} vowel You can use a e i o u.
* @example * @example
* note("c2 <eb2 <g2 g1>>").s('sawtooth') * note("c2 <eb2 <g2 g1>>").s('sawtooth')
* .vowel("<a e i <o u>>") * .vowel("<a e i <o u>>")
@@ -1303,16 +1118,6 @@ const generic_params = [
* @name waveloss * @name waveloss
*/ */
['waveloss'], ['waveloss'],
/*
* Noise crackle density
*
* @name density
* @param {number | Pattern} density between 0 and x
* @example
* s("crackle*4").density("<0.01 0.04 0.2 0.5>".slow(4))
*
*/
['density'],
// TODO: midi effects? // TODO: midi effects?
['dur'], ['dur'],
// ['modwheel'], // ['modwheel'],
@@ -1330,6 +1135,9 @@ const generic_params = [
*/ */
['tremolodepth', 'tremdp'], ['tremolodepth', 'tremdp'],
['tremolorate', 'tremr'], ['tremolorate', 'tremr'],
// TODO: doesn't seem to do anything
['phaserdepth', 'phasdp'],
['phaserrate', 'phasr'],
['fshift'], ['fshift'],
['fshiftnote'], ['fshiftnote'],
@@ -1454,36 +1262,15 @@ generic_params.forEach(([names, ...aliases]) => {
controls.createParams = (...names) => controls.createParams = (...names) =>
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {}); names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
/**
* ADSR envelope: Combination of Attack, Decay, Sustain, and Release.
*
* @name adsr
* @param {number | Pattern} time attack time in seconds
* @param {number | Pattern} time decay time in seconds
* @param {number | Pattern} gain sustain level (0 to 1)
* @param {number | Pattern} time release time in seconds
* @example
* note("<c3 bb2 f3 eb3>").sound("sawtooth").lpf(600).adsr(".1:.1:.5:.2")
*/
controls.adsr = register('adsr', (adsr, pat) => { controls.adsr = register('adsr', (adsr, pat) => {
adsr = !Array.isArray(adsr) ? [adsr] : adsr; adsr = !Array.isArray(adsr) ? [adsr] : adsr;
const [attack, decay, sustain, release] = adsr; const [attack, decay, sustain, release] = adsr;
return pat.set({ attack, decay, sustain, release }); return pat.set({ attack, decay, sustain, release });
}); });
controls.ad = register('ad', (t, pat) => { controls.ds = register('ds', (ds, pat) => {
t = !Array.isArray(t) ? [t] : t; ds = !Array.isArray(ds) ? [ds] : ds;
const [attack, decay = attack] = t; const [decay, sustain] = ds;
return pat.attack(attack).decay(decay);
});
controls.ds = register('ds', (t, pat) => {
t = !Array.isArray(t) ? [t] : t;
const [decay, sustain = 0] = t;
return pat.set({ decay, sustain }); return pat.set({ decay, sustain });
}); });
controls.ds = register('ar', (t, pat) => {
t = !Array.isArray(t) ? [t] : t;
const [attack, release = attack] = t;
return pat.set({ attack, release });
});
export default controls; export default controls;
+3 -20
View File
@@ -11,14 +11,13 @@ 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;
this.num_ticks_since_cps_change = 0;
this.lastTick = 0; // absolute time when last tick (clock callback) happened this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.lastBegin = 0; // query begin of last tick this.lastBegin = 0; // query begin of last tick
this.lastEnd = 0; // query end of last tick this.lastEnd = 0; // query end of last tick
this.getTime = getTime; // get absolute time this.getTime = getTime; // get absolute time
this.num_cycles_since_last_cps_change = 0;
this.onToggle = onToggle; this.onToggle = onToggle;
this.latency = latency; // fixed trigger time offset this.latency = latency; // fixed trigger time offset
const round = (x) => Math.round(x * 1000) / 1000;
this.clock = createClock( this.clock = createClock(
getTime, getTime,
// called slightly before each cycle // called slightly before each cycle
@@ -26,24 +25,14 @@ export class Cyclist {
if (tick === 0) { if (tick === 0) {
this.origin = phase; this.origin = phase;
} }
if (this.num_ticks_since_cps_change === 0) {
this.num_cycles_since_last_cps_change = this.lastEnd;
}
this.num_ticks_since_cps_change++;
try { try {
const time = getTime(); const time = getTime();
const begin = this.lastEnd; const begin = this.lastEnd;
this.lastBegin = begin; this.lastBegin = begin;
const end = round(begin + duration * this.cps);
//convert ticks to cycles, so you can query the pattern for events
const eventLength = duration * this.cps;
const end = this.num_cycles_since_last_cps_change + this.num_ticks_since_cps_change * eventLength;
this.lastEnd = end; this.lastEnd = end;
// query the pattern for events
const haps = this.pattern.queryArc(begin, end); const haps = this.pattern.queryArc(begin, end);
const tickdeadline = phase - time; // time left till phase begins
const tickdeadline = phase - time; // time left until the phase is a whole number
this.lastTick = time + tickdeadline; this.lastTick = time + tickdeadline;
haps.forEach((hap) => { haps.forEach((hap) => {
@@ -70,8 +59,6 @@ export class Cyclist {
this.onToggle?.(v); this.onToggle?.(v);
} }
start() { start() {
this.num_ticks_since_cps_change = 0;
this.num_cycles_since_last_cps_change = 0;
if (!this.pattern) { if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.'); throw new Error('Scheduler: no pattern set! call .setPattern first.');
} }
@@ -97,11 +84,7 @@ export class Cyclist {
} }
} }
setCps(cps = 1) { setCps(cps = 1) {
if (this.cps === cps) {
return;
}
this.cps = cps; this.cps = cps;
this.num_ticks_since_cps_change = 0;
} }
log(begin, end, haps) { log(begin, end, haps) {
const onsets = haps.filter((h) => h.hasOnset()); const onsets = haps.filter((h) => h.hasOnset());
+4 -8
View File
@@ -111,6 +111,8 @@ export class Framer {
// see vite-vanilla-repl-cm6 for an example // see vite-vanilla-repl-cm6 for an example
export class Drawer { export class Drawer {
constructor(onDraw, drawTime) { constructor(onDraw, drawTime) {
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
lookbehind = Math.abs(lookbehind);
this.visibleHaps = []; this.visibleHaps = [];
this.lastFrame = null; this.lastFrame = null;
this.drawTime = drawTime; this.drawTime = drawTime;
@@ -120,8 +122,6 @@ export class Drawer {
console.warn('Drawer: no scheduler'); console.warn('Drawer: no scheduler');
return; return;
} }
const lookbehind = Math.abs(this.drawTime[0]);
const lookahead = this.drawTime[1];
// calculate current frame time (think right side of screen for pianoroll) // calculate current frame time (think right side of screen for pianoroll)
const phase = this.scheduler.now() + lookahead; const phase = this.scheduler.now() + lookahead;
// first frame just captures the phase // first frame just captures the phase
@@ -145,16 +145,12 @@ export class Drawer {
}, },
); );
} }
setDrawTime(drawTime) { invalidate(scheduler = this.scheduler) {
this.drawTime = drawTime;
}
invalidate(scheduler = this.scheduler, t) {
if (!scheduler) { if (!scheduler) {
return; return;
} }
// TODO: scheduler.now() seems to move even when it's stopped, this hints at a bug...
t = t ?? scheduler.now();
this.scheduler = scheduler; this.scheduler = scheduler;
const t = scheduler.now();
let [_, lookahead] = this.drawTime; let [_, lookahead] = this.drawTime;
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1]; const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
// remove all future haps // remove all future haps
-14
View File
@@ -145,8 +145,6 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
* so there will be no gaps. * so there will be no gaps.
* @name euclidLegato * @name euclidLegato
* @memberof Pattern * @memberof Pattern
* @param {number} pulses the number of onsets / beats
* @param {number} steps the number of steps to fill
* @example * @example
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8) * n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
*/ */
@@ -168,18 +166,6 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
return _euclidLegato(pulses, steps, 0, pat); return _euclidLegato(pulses, steps, 0, pat);
}); });
/**
* Similar to `euclid`, but each pulse is held until the next pulse,
* so there will be no gaps, and has an additional parameter for 'rotating'
* the resulting sequence
* @name euclidLegatoRot
* @memberof Pattern
* @param {number} pulses the number of onsets / beats
* @param {number} steps the number of steps to fill
* @param {number} rotation offset in steps
* @example
* note("c3").euclidLegatoRot(3,5,2)
*/
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) { export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
return _euclidLegato(pulses, steps, rotation, pat); return _euclidLegato(pulses, steps, rotation, pat);
}); });
+5
View File
@@ -47,5 +47,10 @@ export const evaluate = async (code, transpiler) => {
// 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 };
let evaluated = await safeEval(code, options); let evaluated = await safeEval(code, options);
if (!isPattern(evaluated)) {
console.log('evaluated', evaluated);
const message = `got "${typeof evaluated}" instead of pattern`;
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, meta };
}; };
@@ -1,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -0,0 +1,8 @@
# vite-vanilla-repl-cm6
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
```sh
npm i
npm run dev
```
@@ -1,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -9,7 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {
"vite": "^5.0.10" "vite": "^4.3.2"
}, },
"dependencies": { "dependencies": {
"@strudel/codemirror": "workspace:*", "@strudel/codemirror": "workspace:*",
@@ -1,6 +1,6 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0 export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main') await samples('github:felixroos/samples/main')
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/') await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2" "<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
// make it 22 edo // make it 22 edo
@@ -34,7 +34,7 @@ await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tid
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0 export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main') await samples('github:felixroos/samples/main')
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/') await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', { addVoicings('hip', {
m11: ['2M 3m 4P 7m'], m11: ['2M 3m 4P 7m'],
@@ -70,7 +70,7 @@ export const funk42 = `// froos - how to funk in 42 lines of code
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4 // thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
await samples('github:felixroos/samples/main') await samples('github:felixroos/samples/main')
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/') await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5) setcps(.5)
@@ -0,0 +1 @@
!dist
@@ -0,0 +1,8 @@
# vite-vanilla-repl
This folder demonstrates how to set up a strudel repl using vite and vanilla JS. Run it using:
```sh
npm i
npm run dev
```
@@ -0,0 +1 @@
import{b as s,h as i,m,a as n,c as p,p as t}from"./index.4cbc0a10.js";export{s as SyntaxError,i as h,m as mini,n as minify,p as parse,t as patternifyAST};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{g as s,f as d,i as t,n as r,l as u,j as o,d as f,k as p,r as g,s as i,w as l,e as m}from"./index.4cbc0a10.js";export{s as getAudioContext,d as getCachedBuffer,t as getLoadedBuffer,r as getLoadedSamples,u as loadBuffer,o as loadGithubSamples,f as panic,p as resetLoadedSamples,g as reverseBuffer,i as samples,l as webaudioOutput,m as webaudioOutputTrigger};
@@ -0,0 +1,36 @@
<!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>
<script type="module" crossorigin src="/tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/assets/index.4cbc0a10.js"></script>
</head>
<body style="margin: 0; background: #222">
<div style="display: grid; height: 100vh">
<textarea
id="text"
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
spellcheck="false"
></textarea>
</div>
<button
id="start"
style="
position: absolute;
border-radius: 10px;
top: 20px;
right: 20px;
padding: 20px;
border: 2px solid white;
background: transparent;
color: white;
cursor: pointer;
"
>
evaluate
</button>
<div id="output"></div>
</body>
</html>
@@ -1,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -10,7 +10,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {
"vite": "^5.0.10" "vite": "^4.3.3"
}, },
"dependencies": { "dependencies": {
"@strudel.cycles/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
+12
View File
@@ -0,0 +1,12 @@
/*
gist.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/gist.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/>.
*/
// this is a shortcut to eval code from a gist
// why? to be able to shorten strudel code + e.g. be able to change instruments after links have been generated
export default (route, cache = true) =>
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${cache ? '' : Date.now()}`)
.then((res) => res.text())
.then((code) => eval(code));
+1
View File
@@ -27,6 +27,7 @@ export * from './pianoroll.mjs';
export * from './spiral.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';
// below won't work with runtime.mjs (json import fails) // below won't work with runtime.mjs (json import fails)
/* import * as p from './package.json'; /* import * as p from './package.json';
export const version = p.version; */ export const version = p.version; */
+5 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/core", "name": "@strudel.cycles/core",
"version": "0.10.0", "version": "0.9.0",
"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,13 +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.3.7" "fraction.js": "^4.2.0"
}, },
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2", "gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": { "devDependencies": {
"vite": "^5.0.10", "vite": "^4.3.3",
"vitest": "^1.1.0" "vitest": "^0.33.0"
} }
} }
+12 -92
View File
@@ -1178,7 +1178,7 @@ export function reify(thing) {
* @return {Pattern} * @return {Pattern}
* @synonyms polyrhythm, pr * @synonyms polyrhythm, pr
* @example * @example
* stack("g3", "b3", ["e4", "d4"]).note() // "g3,b3,[e4,d4]".note() * stack(g3, b3, [e4, d4]).note() // "g3,b3,[e4,d4]".note()
*/ */
export function stack(...pats) { export function stack(...pats) {
// Array test here is to avoid infinite recursions.. // Array test here is to avoid infinite recursions..
@@ -1193,7 +1193,7 @@ export function stack(...pats) {
* *
* @return {Pattern} * @return {Pattern}
* @example * @example
* slowcat("e5", "b4", ["d5", "c5"]) * slowcat(e5, b4, [d5, c5])
* *
*/ */
export function slowcat(...pats) { export function slowcat(...pats) {
@@ -1237,7 +1237,7 @@ export function slowcatPrime(...pats) {
* @synonyms slowcat * @synonyms slowcat
* @return {Pattern} * @return {Pattern}
* @example * @example
* cat("e5", "b4", ["d5", "c5"]).note() // "<e5 b4 [d5 c5]>".note() * cat(e5, b4, [d5, c5]).note() // "<e5 b4 [d5 c5]>".note()
* *
*/ */
export function cat(...pats) { export function cat(...pats) {
@@ -1247,7 +1247,7 @@ export function cat(...pats) {
/** Like {@link Pattern.seq}, but each step has a length, relative to the whole. /** Like {@link Pattern.seq}, but each step has a length, relative to the whole.
* @return {Pattern} * @return {Pattern}
* @example * @example
* timeCat([3,"e3"],[1, "g3"]).note() // "e3@3 g3".note() * timeCat([3,e3],[1, g3]).note() // "e3@3 g3".note()
*/ */
export function timeCat(...timepats) { export function timeCat(...timepats) {
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0)); const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
@@ -1287,7 +1287,7 @@ export function sequence(...pats) {
/** Like **cat**, but the items are crammed into one cycle. /** Like **cat**, but the items are crammed into one cycle.
* @synonyms fastcat, sequence * @synonyms fastcat, sequence
* @example * @example
* seq("e5", "b4", ["d5", "c5"]).note() // "e5 b4 [d5 c5]".note() * seq(e5, b4, [d5, c5]).note() // "e5 b4 [d5 c5]".note()
* *
*/ */
export function seq(...pats) { export function seq(...pats) {
@@ -1975,9 +1975,9 @@ export const press = register('press', function (pat) {
* s("hh*3") * s("hh*3")
* ) * )
*/ */
Pattern.prototype.hush = function () { export const hush = register('hush', function (pat) {
return silence; return silence;
}; });
/** /**
* Applies `rev` to a pattern every other cycle, so that the pattern alternates between forwards and backwards. * Applies `rev` to a pattern every other cycle, so that the pattern alternates between forwards and backwards.
@@ -1985,7 +1985,7 @@ Pattern.prototype.hush = function () {
* note("c d e g").palindrome() * note("c d e g").palindrome()
*/ */
export const palindrome = register('palindrome', function (pat) { export const palindrome = register('palindrome', function (pat) {
return pat.lastOf(2, rev); return pat.every(2, rev);
}); });
/** /**
@@ -2100,43 +2100,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 +2132,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));
@@ -2189,19 +2154,8 @@ export const duration = register('duration', function (value, pat) {
return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value))); return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
}); });
export const hsla = register('hsla', (h, s, l, a, pat) => {
return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`);
});
export const hsl = register('hsl', (h, s, l, pat) => {
return pat.color(`hsl(${h}turn,${s * 100}%,${l * 100}%)`);
});
/** /**
* 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) {
@@ -2263,14 +2217,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 }));
@@ -2403,29 +2349,3 @@ export const ref = (accessor) =>
pure(1) pure(1)
.withValue(() => reify(accessor())) .withValue(() => reify(accessor()))
.innerJoin(); .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);
};
+3 -46
View File
@@ -56,40 +56,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,
@@ -256,21 +222,12 @@ export function getDrawOptions(drawTime, options = {}) {
return { fold: 1, ...options, cycles, playhead }; return { fold: 1, ...options, cycles, playhead };
} }
export const getPunchcardPainter =
(options = {}) =>
(ctx, time, haps, drawTime, paintOptions = {}) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) });
Pattern.prototype.punchcard = function (options) { Pattern.prototype.punchcard = function (options) {
return this.onPaint(getPunchcardPainter(options)); return this.onPaint((ctx, time, haps, drawTime, paintOptions = {}) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) }),
);
}; };
/**
* Displays a vertical pianoroll with event labels.
* Supports all the same options as pianoroll.
*
* @name wordfall
*/
Pattern.prototype.wordfall = function (options) { Pattern.prototype.wordfall = function (options) {
return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options }); return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options });
}; };
+8 -92
View File
@@ -3,7 +3,7 @@ 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 { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; import { register } from './pattern.mjs';
export function repl({ export function repl({
interval, interval,
@@ -16,94 +16,42 @@ export function repl({
transpiler, transpiler,
onToggle, onToggle,
editPattern, editPattern,
onUpdateState, createCyclist,
}) { }) {
const state = { const cyclistParams = {
schedulerError: undefined,
evalError: undefined,
code: '// LOADING',
activeCode: '// LOADING',
pattern: undefined,
miniLocations: [],
widgets: [],
pending: false,
started: false,
};
const updateState = (update) => {
Object.assign(state, update);
state.isDirty = state.code !== state.activeCode;
state.error = state.evalError || state.schedulerError;
onUpdateState?.(state);
};
const scheduler = new Cyclist({
interval, interval,
onTrigger: getTrigger({ defaultOutput, getTime }), onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError, onError: onSchedulerError,
getTime, getTime,
onToggle: (started) => { onToggle,
updateState({ started });
onToggle?.(started);
},
});
let pPatterns = {};
let allTransform;
const hush = function () {
pPatterns = {};
allTransform = undefined;
return silence;
}; };
const scheduler = createCyclist?.(cyclistParams) || new Cyclist(cyclistParams);
const setPattern = (pattern, autostart = true) => { const setPattern = (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern; pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart); scheduler.setPattern(pattern, autostart);
}; };
setTime(() => scheduler.now()); // TODO: refactor? setTime(() => scheduler.now()); // TODO: refactor?
const evaluate = async (code, autostart = true, shouldHush = 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 {
updateState({ code, pending: true });
await beforeEval?.({ code }); await beforeEval?.({ code });
shouldHush && hush();
let { pattern, meta } = await _evaluate(code, transpiler); let { pattern, meta } = await _evaluate(code, transpiler);
if (Object.keys(pPatterns).length) {
pattern = stack(...Object.values(pPatterns));
}
if (allTransform) {
pattern = allTransform(pattern);
}
if (!isPattern(pattern)) {
const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
}
logger(`[eval] code updated`); logger(`[eval] code updated`);
setPattern(pattern, autostart); setPattern(pattern, autostart);
updateState({
miniLocations: meta?.miniLocations || [],
widgets: meta?.widgets || [],
activeCode: code,
pattern,
evalError: undefined,
schedulerError: undefined,
pending: false,
});
afterEval?.({ code, pattern, meta }); 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');
updateState({ evalError: err, 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();
const setCps = (cps) => scheduler.setCps(cps); const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60); const setCpm = (cpm) => scheduler.setCps(cpm / 60);
@@ -112,36 +60,6 @@ export function repl({
return pat.loopAtCps(cycles, scheduler.cps); return pat.loopAtCps(cycles, scheduler.cps);
}); });
Pattern.prototype.p = function (id) {
pPatterns[id] = this;
return this;
};
Pattern.prototype.q = function (id) {
return silence;
};
const all = function (transform) {
allTransform = transform;
return silence;
};
try {
for (let i = 1; i < 10; ++i) {
Object.defineProperty(Pattern.prototype, `d${i}`, {
get() {
return this.p(i);
},
});
Object.defineProperty(Pattern.prototype, `p${i}`, {
get() {
return this.p(i);
},
});
Pattern.prototype[`q${i}`] = silence;
}
} catch (err) {
// already defined..
}
const fit = register('fit', (pat) => const fit = register('fit', (pat) =>
pat.withHap((hap) => pat.withHap((hap) =>
hap.withValue((v) => ({ hap.withValue((v) => ({
@@ -155,15 +73,13 @@ export function repl({
evalScope({ evalScope({
loopAt, loopAt,
fit, fit,
all,
hush,
setCps, setCps,
setcps: setCps, setcps: setCps,
setCpm, setCpm,
setcpm: setCpm, setcpm: setCpm,
}); });
const setCode = (code) => updateState({ code });
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
} }
export const getTrigger = export const getTrigger =
+2 -55
View File
@@ -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
@@ -101,7 +101,6 @@ const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
const timeToRandsPrime = (seed, n) => { const timeToRandsPrime = (seed, n) => {
const result = []; const result = [];
// eslint-disable-next-line
for (let i = 0; i < n; ++n) { for (let i = 0; i < n; ++n) {
result.push(intSeedToRand(seed)); result.push(intSeedToRand(seed));
seed = xorwise(seed); seed = xorwise(seed);
@@ -156,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
@@ -219,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();
+11 -65
View File
@@ -20,7 +20,6 @@ import {
slowcat, slowcat,
cat, cat,
sequence, sequence,
palindrome,
polymeter, polymeter,
polymeterSteps, polymeterSteps,
polyrhythm, polyrhythm,
@@ -183,17 +182,13 @@ describe('Pattern', () => {
}); });
it('can Trig() structure', () => { it('can Trig() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trig(20, 30).early(2),
.add.trig(20, 30)
.early(2),
sequence(26, 27, 36, 37), sequence(26, 27, 36, 37),
); );
}); });
it('can Trigzero() structure', () => { it('can Trigzero() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trigzero(20, 30).early(2),
.add.trigzero(20, 30)
.early(2),
sequence(21, 22, 31, 32), sequence(21, 22, 31, 32),
); );
}); });
@@ -235,17 +230,13 @@ describe('Pattern', () => {
}); });
it('can Trig() structure', () => { it('can Trig() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trig(20, 30).early(2),
.keep.trig(20, 30)
.early(2),
sequence(6, 7, 6, 7), sequence(6, 7, 6, 7),
); );
}); });
it('can Trigzero() structure', () => { it('can Trigzero() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trigzero(20, 30).early(2),
.keep.trigzero(20, 30)
.early(2),
sequence(1, 2, 1, 2), sequence(1, 2, 1, 2),
); );
}); });
@@ -281,17 +272,13 @@ describe('Pattern', () => {
}); });
it('can Trig() structure', () => { it('can Trig() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trig(false, true).early(2),
.keepif.trig(false, true)
.early(2),
sequence(silence, silence, 6, 7), sequence(silence, silence, 6, 7),
); );
}); });
it('can Trigzero() structure', () => { it('can Trigzero() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trigzero(false, true).early(2),
.keepif.trigzero(false, true)
.early(2),
sequence(silence, silence, 1, 2), sequence(silence, silence, 1, 2),
); );
}); });
@@ -584,18 +571,6 @@ describe('Pattern', () => {
expect(sequence(1, 2, 3).firstCycle()).toStrictEqual(fastcat(1, 2, 3).firstCycle()); expect(sequence(1, 2, 3).firstCycle()).toStrictEqual(fastcat(1, 2, 3).firstCycle());
}); });
}); });
describe('palindrome()', () => {
it('Can create palindrome', () => {
expect(
fastcat('a', 'b', 'c')
.palindrome()
.fast(2)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.value),
).toStrictEqual(['a', 'b', 'c', 'c', 'b', 'a']);
});
});
describe('polyrhythm()', () => { describe('polyrhythm()', () => {
it('Can layer up cycles', () => { it('Can layer up cycles', () => {
expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual( expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual(
@@ -663,11 +638,7 @@ describe('Pattern', () => {
}); });
describe('struct()', () => { describe('struct()', () => {
it('Can restructure a discrete pattern', () => { it('Can restructure a discrete pattern', () => {
expect( expect(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle()).toStrictEqual([
sequence('a', 'b')
.struct(sequence(true, true, true))
.firstCycle(),
).toStrictEqual([
hap(ts(0, third), ts(0, third), 'a'), hap(ts(0, third), ts(0, third), 'a'),
hap(ts(third, twothirds), ts(third, 0.5), 'a'), hap(ts(third, twothirds), ts(third, 0.5), 'a'),
hap(ts(third, twothirds), ts(0.5, twothirds), 'b'), hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
@@ -698,11 +669,7 @@ describe('Pattern', () => {
}); });
describe('mask()', () => { describe('mask()', () => {
it('Can fragment a pattern', () => { it('Can fragment a pattern', () => {
expect( expect(sequence('a', 'b').mask(sequence(true, true, true)).firstCycle()).toStrictEqual([
sequence('a', 'b')
.mask(sequence(true, true, true))
.firstCycle(),
).toStrictEqual([
hap(ts(0, 0.5), ts(0, third), 'a'), hap(ts(0, 0.5), ts(0, third), 'a'),
hap(ts(0, 0.5), ts(third, 0.5), 'a'), hap(ts(0, 0.5), ts(third, 0.5), 'a'),
hap(ts(0.5, 1), ts(0.5, twothirds), 'b'), hap(ts(0.5, 1), ts(0.5, twothirds), 'b'),
@@ -971,11 +938,9 @@ describe('Pattern', () => {
expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1); expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1);
}); });
it('Doesnt merge two overlapping haps', () => { it('Doesnt merge two overlapping haps', () => {
expect( expect(stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(
stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1)) 2,
.defragmentHaps() );
.firstCycle().length,
).toStrictEqual(2);
}); });
it('Doesnt merge two touching haps with different values', () => { it('Doesnt merge two touching haps with different values', () => {
expect(stack(pure('a').mask(1, 0), pure('b').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2); expect(stack(pure('a').mask(1, 0), pure('b').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
@@ -1038,23 +1003,4 @@ describe('Pattern', () => {
); );
}); });
}); });
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]);
});
});
}); });
+2
View File
@@ -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
} }
}; };
+8 -50
View File
@@ -4,8 +4,6 @@ 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 { logger } from './logger.mjs';
// 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][#bsf]*[0-9]?$/.test(name);
@@ -86,18 +84,6 @@ export const midi2note = (n) => {
// modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does) // modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does)
export const _mod = (n, m) => ((n % m) + m) % m; export const _mod = (n, m) => ((n % m) + m) % m;
export function nanFallback(value, fallback = 0) {
if (isNaN(Number(value))) {
logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
return fallback;
}
return value;
}
// round to nearest int, negative numbers will output a subtracted index
export const getSoundIndex = (n, numSounds) => {
return _mod(Math.round(nanFallback(n ?? 0, 0)), numSounds);
};
export const getPlayableNoteValue = (hap) => { export const getPlayableNoteValue = (hap) => {
let { value, context } = hap; let { value, context } = hap;
let note = value; let note = value;
@@ -276,43 +262,15 @@ export const sol2note = (n, notation = 'letters') => {
notation === 'solfeggio' notation === 'solfeggio'
? solfeggio /*check if its is any of the following*/ ? solfeggio /*check if its is any of the following*/
: notation === 'indian' : notation === 'indian'
? indian ? indian
: notation === 'german' : notation === 'german'
? german ? german
: notation === 'byzantine' : notation === 'byzantine'
? byzantine ? byzantine
: notation === 'japanese' : notation === 'japanese'
? japanese ? japanese
: english; /*if not use standard version*/ : english; /*if not use standard version*/
const note = pc[n % 12]; /*calculating the midi value to the note*/ const note = pc[n % 12]; /*calculating the midi value to the note*/
const oct = Math.floor(n / 12) - 1; const oct = Math.floor(n / 12) - 1;
return note + oct; return note + oct;
}; };
// code hashing helpers
export function unicodeToBase64(text) {
const utf8Bytes = new TextEncoder().encode(text);
const base64String = btoa(String.fromCharCode(...utf8Bytes));
return base64String;
}
export function base64ToUnicode(base64String) {
const utf8Bytes = new Uint8Array(
atob(base64String)
.split('')
.map((char) => char.charCodeAt(0)),
);
const decodedText = new TextDecoder().decode(utf8Bytes);
return decodedText;
}
export function code2hash(code) {
return encodeURIComponent(unicodeToBase64(code));
//return '#' + encodeURIComponent(btoa(code));
}
export function hash2code(hash) {
return base64ToUnicode(decodeURIComponent(hash));
//return atob(decodeURIComponent(codeParam || ''));
}
+1 -1
View File
@@ -9,7 +9,7 @@ export default defineConfig({
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'], formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext], fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
}, },
rollupOptions: { rollupOptions: {
external: [...Object.keys(dependencies)], external: [...Object.keys(dependencies)],
+2 -1
View File
@@ -43,7 +43,8 @@ function createClock(
clear(); clear();
}; };
const getPhase = () => phase; const getPhase = () => phase;
const setPhase = (_phase) => (phase = _phase);
// setCallback // setCallback
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; return { setDuration, start, stop, pause, duration, interval, getPhase, setPhase, minLatency };
} }
export default createClock; export default createClock;
-3
View File
@@ -1,3 +0,0 @@
# @strudel.cycles/csound
**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/csound](https://www.npmjs.com/package/@strudel/csound).
+1 -1
View File
@@ -137,7 +137,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) => {
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/csound", "name": "@strudel.cycles/csound",
"version": "0.10.0", "version": "0.9.0",
"description": "csound bindings for strudel", "description": "csound bindings for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
@@ -37,6 +37,6 @@
"@strudel.cycles/webaudio": "workspace:*" "@strudel.cycles/webaudio": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"vite": "^5.0.10" "vite": "^4.3.3"
} }
} }
+1 -1
View File
@@ -9,7 +9,7 @@ export default defineConfig({
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'], formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext], fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
}, },
rollupOptions: { rollupOptions: {
external: [...Object.keys(dependencies)], external: [...Object.keys(dependencies)],
+77
View File
@@ -0,0 +1,77 @@
import { Cyclist } from '@strudel.cycles/core';
import { logger } from '../core/logger.mjs';
import { Invoke } from '../../website/src/tauri.mjs';
import { listen } from '@tauri-apps/api/event';
export class CyclistBridge extends Cyclist {
constructor(params) {
super(params);
this.start_timer;
this.abeLinkListener = listen('abelink-event', async (e) => {
const payload = e?.payload;
if (payload == null) {
return;
}
const { started, cps, phase, timestamp } = payload;
if (cps !== this.cps) {
this.setCps(cps);
}
// TODO: I'm not sure how to hook this up this phase adjustment in Strudel
// a phase adjustment message is sent every 30 seconds from backend to keep clocks in sync
const phaseDiff = Math.abs(phase - this.clock.getPhase());
if (phaseDiff > 0.1) {
console.log('set phase from', this.clock.getPhase(), 'to', phase);
// hmmm this seems wrong...
//this.clock.setPhase(phase);
}
if (this.started !== started && started != null) {
if (started) {
// the time delay in ms that seems to occur when starting the clock. Unsure if this is standard across all clients
const evaluationTime = 140;
// when start message comes from abelink, delay starting cyclist clock until the start of the next abelink phase
this.start_timer = window.setTimeout(() => {
// TODO: evaluate the code so if another source triggers the play there will not be an error
logger('[cyclist] start');
this.clock.start();
this.setStarted(true);
}, timestamp - Date.now() - evaluationTime);
} else {
this.stop();
}
}
});
}
start() {
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
const linkmsg = {
cps: this.cps,
started: true,
timestamp: Date.now(),
phase: this.clock.getPhase(),
};
Invoke('sendabelinkmsg', { linkmsg });
}
stop() {
logger('[cyclist] stop');
this.clock.stop();
this.lastEnd = 0;
this.setStarted(false);
const linkmsg = {
// TODO: change this to value of "main" clock cps
cps: 0,
started: false,
timestamp: Date.now(),
phase: this.clock.getPhase(),
};
Invoke('sendabelinkmsg', { linkmsg });
}
}
+1
View File
@@ -7,3 +7,4 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './midibridge.mjs'; export * from './midibridge.mjs';
export * from './utils.mjs'; export * from './utils.mjs';
export * from './oscbridge.mjs'; export * from './oscbridge.mjs';
export * from './cyclistbridge.mjs';
+1 -1
View File
@@ -23,7 +23,7 @@
}, },
"dependencies": { "dependencies": {
"@strudel.cycles/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
"@tauri-apps/api": "^1.5.3" "@tauri-apps/api": "^1.4.0"
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme" "homepage": "https://github.com/tidalcycles/strudel#readme"
} }
-2
View File
@@ -1,7 +1,5 @@
# @strudel.cycles/embed # @strudel.cycles/embed
**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/embed](https://www.npmjs.com/package/@strudel/embed).
This package contains a embeddable web component for the Strudel REPL. This package contains a embeddable web component for the Strudel REPL.
## Usage ## Usage
+1 -1
View File
@@ -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');
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/embed", "name": "@strudel.cycles/embed",
"version": "0.10.0", "version": "0.2.0",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe", "description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js", "main": "embed.js",
"type": "module", "type": "module",
-35
View File
@@ -1,35 +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!
### options
You can also pass options to the `initHydra` function. These can be used to set [hydra options](https://github.com/hydra-synth/hydra-synth#api) + these strudel specific options:
- `feedStrudel`: sends the strudel canvas to `s0`. The strudel canvas is used to draw `pianoroll`, `spiral`, `scope` etc..
## 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')
)
```
-40
View File
@@ -1,40 +0,0 @@
import { getDrawContext } from '@strudel.cycles/core';
let latestOptions;
function appendCanvas(c) {
const { canvas: testCanvas } = getDrawContext();
c.canvas.id = 'hydra-canvas';
c.canvas.style.position = 'fixed';
c.canvas.style.top = '0px';
testCanvas.after(c.canvas);
return testCanvas;
}
export async function initHydra(options = {}) {
// reset if options have changed since last init
if (latestOptions && JSON.stringify(latestOptions) !== JSON.stringify(options)) {
document.getElementById('hydra-canvas').remove();
}
latestOptions = options;
//load and init hydra
if (!document.getElementById('hydra-canvas')) {
console.log('reinit..');
const {
src = 'https://unpkg.com/hydra-synth',
feedStrudel = false,
...hydraConfig
} = { detectAudio: false, ...options };
await import(/* @vite-ignore */ src);
const hydra = new Hydra(hydraConfig);
if (feedStrudel) {
const { canvas } = getDrawContext();
canvas.style.display = 'none';
hydra.synth.s0.init({ src: canvas });
}
appendCanvas(hydra);
}
}
export const H = (p) => () => p.queryArc(getTime(), getTime())[0].value;
-43
View File
@@ -1,43 +0,0 @@
{
"name": "@strudel/hydra",
"version": "0.10.0",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"server": "node server.js",
"tidal-sniffer": "node tidal-sniffer.js",
"client": "npx serve -p 4321",
"build-bin": "npx pkg server.js --targets node16-macos-x64,node16-win-x64,node16-linux-x64 --out-path bin",
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"hydra-synth": "^1.3.29"
},
"devDependencies": {
"pkg": "^5.8.1",
"vite": "^5.0.10"
}
}
-19
View File
@@ -1,19 +0,0 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'hydra.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
-2
View File
@@ -1,7 +1,5 @@
# @strudel.cycles/midi # @strudel.cycles/midi
**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/midi](https://www.npmjs.com/package/@strudel/midi).
This package adds midi functionality to strudel Patterns. This package adds midi functionality to strudel Patterns.
## Install ## Install
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/midi", "name": "@strudel.cycles/midi",
"version": "0.10.0", "version": "0.9.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.8" "webmidi": "^3.1.5"
}, },
"devDependencies": { "devDependencies": {
"vite": "^5.0.10" "vite": "^4.3.3"
} }
} }
+1 -1
View File
@@ -9,7 +9,7 @@ export default defineConfig({
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'], formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext], fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
}, },
rollupOptions: { rollupOptions: {
external: [...Object.keys(dependencies)], external: [...Object.keys(dependencies)],
+2 -4
View File
@@ -1,7 +1,5 @@
# @strudel.cycles/mini # @strudel.cycles/mini
**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/mini](https://www.npmjs.com/package/@strudel/mini).
This package contains the mini notation parser and pattern generator. This package contains the mini notation parser and pattern generator.
## Install ## Install
@@ -34,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
@@ -42,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
``` ```
File diff suppressed because one or more lines are too long
+4 -27
View File
@@ -95,7 +95,7 @@ DIGIT = [0-9]
// ------------------ delimiters --------------------------- // ------------------ delimiters ---------------------------
ws "whitespace" = [ \n\r\t\u00A0]* ws "whitespace" = [ \n\r\t]*
comma = ws "," ws comma = ws "," ws
pipe = ws "|" ws pipe = ws "|" ws
quote = '"' / "'" quote = '"' / "'"
@@ -103,8 +103,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]]
@@ -119,8 +118,8 @@ polymeter_steps = "%"a:slice
// define a step-per-cycle timeline e.g <1 3 [3 5]>. We simply defer to a sequence and // define a step-per-cycle timeline e.g <1 3 [3 5]>. We simply defer to a sequence and
// change the alignment to slowcat // change the alignment to slowcat
slow_sequence = ws "<" ws s:polymeter_stack ws ">" ws slow_sequence = ws "<" ws s:sequence ws ">" ws
{ s.arguments_.alignment = 'polymeter_slowcat'; return s; } { s.arguments_.alignment = 'slowcat'; return s; }
// a slice is either a single step or a sub cycle // a slice is either a single step or a sub cycle
slice = step / sub_cycle / polymeter / slow_sequence slice = step / sub_cycle / polymeter / slow_sequence
@@ -266,25 +265,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]
+12 -18
View File
@@ -42,7 +42,7 @@ const applyOptions = (parent, enter) => (pat, i) => {
} }
case 'tail': { case 'tail': {
const friend = enter(op.arguments_.element); const friend = enter(op.arguments_.element);
pat = pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
break; break;
} }
case 'range': { case 'range': {
@@ -91,10 +91,6 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
if (alignment === 'stack') { if (alignment === 'stack') {
return strudel.stack(...children); return strudel.stack(...children);
} }
if (alignment === 'polymeter_slowcat') {
const aligned = children.map((child) => child._slow(strudel.Fraction(child.__weight ?? 1)));
return strudel.stack(...aligned);
}
if (alignment === 'polymeter') { if (alignment === 'polymeter') {
// polymeter // polymeter
const stepsPerCycle = ast.arguments_.stepsPerCycle const stepsPerCycle = ast.arguments_.stepsPerCycle
@@ -108,9 +104,15 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children); return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
} }
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight); const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (!weightedChildren && alignment === 'slowcat') {
return strudel.slowcat(...children);
}
if (weightedChildren) { if (weightedChildren) {
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0); const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]])); const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
if (alignment === 'slowcat') {
return pat._slow(weightSum); // timecat + slow
}
pat.__weight = weightSum; pat.__weight = weightSum;
return pat; return pat;
} }
@@ -158,19 +160,11 @@ export const getLeafLocation = (code, leaf, globalOffset = 0) => {
}; };
// takes quoted mini string, returns ast // takes quoted mini string, returns ast
export const mini2ast = (code, start, userCode) => { export const mini2ast = (code) => krill.parse(code);
try {
return krill.parse(code);
} catch (error) {
const region = [error.location.start.offset + start, error.location.end.offset + start];
const line = userCode.slice(0, region[0]).split('\n').length;
throw new Error(`[mini] parse error at line ${line}: ${error.message}`);
}
};
// takes quoted mini string, returns all nodes that are leaves // takes quoted mini string, returns all nodes that are leaves
export const getLeaves = (code, start, userCode) => { export const getLeaves = (code) => {
const ast = mini2ast(code, start, userCode); const ast = mini2ast(code);
let leaves = []; let leaves = [];
patternifyAST( patternifyAST(
ast, ast,
@@ -186,8 +180,8 @@ export const getLeaves = (code, start, userCode) => {
}; };
// takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes // takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes
export const getLeafLocations = (code, start = 0, userCode) => { export const getLeafLocations = (code, offset = 0) => {
return getLeaves(code, start, userCode).map((l) => getLeafLocation(code, l, start)); return getLeaves(code).map((l) => getLeafLocation(code, l, offset));
}; };
// mini notation only (wraps in "") // mini notation only (wraps in "")
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/mini", "name": "@strudel.cycles/mini",
"version": "0.10.0", "version": "0.9.0",
"description": "Mini notation for strudel", "description": "Mini notation for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -36,7 +36,7 @@
}, },
"devDependencies": { "devDependencies": {
"peggy": "^3.0.2", "peggy": "^3.0.2",
"vite": "^5.0.10", "vite": "^4.3.3",
"vitest": "^1.1.0" "vitest": "^0.33.0"
} }
} }
+1 -1
View File
@@ -9,7 +9,7 @@ export default defineConfig({
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'], formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext], fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
}, },
rollupOptions: { rollupOptions: {
external: [...Object.keys(dependencies)], external: [...Object.keys(dependencies)],
+2 -4
View File
@@ -1,7 +1,5 @@
# @strudel.cycles/osc # @strudel.cycles/osc
**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/osc](https://www.npmjs.com/package/@strudel/osc).
OSC output for strudel patterns! Currently only tested with super collider / super dirt. OSC output for strudel patterns! Currently only tested with super collider / super dirt.
## Usage ## Usage
@@ -36,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)
+1 -1
View File
@@ -37,7 +37,7 @@ function connect() {
/** /**
* *
* 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
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/osc", "name": "@strudel.cycles/osc",
"version": "0.10.0", "version": "0.9.0",
"description": "OSC messaging for strudel", "description": "OSC messaging for strudel",
"main": "osc.mjs", "main": "osc.mjs",
"publishConfig": { "publishConfig": {
@@ -41,6 +41,6 @@
}, },
"devDependencies": { "devDependencies": {
"pkg": "^5.8.1", "pkg": "^5.8.1",
"vite": "^5.0.10" "vite": "^4.3.3"
} }
} }
+1 -1
View File
@@ -9,7 +9,7 @@ export default defineConfig({
lib: { lib: {
entry: resolve(__dirname, 'osc.mjs'), entry: resolve(__dirname, 'osc.mjs'),
formats: ['es', 'cjs'], formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext], fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
}, },
rollupOptions: { rollupOptions: {
external: [...Object.keys(dependencies)], external: [...Object.keys(dependencies)],

Some files were not shown because too many files have changed in this diff Show More