Compare commits

..

8 Commits

Author SHA1 Message Date
Felix Roos d649d3e940 fix: add flag to reduce dom weight 2023-08-11 01:20:31 +02:00
Felix Roos 5101bb298e add link to reference home 2023-08-11 01:13:30 +02:00
Felix Roos f2c8b76d75 hide superdirt controls 2023-08-11 01:09:14 +02:00
Felix Roos 07c9515de1 fix: ignore empty names 2023-08-11 01:01:54 +02:00
Felix Roos 62c121da42 fix: pages of synonyms 2023-08-11 00:57:07 +02:00
Felix Roos 37a2ee5973 fix: filter out internals
+ use name instead of longname for linking
2023-08-11 00:48:29 +02:00
Felix Roos 60deb461b5 fix: remove duplicate 2023-08-11 00:48:00 +02:00
Felix Roos 6381a8deca basic reference with one page per function 2023-08-11 00:38:24 +02:00
393 changed files with 15682 additions and 24673 deletions
+1 -5
View File
@@ -17,8 +17,4 @@ vite.config.js
**/*.ts
**/*.json
**/dev-dist
**/dist
/src-tauri/target/**/*
reverbGen.mjs
hydra.mjs
jsdoc-synonyms.js
**/dist
+3 -3
View File
@@ -21,13 +21,13 @@ jobs:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- uses: pnpm/action-setup@v2
with:
version: 8.11.0
version: 7
- uses: actions/setup-node@v3
with:
node-version: 20
node-version: 18
cache: "pnpm"
- name: Install Dependencies
run: pnpm install
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf libasound2-dev
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
- name: Install app dependencies from lockfile and build web
run: pnpm install
+3 -3
View File
@@ -7,13 +7,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20]
node-version: [18]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- uses: pnpm/action-setup@v2
with:
version: 8.11.0
version: 7
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
+1 -84
View File
@@ -43,87 +43,4 @@ dev-dist
Dirt-Samples
tidal-drum-machines
webaudiofontdata
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
src-tauri/target
+1 -2
View File
@@ -9,5 +9,4 @@ packages/xen/tunejs.js
paper
pnpm-lock.yaml
pnpm-workspace.yaml
**/dev-dist
website/.astro
**/dev-dist
+5 -5
View File
@@ -13,7 +13,7 @@ To get in touch with the contributors, either
## Ask a Question
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!
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
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.
## 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/)
- [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:
@@ -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.
- 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
If you use VSCode, you can
@@ -114,7 +114,7 @@ You can run the same check with `pnpm check`
## Package Workflow
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
When you run `pnpm i` on the root folder, [pnpm workspaces](https://pnpm.io/workspaces) will install all dependencies of all subpackages. This will allow any js file to import `@strudel/<package-name>` to get the local version,
When you run `pnpm i` on the root folder, [pnpm workspaces](https://pnpm.io/workspaces) will install all dependencies of all subpackages. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
allowing to develop multiple packages at the same time.
## Package Publishing
+4 -4
View File
@@ -2,13 +2,12 @@
[![Strudel test status](https://github.com/tidalcycles/strudel/actions/workflows/test.yml/badge.svg)](https://github.com/tidalcycles/strudel/actions)
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is a bit more stable now, 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>
- Docs: <https://strudel.cc/learn>
- Try it here: <https://strudel.tidalcycles.org/>
- Docs: <https://strudel.tidalcycles.org/learn/>
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
- 2 Years of Strudel Blog Post: <https://strudel.cc/blog/#year-2>
## Running Locally
@@ -31,6 +30,7 @@ There are multiple npm packages you can use to use strudel, or only parts of it,
- [`midi`](./packages/midi): webmidi bindings
- [`serial`](./packages/serial): webserial bindings
- [`tonal`](./packages/tonal): tonal functions
- [`xen`](./packages/xen): microtonal / xenharmonic functions
- ... [and there are more](./packages/)
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
```
-23
View File
@@ -1,23 +0,0 @@
{
"name": "vite-vanilla-repl-cm6",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^5.0.10"
},
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel/core": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/soundfonts": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*"
}
}
-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>
-17
View File
@@ -1,17 +0,0 @@
{
"name": "superdough-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"superdough": "workspace:*"
},
"devDependencies": {
"vite": "^5.0.10"
}
}
-1
View File
@@ -2,7 +2,6 @@
export * from './packages/core/index.mjs';
export * from './packages/csound/index.mjs';
export * from './packages/embed/index.mjs';
export * from './packages/desktopbridge/index.mjs';
export * from './packages/midi/index.mjs';
export * from './packages/mini/index.mjs';
export * from './packages/osc/index.mjs';
@@ -3,7 +3,7 @@
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
},
"plugins": ["plugins/markdown", "jsdoc/jsdoc-synonyms"],
"plugins": ["plugins/markdown"],
"opts": {
"destination": "./out/",
"recurse": true
+4 -2
View File
@@ -1,6 +1,8 @@
{
"packages": ["packages/*"],
"packages": [
"packages/*"
],
"version": "independent",
"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
- 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?
+21 -20
View File
@@ -1,5 +1,5 @@
{
"name": "@strudel/monorepo",
"name": "@strudel.cycles/monorepo",
"version": "0.5.0",
"private": true,
"description": "Port of tidalcycles to javascript",
@@ -18,12 +18,12 @@
"build": "npm run prebuild && cd website && npm run build",
"preview": "cd website && npm run preview",
"osc": "cd packages/osc && npm run server",
"jsdoc": "jsdoc packages/ -c jsdoc/jsdoc.config.json",
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -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.config.json",
"lint": "eslint . --ext mjs,js --quiet",
"codeformat": "prettier --write .",
"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",
"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": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://strudel.cc",
"homepage": "https://strudel.tidalcycles.org",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*",
"@strudel/xen": "workspace:*"
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/xen": "workspace:*",
"acorn": "^8.8.1",
"dependency-tree": "^9.0.0"
},
"devDependencies": {
"dependency-tree": "^10.0.9",
"@tauri-apps/cli": "^1.5.9",
"@vitest/ui": "^1.1.0",
"@tauri-apps/cli": "^1.4.0",
"@vitest/ui": "^0.28.0",
"canvas": "^2.11.2",
"eslint": "^8.56.0",
"eslint-plugin-import": "^2.29.1",
"eslint": "^8.39.0",
"eslint-plugin-import": "^2.27.5",
"events": "^3.3.0",
"jsdoc": "^4.0.2",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^8.0.0",
"lerna": "^8.0.1",
"prettier": "^3.1.1",
"rollup-plugin-visualizer": "^5.12.0",
"vitest": "^1.1.0"
"lerna": "^6.6.1",
"prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.8.1",
"vitest": "^0.33.0"
}
}
+11 -1
View File
@@ -1,5 +1,15 @@
# Packages
Each folder represents one of the @strudel/* packages [published to npm](https://www.npmjs.com/org/strudel).
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.
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 { search, highlightSelectionMatches } from '@codemirror/search';
import { history } from '@codemirror/commands';
import { defaultKeymap } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
import {
EditorView,
highlightActiveLineGutter,
highlightActiveLine,
keymap,
lineNumbers,
drawSelection,
} from '@codemirror/view';
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel/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,
});
import { EditorState } from '@codemirror/state';
import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from '@codemirror/view';
import { Drawer, repl } from '@strudel.cycles/core';
import { flashField, flash } from './flash.mjs';
import { highlightExtension, highlightMiniLocations } from './highlight.mjs';
import { oneDark } from './themes/one-dark';
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root }) {
const settings = codemirrorSettings.get();
const initialSettings = Object.keys(compartments).map((key) =>
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
);
initTheme(settings.theme);
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
let state = EditorState.create({
doc: initialCode,
extensions: [
/* search(),
highlightSelectionMatches(), */
...initialSettings,
theme,
javascript(),
sliderPlugin,
// indentOnInput(), // works without. already brought with javascript extension?
// bracketMatching(), // does not do anything
closeBrackets(),
lineNumbers(),
highlightExtension,
highlightActiveLineGutter(),
syntaxHighlighting(defaultHighlightStyle),
history(),
keymap.of(defaultKeymap),
flashField,
EditorView.updateListener.of((v) => onChange(v)),
drawSelection({ cursorBlinkRate: 0 }),
Prec.highest(
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate?.(),
},
{
key: 'Alt-Enter',
run: () => onEvaluate?.(),
},
{
key: 'Ctrl-.',
run: () => onStop?.(),
},
{
key: 'Alt-.',
run: (_, e) => {
e.preventDefault();
onStop?.();
},
},
/* {
key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()),
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate(),
},
{
key: 'Ctrl-Shift-Enter',
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
}, */
]),
),
key: 'Ctrl-.',
run: () => onStop(),
},
]),
],
});
@@ -118,76 +43,45 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
export class StrudelMirror {
constructor(options) {
const {
root,
id,
initialCode = '',
onDraw,
drawTime = [0, 0],
autodraw,
prebake,
bgFill = true,
...replOptions
} = options;
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
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) => {
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
this.highlight(currentFrame, time);
this.onDraw?.(haps, time, currentFrame, this.painters);
onDraw?.(haps, time, currentFrame);
}, drawTime);
// this approach does not work with multiple repls on screen
// TODO: refactor onPaint usages + find fix, maybe remove painters here?
Pattern.prototype.onPaint = function (onPaint) {
self.painters.push(onPaint);
return this;
};
this.prebaked = prebake();
autodraw && this.drawFirstFrame();
const prebaked = prebake();
prebaked.then(async () => {
if (!onDraw) {
return;
}
const { scheduler, evaluate } = await this.repl;
// draw first frame instantly
prebaked.then(async () => {
await evaluate(this.code, false);
this.drawer.invalidate(scheduler);
onDraw?.(this.drawer.visibleHaps, 0, []);
});
});
this.repl = repl({
...replOptions,
onToggle: (started) => {
onToggle: async (started) => {
replOptions?.onToggle?.(started);
const { scheduler } = await this.repl;
if (started) {
this.adjustDrawTime();
this.drawer.start(this.repl.scheduler);
// stop other repls when this one is started
document.dispatchEvent(
new CustomEvent('start-repl', {
detail: this.id,
}),
);
this.drawer.start(scheduler);
} else {
this.drawer.stop();
updateMiniLocations(this.editor, []);
cleanupDraw(false);
}
},
beforeEval: async () => {
cleanupDraw();
this.painters = [];
await this.prebaked;
await replOptions?.beforeEval?.();
await prebaked;
},
afterEval: (options) => {
// remember for when highlighting is toggled on
this.miniLocations = options.meta?.miniLocations;
this.widgets = options.meta?.widgets;
updateWidgets(this.editor, this.widgets);
updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options);
this.adjustDrawTime();
this.drawer.invalidate();
},
});
@@ -195,145 +89,25 @@ export class StrudelMirror {
root,
initialCode,
onChange: (v) => {
if (v.docChanged) {
this.code = v.state.doc.toString();
this.repl.setCode?.(this.code);
}
this.code = v.state.doc.toString();
},
onEvaluate: () => this.evaluate(),
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() {
const { evaluate } = await this.repl;
this.flash();
await this.repl.evaluate(this.code);
await evaluate(this.code);
}
async stop() {
this.repl.scheduler.stop();
}
async toggle() {
if (this.repl.scheduler.started) {
this.repl.stop();
} else {
this.evaluate();
}
const { scheduler } = await this.repl;
scheduler.stop();
}
flash(ms) {
flash(this.editor, ms);
}
highlight(haps, time) {
highlightMiniLocations(this.editor, time, haps);
}
setFontSize(size) {
this.root.style.fontSize = size + 'px';
}
setFontFamily(family) {
this.root.style.fontFamily = family;
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);
highlightMiniLocations(this.editor.view, time, haps);
}
}
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) });
}, ms);
};
export const isFlashEnabled = (on) => (on ? flashField : []);
+1 -12
View File
@@ -57,9 +57,6 @@ const visibleMiniLocations = StateField.define({
// this is why we need to find a way to update the existing decorations, showing the ones that have an active range
const haps = new Map();
for (let hap of e.value.haps) {
if (!hap.context?.locations || !hap.whole) {
continue;
}
for (let { start, end } of hap.context.locations) {
let id = `${start}:${end}`;
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
@@ -67,6 +64,7 @@ const visibleMiniLocations = StateField.define({
}
}
}
visible = { atTime: e.value.atTime, haps };
}
}
@@ -126,12 +124,3 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
});
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);
};
-2
View File
@@ -1,5 +1,3 @@
export * from './codemirror.mjs';
export * from './highlight.mjs';
export * from './flash.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),
}
+11 -19
View File
@@ -1,10 +1,11 @@
{
"name": "@strudel/codemirror",
"version": "1.0.1",
"version": "0.8.4",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
"main": "dist/index.mjs"
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
@@ -32,24 +33,15 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.11.1",
"@codemirror/commands": "^6.3.3",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/language": "^6.10.0",
"@codemirror/search": "^6.5.5",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.23.0",
"@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/core": "workspace:*",
"@uiw/codemirror-themes": "^4.21.21",
"@uiw/codemirror-themes-all": "^4.21.21",
"nanostores": "^0.9.5"
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/language": "^6.6.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^5.0.10"
"vite": "^4.3.3"
}
}
-144
View File
@@ -1,144 +0,0 @@
import { ref, pure } from '@strudel/core';
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
export let sliderValues = {};
const getSliderID = (from) => `slider_${from}`;
export class SliderWidget extends WidgetType {
constructor(value, min, max, from, to, step, view) {
super();
this.value = value;
this.min = min;
this.max = max;
this.from = from;
this.originalFrom = from;
this.to = to;
this.step = step;
this.view = view;
}
eq() {
return false;
}
toDOM() {
let wrap = document.createElement('span');
wrap.setAttribute('aria-hidden', 'true');
wrap.className = 'cm-slider'; // inline-flex items-center
let slider = wrap.appendChild(document.createElement('input'));
slider.type = 'range';
slider.min = this.min;
slider.max = this.max;
slider.step = this.step ?? (this.max - this.min) / 1000;
slider.originalValue = this.value;
// to make sure the code stays in sync, let's save the original value
// becuase .value automatically clamps values so it'll desync with the code
slider.value = slider.originalValue;
slider.from = this.from;
slider.originalFrom = this.originalFrom;
slider.to = this.to;
slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
this.slider = slider;
slider.addEventListener('input', (e) => {
const next = e.target.value;
let insert = next;
//let insert = next.toFixed(2);
const to = slider.from + slider.originalValue.length;
let change = { from: slider.from, to, insert };
slider.originalValue = insert;
slider.value = insert;
this.view.dispatch({ changes: change });
const id = getSliderID(slider.originalFrom); // matches id generated in transpiler
window.postMessage({ type: 'cm-slider', value: Number(next), id });
});
return wrap;
}
ignoreEvent(e) {
return true;
}
}
export const setWidgets = StateEffect.define();
export const updateWidgets = (view, widgets) => {
view.dispatch({ effects: setWidgets.of(widgets) });
};
function getWidgets(widgetConfigs, view) {
return widgetConfigs.map(({ from, to, value, min, max, step }) => {
return Decoration.widget({
widget: new SliderWidget(value, min, max, from, to, step, view),
side: 0,
}).range(from /* , to */);
});
}
export const sliderPlugin = ViewPlugin.fromClass(
class {
decorations; //: DecorationSet
constructor(view /* : EditorView */) {
this.decorations = Decoration.set([]);
}
update(update /* : ViewUpdate */) {
update.transactions.forEach((tr) => {
if (tr.docChanged) {
this.decorations = this.decorations.map(tr.changes);
const iterator = this.decorations.iter();
while (iterator.value) {
// when the widgets are moved, we need to tell the dom node the current position
// this is important because the updateSliderValue function has to work with the dom node
if (iterator.value?.widget?.slider) {
iterator.value.widget.slider.from = iterator.from;
iterator.value.widget.slider.to = iterator.to;
}
iterator.next();
}
}
for (let e of tr.effects) {
if (e.is(setWidgets)) {
this.decorations = Decoration.set(getWidgets(e.value, update.view));
}
}
});
}
},
{
decorations: (v) => v.decorations,
},
);
/**
* 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) => {
console.warn('slider will only work when the transpiler is used... passing value as is');
return pure(value);
};
// function transpiled from slider = (value, min, max)
export let sliderWithID = (id, value, min, max) => {
sliderValues[id] = value; // sync state at eval time (code -> state)
return ref(() => sliderValues[id]); // use state at query time
};
// update state when sliders are moved
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (e.data.type === 'cm-slider') {
if (sliderValues[e.data.id] !== undefined) {
// update state when slider is moved
sliderValues[e.data.id] = e.data.value;
} else {
console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
}
}
});
}
+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)];
-50
View File
@@ -1,50 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
let colorA = '#6edee4';
//let colorB = 'magenta';
let colorB = 'white';
let colorC = 'red';
let colorD = '#f8fc55';
export const settings = {
background: '#000000',
foreground: colorA, // whats that?
caret: colorC,
selection: colorD,
selectionMatch: colorA,
lineHighlight: '#6edee440', // panel bg
lineBackground: '#00000040',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
customStyle: '.cm-line { line-height: 1 }',
};
let punctuation = colorD;
let mini = colorB;
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: colorA },
{ tag: t.operator, color: mini },
{ tag: t.special(t.variableName), color: colorA },
{ tag: t.typeName, color: colorA },
{ tag: t.atom, color: colorA },
{ tag: t.number, color: mini },
{ tag: t.definition(t.variableName), color: colorA },
{ tag: t.string, color: mini },
{ tag: t.special(t.string), color: mini },
{ tag: t.comment, color: punctuation },
{ tag: t.variableName, color: colorA },
{ tag: t.tagName, color: colorA },
{ tag: t.bracket, color: punctuation },
{ tag: t.meta, color: colorA },
{ tag: t.attributeName, color: colorA },
{ tag: t.propertyName, color: colorA }, // methods
{ tag: t.className, color: colorA },
{ tag: t.invalid, color: colorC },
{ tag: [t.unit, t.punctuation], color: punctuation },
],
});
-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 : []);
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
+4 -4
View File
@@ -1,17 +1,17 @@
# @strudel/core
# @strudel.cycles/core
This package contains the bare essence of strudel.
## Install
```sh
npm i @strudel/core --save
npm i @strudel.cycles/core --save
```
## Example
```js
import { sequence } from '@strudel/core';
import { sequence } from '@strudel.cycles/core';
const pattern = sequence('a', ['b', 'c']);
@@ -33,7 +33,7 @@ b: 3/2 - 7/4
c: 7/4 - 2
```
- [play with @strudel/core on codesandbox](https://codesandbox.io/s/strudel-core-test-forked-9ywhv7?file=/src/index.js).
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-forked-9ywhv7?file=/src/index.js).
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/canvas.html)
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/vanilla.html)
- [open minimal vite example](./examples/vite-vanilla-repl/)
File diff suppressed because it is too large Load Diff
+6 -23
View File
@@ -10,15 +10,14 @@ import { logger } from './logger.mjs';
export class Cyclist {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
this.started = false;
this.cps = 0.5;
this.num_ticks_since_cps_change = 0;
this.cps = 1;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.lastBegin = 0; // query begin of last tick
this.lastEnd = 0; // query end of last tick
this.getTime = getTime; // get absolute time
this.num_cycles_since_last_cps_change = 0;
this.onToggle = onToggle;
this.latency = latency; // fixed trigger time offset
const round = (x) => Math.round(x * 1000) / 1000;
this.clock = createClock(
getTime,
// called slightly before each cycle
@@ -26,24 +25,14 @@ export class Cyclist {
if (tick === 0) {
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 {
const time = getTime();
const begin = this.lastEnd;
this.lastBegin = begin;
//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;
const end = round(begin + duration * this.cps);
this.lastEnd = end;
// query the pattern for events
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
const tickdeadline = phase - time; // time left until the phase is a whole number
const haps = this.pattern.queryArc(begin, end);
const tickdeadline = phase - time; // time left till phase begins
this.lastTick = time + tickdeadline;
haps.forEach((hap) => {
@@ -70,8 +59,6 @@ export class Cyclist {
this.onToggle?.(v);
}
start() {
this.num_ticks_since_cps_change = 0;
this.num_cycles_since_last_cps_change = 0;
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
@@ -96,12 +83,8 @@ export class Cyclist {
this.start();
}
}
setCps(cps = 0.5) {
if (this.cps === cps) {
return;
}
setCps(cps = 1) {
this.cps = cps;
this.num_ticks_since_cps_change = 0;
}
log(begin, end, haps) {
const onsets = haps.filter((h) => h.hasOnset());
+11 -19
View File
@@ -9,29 +9,25 @@ import { Pattern, getTime, State, TimeSpan } from './index.mjs';
export const getDrawContext = (id = 'test-canvas') => {
let canvas = document.querySelector('#' + id);
if (!canvas) {
const scale = 2; // 2 = crisp on retina screens
canvas = document.createElement('canvas');
canvas.id = id;
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
document.body.prepend(canvas);
let timeout;
window.addEventListener('resize', () => {
timeout && clearTimeout(timeout);
timeout = setTimeout(() => {
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}, 200);
});
}
return canvas.getContext('2d');
};
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
if (typeof window === 'undefined') {
return this;
}
Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
@@ -63,7 +59,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
export const cleanupDraw = (clearScreen = true) => {
const ctx = getDrawContext();
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
@@ -111,6 +107,8 @@ export class Framer {
// see vite-vanilla-repl-cm6 for an example
export class Drawer {
constructor(onDraw, drawTime) {
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
lookbehind = Math.abs(lookbehind);
this.visibleHaps = [];
this.lastFrame = null;
this.drawTime = drawTime;
@@ -120,8 +118,6 @@ export class Drawer {
console.warn('Drawer: no scheduler');
return;
}
const lookbehind = Math.abs(this.drawTime[0]);
const lookahead = this.drawTime[1];
// calculate current frame time (think right side of screen for pianoroll)
const phase = this.scheduler.now() + lookahead;
// first frame just captures the phase
@@ -134,7 +130,7 @@ export class Drawer {
this.lastFrame = phase;
this.visibleHaps = (this.visibleHaps || [])
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
.filter((h) => h.whole?.end >= phase - lookbehind - lookahead)
.filter((h) => h.whole.end >= phase - lookbehind - lookahead)
// add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead;
@@ -145,16 +141,12 @@ export class Drawer {
},
);
}
setDrawTime(drawTime) {
this.drawTime = drawTime;
}
invalidate(scheduler = this.scheduler, t) {
invalidate(scheduler = this.scheduler) {
if (!scheduler) {
return;
}
// TODO: scheduler.now() seems to move even when it's stopped, this hints at a bug...
t = t ?? scheduler.now();
this.scheduler = scheduler;
const t = scheduler.now();
let [_, lookahead] = this.drawTime;
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
// remove all future haps
+1 -15
View File
@@ -145,10 +145,8 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
* so there will be no gaps.
* @name euclidLegato
* @memberof Pattern
* @param {number} pulses the number of onsets / beats
* @param {number} steps the number of steps to fill
* @example
* note("c3").euclidLegato(3,8)
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
*/
const _euclidLegato = function (pulses, steps, rotation, pat) {
@@ -168,18 +166,6 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
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) {
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)
const options = { wrapExpression: !!transpiler };
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 };
};
@@ -7,7 +7,7 @@
/>
<div id="output"></div>
<script type="module">
const strudel = await import('https://cdn.skypack.dev/@strudel/core@0.6.8');
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8');
Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
const input = document.getElementById('text');
const getEvents = () => {
@@ -8,7 +8,7 @@
/>
<canvas id="canvas"></canvas>
<script type="module">
const strudel = await import('https://cdn.skypack.dev/@strudel/core@0.6.8');
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8');
// this adds all strudel functions to the global scope, to be used by eval
Object.assign(window, strudel);
// setup elements
@@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -16,40 +16,34 @@
</div>
<div id="output"></div>
<script type="module">
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel/core@0.11.0';
import { mini } from 'https://cdn.skypack.dev/@strudel/mini@0.11.0';
import { transpiler } from 'https://cdn.skypack.dev/@strudel/transpiler@0.11.0';
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.6.8';
import { mini } from 'https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0';
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.6.0';
import {
getAudioContext,
webaudioOutput,
initAudioOnFirstClick,
registerSynthSounds,
} from 'https://cdn.skypack.dev/@strudel/webaudio@0.11.0';
} from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0';
initAudioOnFirstClick();
const ctx = getAudioContext();
const input = document.getElementById('text');
input.innerHTML = getTune();
const loadModules = evalScope(
evalScope(
controls,
import('https://cdn.skypack.dev/@strudel/core@0.11.0'),
import('https://cdn.skypack.dev/@strudel/mini@0.11.0'),
import('https://cdn.skypack.dev/@strudel/tonal@0.11.0'),
import('https://cdn.skypack.dev/@strudel/webaudio@0.11.0'),
import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8'),
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0'),
import('https://cdn.skypack.dev/@strudel.cycles/tonal@0.6.0'),
import('https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0'),
);
const initAudio = Promise.all([initAudioOnFirstClick(), registerSynthSounds()]);
const { evaluate } = repl({
defaultOutput: webaudioOutput,
getTime: () => ctx.currentTime,
transpiler,
});
document.getElementById('start').addEventListener('click', async () => {
await loadModules;
await initAudio;
evaluate(input.value);
});
document.getElementById('start').addEventListener('click', () => evaluate(input.value));
function getTune() {
return `await samples('github:tidalcycles/Dirt-Samples/master')
@@ -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">
<head>
<meta charset="UTF-8" />
@@ -1,11 +1,11 @@
import { StrudelMirror } from '@strudel/codemirror';
import { funk42 } from './tunes';
import { drawPianoroll, evalScope, controls } from '@strudel/core';
import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
import './style.css';
import { initAudioOnFirstClick } from '@strudel/webaudio';
import { transpiler } from '@strudel/transpiler';
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel/webaudio';
import { registerSoundfonts } from '@strudel/soundfonts';
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
// init canvas
const canvas = document.getElementById('roll');
@@ -26,10 +26,10 @@ const editor = new StrudelMirror({
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
const loadModules = evalScope(
controls,
import('@strudel/core'),
import('@strudel/mini'),
import('@strudel/tonal'),
import('@strudel/webaudio'),
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
},
@@ -0,0 +1,23 @@
{
"name": "vite-vanilla-repl-cm6",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.3.2"
},
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
}
}
@@ -1,6 +1,6 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
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"
// 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
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', {
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
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)
@@ -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">
<head>
<meta charset="UTF-8" />
@@ -1,6 +1,6 @@
import { controls, repl, evalScope } from '@strudel/core';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
import { transpiler } from '@strudel/transpiler';
import { controls, repl, evalScope } from '@strudel.cycles/core';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { transpiler } from '@strudel.cycles/transpiler';
import tune from './tune.mjs';
const ctx = getAudioContext();
@@ -10,10 +10,10 @@ initAudioOnFirstClick();
evalScope(
controls,
import('@strudel/core'),
import('@strudel/mini'),
import('@strudel/webaudio'),
import('@strudel/tonal'),
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/tonal'),
);
const { evaluate } = repl({
@@ -10,13 +10,13 @@
"preview": "vite preview"
},
"devDependencies": {
"vite": "^5.0.10"
"vite": "^4.3.3"
},
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*",
"@strudel/tonal": "workspace:*"
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/tonal": "workspace:*"
}
}
@@ -1,13 +1,12 @@
/*
index.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/superdough/index.mjs>
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/>.
*/
export * from './superdough.mjs';
export * from './sampler.mjs';
export * from './helpers.mjs';
export * from './synth.mjs';
export * from './zzfx.mjs';
export * from './logger.mjs';
export * from './dspworklet.mjs';
// 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));
+4 -3
View File
@@ -27,15 +27,16 @@ export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './ui.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)
/* import * as p from './package.json';
export const version = p.version; */
logger('🌀 @strudel/core loaded 🌀');
logger('🌀 @strudel.cycles/core loaded 🌀');
if (globalThis._strudelLoaded) {
console.warn(
`@strudel/core was loaded more than once...
`@strudel.cycles/core was loaded more than once...
This might happen when you have multiple versions of strudel installed.
Please check with "npm ls @strudel/core".`,
Please check with "npm ls @strudel.cycles/core".`,
);
}
globalThis._strudelLoaded = true;
+8 -7
View File
@@ -1,11 +1,12 @@
{
"name": "@strudel/core",
"version": "1.0.1",
"name": "@strudel.cycles/core",
"version": "0.8.2",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"test": "vitest run",
@@ -28,13 +29,13 @@
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://strudel.cc",
"homepage": "https://strudel.tidalcycles.org",
"dependencies": {
"fraction.js": "^4.3.7"
"fraction.js": "^4.2.0"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^1.1.0"
"vite": "^4.3.3",
"vitest": "^0.33.0"
}
}
+107 -221
View File
@@ -26,7 +26,7 @@ export class Pattern {
/**
* Create a pattern. As an end user, you will most likely not create a Pattern directly.
*
* @param {function} query - The function that maps a `State` to an array of `Hap`.
* @param {function} query - The function that maps a {@link State} to an array of {@link Hap}.
* @noAutocomplete
*/
constructor(query) {
@@ -39,7 +39,7 @@ export class Pattern {
/**
* Returns a new pattern, with the function applied to the value of
* each hap. It has the alias `fmap`.
* each hap. It has the alias {@link Pattern#fmap}.
* @synonyms fmap
* @param {Function} func to to apply to the value
* @returns Pattern
@@ -51,7 +51,7 @@ export class Pattern {
}
/**
* see `withValue`
* see {@link Pattern#withValue}
* @noAutocomplete
*/
fmap(func) {
@@ -115,7 +115,7 @@ export class Pattern {
}
/**
* As with `appBoth`, but the `whole` timespan is not the intersection,
* As with {@link Pattern#appBoth}, but the `whole` timespan is not the intersection,
* but the timespan from the function of patterns that this method is called
* on. In practice, this means that the pattern structure, including onsets,
* are preserved from the pattern of functions (often referred to as the left
@@ -148,7 +148,7 @@ export class Pattern {
}
/**
* As with `appLeft`, but `whole` timespans are instead taken from the
* As with {@link Pattern#appLeft}, but `whole` timespans are instead taken from the
* pattern of values, i.e. structure is preserved from the right hand/outer
* pattern.
* @param {Pattern} pat_val
@@ -340,9 +340,9 @@ export class Pattern {
* silence
* @noAutocomplete
*/
queryArc(begin, end, controls = {}) {
queryArc(begin, end) {
try {
return this.query(new State(new TimeSpan(begin, end), controls));
return this.query(new State(new TimeSpan(begin, end)));
} catch (err) {
logger(`[query]: ${err.message}`, 'error');
return [];
@@ -387,7 +387,7 @@ export class Pattern {
}
/**
* As with `withQuerySpan`, but the function is applied to both the
* As with {@link Pattern#withQuerySpan}, but the function is applied to both the
* begin and end time of the query timespan.
* @param {Function} func the function to apply
* @returns Pattern
@@ -398,7 +398,7 @@ export class Pattern {
}
/**
* Similar to `withQuerySpan`, but the function is applied to the timespans
* Similar to {@link Pattern#withQuerySpan}, but the function is applied to the timespans
* of all haps returned by pattern queries (both `part` timespans, and where
* present, `whole` timespans).
* @param {Function} func
@@ -410,7 +410,7 @@ export class Pattern {
}
/**
* As with `withHapSpan`, but the function is applied to both the
* As with {@link Pattern#withHapSpan}, but the function is applied to both the
* begin and end time of the hap timespans.
* @param {Function} func the function to apply
* @returns Pattern
@@ -427,11 +427,11 @@ export class Pattern {
* @noAutocomplete
*/
withHaps(func) {
return new Pattern((state) => func(this.query(state), state));
return new Pattern((state) => func(this.query(state)));
}
/**
* As with `withHaps`, but applies the function to every hap, rather than every list of haps.
* As with {@link Pattern#withHaps}, but applies the function to every hap, rather than every list of haps.
* @param {Function} func
* @returns Pattern
* @noAutocomplete
@@ -499,7 +499,7 @@ export class Pattern {
}
/**
* As with `filterHaps`, but the function is applied to values
* As with {@link Pattern#filterHaps}, but the function is applied to values
* inside haps.
* @param {Function} value_test
* @returns Pattern
@@ -621,7 +621,7 @@ export class Pattern {
}
/**
* More human-readable version of the `firstCycleValues` accessor.
* More human-readable version of the {@link Pattern#firstCycleValues} accessor.
* @noAutocomplete
*/
get showFirstCycle() {
@@ -691,13 +691,13 @@ export class Pattern {
// Methods without corresponding toplevel functions
/**
* Layers the result of the given function(s). Like `superimpose`, but without the original pattern:
* Layers the result of the given function(s). Like {@link Pattern.superimpose}, but without the original pattern:
* @name layer
* @memberof Pattern
* @synonyms apply
* @returns Pattern
* @example
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8"
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
* .layer(x=>x.add("0,2"))
* .scale('C minor').note()
*/
@@ -711,7 +711,7 @@ export class Pattern {
* @memberof Pattern
* @returns Pattern
* @example
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8"
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
* .superimpose(x=>x.add(2))
* .scale('C minor').note()
*/
@@ -727,8 +727,8 @@ export class Pattern {
* @name stack
* @memberof Pattern
* @example
* s("hh*4").stack(
* note("c4(5,8)")
* s("hh*2").stack(
* note("c2(3,8)")
* )
*/
stack(...pats) {
@@ -745,8 +745,8 @@ export class Pattern {
* @memberof Pattern
* @synonyms sequence, fastcat
* @example
* s("hh*4").seq(
* note("c4(5,8)")
* s("hh*2").seq(
* note("c2(3,8)")
* )
*/
seq(...pats) {
@@ -759,8 +759,8 @@ export class Pattern {
* @memberof Pattern
* @synonyms slowcat
* @example
* s("hh*4").cat(
* note("c4(5,8)")
* s("hh*2").cat(
* note("c2(3,8)")
* )
*/
cat(...pats) {
@@ -858,7 +858,7 @@ Pattern.prototype.arpWith = function (func) {
* Selects indices in in stacked notes.
* @example
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arp("0 [0,2] 1 [0,2]")
* .arp("0 [0,2] 1 [0,2]").slow(2)
* */
Pattern.prototype.arp = function (pat) {
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
@@ -929,14 +929,14 @@ function _composeOp(a, b, func) {
* @memberof Pattern
* @example
* // Here, the triad 0, 2, 4 is shifted by different amounts
* n("0 2 4".add("<0 3 4 0>")).scale("C:major")
* "0 2 4".add("<0 3 4 0>").scale('C major').note()
* // Without add, the equivalent would be:
* // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major")
* // "<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>".scale('C major').note()
* @example
* // You can also use add with notes:
* note("c3 e3 g3".add("<0 5 7 0>"))
* "c3 e3 g3".add("<0 5 7 0>").note()
* // Behind the scenes, the notes are converted to midi numbers:
* // note("48 52 55".add("<0 5 7 0>"))
* // "48 52 55".add("<0 5 7 0>").note()
*/
add: [numeralArgs((a, b) => a + b)], // support string concatenation
/**
@@ -945,7 +945,7 @@ function _composeOp(a, b, func) {
* @name sub
* @memberof Pattern
* @example
* n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor")
* "0 2 4".sub("<0 1 2 3>").scale('C4 minor').note()
* // See add for more information.
*/
sub: [numeralArgs((a, b) => a - b)],
@@ -955,7 +955,7 @@ function _composeOp(a, b, func) {
* @name mul
* @memberof Pattern
* @example
* "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq()
* "1 1.5 [1.66, <2 2.33>]".mul(150).freq()
*/
mul: [numeralArgs((a, b) => a * b)],
/**
@@ -1049,9 +1049,9 @@ function _composeOp(a, b, func) {
* Applies the given structure to the pattern:
*
* @example
* note("c,eb,g")
* note("c3,eb3,g3")
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
* .slow(2)
* .slow(4)
*/
Pattern.prototype.struct = function (...args) {
return this.keepif.out(...args);
@@ -1063,7 +1063,7 @@ function _composeOp(a, b, func) {
* Returns silence when mask is 0 or "~"
*
* @example
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>")
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>").slow(2)
*/
Pattern.prototype.mask = function (...args) {
return this.keepif.in(...args);
@@ -1075,7 +1075,7 @@ function _composeOp(a, b, func) {
* Resets the pattern to the start of the cycle for each onset of the reset pattern.
*
* @example
* s("[<bd lt> sd]*2, hh*8").reset("<x@3 x(5,8)>")
* s("<bd lt> sd, hh*4").reset("<x@3 x(3,8)>")
*/
Pattern.prototype.reset = function (...args) {
return this.keepif.trig(...args);
@@ -1088,7 +1088,7 @@ function _composeOp(a, b, func) {
* While reset will only reset the current cycle, restart will start from cycle 0.
*
* @example
* s("[<bd lt> sd]*2, hh*8").restart("<x@3 x(5,8)>")
* s("<bd lt> sd, hh*4").restart("<x@3 x(3,8)>")
*/
Pattern.prototype.restart = function (...args) {
return this.keepif.trigzero(...args);
@@ -1154,8 +1154,8 @@ export function isPattern(thing) {
/* if (!thing instanceof Pattern) {
console.warn(
`Found Pattern that fails "instanceof Pattern" check.
This may happen if you are using multiple versions of @strudel/core.
Please check by running "npm ls @strudel/core".`,
This may happen if you are using multiple versions of @strudel.cycles/core.
Please check by running "npm ls @strudel.cycles/core".`,
);
console.log(thing);
} */
@@ -1178,8 +1178,7 @@ export function reify(thing) {
* @return {Pattern}
* @synonyms polyrhythm, pr
* @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) {
// Array test here is to avoid infinite recursions..
@@ -1190,11 +1189,11 @@ export function stack(...pats) {
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
*
* synonyms: `cat`
* synonyms: {@link cat}
*
* @return {Pattern}
* @example
* slowcat("e5", "b4", ["d5", "c5"])
* slowcat(e5, b4, [d5, c5])
*
*/
export function slowcat(...pats) {
@@ -1238,19 +1237,17 @@ export function slowcatPrime(...pats) {
* @synonyms slowcat
* @return {Pattern}
* @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) {
return slowcat(...pats);
}
/** Like `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}
* @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) {
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
@@ -1270,10 +1267,7 @@ export function timeCat(...timepats) {
*
* @return {Pattern}
* @example
* arrange(
* [4, "<c a f e>(3,8)"],
* [2, "<g a>(5,8)"]
* ).note()
* arrange([4, "<c a f e>(3,8)"],[2, "<g a>(5,8)"]).note()
*/
export function arrange(...sections) {
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
@@ -1285,7 +1279,7 @@ export function fastcat(...pats) {
return slowcat(...pats)._fast(pats.length);
}
/** See `fastcat` */
/** See {@link fastcat} */
export function sequence(...pats) {
return fastcat(...pats);
}
@@ -1293,8 +1287,7 @@ export function sequence(...pats) {
/** Like **cat**, but the items are crammed into one cycle.
* @synonyms fastcat, sequence
* @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) {
@@ -1320,9 +1313,9 @@ function _sequenceCount(x) {
* @param {number} steps how many items are placed in one cycle
* @param {any[]} sequences one or more arrays of Patterns / values
* @example
* polymeterSteps(4, ["c", "d", "e"])
* .note().stack(s("bd"))
* // note("{c d e}%4").stack(s("bd"))
* polymeterSteps(2, ["c", "d", "e", "f", "g", "f", "e", "d"])
* .note().stack(s("bd")) // 1 cycle = 1 bd = 2 notes
* // note("{c d e f g f e d}%2").stack(s("bd"))
*/
export function polymeterSteps(steps, ...args) {
const seqs = args.map((a) => _sequenceCount(a));
@@ -1470,7 +1463,7 @@ export function register(name, func, patternify = true) {
* @memberof Pattern
* @returns Pattern
* @example
* n("0.5 1.5 2.5".round()).scale("C:major")
* "0.5 1.5 2.5".round().scale('C major').note()
*/
export const round = register('round', function (pat) {
return pat.asNumber().fmap((v) => Math.round(v));
@@ -1484,7 +1477,7 @@ export const round = register('round', function (pat) {
* @memberof Pattern
* @returns Pattern
* @example
* note("42 42.1 42.5 43".floor())
* "42 42.1 42.5 43".floor().note()
*/
export const floor = register('floor', function (pat) {
return pat.asNumber().fmap((v) => Math.floor(v));
@@ -1498,7 +1491,7 @@ export const floor = register('floor', function (pat) {
* @memberof Pattern
* @returns Pattern
* @example
* note("42 42.1 42.5 43".ceil())
* "42 42.1 42.5 43".ceil().note()
*/
export const ceil = register('ceil', function (pat) {
return pat.asNumber().fmap((v) => Math.ceil(v));
@@ -1531,8 +1524,7 @@ export const fromBipolar = register('fromBipolar', function (pat) {
* @memberof Pattern
* @returns Pattern
* @example
* s("[bd sd]*2,hh*8")
* .cutoff(sine.range(500,4000))
* s("bd sd,hh*4").cutoff(sine.range(500,2000).slow(4))
*/
export const range = register('range', function (min, max, pat) {
return pat.mul(max - min).add(min);
@@ -1546,8 +1538,7 @@ export const range = register('range', function (min, max, pat) {
* @memberof Pattern
* @returns Pattern
* @example
* s("[bd sd]*2,hh*8")
* .cutoff(sine.rangex(500,4000))
* s("bd sd,hh*4").cutoff(sine.rangex(500,2000).slow(4))
*/
export const rangex = register('rangex', function (min, max, pat) {
return pat._range(Math.log(min), Math.log(max)).fmap(Math.exp);
@@ -1560,8 +1551,7 @@ export const rangex = register('rangex', function (min, max, pat) {
* @memberof Pattern
* @returns Pattern
* @example
* s("[bd sd]*2,hh*8")
* .cutoff(sine2.range2(500,4000))
* s("bd sd,hh*4").cutoff(sine2.range2(500,2000).slow(4))
*/
export const range2 = register('range2', function (min, max, pat) {
return pat.fromBipolar()._range(min, max);
@@ -1574,8 +1564,7 @@ export const range2 = register('range2', function (min, max, pat) {
* @memberof Pattern
* @returns Pattern
* @example
* ratio("1, 5:4, 3:2").mul(110)
* .freq().s("piano")
* ratio("1, 5:4, 3:2").mul(110).freq().s("piano").slow(2)
*/
export const ratio = register('ratio', (pat) =>
pat.fmap((v) => {
@@ -1647,7 +1636,7 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f
});
/**
* Similar to `compress`, but doesn't leave gaps, and the 'focus' can be bigger than a cycle
* Similar to compress, but doesn't leave gaps, and the 'focus' can be bigger than a cycle
* @example
* s("bd hh sd hh").focus(1/4, 3/4)
*/
@@ -1678,7 +1667,7 @@ export const ply = register('ply', function (factor, pat) {
* @param {number | Pattern} factor speed up factor
* @returns Pattern
* @example
* s("bd hh sd hh").fast(2) // s("[bd hh sd hh]*2")
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
*/
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
if (factor === 0) {
@@ -1707,7 +1696,7 @@ export const hurry = register('hurry', function (r, pat) {
* @param {number | Pattern} factor slow down factor
* @returns Pattern
* @example
* s("bd hh sd hh").slow(2) // s("[bd hh sd hh]/2")
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
*/
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
if (factor === 0) {
@@ -1764,7 +1753,7 @@ export const lastOf = register('lastOf', function (n, func, pat) {
*/
/**
* An alias for `firstOf`
* An alias for {@link firstOf}
* @name every
* @memberof Pattern
* @param {number} n how many cycles
@@ -1796,9 +1785,9 @@ export const apply = register('apply', function (func, pat) {
* @example
* s("<bd sd>,hh*2").cpm(90) // = 90 bpm
*/
// this is redefined in repl.mjs, using the current cps as divisor
// TODO - global clock
export const cpm = register('cpm', function (cpm, pat) {
return pat._fast(cpm / 60 / 1);
return pat._fast(cpm / 60);
});
/**
@@ -1871,7 +1860,7 @@ export const linger = register('linger', function (t, pat) {
* Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one.
* @param {number} segments number of segments per cycle
* @example
* note(saw.range(40,52).segment(24))
* note(saw.range(0,12).segment(24)).add(40)
*/
export const segment = register('segment', function (rate, pat) {
return pat.struct(pure(true)._fast(rate));
@@ -1897,7 +1886,7 @@ export const { invert, inv } = register(['invert', 'inv'], function (pat) {
* @param {function} func
* @returns Pattern
* @example
* "c3 eb3 g3".when("<0 1>/2", x=>x.sub("5")).note()
* "c3 eb3 g3".when("<0 1>/2", x=>x.sub(5)).note()
*/
export const when = register('when', function (on, func, pat) {
return on ? func(pat) : pat;
@@ -1934,7 +1923,7 @@ export const brak = register('brak', function (pat) {
* @memberof Pattern
* @returns Pattern
* @example
* note("c d e g").rev()
* note("c3 d3 e3 g3").rev()
*/
export const rev = register('rev', function (pat) {
const query = function (state) {
@@ -1986,9 +1975,9 @@ export const press = register('press', function (pat) {
* s("hh*3")
* )
*/
Pattern.prototype.hush = function () {
export const hush = register('hush', function (pat) {
return silence;
};
});
/**
* Applies `rev` to a pattern every other cycle, so that the pattern alternates between forwards and backwards.
@@ -1996,7 +1985,7 @@ Pattern.prototype.hush = function () {
* note("c d e g").palindrome()
*/
export const palindrome = register('palindrome', function (pat) {
return pat.lastOf(2, rev);
return pat.every(2, rev);
});
/**
@@ -2004,7 +1993,7 @@ export const palindrome = register('palindrome', function (pat) {
* @name juxBy
* @synonyms juxby
* @example
* s("bd lt [~ ht] mt cp ~ bd hh").juxBy("<0 .5 1>/2", rev)
* s("lt ht mt ht hh").juxBy("<0 .5 1>/2", rev)
*/
export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, pat) {
by /= 2;
@@ -2023,11 +2012,7 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func,
/**
* The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.
* @example
* s("bd lt [~ ht] mt cp ~ bd hh").jux(rev)
* @example
* s("bd lt [~ ht] mt cp ~ bd hh").jux(press)
* @example
* s("bd lt [~ ht] mt cp ~ bd hh").jux(iter(4))
* s("lt ht mt ht hh").jux(rev)
*/
export const jux = register('jux', function (func, pat) {
return pat._juxBy(1, func, pat);
@@ -2043,7 +2028,7 @@ export const jux = register('jux', function (func, pat) {
* @example
* "<0 [2 4]>"
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
* .scale("C:minor").note()
* .scale('C minor').note().clip(.2)
*/
export const { echoWith, echowith, stutWith, stutwith } = register(
['echoWith', 'echowith', 'stutWith', 'stutwith'],
@@ -2115,44 +2100,23 @@ export const { iterBack, iterback } = register(['iterBack', 'iterback'], functio
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).
* @name chunk
* @synonyms slowChunk, slowchunk
* @memberof Pattern
* @returns Pattern
* @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);
binary.unshift(true);
// Invert the 'back' because we want to shift the pattern forwards,
// and so time backwards
const binary_pat = _iter(n, sequence(...binary), !back);
if (!fast) {
pat = pat.repeatCycles(n);
}
const binary_pat = _iter(n, sequence(...binary), back);
return pat.when(binary_pat, func);
};
const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) {
return _chunk(n, func, pat, false, false);
export const chunk = register('chunk', function (n, func, pat) {
return _chunk(n, func, pat, false);
});
/**
@@ -2162,29 +2126,12 @@ const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChu
* @memberof Pattern
* @returns Pattern
* @example
* "0 1 2 3".chunkBack(4, x=>x.add(7))
* .scale("A:minor").note()
* "0 1 2 3".chunkBack(4, x=>x.add(7)).scale('A minor').note()
*/
export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], function (n, func, pat) {
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(2)
* .scale("C2:major").note()
*/
export const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], function (n, func, pat) {
return _chunk(n, func, pat, false, true);
});
// TODO - redefine elsewhere in terms of mask
export const bypass = register('bypass', function (on, pat) {
on = Boolean(parseInt(on));
@@ -2196,8 +2143,6 @@ export const bypass = register('bypass', function (on, pat) {
* @param {number} offset start point of loop in cycles
* @param {number} cycles loop length in cycles
* @example
* note("<c d e f>").ribbon(1, 2).fast(2)
* @example
* // Looping a portion of randomness
* note(irand(8).segment(4).scale('C3 minor')).ribbon(1337, 2)
*/
@@ -2209,19 +2154,8 @@ export const duration = register('duration', function (value, pat) {
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.
* @name color
* @synonyms colour
* @param {string} color Hexadecimal or CSS color name
*/
// TODO: move this to controls https://github.com/tidalcycles/strudel/issues/288
export const { color, colour } = register(['color', 'colour'], function (color, pat) {
@@ -2271,7 +2205,7 @@ export const legato = register('legato', function (value, pat) {
* s("rhodes")
* .chop(4)
* .rev() // reverse order of chops
* .loopAt(2) // fit sample into 2 cycles
* .loopAt(4) // fit sample into 4 cycles
*
*/
export const chop = register('chop', function (n, pat) {
@@ -2283,14 +2217,6 @@ export const chop = register('chop', function (n, pat) {
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(3)
*/
export const striate = register('striate', function (n, pat) {
const slices = Array.from({ length: n }, (x, i) => i);
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
@@ -2305,10 +2231,10 @@ export const striate = register('striate', function (n, pat) {
* @returns Pattern
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes").loopAt(2)
* s("rhodes").loopAt(4)
*/
// TODO - global cps clock
const _loopAt = function (factor, pat, cps = 0.5) {
const _loopAt = function (factor, pat, cps = 1) {
return pat
.speed((1 / factor) * cps)
.unit('c')
@@ -2323,10 +2249,10 @@ const _loopAt = function (factor, pat, cps = 0.5) {
* @returns Pattern
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(0.75)
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks125").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
* s("breaks125/2").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
*/
export const slice = register(
@@ -2347,58 +2273,50 @@ export const slice = register(
false, // turns off auto-patternification
);
/**
/*
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
* @name splice
* @memberof Pattern
* @returns Pattern
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165")
* .splice(8, "0 1 [2 3 0]@2 3 0@2 7")
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
*/
export const splice = register(
'splice',
function (npat, ipat, opat) {
const sliced = slice(npat, ipat, opat);
return new Pattern((state) => {
// TODO - default cps to 0.5
const cps = state.controls._cps || 1;
const haps = sliced.query(state);
return haps.map((hap) =>
hap.withValue((v) => ({
...{
speed: (cps / v._slices / hap.whole.duration) * (v.speed || 1),
unit: 'c',
},
...v,
})),
);
return sliced.withHap(function (hap) {
return hap.withValue((v) => ({
...{
speed: (1 / v._slices / hap.whole.duration) * (v.speed || 1),
unit: 'c',
},
...v,
}));
});
},
false, // turns off auto-patternification
);
// this function will be redefined in repl.mjs to use the correct cps value.
// It is still here to work in cases where repl.mjs is not used
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state));
return _loopAt(factor, pat, 1);
});
/**
* Makes the sample fit its event duration. Good for rhythmical loops like drum breaks.
* Similar to `loopAt`.
* @name fit
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes/2").fit()
*/
// this function will be redefined in repl.mjs to use the correct cps value.
// It is still here to work in cases where repl.mjs is not used
export const fit = register('fit', (pat) =>
pat.withHaps((haps, state) =>
haps.map((hap) =>
hap.withValue((v) => ({
...v,
speed: (state.controls._cps || 1) / hap.whole.duration,
unit: 'c',
})),
),
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: 1 / hap.whole.duration,
unit: 'c',
})),
),
);
@@ -2418,35 +2336,3 @@ export const fit = register('fit', (pat) =>
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
return _loopAt(factor, pat, cps);
});
/** exposes a custom value at query time. basically allows mutating state without evaluation */
export const ref = (accessor) =>
pure(1)
.withValue(() => reify(accessor()))
.innerJoin();
let fadeGain = (p) => (p < 0.5 ? 1 : 1 - (p - 0.5) / 0.5);
/**
* Cross-fades between left and right from 0 to 1:
* - 0 = (full left, no right)
* - .5 = (both equal)
* - 1 = (no left, full right)
*
* @name xfade
* @example
* xfade(s("bd*2"), "<0 .25 .5 .75 1>", s("hh*8"))
*/
export let xfade = (a, pos, b) => {
pos = reify(pos);
a = reify(a);
b = reify(b);
let gaina = pos.fmap((v) => ({ gain: fadeGain(v) }));
let gainb = pos.fmap((v) => ({ gain: fadeGain(1 - v) }));
return stack(a.mul(gaina), b.mul(gainb));
};
// the prototype version is actually flipped so left/right makes sense
Pattern.prototype.xfade = function (pos, b) {
return xfade(this, pos, b);
};
+172 -136
View File
@@ -29,26 +29,138 @@ const getValue = (e) => {
return value;
};
Pattern.prototype.pianoroll = function (options = {}) {
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
Pattern.prototype.pianoroll = function ({
cycles = 4,
playhead = 0.5,
overscan = 1,
flipTime = 0,
flipValues = 0,
hideNegative = false,
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
minMidi = 10,
maxMidi = 90,
autorange = 0,
timeframe: timeframeProp,
fold = 0,
vertical = 0,
labels = 0,
} = {}) {
const ctx = getDrawContext();
const w = ctx.canvas.width;
const h = ctx.canvas.height;
let from = -cycles * playhead;
let to = cycles * (1 - playhead);
if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead');
from = 0;
to = timeframeProp;
}
const timeAxis = vertical ? h : w;
const valueAxis = vertical ? w : h;
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
let foldValues = [];
flipTime && timeRange.reverse();
flipValues && valueRange.reverse();
this.draw(
(ctx, haps, t) => {
(ctx, events, t) => {
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
pianoroll({
...options,
time: t,
ctx,
haps: haps.filter(inFrame),
events.filter(inFrame).forEach((event) => {
const isActive = event.whole.begin <= t && event.endClipped > t;
ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valuePx = scale(
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
...valueRange,
);
let margin = 0;
const offset = scale(t / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
ctx.strokeStyle = playheadColor;
ctx.beginPath();
if (vertical) {
ctx.moveTo(0, playheadPosition);
ctx.lineTo(valueAxis, playheadPosition);
} else {
ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
},
{
from: from - overscan,
to: to + overscan,
onQuery: (events) => {
const { min, max, values } = events.reduce(
({ min, max, values }, e) => {
const v = getValue(e);
return {
min: v < min ? v : min,
max: v > max ? v : max,
values: values.includes(v) ? values : [...values, v],
};
},
{ min: Infinity, max: -Infinity, values: [] },
);
if (autorange) {
minMidi = min;
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
},
},
);
return this;
@@ -56,40 +168,6 @@ Pattern.prototype.pianoroll = function (options = {}) {
// this function allows drawing a pianoroll without ties to Pattern.prototype
// 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({
time,
haps,
@@ -113,13 +191,6 @@ export function pianoroll({
fold = 0,
vertical = 0,
labels = false,
fill = 1,
fillActive = false,
strokeActive = true,
stroke,
hideInactive = 0,
colorizeInactive = 1,
fontFamily,
ctx,
} = {}) {
const w = ctx.canvas.width;
@@ -160,83 +231,61 @@ export function pianoroll({
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
foldValues = values.sort((a, b) =>
typeof a === 'number' && typeof b === 'number'
? a - b
: typeof a === 'number'
? 1
: String(a).localeCompare(String(b)),
);
// foldValues = values.sort((a, b) => a - b);
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
haps.forEach((event) => {
const isActive = event.whole.begin <= time && event.endClipped > time;
let strokeCurrent = stroke ?? (strokeActive && isActive);
let fillCurrent = (!isActive && fill) || (isActive && fillActive);
if (hideInactive && !isActive) {
return;
}
let color = event.value?.color || event.context?.color;
active = color || active;
inactive = colorizeInactive ? color || inactive : inactive;
color = isActive ? active : inactive;
ctx.fillStyle = fillCurrent ? color : 'transparent';
ctx.strokeStyle = color;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
const timePx = scale(timeProgress, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valueProgress = fold
? foldValues.indexOf(value) / foldValues.length
: (Number(value) - minMidi) / valueExtent;
const valuePx = scale(valueProgress, ...valueRange);
let margin = 0;
const offset = scale(time / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
/* const xFactor = Math.sin(performance.now() / 500) + 1;
coords[0] *= xFactor; */
if (strokeCurrent) {
ctx.strokeRect(...coords);
}
if (fillCurrent) {
ctx.fillRect(...coords);
}
//ctx.ellipse(...ellipseFromRect(...coords))
if (labels) {
const defaultLabel = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
const { label: inactiveLabel, activeLabel } = event.value;
const customLabel = isActive ? activeLabel || inactiveLabel : inactiveLabel;
const label = customLabel ?? defaultLabel;
let measure = vertical ? durationPx : barThickness * 0.75;
ctx.font = `${measure}px ${fontFamily || 'monospace'}`;
// font color
ctx.fillStyle = /* isActive && */ !fillCurrent ? color : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
/* const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
haps
// .filter(inFrame)
.forEach((event) => {
const isActive = event.whole.begin <= time && event.endClipped > time;
const color = event.value?.color || event.context?.color;
ctx.fillStyle = color || inactive;
ctx.strokeStyle = color || active;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valuePx = scale(
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
...valueRange,
);
let margin = 0;
const offset = scale(time / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
@@ -261,23 +310,10 @@ export function getDrawOptions(drawTime, options = {}) {
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) {
return this.onPaint(getPunchcardPainter(options));
};
/**
* Displays a vertical pianoroll with event labels.
* Supports all the same options as pianoroll.
*
* @name wordfall
*/
Pattern.prototype.wordfall = function (options) {
return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options });
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }),
);
};
/* Pattern.prototype.pianoroll = function (options) {
+35 -114
View File
@@ -3,7 +3,7 @@ import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
import { register } from './pattern.mjs';
export function repl({
interval,
@@ -16,146 +16,67 @@ export function repl({
transpiler,
onToggle,
editPattern,
onUpdateState,
}) {
const state = {
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,
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
onToggle: (started) => {
updateState({ started });
onToggle?.(started);
},
onToggle,
});
let pPatterns = {};
let allTransform;
const hush = function () {
pPatterns = {};
allTransform = undefined;
return silence;
};
const setPattern = (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
};
setTime(() => scheduler.now()); // TODO: refactor?
const stop = () => scheduler.stop();
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
const all = function (transform) {
allTransform = transform;
return silence;
};
// set pattern methods that use this repl via closure
const injectPatternMethods = () => {
Pattern.prototype.p = function (id) {
pPatterns[id] = this;
return this;
};
Pattern.prototype.q = function (id) {
return silence;
};
try {
for (let i = 1; i < 10; ++i) {
Object.defineProperty(Pattern.prototype, `d${i}`, {
get() {
return this.p(i);
},
configurable: true,
});
Object.defineProperty(Pattern.prototype, `p${i}`, {
get() {
return this.p(i);
},
configurable: true,
});
Pattern.prototype[`q${i}`] = silence;
}
} catch (err) {
console.warn('injectPatternMethods: error:', err);
}
const cpm = register('cpm', function (cpm, pat) {
return pat._fast(cpm / 60 / scheduler.cps);
});
evalScope({
all,
hush,
cpm,
setCps,
setcps: setCps,
setCpm,
setcpm: setCpm,
});
};
const evaluate = async (code, autostart = true, shouldHush = true) => {
const evaluate = async (code, autostart = true) => {
if (!code) {
throw new Error('no code to evaluate');
}
try {
updateState({ code, pending: true });
injectPatternMethods();
await beforeEval?.({ code });
shouldHush && hush();
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`);
setPattern(pattern, autostart);
updateState({
miniLocations: meta?.miniLocations || [],
widgets: meta?.widgets || [],
activeCode: code,
pattern,
evalError: undefined,
schedulerError: undefined,
pending: false,
});
afterEval?.({ code, pattern, meta });
return pattern;
} catch (err) {
// console.warn(`[repl] eval error: ${err.message}`);
logger(`[eval] error: ${err.message}`, 'error');
updateState({ evalError: err, pending: false });
onEvalError?.(err);
}
};
const setCode = (code) => updateState({ code });
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state };
const stop = () => scheduler.stop();
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
// the following functions use the cps value, which is why they are defined here..
const loopAt = register('loopAt', (cycles, pat) => {
return pat.loopAtCps(cycles, scheduler.cps);
});
const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: scheduler.cps / hap.whole.duration, // overwrite speed completely?
unit: 'c',
})),
),
);
evalScope({
loopAt,
fit,
setCps,
setcps: setCps,
setCpm,
setcpm: setCpm,
});
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
}
export const getTrigger =
+22 -177
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 { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
import Fraction from './fraction.mjs';
import { id, _mod, clamp, objectMap } from './util.mjs';
import { id } from './util.mjs';
export function steady(value) {
// A continuous value
@@ -27,11 +27,9 @@ export const isaw2 = isaw.toBipolar();
*
* @return {Pattern}
* @example
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
* .clip(saw.slow(2))
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
* @example
* n(saw.range(0,8).segment(8))
* .scale('C major')
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
*
*/
export const saw = signal((t) => t % 1);
@@ -44,8 +42,7 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
*
* @return {Pattern}
* @example
* n(sine.segment(16).range(0,15))
* .scale("C:minor")
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
*
*/
export const sine = sine2.fromBipolar();
@@ -55,8 +52,7 @@ export const sine = sine2.fromBipolar();
*
* @return {Pattern}
* @example
* n(stack(sine,cosine).segment(16).range(0,15))
* .scale("C:minor")
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor').note()
*
*/
export const cosine = sine._early(Fraction(1).div(4));
@@ -67,7 +63,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
*
* @return {Pattern}
* @example
* n(square.segment(4).range(0,7)).scale("C:minor")
* square.segment(2).range(0,7).scale('C minor').note()
*
*/
export const square = signal((t) => Math.floor((t * 2) % 2));
@@ -78,7 +74,7 @@ export const square2 = square.toBipolar();
*
* @return {Pattern}
* @example
* n(tri.segment(8).range(0,7)).scale("C:minor")
* tri.segment(8).range(0,7).scale('C minor').note()
*
*/
export const tri = fastcat(isaw, saw);
@@ -105,7 +101,6 @@ const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
const timeToRandsPrime = (seed, n) => {
const result = [];
// eslint-disable-next-line
for (let i = 0; i < n; ++n) {
result.push(intSeedToRand(seed));
seed = xorwise(seed);
@@ -122,8 +117,8 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
/**
* A discrete pattern of numbers from 0 to n-1
* @example
* n(run(4)).scale("C4:pentatonic")
* // n("0 1 2 3").scale("C4:pentatonic")
* run(4).scale('C4 major').note()
* // "0 1 2 3".scale('C4 major').note()
*/
export const run = (n) => saw.range(0, n).floor().segment(n);
@@ -133,7 +128,7 @@ export const run = (n) => saw.range(0, n).floor().segment(n);
* @name rand
* @example
* // randomly change the cutoff
* s("bd*4,hh*8").cutoff(rand.range(500,8000))
* s("bd sd,hh*4").cutoff(rand.range(500,2000))
*
*/
export const rand = signal(timeToRand);
@@ -155,158 +150,17 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
* @param {number} n max value (exclusive)
* @example
* // randomly select scale notes from 0 - 7 (= C to C)
* n(irand(8)).struct("x x*2 x x*3").scale("C:minor")
* irand(8).struct("x(3,8)").scale('C minor').note()
*
*/
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
const _pick = function (lookup, pat, modulo = true) {
const array = Array.isArray(lookup);
const len = Object.keys(lookup).length;
lookup = objectMap(lookup, reify);
if (len === 0) {
return silence;
}
return pat.fmap((i) => {
let key = i;
if (array) {
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
}
return lookup[key];
});
};
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `inhabit`, but maintains the structure of the original patterns.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
* @example
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
* @example
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
* @example
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
*/
export const pick = function (lookup, pat) {
// backward compatibility - the args used to be flipped
if (Array.isArray(pat)) {
[pat, lookup] = [lookup, pat];
}
return __pick(lookup, pat);
};
const __pick = register('pick', function (lookup, pat) {
return _pick(lookup, pat, false).innerJoin();
});
/** * The same as `pick`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmod = register('pickmod', function (lookup, pat) {
return _pick(lookup, pat, true).innerJoin();
});
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
* @example
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
* @example
* note("<c2 d2>(3,8)").s("square")
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
*/
export const pickF = register('pickF', function (lookup, funcs, pat) {
return pat.apply(pick(lookup, funcs));
});
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
*/
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
return pat.apply(pickmod(lookup, funcs));
});
/**
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
b: s("cp sd")
})
* @example
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
*/
export const inhabit = register('inhabit', function (lookup, pat) {
return _pick(lookup, pat, true).squeezeJoin();
});
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const inhabitmod = register('inhabit', function (lookup, pat) {
return _pick(lookup, pat, false).squeezeJoin();
});
/**
* 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) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat.range(0, xs.length).fmap((i) => {
const key = Math.min(Math.max(Math.floor(i), 0), xs.length - 1);
return xs[key];
});
return pat.range(0, xs.length).fmap((i) => xs[Math.floor(i)]);
};
/**
* Choose from the list of values (or patterns of values) using the given
@@ -314,8 +168,6 @@ export const __chooseWith = (pat, xs) => {
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note("c2 g2!2 d2 f1").s(chooseWith(sine.fast(2), ["sawtooth", "triangle", "bd:6"]))
*/
export const chooseWith = (pat, xs) => {
return __chooseWith(pat, xs).outerJoin();
@@ -339,13 +191,6 @@ export const chooseInWith = (pat, xs) => {
*/
export const choose = (...xs) => chooseWith(rand, xs);
/**
* Chooses from the given list of values (or patterns of values), according
* to the pattern that the method is called on. The pattern should be in
* the range 0 .. 1.
* @param {...any} xs
* @returns {Pattern}
*/
Pattern.prototype.choose = function (...xs) {
return chooseWith(this, xs);
};
@@ -364,9 +209,9 @@ Pattern.prototype.choose2 = function (...xs) {
* Picks one of the elements at random each cycle.
* @returns {Pattern}
* @example
* chooseCycles("bd", "hh", "sd").s().fast(8)
* chooseCycles("bd", "hh", "sd").s().fast(4)
* @example
* s("bd | hh | sd").fast(8)
* "bd | hh | sd".s().fast(4)
*/
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
@@ -409,7 +254,7 @@ export const perlinWith = (pat) => {
* @name perlin
* @example
* // randomly change the cutoff
* s("bd*4,hh*8").cutoff(perlin.range(500,8000))
* s("bd sd,hh*4").cutoff(perlin.range(500,2000))
*
*/
export const perlin = perlinWith(time.fmap((v) => Number(v)));
@@ -451,7 +296,7 @@ export const degradeBy = register('degradeBy', function (x, pat) {
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
/**
* Inverse of `degradeBy`: Randomly removes events from the pattern by a given amount.
* Inverse of {@link Pattern#degradeBy}: Randomly removes events from the pattern by a given amount.
* 0 = 100% chance of removal
* 1 = 0% chance of removal
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
@@ -475,7 +320,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
/**
*
* Randomly applies the given function by the given probability.
* Similar to `someCyclesBy`
* Similar to {@link Pattern#someCyclesBy}
*
* @name sometimesBy
* @memberof Pattern
@@ -483,7 +328,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
* @param {function} function - the transformation to apply
* @returns Pattern
* @example
* s("hh*8").sometimesBy(.4, x=>x.speed("0.5"))
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
*/
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
@@ -501,7 +346,7 @@ export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
* @param {function} function - the transformation to apply
* @returns Pattern
* @example
* s("hh*8").sometimes(x=>x.speed("0.5"))
* s("hh*4").sometimes(x=>x.speed("0.5"))
*/
export const sometimes = register('sometimes', function (func, pat) {
return pat._sometimesBy(0.5, func);
@@ -510,7 +355,7 @@ export const sometimes = register('sometimes', function (func, pat) {
/**
*
* Randomly applies the given function by the given probability on a cycle by cycle basis.
* Similar to `sometimesBy`
* Similar to {@link Pattern#sometimesBy}
*
* @name someCyclesBy
* @memberof Pattern
@@ -518,7 +363,7 @@ export const sometimes = register('sometimes', function (func, pat) {
* @param {function} function - the transformation to apply
* @returns Pattern
* @example
* s("bd,hh*8").someCyclesBy(.3, x=>x.speed("0.5"))
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
*/
export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
@@ -540,7 +385,7 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat)
* @memberof Pattern
* @returns Pattern
* @example
* s("bd,hh*8").someCycles(x=>x.speed("0.5"))
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
*/
export const someCycles = register('someCycles', function (func, pat) {
return pat._someCyclesBy(0.5, func);
+11 -125
View File
@@ -20,7 +20,6 @@ import {
slowcat,
cat,
sequence,
palindrome,
polymeter,
polymeterSteps,
polyrhythm,
@@ -46,7 +45,6 @@ import {
rev,
time,
run,
pick,
} from '../index.mjs';
import { steady } from '../signal.mjs';
@@ -184,17 +182,13 @@ describe('Pattern', () => {
});
it('can Trig() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.trig(20, 30)
.early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trig(20, 30).early(2),
sequence(26, 27, 36, 37),
);
});
it('can Trigzero() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.trigzero(20, 30)
.early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trigzero(20, 30).early(2),
sequence(21, 22, 31, 32),
);
});
@@ -236,17 +230,13 @@ describe('Pattern', () => {
});
it('can Trig() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.trig(20, 30)
.early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trig(20, 30).early(2),
sequence(6, 7, 6, 7),
);
});
it('can Trigzero() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.trigzero(20, 30)
.early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trigzero(20, 30).early(2),
sequence(1, 2, 1, 2),
);
});
@@ -282,17 +272,13 @@ describe('Pattern', () => {
});
it('can Trig() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.trig(false, true)
.early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trig(false, true).early(2),
sequence(silence, silence, 6, 7),
);
});
it('can Trigzero() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.trigzero(false, true)
.early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trigzero(false, true).early(2),
sequence(silence, silence, 1, 2),
);
});
@@ -585,18 +571,6 @@ describe('Pattern', () => {
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()', () => {
it('Can layer up cycles', () => {
expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual(
@@ -664,11 +638,7 @@ describe('Pattern', () => {
});
describe('struct()', () => {
it('Can restructure a discrete pattern', () => {
expect(
sequence('a', 'b')
.struct(sequence(true, true, true))
.firstCycle(),
).toStrictEqual([
expect(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle()).toStrictEqual([
hap(ts(0, third), ts(0, third), 'a'),
hap(ts(third, twothirds), ts(third, 0.5), 'a'),
hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
@@ -699,11 +669,7 @@ describe('Pattern', () => {
});
describe('mask()', () => {
it('Can fragment a pattern', () => {
expect(
sequence('a', 'b')
.mask(sequence(true, true, true))
.firstCycle(),
).toStrictEqual([
expect(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(third, 0.5), 'a'),
hap(ts(0.5, 1), ts(0.5, twothirds), 'b'),
@@ -972,11 +938,9 @@ describe('Pattern', () => {
expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1);
});
it('Doesnt merge two overlapping haps', () => {
expect(
stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1))
.defragmentHaps()
.firstCycle().length,
).toStrictEqual(2);
expect(stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(
2,
);
});
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);
@@ -1039,82 +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]);
});
});
describe('inhabit', () => {
it('Can pattern named patterns', () => {
expect(
sameFirst(
sequence('a', 'b', stack('a', 'b')).inhabit({ a: sequence(1, 2), b: sequence(10, 20, 30) }),
sequence([1, 2], [10, 20, 30], stack([1, 2], [10, 20, 30])),
),
);
});
it('Can pattern indexed patterns', () => {
expect(
sameFirst(
sequence('0', '1', stack('0', '1')).inhabit([sequence(1, 2), sequence(10, 20, 30)]),
sequence([1, 2], [10, 20, 30], stack([1, 2], [10, 20, 30])),
),
);
});
});
describe('pick', () => {
it('Can pattern named patterns', () => {
expect(
sameFirst(
sequence('a', 'b', 'a', stack('a', 'b')).pick({ a: sequence(1, 2, 3, 4), b: sequence(10, 20, 30, 40) }),
sequence(1, 20, 3, stack(4, 40)),
),
);
});
it('Can pattern indexed patterns', () => {
expect(
sameFirst(
sequence(0, 1, 0, stack(0, 1)).pick([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]),
sequence(1, 20, 3, stack(4, 40)),
),
);
});
it('Clamps indexes', () => {
expect(
sameFirst(sequence(0, 1, 2, 3).pick([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]), sequence(1, 20, 30, 40)),
);
});
it('Is backwards compatible', () => {
expect(
sameFirst(
pick([sequence('a', 'b'), sequence('c', 'd')], sequence(0, 1)),
pick(sequence(0, 1), [sequence('a', 'b'), sequence('c', 'd')]),
),
);
});
});
describe('pickmod', () => {
it('Wraps indexes', () => {
expect(
sameFirst(
sequence(0, 1, 2, 3).pickmod([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]),
sequence(1, 20, 3, 40),
),
);
});
});
});
+2
View File
@@ -46,5 +46,7 @@ export const cleanupUi = () => {
const container = document.getElementById('code');
if (container) {
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 -57
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/>.
*/
import { logger } from './logger.mjs';
// returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const isNote = (name) => /^[a-gA-G][#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)
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) => {
let { value, context } = hap;
let note = value;
@@ -276,50 +262,15 @@ export const sol2note = (n, notation = 'letters') => {
notation === 'solfeggio'
? solfeggio /*check if its is any of the following*/
: notation === 'indian'
? indian
: notation === 'german'
? german
: notation === 'byzantine'
? byzantine
: notation === 'japanese'
? japanese
: english; /*if not use standard version*/
? indian
: notation === 'german'
? german
: notation === 'byzantine'
? byzantine
: notation === 'japanese'
? japanese
: english; /*if not use standard version*/
const note = pc[n % 12]; /*calculating the midi value to the note*/
const oct = Math.floor(n / 12) - 1;
return note + oct;
};
// 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 || ''));
}
export function objectMap(obj, fn) {
if (Array.isArray(obj)) {
return obj.map(fn);
}
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
}
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
-1
View File
@@ -1 +0,0 @@
# @strudel/csound
+3 -3
View File
@@ -1,5 +1,5 @@
import { getFrequency, logger, register } from '@strudel/core';
import { getAudioContext } from '@strudel/webaudio';
import { getFrequency, logger, register } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import csd from './project.csd?raw';
// import livecodeOrc from './livecode.orc?raw';
import presetsOrc from './presets.orc?raw';
@@ -137,7 +137,7 @@ export async function loadOrc(url) {
export const csoundm = register('csoundm', (instrument, pat) => {
let p1 = instrument;
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
return pat.onTrigger((tidal_time, hap) => {
+7 -7
View File
@@ -1,11 +1,11 @@
{
"name": "@strudel/csound",
"version": "1.0.1",
"name": "@strudel.cycles/csound",
"version": "0.8.0",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
@@ -33,10 +33,10 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@csound/browser": "6.18.7",
"@strudel/core": "workspace:*",
"@strudel/webaudio": "workspace:*"
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
},
"devDependencies": {
"vite": "^5.0.10"
"vite": "^4.3.3"
}
}
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
-3
View File
@@ -1,3 +0,0 @@
# @strudel/desktopbridge
This package contains utilities used to communicate with the Tauri backend
-11
View File
@@ -1,11 +0,0 @@
import { listen } from '@tauri-apps/api/event';
import { logger } from '../core/logger.mjs';
// listen for log events from the Tauri backend and log in the UI
await listen('log-event', (e) => {
if (e.payload == null) {
return;
}
const { message, message_type } = e.payload;
logger(message, message_type);
});
-52
View File
@@ -1,52 +0,0 @@
import { Invoke } from './utils.mjs';
import { Pattern, noteToMidi } from '@strudel/core';
const ON_MESSAGE = 0x90;
const OFF_MESSAGE = 0x80;
const CC_MESSAGE = 0xb0;
Pattern.prototype.midi = function (output) {
return this.onTrigger((time, hap, currentTime) => {
const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
const offset = (time - currentTime) * 1000;
const velocity = Math.floor((hap.context?.velocity ?? 0.9) * 100); // TODO: refactor velocity
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
const roundedOffset = Math.round(offset);
const midichan = (hap.value.midichan ?? 1) - 1;
const requestedport = output ?? 'IAC';
const messagesfromjs = [];
if (note != null) {
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
messagesfromjs.push({
requestedport,
message: [ON_MESSAGE + midichan, midiNumber, velocity],
offset: roundedOffset,
});
messagesfromjs.push({
requestedport,
message: [OFF_MESSAGE + midichan, midiNumber, velocity],
offset: roundedOffset + duration,
});
}
if (ccv && ccn) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1');
}
if (!['string', 'number'].includes(typeof ccn)) {
throw new Error('expected ccn to be a number or a string');
}
const scaled = Math.round(ccv * 127);
messagesfromjs.push({
requestedport,
message: [CC_MESSAGE + midichan, ccn, scaled],
offset: roundedOffset,
});
}
// invoke is temporarily blocking, run in an async process
if (messagesfromjs.length) {
setTimeout(() => {
Invoke('sendmidi', { messagesfromjs });
});
}
});
};
-43
View File
@@ -1,43 +0,0 @@
import { parseNumeral, Pattern } from '@strudel/core';
import { Invoke } from './utils.mjs';
Pattern.prototype.osc = function () {
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
hap.ensureObjectValue();
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
// make sure n and note are numbers
controls.n && (controls.n = parseNumeral(controls.n));
controls.note && (controls.note = parseNumeral(controls.note));
const params = [];
const timestamp = Math.round(Date.now() + (time - currentTime) * 1000);
Object.keys(controls).forEach((key) => {
const val = controls[key];
const value = typeof val === 'number' ? val.toString() : val;
if (value == null) {
return;
}
params.push({
name: key,
value,
valueisnumber: typeof val === 'number',
});
});
const messagesfromjs = [];
if (params.length) {
messagesfromjs.push({ target: '/dirt/play', timestamp, params });
}
if (messagesfromjs.length) {
setTimeout(() => {
Invoke('sendosc', { messagesfromjs });
});
}
});
};
-29
View File
@@ -1,29 +0,0 @@
{
"name": "@strudel/desktopbridge",
"version": "0.1.0",
"private": true,
"description": "tools/shims for communicating between the JS and Tauri (Rust) sides of the Studel desktop app",
"main": "index.mjs",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Jade Rowland <jaderowlanddev@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"dependencies": {
"@strudel/core": "workspace:*",
"@tauri-apps/api": "^1.5.3"
},
"homepage": "https://github.com/tidalcycles/strudel#readme"
}
-4
View File
@@ -1,4 +0,0 @@
import { invoke } from '@tauri-apps/api/tauri';
export const Invoke = invoke;
export const isTauri = () => window.__TAURI_IPC__ != null;
+3 -3
View File
@@ -1,13 +1,13 @@
# @strudel/embed
# @strudel.cycles/embed
This package contains a embeddable web component for the Strudel REPL.
## Usage
Either install with `npm i @strudel/embed` or just use a cdn to import the script:
Either install with `npm i @strudel.cycles/embed` or just use a cdn to import the script:
```html
<script src="https://unpkg.com/@strudel/embed@latest"></script>
<script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
<strudel-repl>
<!--
note(`[[e5 [b4 c5] d5 [c5 b4]]
+1 -1
View File
@@ -6,7 +6,7 @@ class Strudel extends HTMLElement {
setTimeout(() => {
const code = (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
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))}`;
iframe.setAttribute('src', src);
iframe.setAttribute('width', '600');
@@ -1,4 +1,4 @@
<script src="https://unpkg.com/@strudel/embed@0.11.0"></script>
<script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
<!-- <script src="./embed.js"></script> -->
<strudel-repl>
<!--
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/embed",
"version": "1.0.0",
"name": "@strudel.cycles/embed",
"version": "0.2.0",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js",
"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/core';
evalScope(
import('@strudel/hydra')
)
```
-40
View File
@@ -1,40 +0,0 @@
import { getDrawContext } from '@strudel/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": "1.0.1",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
"publishConfig": {
"main": "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/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'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+2 -2
View File
@@ -1,9 +1,9 @@
# @strudel/midi
# @strudel.cycles/midi
This package adds midi functionality to strudel Patterns.
## Install
```sh
npm i @strudel/midi --save
npm i @strudel.cycles/midi --save
```
+64 -121
View File
@@ -5,9 +5,10 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import { Pattern, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi } from '@strudel/core';
import { Note } from 'webmidi';
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { noteToMidi } from '@strudel.cycles/core';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -15,28 +16,12 @@ function supportsMidi() {
return typeof navigator.requestMIDIAccess === 'function';
}
function getMidiDeviceNamesString(devices) {
return devices.map((o) => `'${o.name}'`).join(' | ');
}
export function enableWebMidi(options = {}) {
const { onReady, onConnected, onDisconnected, onEnabled } = options;
if (WebMidi.enabled) {
return;
}
const { onReady, onConnected, onDisconnected } = options;
if (!supportsMidi()) {
throw new Error('Your Browser does not support WebMIDI.');
}
WebMidi.addListener('connected', () => {
onConnected?.(WebMidi);
});
WebMidi.addListener('enabled', () => {
onEnabled?.(WebMidi);
});
// Reacting when a device becomes unavailable
WebMidi.addListener('disconnected', (e) => {
onDisconnected?.(WebMidi, e);
});
return new Promise((resolve, reject) => {
if (WebMidi.enabled) {
// if already enabled, just resolve WebMidi
@@ -47,49 +32,57 @@ export function enableWebMidi(options = {}) {
if (err) {
reject(err);
}
WebMidi.addListener('connected', (e) => {
onConnected?.(WebMidi);
});
// Reacting when a device becomes unavailable
WebMidi.addListener('disconnected', (e) => {
onDisconnected?.(WebMidi, e);
});
onReady?.(WebMidi);
resolve(WebMidi);
});
});
}
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
const outputByName = (name) => WebMidi.getOutputByName(name);
function getDevice(indexOrName, devices) {
if (!devices.length) {
let midiReady;
// output?: string | number, outputs: typeof WebMidi.outputs
function getDevice(output, outputs) {
if (!outputs.length) {
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
}
if (typeof indexOrName === 'number') {
return devices[indexOrName];
if (typeof output === 'number') {
return outputs[output];
}
const byName = (name) => devices.find((output) => output.name.includes(name));
if (typeof indexOrName === 'string') {
return byName(indexOrName);
if (typeof output === 'string') {
return outputByName(output);
}
// attempt to default to first IAC device if none is specified
const IACOutput = byName('IAC');
const device = IACOutput ?? devices[0];
if (!device) {
throw new Error(
`🔌 MIDI device '${device ? device : ''}' not found. Use one of ${getMidiDeviceNamesString(devices)}`,
);
}
return IACOutput ?? devices[0];
}
// send start/stop messages to outputs when repl starts/stops
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (!WebMidi?.enabled) {
return;
}
if (e.data === 'strudel-stop') {
WebMidi.outputs.forEach((output) => output.sendStop());
}
// cannot start here, since we have no timing info, see sendStart below
});
return outputs[0];
}
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.midi = function (output) {
if (!supportsMidi()) {
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
}
/* await */ enableWebMidi({
onConnected: ({ outputs }) =>
logger(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
onDisconnected: ({ outputs }) =>
logger(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
onReady: ({ outputs }) => {
const device = getDevice(output, outputs);
const otherOutputs = outputs
.filter((o) => o.name !== device.name)
.map((o) => `'${o.name}'`)
.join(' | ');
midiReady = true;
logger(`Midi connected! Using "${device.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
},
});
if (isPattern(output)) {
throw new Error(
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
@@ -97,47 +90,38 @@ Pattern.prototype.midi = function (output) {
}')`,
);
}
enableWebMidi({
onEnabled: ({ outputs }) => {
const device = getDevice(output, outputs);
const otherOutputs = outputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
}`,
);
},
onDisconnected: ({ outputs }) =>
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});
return this.onTrigger((time, hap, currentTime, cps) => {
if (!WebMidi.enabled) {
console.log('not enabled');
return this.onTrigger((time, hap) => {
if (!midiReady) {
return;
}
const device = getDevice(output, WebMidi.outputs);
if (!device) {
throw new Error(
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
.map((o) => `'${o.name}'`)
.join(' | ')}`,
);
}
hap.ensureObjectValue();
const offset = (time - currentTime) * 1000;
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
const timeOffsetString = `+${offset}`;
// calculate time
const timingOffset = WebMidi.time - getAudioContext().getOutputTimestamp().contextTime * 1000;
time = time * 1000 + timingOffset;
// destructure value
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd } = hap.value;
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
const duration = hap.duration.valueOf() * 1000 - 5;
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
if (note != null) {
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
const midiNote = new Note(midiNumber, { attack: velocity, duration });
device.playNote(midiNote, midichan, {
time: timeOffsetString,
device.playNote(midiNumber, midichan, {
time,
duration,
attack: velocity,
});
}
if (ccv !== undefined && ccn !== undefined) {
if (ccv && ccn) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1');
}
@@ -145,48 +129,7 @@ Pattern.prototype.midi = function (output) {
throw new Error('expected ccn to be a number or a string');
}
const scaled = Math.round(ccv * 127);
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
}
if (hap.whole.begin + 0 === 0) {
// we need to start here because we have the timing info
device.sendStart({ time: timeOffsetString });
}
if (['clock', 'midiClock'].includes(midicmd)) {
device.sendClock({ time: timeOffsetString });
} else if (['start'].includes(midicmd)) {
device.sendStart({ time: timeOffsetString });
} else if (['stop'].includes(midicmd)) {
device.sendStop({ time: timeOffsetString });
} else if (['continue'].includes(midicmd)) {
device.sendContinue({ time: timeOffsetString });
device.sendControlChange(ccn, scaled, midichan, { time });
}
});
};
let listeners = {};
const refs = {};
export async function midin(input) {
const initial = await enableWebMidi(); // only returns on first init
const device = getDevice(input, WebMidi.inputs);
if (initial) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`,
);
refs[input] = {};
}
const cc = (cc) => ref(() => refs[input][cc] || 0);
listeners[input] && device.removeListener('midimessage', listeners[input]);
listeners[input] = (e) => {
const cc = e.dataBytes[0];
const v = e.dataBytes[1];
refs[input] && (refs[input][cc] = v / 127);
};
device.addListener('midimessage', listeners[input]);
return cc;
}
+8 -8
View File
@@ -1,11 +1,11 @@
{
"name": "@strudel/midi",
"version": "1.0.1",
"name": "@strudel.cycles/midi",
"version": "0.8.0",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
@@ -29,11 +29,11 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/webaudio": "workspace:*",
"webmidi": "^3.1.8"
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"webmidi": "^3.1.5"
},
"devDependencies": {
"vite": "^5.0.10"
"vite": "^4.3.3"
}
}
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
+6 -6
View File
@@ -1,17 +1,17 @@
# @strudel/mini
# @strudel.cycles/mini
This package contains the mini notation parser and pattern generator.
## Install
```sh
npm i @strudel/mini --save
npm i @strudel.cycles/mini --save
```
## Example
```js
import { mini } from '@strudel/mini';
import { mini } from '@strudel.cycles/mini';
const pattern = mini('a [b c*2]');
@@ -28,11 +28,11 @@ yields:
(7/8 -> 1/1, 7/8 -> 1/1, c)
```
[Play with @strudel/mini codesandbox](https://codesandbox.io/s/strudel-mini-example-oe9wcu?file=/src/index.js)
[Play with @strudel.cycles/mini codesandbox](https://codesandbox.io/s/strudel-mini-example-oe9wcu?file=/src/index.js)
## 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
@@ -40,5 +40,5 @@ The parser [krill-parser.js] is generated from [krill.pegjs](./krill.pegjs) usin
To generate the parser, run
```js
npm run build:parser
npm build:parser
```
File diff suppressed because one or more lines are too long

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