Compare commits

..

1 Commits

Author SHA1 Message Date
Felix Roos 0506e4a635 work on transpose:
- support  octaves
- still some bugs
- add experimental emitNotes flag to stepInNamedScale
2024-01-07 22:15:00 +01:00
396 changed files with 17472 additions and 25979 deletions
+1 -2
View File
@@ -21,5 +21,4 @@ vite.config.js
/src-tauri/target/**/* /src-tauri/target/**/*
reverbGen.mjs reverbGen.mjs
hydra.mjs hydra.mjs
jsdoc-synonyms.js jsdoc-synonyms.js
packages/node/pattern.mjs
-3
View File
@@ -127,6 +127,3 @@ fabric.properties
.idea/caches/build_file_checksums.ser .idea/caches/build_file_checksums.ser
# END JetBrains -> BEGIN JetBrains # END JetBrains -> BEGIN JetBrains
samples/*
!samples/README.md
-2
View File
@@ -10,5 +10,3 @@ paper
pnpm-lock.yaml pnpm-lock.yaml
pnpm-workspace.yaml pnpm-workspace.yaml
**/dev-dist **/dev-dist
website/.astro
packages/node/pattern.mjs
+1 -1
View File
@@ -114,7 +114,7 @@ You can run the same check with `pnpm check`
## Package Workflow ## Package Workflow
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning. 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. allowing to develop multiple packages at the same time.
## Package Publishing ## Package Publishing
+15 -6
View File
@@ -2,28 +2,37 @@
[![Strudel test status](https://github.com/tidalcycles/strudel/actions/workflows/test.yml/badge.svg)](https://github.com/tidalcycles/strudel/actions) [![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> - Try it here: <https://strudel.cc>
- Docs: <https://strudel.cc/learn> - Docs: <https://strudel.cc/learn>
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel> - Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year> - 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
- 2 Years of Strudel Blog Post: <https://strudel.cc/blog/#year-2>
## Running Locally ## Running Locally
After cloning the project, you can run the REPL locally: After cloning the project, you can run the REPL locally:
```bash ```bash
pnpm i pnpm run setup
pnpm dev pnpm run repl
``` ```
## Using Strudel In Your Project ## Using Strudel In Your Project
This project is organized into many [packages](./packages), which are also available on [npm](https://www.npmjs.com/search?q=%40strudel). There are multiple npm packages you can use to use strudel, or only parts of it, in your project:
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start). - [`core`](./packages/core/): tidal pattern engine
- [`mini`](./packages/mini): mini notation parser + core binding
- [`transpiler`](./packages/transpiler): user code transpiler
- [`webaudio`](./packages/webaudio): webaudio output
- [`osc`](./packages/osc): bindings to communicate via OSC
- [`midi`](./packages/midi): webmidi bindings
- [`serial`](./packages/serial): webserial bindings
- [`tonal`](./packages/tonal): tonal functions
- ... [and there are more](./packages/)
Click on the package names to find out more about each one.
## Contributing ## Contributing
+1 -1
View File
@@ -7,7 +7,7 @@
/> />
<div id="output"></div> <div id="output"></div>
<script type="module"> <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 Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
const input = document.getElementById('text'); const input = document.getElementById('text');
const getEvents = () => { const getEvents = () => {
+1 -1
View File
@@ -8,7 +8,7 @@
/> />
<canvas id="canvas"></canvas> <canvas id="canvas"></canvas>
<script type="module"> <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 // this adds all strudel functions to the global scope, to be used by eval
Object.assign(window, strudel); Object.assign(window, strudel);
// setup elements // setup elements
+4 -3
View File
@@ -1,9 +1,10 @@
<!doctype html> <!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="play">play</button> <button id="play">play</button>
<button id="stop">stop</button> <button id="stop">stop</button>
<script> <script type="module">
strudel.initStrudel(); import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)')); document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
document.getElementById('play').addEventListener('stop', () => hush()); document.getElementById('play').addEventListener('stop', () => hush());
</script> </script>
@@ -1,12 +1,12 @@
<!doctype html> <!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="a">A</button> <button id="a">A</button>
<button id="b">B</button> <button id="b">B</button>
<button id="c">C</button> <button id="c">C</button>
<button id="stop">stop</button> <button id="stop">stop</button>
<script> <script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel({ initStrudel({
prebake: () => samples('github:tidalcycles/dirt-samples'), prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
}); });
const click = (id, action) => document.getElementById(id).addEventListener('click', action); const click = (id, action) => document.getElementById(id).addEventListener('click', action);
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`)); click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
+13 -20
View File
@@ -16,45 +16,38 @@
</div> </div>
<div id="output"></div> <div id="output"></div>
<script type="module"> <script type="module">
// TODO: refactor to use newer version without controls import import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.6.8';
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel/core@0.11.0'; import { mini } from 'https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0';
import { mini } from 'https://cdn.skypack.dev/@strudel/mini@0.11.0'; import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.6.0';
import { transpiler } from 'https://cdn.skypack.dev/@strudel/transpiler@0.11.0';
import { import {
getAudioContext, getAudioContext,
webaudioOutput, webaudioOutput,
initAudioOnFirstClick, initAudioOnFirstClick,
registerSynthSounds, } from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0';
} from 'https://cdn.skypack.dev/@strudel/webaudio@0.11.0';
initAudioOnFirstClick();
const ctx = getAudioContext(); const ctx = getAudioContext();
const input = document.getElementById('text'); const input = document.getElementById('text');
input.innerHTML = getTune(); input.innerHTML = getTune();
const loadModules = evalScope( evalScope(
controls, controls,
import('https://cdn.skypack.dev/@strudel/core@0.11.0'), import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8'),
import('https://cdn.skypack.dev/@strudel/mini@0.11.0'), import('https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0'),
import('https://cdn.skypack.dev/@strudel/tonal@0.11.0'), import('https://cdn.skypack.dev/@strudel.cycles/tonal@0.6.0'),
import('https://cdn.skypack.dev/@strudel/webaudio@0.11.0'), import('https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0'),
); );
const initAudio = Promise.all([initAudioOnFirstClick(), registerSynthSounds()]);
const { evaluate } = repl({ const { evaluate } = repl({
defaultOutput: webaudioOutput, defaultOutput: webaudioOutput,
getTime: () => ctx.currentTime, getTime: () => ctx.currentTime,
transpiler, transpiler,
}); });
document.getElementById('start').addEventListener('click', async () => { document.getElementById('start').addEventListener('click', () => evaluate(input.value));
await loadModules;
await initAudio;
evaluate(input.value);
});
function getTune() { function getTune() {
return `samples('github:tidalcycles/dirt-samples') return `await samples('github:tidalcycles/Dirt-Samples/master')
setcps(1);
stack( stack(
// amen // amen
n("0 1 2 3 4 5 6 7") n("0 1 2 3 4 5 6 7")
+1 -1
View File
@@ -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> --> <!-- <script src="./embed.js"></script> -->
<strudel-repl> <strudel-repl>
<!-- <!--
@@ -1,4 +1,4 @@
<script src="https://unpkg.com/@strudel/repl@1.0.2"></script> <script src="https://unpkg.com/@strudel/repl@0.9.4"></script>
<strudel-editor> <strudel-editor>
<!-- <!--
// @date 23-08-15 // @date 23-08-15
-8
View File
@@ -1,8 +0,0 @@
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="play">PLAY</button>
<script>
initStrudel({
prebake: () => samples('github:tidalcycles/dirt-samples'),
});
document.getElementById('play').addEventListener('click', () => s('bd sd').play());
</script>
+10 -9
View File
@@ -1,11 +1,11 @@
import { StrudelMirror } from '@strudel/codemirror'; import { StrudelMirror } from '@strudel/codemirror';
import { funk42 } from './tunes'; import { funk42 } from './tunes';
import { drawPianoroll, evalScope } from '@strudel/core'; import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
import './style.css'; import './style.css';
import { initAudioOnFirstClick } from '@strudel/webaudio'; import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { transpiler } from '@strudel/transpiler'; import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel/webaudio'; import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel/soundfonts'; import { registerSoundfonts } from '@strudel.cycles/soundfonts';
// init canvas // init canvas
const canvas = document.getElementById('roll'); const canvas = document.getElementById('roll');
@@ -25,10 +25,11 @@ const editor = new StrudelMirror({
prebake: async () => { prebake: async () => {
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..) initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
const loadModules = evalScope( const loadModules = evalScope(
import('@strudel/core'), controls,
import('@strudel/mini'), import('@strudel.cycles/core'),
import('@strudel/tonal'), import('@strudel.cycles/mini'),
import('@strudel/webaudio'), import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
); );
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]); await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
}, },
+6 -6
View File
@@ -13,11 +13,11 @@
}, },
"dependencies": { "dependencies": {
"@strudel/codemirror": "workspace:*", "@strudel/codemirror": "workspace:*",
"@strudel/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
"@strudel/mini": "workspace:*", "@strudel.cycles/mini": "workspace:*",
"@strudel/soundfonts": "workspace:*", "@strudel.cycles/soundfonts": "workspace:*",
"@strudel/tonal": "workspace:*", "@strudel.cycles/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*", "@strudel.cycles/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*" "@strudel.cycles/webaudio": "workspace:*"
} }
} }
+6 -6
View File
@@ -1,6 +1,6 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0 export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
samples('github:felixroos/samples') await samples('github:felixroos/samples/main')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/') await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2" "<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
// make it 22 edo // make it 22 edo
@@ -33,8 +33,8 @@ samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-dru
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0 export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
samples('github:felixroos/samples') await samples('github:felixroos/samples/main')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/') await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', { addVoicings('hip', {
m11: ['2M 3m 4P 7m'], m11: ['2M 3m 4P 7m'],
@@ -69,8 +69,8 @@ export const funk42 = `// froos - how to funk in 42 lines of code
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg // adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4 // thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
samples('github:felixroos/samples') await samples('github:felixroos/samples/main')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/') await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5) setcps(.5)
+1 -1
View File
@@ -15,7 +15,7 @@
<script type="module"> <script type="module">
import { initStrudel } from '@strudel/web'; import { initStrudel } from '@strudel/web';
initStrudel({ initStrudel({
prebake: () => samples('github:tidalcycles/dirt-samples'), prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
}); });
const click = (id, action) => document.getElementById(id).addEventListener('click', action); const click = (id, action) => document.getElementById(id).addEventListener('click', action);
+10 -5
View File
@@ -1,15 +1,20 @@
import { repl, evalScope } from '@strudel/core'; import { controls, repl, evalScope } from '@strudel.cycles/core';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick, registerSynthSounds } from '@strudel/webaudio'; import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { transpiler } from '@strudel/transpiler'; import { transpiler } from '@strudel.cycles/transpiler';
import tune from './tune.mjs'; import tune from './tune.mjs';
const ctx = getAudioContext(); const ctx = getAudioContext();
const input = document.getElementById('text'); const input = document.getElementById('text');
input.innerHTML = tune; input.innerHTML = tune;
initAudioOnFirstClick(); initAudioOnFirstClick();
registerSynthSounds();
evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/webaudio'), import('@strudel/tonal')); evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/tonal'),
);
const { evaluate } = repl({ const { evaluate } = repl({
defaultOutput: webaudioOutput, defaultOutput: webaudioOutput,
+5 -5
View File
@@ -13,10 +13,10 @@
"vite": "^5.0.10" "vite": "^5.0.10"
}, },
"dependencies": { "dependencies": {
"@strudel/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
"@strudel/mini": "workspace:*", "@strudel.cycles/mini": "workspace:*",
"@strudel/transpiler": "workspace:*", "@strudel.cycles/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*", "@strudel.cycles/webaudio": "workspace:*",
"@strudel/tonal": "workspace:*" "@strudel.cycles/tonal": "workspace:*"
} }
} }
+2 -2
View File
@@ -1,5 +1,5 @@
export default `samples('github:tidalcycles/dirt-samples') export default `await samples('github:tidalcycles/Dirt-Samples/master')
setcps(1)
stack( stack(
// amen // amen
n("0 1 2 3 4 5 6 7") n("0 1 2 3 4 5 6 7")
+1 -1
View File
@@ -12,7 +12,7 @@
const init = Promise.all([ const init = Promise.all([
initAudioOnFirstClick(), initAudioOnFirstClick(),
samples('github:tidalcycles/dirt-samples'), samples('github:tidalcycles/Dirt-Samples/master'),
registerSynthSounds(), registerSynthSounds(),
]); ]);
+2 -7
View File
@@ -1,20 +1,15 @@
// this barrel export is currently only used to find undocumented exports // this barrel export is currently only used to find undocumented exports
export * from './packages/codemirror/index.mjs';
export * from './packages/core/index.mjs'; export * from './packages/core/index.mjs';
export * from './packages/csound/index.mjs'; export * from './packages/csound/index.mjs';
export * from './packages/desktopbridge/index.mjs';
export * from './packages/draw/index.mjs';
export * from './packages/embed/index.mjs'; export * from './packages/embed/index.mjs';
export * from './packages/hydra/index.mjs'; export * from './packages/desktopbridge/index.mjs';
export * from './packages/midi/index.mjs'; export * from './packages/midi/index.mjs';
export * from './packages/mini/index.mjs'; export * from './packages/mini/index.mjs';
export * from './packages/osc/index.mjs'; export * from './packages/osc/index.mjs';
export * from './packages/repl/index.mjs'; export * from './packages/react/index.mjs';
export * from './packages/serial/index.mjs'; export * from './packages/serial/index.mjs';
export * from './packages/soundfonts/index.mjs'; export * from './packages/soundfonts/index.mjs';
export * from './packages/superdough/index.mjs';
export * from './packages/tonal/index.mjs'; export * from './packages/tonal/index.mjs';
export * from './packages/transpiler/index.mjs'; export * from './packages/transpiler/index.mjs';
export * from './packages/web/index.mjs';
export * from './packages/webaudio/index.mjs'; export * from './packages/webaudio/index.mjs';
export * from './packages/xen/index.mjs'; export * from './packages/xen/index.mjs';
+9 -10
View File
@@ -1,5 +1,5 @@
{ {
"name": "@strudel/monorepo", "name": "@strudel.cycles/monorepo",
"version": "0.5.0", "version": "0.5.0",
"private": true, "private": true,
"description": "Port of tidalcycles to javascript", "description": "Port of tidalcycles to javascript",
@@ -25,7 +25,6 @@
"format-check": "prettier --check .", "format-check": "prettier --check .",
"report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json", "report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json",
"check": "npm run format-check && npm run lint && npm run test", "check": "npm run format-check && npm run lint && npm run test",
"sampler": "cd samples && node ../packages/sampler/sample-server.mjs",
"iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf" "iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf"
}, },
"repository": { "repository": {
@@ -46,18 +45,18 @@
}, },
"homepage": "https://strudel.cc", "homepage": "https://strudel.cc",
"dependencies": { "dependencies": {
"@strudel/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
"@strudel/mini": "workspace:*", "@strudel.cycles/mini": "workspace:*",
"@strudel/tonal": "workspace:*", "@strudel.cycles/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*", "@strudel.cycles/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*", "@strudel.cycles/webaudio": "workspace:*",
"@strudel/xen": "workspace:*" "@strudel.cycles/xen": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"dependency-tree": "^10.0.9",
"@tauri-apps/cli": "^1.5.9", "@tauri-apps/cli": "^1.5.9",
"@vitest/ui": "^1.1.0", "@vitest/ui": "^1.1.0",
"acorn": "^8.11.3", "canvas": "^2.11.2",
"dependency-tree": "^10.0.9",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"events": "^3.3.0", "events": "^3.3.0",
+1 -1
View File
@@ -1,5 +1,5 @@
# Packages # 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. To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs.
+1 -7
View File
@@ -3,12 +3,6 @@ import jsdoc from '../../doc.json';
import { autocompletion } from '@codemirror/autocomplete'; import { autocompletion } from '@codemirror/autocomplete';
import { h } from './html'; import { h } from './html';
function plaintext(str) {
const div = document.createElement('div');
div.innerText = str;
return div.innerHTML;
}
const getDocLabel = (doc) => doc.name || doc.longname; const getDocLabel = (doc) => doc.name || doc.longname;
const getInnerText = (html) => { const getInnerText = (html) => {
var div = document.createElement('div'); var div = document.createElement('div');
@@ -27,7 +21,7 @@ ${doc.description}
)} )}
</ul> </ul>
<div> <div>
${doc.examples?.map((example) => `<div><pre>${plaintext(example)}</pre></div>`)} ${doc.examples?.map((example) => `<div><pre>${example}</pre></div>`)}
</div> </div>
</div>`[0]; </div>`[0];
/* /*
+17 -37
View File
@@ -2,7 +2,7 @@ import { closeBrackets } from '@codemirror/autocomplete';
// import { search, highlightSelectionMatches } from '@codemirror/search'; // import { search, highlightSelectionMatches } from '@codemirror/search';
import { history } from '@codemirror/commands'; import { history } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language'; import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state'; import { Compartment, EditorState, Prec } from '@codemirror/state';
import { import {
EditorView, EditorView,
@@ -12,22 +12,18 @@ import {
lineNumbers, lineNumbers,
drawSelection, drawSelection,
} from '@codemirror/view'; } from '@codemirror/view';
import { Pattern, repl } from '@strudel/core'; import { Pattern, Drawer, repl, cleanupDraw } from '@strudel.cycles/core';
import { Drawer, cleanupDraw } from '@strudel/draw';
import { isAutoCompletionEnabled } from './autocomplete.mjs'; import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { isTooltipEnabled } from './tooltip.mjs'; import { isTooltipEnabled } from './tooltip.mjs';
import { flash, isFlashEnabled } from './flash.mjs'; import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs'; import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs'; import { keybindings } from './keybindings.mjs';
import { initTheme, activateTheme, theme } from './themes.mjs'; import { initTheme, activateTheme, theme } from './themes.mjs';
import { sliderPlugin, updateSliderWidgets } from './slider.mjs'; import { updateWidgets, sliderPlugin } from './slider.mjs';
import { widgetPlugin, updateWidgets } from './widget.mjs';
import { persistentAtom } from '@nanostores/persistent'; import { persistentAtom } from '@nanostores/persistent';
const extensions = { const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []), isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
isBracketClosingEnabled: (on) => (on ? closeBrackets() : []),
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []), isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
theme, theme,
isAutoCompletionEnabled, isAutoCompletionEnabled,
@@ -41,8 +37,6 @@ const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [ke
export const defaultSettings = { export const defaultSettings = {
keybindings: 'codemirror', keybindings: 'codemirror',
isBracketMatchingEnabled: false,
isBracketClosingEnabled: true,
isLineNumbersDisplayed: true, isLineNumbersDisplayed: true,
isActiveLineHighlighted: false, isActiveLineHighlighted: false,
isAutoCompletionEnabled: false, isAutoCompletionEnabled: false,
@@ -75,9 +69,9 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
...initialSettings, ...initialSettings,
javascript(), javascript(),
sliderPlugin, sliderPlugin,
widgetPlugin,
// indentOnInput(), // works without. already brought with javascript extension? // indentOnInput(), // works without. already brought with javascript extension?
// bracketMatching(), // does not do anything // bracketMatching(), // does not do anything
closeBrackets(),
syntaxHighlighting(defaultHighlightStyle), syntaxHighlighting(defaultHighlightStyle),
history(), history(),
EditorView.updateListener.of((v) => onChange(v)), EditorView.updateListener.of((v) => onChange(v)),
@@ -129,7 +123,6 @@ export class StrudelMirror {
id, id,
initialCode = '', initialCode = '',
onDraw, onDraw,
drawContext,
drawTime = [0, 0], drawTime = [0, 0],
autodraw, autodraw,
prebake, prebake,
@@ -142,16 +135,23 @@ export class StrudelMirror {
this.widgets = []; this.widgets = [];
this.painters = []; this.painters = [];
this.drawTime = drawTime; this.drawTime = drawTime;
this.drawContext = drawContext; this.onDraw = onDraw;
this.onDraw = onDraw || this.draw; const self = this;
this.id = id || s4(); this.id = id || s4();
this.drawer = new Drawer((haps, time) => { this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => hap.isActive(time)); const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
this.highlight(currentFrame, time); this.highlight(currentFrame, time);
this.onDraw(haps, time, this.painters); this.onDraw?.(haps, time, currentFrame, this.painters);
}, drawTime); }, 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(); this.prebaked = prebake();
autodraw && this.drawFirstFrame(); autodraw && this.drawFirstFrame();
@@ -177,14 +177,6 @@ export class StrudelMirror {
beforeEval: async () => { beforeEval: async () => {
cleanupDraw(); cleanupDraw();
this.painters = []; this.painters = [];
const self = this;
// this is similar to repl.mjs > injectPatternMethods
// maybe there is a solution without prototype hacking, but hey, it works
// we need to do this befor every eval to make sure it works with multiple StrudelMirror's side by side
Pattern.prototype.onPaint = function (onPaint) {
self.painters.push(onPaint);
return this;
};
await this.prebaked; await this.prebaked;
await replOptions?.beforeEval?.(); await replOptions?.beforeEval?.();
}, },
@@ -192,10 +184,7 @@ export class StrudelMirror {
// remember for when highlighting is toggled on // remember for when highlighting is toggled on
this.miniLocations = options.meta?.miniLocations; this.miniLocations = options.meta?.miniLocations;
this.widgets = options.meta?.widgets; this.widgets = options.meta?.widgets;
const sliders = this.widgets.filter((w) => w.type === 'slider'); updateWidgets(this.editor, this.widgets);
updateSliderWidgets(this.editor, sliders);
const widgets = this.widgets.filter((w) => w.type !== 'slider');
updateWidgets(this.editor, widgets);
updateMiniLocations(this.editor, this.miniLocations); updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options); replOptions?.afterEval?.(options);
this.adjustDrawTime(); this.adjustDrawTime();
@@ -239,9 +228,6 @@ export class StrudelMirror {
// when no painters are set, [0,0] is enough (just highlighting) // when no painters are set, [0,0] is enough (just highlighting)
this.drawer.setDrawTime(this.painters.length ? this.drawTime : [0, 0]); this.drawer.setDrawTime(this.painters.length ? this.drawTime : [0, 0]);
} }
draw(haps, time) {
this.painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
}
async drawFirstFrame() { async drawFirstFrame() {
if (!this.onDraw) { if (!this.onDraw) {
return; return;
@@ -252,7 +238,7 @@ export class StrudelMirror {
await this.repl.evaluate(this.code, false); await this.repl.evaluate(this.code, false);
this.drawer.invalidate(this.repl.scheduler, -0.001); this.drawer.invalidate(this.repl.scheduler, -0.001);
// draw at -0.001 to avoid haps at 0 to be visualized as active // draw at -0.001 to avoid haps at 0 to be visualized as active
this.onDraw?.(this.drawer.visibleHaps, -0.001, this.painters); this.onDraw?.(this.drawer.visibleHaps, -0.001, [], this.painters);
} catch (err) { } catch (err) {
console.warn('first frame could not be painted'); console.warn('first frame could not be painted');
} }
@@ -304,15 +290,9 @@ export class StrudelMirror {
setLineWrappingEnabled(enabled) { setLineWrappingEnabled(enabled) {
this.reconfigureExtension('isLineWrappingEnabled', enabled); this.reconfigureExtension('isLineWrappingEnabled', enabled);
} }
setBracketMatchingEnabled(enabled) {
this.reconfigureExtension('isBracketMatchingEnabled', enabled);
}
setLineNumbersDisplayed(enabled) { setLineNumbersDisplayed(enabled) {
this.reconfigureExtension('isLineNumbersDisplayed', enabled); this.reconfigureExtension('isLineNumbersDisplayed', enabled);
} }
setBracketClosingEnabled(enabled) {
this.reconfigureExtension('isBracketClosingEnabled', enabled);
}
setTheme(theme) { setTheme(theme) {
this.reconfigureExtension('theme', theme); this.reconfigureExtension('theme', theme);
} }
+2 -4
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 // 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(); const haps = new Map();
for (let hap of e.value.haps) { for (let hap of e.value.haps) {
if (!hap.context?.locations || !hap.whole) {
continue;
}
for (let { start, end } of hap.context.locations) { for (let { start, end } of hap.context.locations) {
let id = `${start}:${end}`; let id = `${start}:${end}`;
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) { 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 }; visible = { atTime: e.value.atTime, haps };
} }
} }
@@ -92,7 +90,7 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
if (haps.has(id)) { if (haps.has(id)) {
const hap = haps.get(id); const hap = haps.get(id);
const color = hap.value?.color ?? 'var(--foreground)'; const color = hap.context.color ?? 'var(--foreground)';
// Get explicit channels for color values // Get explicit channels for color values
/* /*
const swatch = document.createElement('div'); const swatch = document.createElement('div');
-1
View File
@@ -3,4 +3,3 @@ export * from './highlight.mjs';
export * from './flash.mjs'; export * from './flash.mjs';
export * from './slider.mjs'; export * from './slider.mjs';
export * from './themes.mjs'; export * from './themes.mjs';
export * from './widget.mjs';
+6 -7
View File
@@ -1,10 +1,11 @@
{ {
"name": "@strudel/codemirror", "name": "@strudel/codemirror",
"version": "1.0.1", "version": "0.9.0",
"description": "Codemirror Extensions for Strudel", "description": "Codemirror Extensions for Strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
"main": "dist/index.mjs" "main": "dist/index.js",
"module": "dist/index.mjs"
}, },
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
@@ -40,16 +41,14 @@
"@codemirror/state": "^6.4.0", "@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.23.0", "@codemirror/view": "^6.23.0",
"@lezer/highlight": "^1.2.0", "@lezer/highlight": "^1.2.0",
"@nanostores/persistent": "^0.9.1",
"@replit/codemirror-emacs": "^6.0.1", "@replit/codemirror-emacs": "^6.0.1",
"@replit/codemirror-vim": "^6.1.0", "@replit/codemirror-vim": "^6.1.0",
"@replit/codemirror-vscode-keymap": "^6.0.2", "@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@uiw/codemirror-themes": "^4.21.21", "@uiw/codemirror-themes": "^4.21.21",
"@uiw/codemirror-themes-all": "^4.21.21", "@uiw/codemirror-themes-all": "^4.21.21",
"nanostores": "^0.9.5" "nanostores": "^0.9.5",
"@nanostores/persistent": "^0.9.1"
}, },
"devDependencies": { "devDependencies": {
"vite": "^5.0.10" "vite": "^5.0.10"
+14 -16
View File
@@ -1,6 +1,6 @@
import { ref, pure } from '@strudel/core'; import { ref, pure } from '@strudel.cycles/core';
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view'; import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
import { StateEffect } from '@codemirror/state'; import { StateEffect, StateField } from '@codemirror/state';
export let sliderValues = {}; export let sliderValues = {};
const getSliderID = (from) => `slider_${from}`; const getSliderID = (from) => `slider_${from}`;
@@ -60,21 +60,19 @@ export class SliderWidget extends WidgetType {
} }
} }
export const setSliderWidgets = StateEffect.define(); export const setWidgets = StateEffect.define();
export const updateSliderWidgets = (view, widgets) => { export const updateWidgets = (view, widgets) => {
view.dispatch({ effects: setSliderWidgets.of(widgets) }); view.dispatch({ effects: setWidgets.of(widgets) });
}; };
function getSliders(widgetConfigs, view) { function getWidgets(widgetConfigs, view) {
return widgetConfigs return widgetConfigs.map(({ from, to, value, min, max, step }) => {
.filter((w) => w.type === 'slider') return Decoration.widget({
.map(({ from, to, value, min, max, step }) => { widget: new SliderWidget(value, min, max, from, to, step, view),
return Decoration.widget({ side: 0,
widget: new SliderWidget(value, min, max, from, to, step, view), }).range(from /* , to */);
side: 0, });
}).range(from /* , to */);
});
} }
export const sliderPlugin = ViewPlugin.fromClass( export const sliderPlugin = ViewPlugin.fromClass(
@@ -101,8 +99,8 @@ export const sliderPlugin = ViewPlugin.fromClass(
} }
} }
for (let e of tr.effects) { for (let e of tr.effects) {
if (e.is(setSliderWidgets)) { if (e.is(setWidgets)) {
this.decorations = Decoration.set(getSliders(e.value, update.view)); this.decorations = Decoration.set(getWidgets(e.value, update.view));
} }
} }
}); });
-2
View File
@@ -37,7 +37,6 @@ import whitescreen, { settings as whitescreenSettings } from './themes/whitescre
import teletext, { settings as teletextSettings } from './themes/teletext'; import teletext, { settings as teletextSettings } from './themes/teletext';
import algoboy, { settings as algoboySettings } from './themes/algoboy'; import algoboy, { settings as algoboySettings } from './themes/algoboy';
import terminal, { settings as terminalSettings } from './themes/terminal'; import terminal, { settings as terminalSettings } from './themes/terminal';
import { setTheme } from '@strudel/draw';
export const themes = { export const themes = {
strudelTheme, strudelTheme,
@@ -514,7 +513,6 @@ export function activateTheme(name) {
.map(([key, value]) => `--${key}: ${value} !important;`) .map(([key, value]) => `--${key}: ${value} !important;`)
.join('\n')} .join('\n')}
}`; }`;
setTheme(themeSettings);
// tailwind dark mode // tailwind dark mode
if (themeSettings.light) { if (themeSettings.light) {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
-1
View File
@@ -18,7 +18,6 @@ export default createTheme({
theme: 'light', theme: 'light',
settings, settings,
styles: [ styles: [
{ tag: t.labelName, color: '#0f380f' },
{ tag: t.keyword, color: '#0f380f' }, { tag: t.keyword, color: '#0f380f' },
{ tag: t.operator, color: '#0f380f' }, { tag: t.operator, color: '#0f380f' },
{ tag: t.special(t.variableName), color: '#0f380f' }, { tag: t.special(t.variableName), color: '#0f380f' },
-1
View File
@@ -15,7 +15,6 @@ export default createTheme({
theme: 'dark', theme: 'dark',
settings, settings,
styles: [ styles: [
{ tag: t.labelName, color: 'white' },
{ tag: t.keyword, color: 'white' }, { tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' }, { tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' }, { tag: t.special(t.variableName), color: 'white' },
-1
View File
@@ -18,7 +18,6 @@ export default createTheme({
theme: 'dark', theme: 'dark',
settings, settings,
styles: [ styles: [
{ tag: t.labelName, color: 'white' },
{ tag: t.keyword, color: 'white' }, { tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' }, { tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' }, { tag: t.special(t.variableName), color: 'white' },
-1
View File
@@ -15,7 +15,6 @@ export default createTheme({
gutterForeground: '#8a919966', gutterForeground: '#8a919966',
}, },
styles: [ styles: [
{ tag: t.labelName, color: '#89ddff' },
{ tag: t.keyword, color: '#c792ea' }, { tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' }, { tag: t.operator, color: '#89ddff' },
{ tag: t.special(t.variableName), color: '#eeffff' }, { tag: t.special(t.variableName), color: '#eeffff' },
-1
View File
@@ -27,7 +27,6 @@ export default createTheme({
theme: 'dark', theme: 'dark',
settings, settings,
styles: [ styles: [
{ tag: t.labelName, color: colorB },
{ tag: t.keyword, color: colorA }, { tag: t.keyword, color: colorA },
{ tag: t.operator, color: mini }, { tag: t.operator, color: mini },
{ tag: t.special(t.variableName), color: colorA }, { tag: t.special(t.variableName), color: colorA },
-1
View File
@@ -14,7 +14,6 @@ export default createTheme({
theme: 'dark', theme: 'dark',
settings, settings,
styles: [ styles: [
{ tag: t.labelName, color: '#41FF00' },
{ tag: t.keyword, color: '#41FF00' }, { tag: t.keyword, color: '#41FF00' },
{ tag: t.operator, color: '#41FF00' }, { tag: t.operator, color: '#41FF00' },
{ tag: t.special(t.variableName), color: '#41FF00' }, { tag: t.special(t.variableName), color: '#41FF00' },
-1
View File
@@ -16,7 +16,6 @@ export default createTheme({
theme: 'light', theme: 'light',
settings, settings,
styles: [ styles: [
{ tag: t.labelName, color: 'black' },
{ tag: t.keyword, color: 'black' }, { tag: t.keyword, color: 'black' },
{ tag: t.operator, color: 'black' }, { tag: t.operator, color: 'black' },
{ tag: t.special(t.variableName), color: 'black' }, { tag: t.special(t.variableName), color: 'black' },
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
formats: ['es'], formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs' })[ext], fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
}, },
rollupOptions: { rollupOptions: {
external: [...Object.keys(dependencies)], external: [...Object.keys(dependencies)],
-135
View File
@@ -1,135 +0,0 @@
import { StateEffect, StateField } from '@codemirror/state';
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
import { getWidgetID, registerWidgetType } from '@strudel/transpiler';
import { Pattern } from '@strudel/core';
export const addWidget = StateEffect.define({
map: ({ from, to }, change) => {
return { from: change.mapPos(from), to: change.mapPos(to) };
},
});
export const updateWidgets = (view, widgets) => {
view.dispatch({ effects: addWidget.of(widgets) });
};
function getWidgets(widgetConfigs) {
return (
widgetConfigs
// codemirror throws an error if we don't sort
.sort((a, b) => a.to - b.to)
.map((widgetConfig) => {
return Decoration.widget({
widget: new BlockWidget(widgetConfig),
side: 0,
block: true,
}).range(widgetConfig.to);
})
);
}
const widgetField = StateField.define(
/* <DecorationSet> */ {
create() {
return Decoration.none;
},
update(widgets, tr) {
widgets = widgets.map(tr.changes);
for (let e of tr.effects) {
if (e.is(addWidget)) {
try {
widgets = widgets.update({
filter: () => false,
add: getWidgets(e.value),
});
} catch (error) {
console.log('err', error);
}
}
}
return widgets;
},
provide: (f) => EditorView.decorations.from(f),
},
);
const widgetElements = {};
export function setWidget(id, el) {
widgetElements[id] = el;
el.id = id;
}
export class BlockWidget extends WidgetType {
constructor(widgetConfig) {
super();
this.widgetConfig = widgetConfig;
}
eq() {
return true;
}
toDOM() {
const id = getWidgetID(this.widgetConfig);
const el = widgetElements[id];
return el;
}
ignoreEvent(e) {
return true;
}
}
export const widgetPlugin = [widgetField];
// widget implementer API to create a new widget type
export function registerWidget(type, fn) {
registerWidgetType(type);
if (fn) {
Pattern.prototype[type] = function (id, options = { fold: 1 }) {
// fn is expected to create a dom element and call setWidget(id, el);
// fn should also return the pattern
return fn(id, options, this);
};
}
}
// wire up @strudel/draw functions
function getCanvasWidget(id, options = {}) {
const { width = 500, height = 60, pixelRatio = window.devicePixelRatio } = options;
let canvas = document.getElementById(id) || document.createElement('canvas');
canvas.width = width * pixelRatio;
canvas.height = height * pixelRatio;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
setWidget(id, canvas);
return canvas;
}
registerWidget('_pianoroll', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).pianoroll({ fold: 1, ...options, ctx, id });
});
registerWidget('_punchcard', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).punchcard({ fold: 1, ...options, ctx, id });
});
registerWidget('_spiral', (id, options = {}, pat) => {
let _size = options.size || 275;
options = { width: _size, height: _size, ...options, size: _size / 5 };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).spiral({ ...options, ctx, id });
});
registerWidget('_scope', (id, options = {}, pat) => {
options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).scope({ ...options, ctx, id });
});
registerWidget('_pitchwheel', (id, options = {}, pat) => {
let _size = options.size || 200;
options = { width: _size, height: _size, ...options, size: _size / 5 };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.pitchwheel({ ...options, ctx, id });
});
+4 -4
View File
@@ -1,17 +1,17 @@
# @strudel/core # @strudel.cycles/core
This package contains the bare essence of strudel. This package contains the bare essence of strudel.
## Install ## Install
```sh ```sh
npm i @strudel/core --save npm i @strudel.cycles/core --save
``` ```
## Example ## Example
```js ```js
import { sequence } from '@strudel/core'; import { sequence } from '@strudel.cycles/core';
const pattern = sequence('a', ['b', 'c']); const pattern = sequence('a', ['b', 'c']);
@@ -33,7 +33,7 @@ b: 3/2 - 7/4
c: 7/4 - 2 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 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 repl example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/vanilla.html)
- [open minimal vite example](./examples/vite-vanilla-repl/) - [open minimal vite example](./examples/vite-vanilla-repl/)
@@ -1,14 +1,13 @@
import { Pattern, silence, register, pure, createParams } from '@strudel/core'; import { Pattern, getDrawContext, silence, register, pure } from './index.mjs';
import { getDrawContext } from './draw.mjs'; import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
const { createParams } = controls;
let clearColor = '#22222210'; let clearColor = '#22222210';
Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } = {}) { Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } = {}) {
window.frame && cancelAnimationFrame(window.frame); window.frame && cancelAnimationFrame(window.frame);
const ctx = getDrawContext(); const ctx = getDrawContext();
let { clientWidth: ww, clientHeight: wh } = ctx.canvas; const { clientWidth: ww, clientHeight: wh } = ctx.canvas;
ww *= window.devicePixelRatio;
wh *= window.devicePixelRatio;
let smearPart = smear === 0 ? '99' : Number((1 - smear) * 100).toFixed(0); let smearPart = smear === 0 ? '99' : Number((1 - smear) * 100).toFixed(0);
smearPart = smearPart.length === 1 ? `0${smearPart}` : smearPart; smearPart = smearPart.length === 1 ? `0${smearPart}` : smearPart;
clearColor = `#200010${smearPart}`; clearColor = `#200010${smearPart}`;
-168
View File
@@ -1,168 +0,0 @@
// eslint-disable-next-line no-undef
// TODO: swap below line with above one when firefox supports esm imports in service workers
// see https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker?retiredLocale=de#browser_compatibility
// import createClock from './zyklus.mjs';
function getTime() {
const precision = 10 ** 4;
const seconds = performance.now() / 1000;
return Math.round(seconds * precision) / precision;
}
let num_cycles_at_cps_change = 0;
let num_ticks_since_cps_change = 0;
let num_seconds_at_cps_change = 0;
let cps = 0.5;
// {id: {started: boolean}}
const clients = new Map();
const duration = 0.1;
const channel = new BroadcastChannel('strudeltick');
const sendMessage = (type, payload) => {
channel.postMessage({ type, payload });
};
const sendTick = (phase, duration, tick, time) => {
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
const tickdeadline = phase - time;
const lastTick = time + tickdeadline;
const num_cycles_since_cps_change = num_seconds_since_cps_change * cps;
const begin = num_cycles_at_cps_change + num_cycles_since_cps_change;
const secondsSinceLastTick = time - lastTick - duration;
const eventLength = duration * cps;
const end = begin + eventLength;
const cycle = begin + secondsSinceLastTick * cps;
sendMessage('tick', {
begin,
end,
cps,
tickdeadline,
num_cycles_at_cps_change,
num_seconds_at_cps_change,
num_seconds_since_cps_change,
cycle,
});
num_ticks_since_cps_change++;
};
//create clock method from zyklus
const clock = createClock(getTime, sendTick, duration);
let started = false;
const startClock = (id) => {
clients.set(id, { started: true });
if (started) {
return;
}
clock.start();
started = true;
};
const stopClock = async (id) => {
clients.set(id, { started: false });
const otherClientStarted = Array.from(clients.values()).some((c) => c.started);
//dont stop the clock if other instances are running...
if (!started || otherClientStarted) {
return;
}
clock.stop();
setCycle(0);
started = false;
};
const setCycle = (cycle) => {
num_ticks_since_cps_change = 0;
num_cycles_at_cps_change = cycle;
};
const processMessage = (message) => {
const { type, payload } = message;
switch (type) {
case 'cpschange': {
if (payload.cps !== cps) {
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
num_cycles_at_cps_change = num_cycles_at_cps_change + num_seconds_since_cps_change * cps;
num_seconds_at_cps_change = num_seconds_at_cps_change + num_seconds_since_cps_change;
cps = payload.cps;
num_ticks_since_cps_change = 0;
}
break;
}
case 'setcycle': {
setCycle(payload.cycle);
break;
}
case 'toggle': {
if (payload.started) {
startClock(message.id);
} else {
stopClock(message.id);
}
break;
}
}
};
self.onconnect = function (e) {
// the incoming port
const port = e.ports[0];
port.addEventListener('message', function (e) {
processMessage(e.data);
});
port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
};
// used to consistently schedule events, for use in a service worker - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/clockworker.mjs>
function createClock(
getTime,
callback, // called slightly before each cycle
duration = 0.05, // duration of each cycle
interval = 0.1, // interval between callbacks
overlap = 0.1, // overlap between callbacks
) {
let tick = 0; // counts callbacks
let phase = 0; // next callback time
let precision = 10 ** 4; // used to round phase
let minLatency = 0.01;
const setDuration = (setter) => (duration = setter(duration));
overlap = overlap || interval / 2;
const onTick = () => {
const t = getTime();
const lookahead = t + interval + overlap; // the time window for this tick
if (phase === 0) {
phase = t + minLatency;
}
// callback as long as we're inside the lookahead
while (phase < lookahead) {
phase = Math.round(phase * precision) / precision;
phase >= t && callback(phase, duration, tick, t);
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
phase += duration; // increment phase by duration
tick++;
}
};
let intervalID;
const start = () => {
clear(); // just in case start was called more than once
onTick();
intervalID = setInterval(onTick, interval * 1000);
};
const clear = () => intervalID !== undefined && clearInterval(intervalID);
const pause = () => clear();
const stop = () => {
tick = 0;
phase = 0;
clear();
};
const getPhase = () => phase;
// setCallback
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
}
+1360 -1535
View File
File diff suppressed because it is too large Load Diff
+23 -36
View File
@@ -1,5 +1,5 @@
/* /*
cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs> cyclist.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
@@ -8,55 +8,49 @@ import createClock from './zyklus.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
export class Cyclist { export class Cyclist {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) { constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
this.started = false; this.started = false;
this.cps = 0.5; this.cps = 1;
this.num_ticks_since_cps_change = 0; this.num_ticks_since_cps_change = 0;
this.lastTick = 0; // absolute time when last tick (clock callback) happened this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.lastBegin = 0; // query begin of last tick this.lastBegin = 0; // query begin of last tick
this.lastEnd = 0; // query end of last tick this.lastEnd = 0; // query end of last tick
this.getTime = getTime; // get absolute time this.getTime = getTime; // get absolute time
this.num_cycles_at_cps_change = 0; this.num_cycles_since_last_cps_change = 0;
this.seconds_at_cps_change; // clock phase when cps was changed
this.onToggle = onToggle; this.onToggle = onToggle;
this.latency = latency; // fixed trigger time offset this.latency = latency; // fixed trigger time offset
this.clock = createClock( this.clock = createClock(
getTime, getTime,
// called slightly before each cycle // called slightly before each cycle
(phase, duration, _, t) => { (phase, duration, tick) => {
if (tick === 0) {
this.origin = phase;
}
if (this.num_ticks_since_cps_change === 0) { if (this.num_ticks_since_cps_change === 0) {
this.num_cycles_at_cps_change = this.lastEnd; this.num_cycles_since_last_cps_change = this.lastEnd;
this.seconds_at_cps_change = phase;
} }
this.num_ticks_since_cps_change++; this.num_ticks_since_cps_change++;
const seconds_since_cps_change = this.num_ticks_since_cps_change * duration;
const num_cycles_since_cps_change = seconds_since_cps_change * this.cps;
try { try {
const time = getTime();
const begin = this.lastEnd; const begin = this.lastEnd;
this.lastBegin = begin; this.lastBegin = begin;
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
this.lastEnd = end;
this.lastTick = phase;
if (phase < t) { //convert ticks to cycles, so you can query the pattern for events
// avoid querying haps that are in the past anyway const eventLength = duration * this.cps;
console.log(`skip query: too late`); const end = this.num_cycles_since_last_cps_change + this.num_ticks_since_cps_change * eventLength;
return; this.lastEnd = end;
}
// query the pattern for events // query the pattern for events
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); const haps = this.pattern.queryArc(begin, end);
const tickdeadline = phase - time; // time left until the phase is a whole number
this.lastTick = time + tickdeadline;
haps.forEach((hap) => { haps.forEach((hap) => {
if (hap.hasOnset()) { if (hap.part.begin.equals(hap.whole.begin)) {
const targetTime = const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
const duration = hap.duration / this.cps; const duration = hap.duration / this.cps;
// the following line is dumb and only here for backwards compatibility onTrigger?.(hap, deadline, duration, this.cps);
// see https://github.com/tidalcycles/strudel/pull/1004
const deadline = targetTime - phase;
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
} }
}); });
} catch (e) { } catch (e) {
@@ -65,16 +59,9 @@ export class Cyclist {
} }
}, },
interval, // duration of each cycle interval, // duration of each cycle
0.1,
0.1,
setInterval,
clearInterval,
); );
} }
now() { now() {
if (!this.started) {
return 0;
}
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
} }
@@ -84,7 +71,7 @@ export class Cyclist {
} }
start() { start() {
this.num_ticks_since_cps_change = 0; this.num_ticks_since_cps_change = 0;
this.num_cycles_at_cps_change = 0; this.num_cycles_since_last_cps_change = 0;
if (!this.pattern) { if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.'); throw new Error('Scheduler: no pattern set! call .setPattern first.');
} }
@@ -109,7 +96,7 @@ export class Cyclist {
this.start(); this.start();
} }
} }
setCps(cps = 0.5) { setCps(cps = 1) {
if (this.cps === cps) { if (this.cps === cps) {
return; return;
} }
@@ -1,88 +1,80 @@
/* /*
draw.mjs - <short description TODO> draw.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/canvas/draw.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Pattern, getTime, State, TimeSpan } from '@strudel/core'; import { Pattern, getTime, State, TimeSpan } from './index.mjs';
export const getDrawContext = (id = 'test-canvas', options) => { export const getDrawContext = (id = 'test-canvas') => {
let { contextType = '2d', pixelated = false, pixelRatio = window.devicePixelRatio } = options || {};
let canvas = document.querySelector('#' + id); let canvas = document.querySelector('#' + id);
if (!canvas) { if (!canvas) {
const scale = 2; // 2 = crisp on retina screens
canvas = document.createElement('canvas'); canvas = document.createElement('canvas');
canvas.id = id; canvas.id = id;
canvas.width = window.innerWidth * pixelRatio; canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * pixelRatio; canvas.height = window.innerHeight * scale;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0'; canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
pixelated && (canvas.style.imageRendering = 'pixelated');
document.body.prepend(canvas); document.body.prepend(canvas);
let timeout; let timeout;
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
timeout && clearTimeout(timeout); timeout && clearTimeout(timeout);
timeout = setTimeout(() => { timeout = setTimeout(() => {
canvas.width = window.innerWidth * pixelRatio; canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * pixelRatio; canvas.height = window.innerHeight * scale;
}, 200); }, 200);
}); });
} }
return canvas.getContext(contextType); return canvas.getContext('2d');
}; };
let animationFrames = {}; Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
function stopAnimationFrame(id) {
if (animationFrames[id] !== undefined) {
cancelAnimationFrame(animationFrames[id]);
delete animationFrames[id];
}
}
function stopAllAnimations() {
Object.keys(animationFrames).forEach((id) => stopAnimationFrame(id));
}
let memory = {};
Pattern.prototype.draw = function (fn, options) {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return this; return this;
} }
let { id = 1, lookbehind = 0, lookahead = 0 } = options; if (window.strudelAnimation) {
let __t = Math.max(getTime(), 0); cancelAnimationFrame(window.strudelAnimation);
stopAnimationFrame(id); }
lookbehind = Math.abs(lookbehind); const ctx = getDrawContext();
// init memory, clear future haps of old pattern let cycle,
memory[id] = (memory[id] || []).filter((h) => !h.isInFuture(__t)); events = [];
let newFuture = this.queryArc(__t, __t + lookahead).filter((h) => h.hasOnset()); const animate = (time) => {
memory[id] = memory[id].concat(newFuture); const t = getTime();
if (from !== undefined && to !== undefined) {
let last; const currentCycle = Math.floor(t);
const animate = () => { if (cycle !== currentCycle) {
const _t = getTime(); cycle = currentCycle;
const t = _t + lookahead; const begin = currentCycle + from;
// filter out haps that are too far in the past const end = currentCycle + to;
memory[id] = memory[id].filter((h) => h.isInNearPast(lookbehind, _t)); setTimeout(() => {
// begin where we left off in last frame, but max -0.1s (inactive tab throttles to 1fps) events = this.query(new State(new TimeSpan(begin, end)))
let begin = Math.max(last || t, t - 1 / 10); .filter(Boolean)
const haps = this.queryArc(begin, t).filter((h) => h.hasOnset()); .filter((event) => event.part.begin.equals(event.whole.begin));
memory[id] = memory[id].concat(haps); onQuery?.(events);
last = t; // makes sure no haps are missed }, 0);
fn(memory[id], _t, t, this); }
animationFrames[id] = requestAnimationFrame(animate); }
callback(ctx, events, t, time);
window.strudelAnimation = requestAnimationFrame(animate);
}; };
animationFrames[id] = requestAnimationFrame(animate); requestAnimationFrame(animate);
return this; return this;
}; };
export const cleanupDraw = (clearScreen = true) => { export const cleanupDraw = (clearScreen = true) => {
const ctx = getDrawContext(); const ctx = getDrawContext();
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width); clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
stopAllAnimations(); if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
if (window.strudelScheduler) { if (window.strudelScheduler) {
clearInterval(window.strudelScheduler); clearInterval(window.strudelScheduler);
} }
}; };
Pattern.prototype.onPaint = function () { Pattern.prototype.onPaint = function (onPaint) {
console.warn('[draw] onPaint was not overloaded. Some drawings might not work'); // this is evil! TODO: add pattern.context
this.context = { onPaint };
return this; return this;
}; };
@@ -142,7 +134,7 @@ export class Drawer {
this.lastFrame = phase; this.lastFrame = phase;
this.visibleHaps = (this.visibleHaps || []) this.visibleHaps = (this.visibleHaps || [])
// filter out haps that are too far in the past (think left edge of screen for pianoroll) // filter out haps that are too far in the past (think left edge of screen for pianoroll)
.filter((h) => h.endClipped >= phase - lookbehind - lookahead) .filter((h) => h.whole.end >= phase - lookbehind - lookahead)
// add new haps with onset (think right edge bars scrolling in) // add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset())); .concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead; const time = phase - lookahead;
@@ -183,18 +175,3 @@ export class Drawer {
} }
} }
} }
export function getComputedPropertyValue(name) {
if (typeof window === 'undefined') {
return '#fff';
}
return getComputedStyle(document.documentElement).getPropertyValue(name);
}
let theme = {};
export function getTheme() {
return theme;
}
export function setTheme(_theme) {
theme = _theme;
}
+2 -8
View File
@@ -41,17 +41,11 @@ const _bjork = function (n, x) {
}; };
export const bjork = function (ons, steps) { export const bjork = function (ons, steps) {
const inverted = ons < 0;
ons = Math.abs(ons);
const offs = steps - ons; const offs = steps - ons;
const x = Array(ons).fill([1]); const x = Array(ons).fill([1]);
const y = Array(offs).fill([0]); const y = Array(offs).fill([0]);
const result = _bjork([ons, offs], [x, y]); const result = _bjork([ons, offs], [x, y]);
const p = flatten(result[1][0]).concat(flatten(result[1][1])); return flatten(result[1][0]).concat(flatten(result[1][1]));
if (inverted) {
return p.map((x) => (x === 0 ? 1 : 0));
}
return p;
}; };
/** /**
@@ -154,7 +148,7 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
* @param {number} pulses the number of onsets / beats * @param {number} pulses the number of onsets / beats
* @param {number} steps the number of steps to fill * @param {number} steps the number of steps to fill
* @example * @example
* note("c3").euclidLegato(3,8) * n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
*/ */
const _euclidLegato = function (pulses, steps, rotation, pat) { const _euclidLegato = function (pulses, steps, rotation, pat) {
-1
View File
@@ -22,7 +22,6 @@ export const evalScope = async (...args) => {
globalThis[name] = value; globalThis[name] = value;
}); });
}); });
return modules;
}; };
function safeEval(str, options = {}) { function safeEval(str, options = {}) {
-43
View File
@@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
import Fraction from 'fraction.js'; import Fraction from 'fraction.js';
import { TimeSpan } from './timespan.mjs'; import { TimeSpan } from './timespan.mjs';
import { removeUndefineds } from './util.mjs';
// Returns the start of the cycle. // Returns the start of the cycle.
Fraction.prototype.sam = function () { Fraction.prototype.sam = function () {
@@ -48,39 +47,14 @@ Fraction.prototype.eq = function (other) {
return this.compare(other) == 0; return this.compare(other) == 0;
}; };
Fraction.prototype.ne = function (other) {
return this.compare(other) != 0;
};
Fraction.prototype.max = function (other) { Fraction.prototype.max = function (other) {
return this.gt(other) ? this : other; return this.gt(other) ? this : other;
}; };
Fraction.prototype.maximum = function (...others) {
others = others.map((x) => new Fraction(x));
return others.reduce((max, other) => other.max(max), this);
};
Fraction.prototype.min = function (other) { Fraction.prototype.min = function (other) {
return this.lt(other) ? this : other; return this.lt(other) ? this : other;
}; };
Fraction.prototype.mulmaybe = function (other) {
return other !== undefined ? this.mul(other) : undefined;
};
Fraction.prototype.divmaybe = function (other) {
return other !== undefined ? this.div(other) : undefined;
};
Fraction.prototype.addmaybe = function (other) {
return other !== undefined ? this.add(other) : undefined;
};
Fraction.prototype.submaybe = function (other) {
return other !== undefined ? this.sub(other) : undefined;
};
Fraction.prototype.show = function (/* excludeWhole = false */) { Fraction.prototype.show = function (/* excludeWhole = false */) {
// return this.toFraction(excludeWhole); // return this.toFraction(excludeWhole);
return this.s * this.n + '/' + this.d; return this.s * this.n + '/' + this.d;
@@ -106,26 +80,9 @@ const fraction = (n) => {
}; };
export const gcd = (...fractions) => { export const gcd = (...fractions) => {
fractions = removeUndefineds(fractions);
if (fractions.length === 0) {
return undefined;
}
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1)); return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
}; };
export const lcm = (...fractions) => {
fractions = removeUndefineds(fractions);
if (fractions.length === 0) {
return undefined;
}
return fractions.reduce(
(lcm, fraction) => (lcm === undefined || fraction === undefined ? undefined : lcm.lcm(fraction)),
fraction(1),
);
};
fraction._original = Fraction; fraction._original = Fraction;
export default fraction; export default fraction;
+1 -36
View File
@@ -3,7 +3,6 @@ hap.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/hap.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/hap.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import Fraction from './fraction.mjs';
export class Hap { export class Hap {
/* /*
@@ -33,43 +32,13 @@ export class Hap {
} }
get duration() { get duration() {
let duration; return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
if (typeof this.value?.duration === 'number') {
duration = Fraction(this.value.duration);
} else {
duration = this.whole.end.sub(this.whole.begin);
}
if (typeof this.value?.clip === 'number') {
return duration.mul(this.value.clip);
}
return duration;
} }
get endClipped() { get endClipped() {
return this.whole.begin.add(this.duration); return this.whole.begin.add(this.duration);
} }
isActive(currentTime) {
return this.whole.begin <= currentTime && this.endClipped >= currentTime;
}
isInPast(currentTime) {
return currentTime > this.endClipped;
}
isInNearPast(margin, currentTime) {
return currentTime - margin <= this.endClipped;
}
isInFuture(currentTime) {
return currentTime < this.whole.begin;
}
isInNearFuture(margin, currentTime) {
return currentTime < this.whole.begin && currentTime > this.whole.begin - margin;
}
isWithinTime(min, max) {
return this.whole.begin <= max && this.endClipped >= min;
}
wholeOrPart() { wholeOrPart() {
return this.whole ? this.whole : this.part; return this.whole ? this.whole : this.part;
} }
@@ -91,10 +60,6 @@ export class Hap {
return this.whole != undefined && this.whole.begin.equals(this.part.begin); return this.whole != undefined && this.whole.begin.equals(this.part.begin);
} }
hasTag(tag) {
return this.context.tags?.includes(tag);
}
resolveState(state) { resolveState(state) {
if (this.stateful && this.hasOnset()) { if (this.stateful && this.hasOnset()) {
console.log('stateful'); console.log('stateful');
+9 -7
View File
@@ -4,13 +4,11 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as controls from './controls.mjs'; // legacy import controls from './controls.mjs';
export * from './euclid.mjs'; export * from './euclid.mjs';
import Fraction from './fraction.mjs'; import Fraction from './fraction.mjs';
import createClock from './zyklus.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
export { Fraction, controls, createClock }; export { Fraction, controls };
export * from './controls.mjs';
export * from './hap.mjs'; export * from './hap.mjs';
export * from './pattern.mjs'; export * from './pattern.mjs';
export * from './signal.mjs'; export * from './signal.mjs';
@@ -23,17 +21,21 @@ export * from './repl.mjs';
export * from './cyclist.mjs'; export * from './cyclist.mjs';
export * from './logger.mjs'; export * from './logger.mjs';
export * from './time.mjs'; export * from './time.mjs';
export * from './draw.mjs';
export * from './animate.mjs';
export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './ui.mjs'; export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs'; export { default as drawLine } from './drawLine.mjs';
// below won't work with runtime.mjs (json import fails) // below won't work with runtime.mjs (json import fails)
/* import * as p from './package.json'; /* import * as p from './package.json';
export const version = p.version; */ export const version = p.version; */
logger('🌀 @strudel/core loaded 🌀'); logger('🌀 @strudel.cycles/core loaded 🌀');
if (globalThis._strudelLoaded) { if (globalThis._strudelLoaded) {
console.warn( 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. 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; globalThis._strudelLoaded = true;
-10
View File
@@ -1,16 +1,6 @@
export const logKey = 'strudel.log'; export const logKey = 'strudel.log';
let debounce = 1000,
lastMessage,
lastTime;
export function logger(message, type, data = {}) { export function logger(message, type, data = {}) {
let t = performance.now();
if (lastMessage === message && t - lastTime < debounce) {
return;
}
lastMessage = message;
lastTime = t;
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px'); console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') { if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
document.dispatchEvent( document.dispatchEvent(
-147
View File
@@ -1,147 +0,0 @@
/*
neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances.
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { logger } from './logger.mjs';
export class NeoCyclist {
constructor({ onTrigger, onToggle, getTime }) {
this.started = false;
this.cps = 0.5;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.getTime = getTime; // get absolute time
this.time_at_last_tick_message = 0;
this.num_cycles_at_cps_change = 0;
this.onToggle = onToggle;
this.latency = 0.1; // fixed trigger time offset
this.cycle = 0;
this.id = Math.round(Date.now() * Math.random());
this.worker_time_dif;
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
this.worker.port.start();
this.channel = new BroadcastChannel('strudeltick');
let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif
const maxWeight = 20;
const precision = 10 ** 3; //round off time diff to prevent accumulating outliers
// the clock of the worker and the audio context clock can drift apart over time
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
// we need to keep a rolling weighted average of the time difference between the worker clock and audio context clock
// in order to schedule events consistently.
const setTimeReference = (num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline) => {
const time_dif = getTime() - (num_seconds_at_cps_change + num_seconds_since_cps_change) + tickdeadline;
if (this.worker_time_dif == null) {
this.worker_time_dif = time_dif;
} else {
const w = 1; //weight of new time diff;
const new_dif =
Math.round(((this.worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision;
if (new_dif != this.worker_time_dif) {
// reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens
weight = 4;
}
this.worker_time_dif = new_dif;
}
weight = Math.min(weight + 1, maxWeight);
};
const tickCallback = (payload) => {
const {
num_cycles_at_cps_change,
cps,
num_seconds_at_cps_change,
num_seconds_since_cps_change,
begin,
end,
tickdeadline,
cycle,
} = payload;
this.cps = cps;
this.cycle = cycle;
setTimeReference(num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline);
processHaps(begin, end, num_cycles_at_cps_change, num_seconds_at_cps_change);
this.time_at_last_tick_message = this.getTime();
};
const processHaps = (begin, end, num_cycles_at_cps_change, seconds_at_cps_change) => {
if (this.started === false) {
return;
}
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
haps.forEach((hap) => {
if (hap.hasOnset()) {
const targetTime =
(hap.whole.begin - num_cycles_at_cps_change) / this.cps +
seconds_at_cps_change +
this.latency +
this.worker_time_dif;
const duration = hap.duration / this.cps;
onTrigger?.(hap, 0, duration, this.cps, targetTime);
}
});
};
// receive messages from worker clock and process them
this.channel.onmessage = (message) => {
if (!this.started) {
return;
}
const { payload, type } = message.data;
switch (type) {
case 'tick': {
tickCallback(payload);
}
}
};
}
sendMessage(type, payload) {
this.worker.port.postMessage({ type, payload, id: this.id });
}
now() {
const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps;
return this.cycle + gap;
}
setCps(cps = 1) {
this.sendMessage('cpschange', { cps });
}
setCycle(cycle) {
this.sendMessage('setcycle', { cycle });
}
setStarted(started) {
this.sendMessage('toggle', { started });
this.started = started;
this.onToggle?.(started);
}
start() {
logger('[cyclist] start');
this.setStarted(true);
}
stop() {
this.worker_time_dif = null;
logger('[cyclist] stop');
this.setStarted(false);
}
setPattern(pat, autostart = false) {
this.pattern = pat;
if (autostart && !this.started) {
this.start();
}
}
log(begin, end, haps) {
const onsets = haps.filter((h) => h.hasOnset());
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
}
}
+4 -3
View File
@@ -1,11 +1,12 @@
{ {
"name": "@strudel/core", "name": "@strudel.cycles/core",
"version": "1.0.1", "version": "0.9.0",
"description": "Port of Tidal Cycles to JavaScript", "description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
"publishConfig": { "publishConfig": {
"main": "dist/index.mjs" "main": "dist/index.js",
"module": "dist/index.mjs"
}, },
"scripts": { "scripts": {
"test": "vitest run", "test": "vitest run",
+347 -890
View File
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,10 @@
/* /*
pianoroll.mjs - <short description TODO> pianoroll.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/canvas/pianoroll.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pianoroll.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Pattern, noteToMidi, freqToMidi } from '@strudel/core'; import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
import { getTheme, getDrawContext } from './draw.mjs';
const scale = (normalized, min, max) => normalized * (max - min) + min; const scale = (normalized, min, max) => normalized * (max - min) + min;
const getValue = (e) => { const getValue = (e) => {
@@ -19,13 +18,7 @@ const getValue = (e) => {
} }
note = note ?? n; note = note ?? n;
if (typeof note === 'string') { if (typeof note === 'string') {
try { return noteToMidi(note);
// TODO: n(run(32)).scale("D:minor") fails when trying to query negative time..
return noteToMidi(note);
} catch (err) {
// console.warn(`error converting note to midi: ${err}`); // this spams to crazy
return 0;
}
} }
if (typeof note === 'number') { if (typeof note === 'number') {
return note; return note;
@@ -37,24 +30,25 @@ const getValue = (e) => {
}; };
Pattern.prototype.pianoroll = function (options = {}) { Pattern.prototype.pianoroll = function (options = {}) {
let { cycles = 4, playhead = 0.5, overscan = 0, hideNegative = false, ctx = getDrawContext(), id = 1 } = options; let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
let from = -cycles * playhead; let from = -cycles * playhead;
let to = cycles * (1 - playhead); let to = cycles * (1 - playhead);
const inFrame = (hap, t) => (!hideNegative || hap.whole.begin >= 0) && hap.isWithinTime(t + from, t + to);
this.draw( this.draw(
(haps, time) => { (ctx, haps, t) => {
const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
pianoroll({ pianoroll({
...options, ...options,
time, time: t,
ctx, ctx,
haps: haps.filter((hap) => inFrame(hap, time)), haps: haps.filter(inFrame),
}); });
}, },
{ {
lookbehind: from - overscan, from: from - overscan,
lookahead: to + overscan, to: to + overscan,
id,
}, },
); );
return this; return this;
@@ -104,8 +98,11 @@ export function pianoroll({
flipTime = 0, flipTime = 0,
flipValues = 0, flipValues = 0,
hideNegative = false, hideNegative = false,
inactive = getTheme().foreground, // inactive = '#C9E597',
active = getTheme().foreground, // inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent', background = 'transparent',
smear = 0, smear = 0,
playheadColor = 'white', playheadColor = 'white',
@@ -124,17 +121,12 @@ export function pianoroll({
colorizeInactive = 1, colorizeInactive = 1,
fontFamily, fontFamily,
ctx, ctx,
id,
} = {}) { } = {}) {
const w = ctx.canvas.width; const w = ctx.canvas.width;
const h = ctx.canvas.height; const h = ctx.canvas.height;
let from = -cycles * playhead; let from = -cycles * playhead;
let to = cycles * (1 - playhead); let to = cycles * (1 - playhead);
if (id) {
haps = haps.filter((hap) => hap.hasTag(id));
}
if (timeframeProp) { if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead'); console.warn('timeframe is deprecated! use from/to instead');
from = 0; from = 0;
@@ -168,13 +160,8 @@ export function pianoroll({
maxMidi = max; maxMidi = max;
valueExtent = maxMidi - minMidi + 1; valueExtent = maxMidi - minMidi + 1;
} }
foldValues = values.sort((a, b) => // foldValues = values.sort((a, b) => a - b);
typeof a === 'number' && typeof b === 'number' foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
? a - b
: typeof a === 'number'
? 1
: String(a).localeCompare(String(b)),
);
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent; barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
ctx.fillStyle = background; ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset! ctx.globalAlpha = 1; // reset!
@@ -189,14 +176,13 @@ export function pianoroll({
if (hideInactive && !isActive) { if (hideInactive && !isActive) {
return; return;
} }
let color = event.value?.color; let color = event.value?.color || event.context?.color;
active = color || active; active = color || active;
inactive = colorizeInactive ? color || inactive : inactive; inactive = colorizeInactive ? color || inactive : inactive;
color = isActive ? active : inactive; color = isActive ? active : inactive;
ctx.fillStyle = fillCurrent ? color : 'transparent'; ctx.fillStyle = fillCurrent ? color : 'transparent';
ctx.strokeStyle = color; ctx.strokeStyle = color;
const { velocity = 1, gain = 1 } = event.value || {}; ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
ctx.globalAlpha = velocity * gain;
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent; const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
const timePx = scale(timeProgress, ...timeRange); const timePx = scale(timeProgress, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis); let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
@@ -272,8 +258,8 @@ export function getDrawOptions(drawTime, options = {}) {
export const getPunchcardPainter = export const getPunchcardPainter =
(options = {}) => (options = {}) =>
(ctx, time, haps, drawTime) => (ctx, time, haps, drawTime, paintOptions = {}) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }); pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) });
Pattern.prototype.punchcard = function (options) { Pattern.prototype.punchcard = function (options) {
return this.onPaint(getPunchcardPainter(options)); return this.onPaint(getPunchcardPainter(options));
+72 -83
View File
@@ -1,4 +1,3 @@
import { NeoCyclist } from './neocyclist.mjs';
import { Cyclist } from './cyclist.mjs'; import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs'; import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
@@ -7,7 +6,9 @@ import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
export function repl({ export function repl({
interval,
defaultOutput, defaultOutput,
onSchedulerError,
onEvalError, onEvalError,
beforeEval, beforeEval,
afterEval, afterEval,
@@ -16,9 +17,6 @@ export function repl({
onToggle, onToggle,
editPattern, editPattern,
onUpdateState, onUpdateState,
sync = false,
setInterval,
clearInterval,
}) { }) {
const state = { const state = {
schedulerError: undefined, schedulerError: undefined,
@@ -39,27 +37,21 @@ export function repl({
onUpdateState?.(state); onUpdateState?.(state);
}; };
const schedulerOptions = { const scheduler = new Cyclist({
interval,
onTrigger: getTrigger({ defaultOutput, getTime }), onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime, getTime,
onToggle: (started) => { onToggle: (started) => {
updateState({ started }); updateState({ started });
onToggle?.(started); onToggle?.(started);
}, },
setInterval, });
clearInterval,
};
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
const scheduler =
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
let pPatterns = {}; let pPatterns = {};
let anonymousIndex = 0;
let allTransform; let allTransform;
const hush = function () { const hush = function () {
pPatterns = {}; pPatterns = {};
anonymousIndex = 0;
allTransform = undefined; allTransform = undefined;
return silence; return silence;
}; };
@@ -69,76 +61,12 @@ export function repl({
scheduler.setPattern(pattern, autostart); scheduler.setPattern(pattern, autostart);
}; };
setTime(() => scheduler.now()); // TODO: refactor? 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) {
if (id.startsWith('_') || id.endsWith('_')) {
// allows muting a pattern x with x_ or _x
return silence;
}
if (id === '$') {
// allows adding anonymous patterns with $:
id = `$${anonymousIndex}`;
anonymousIndex++;
}
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);
});
return evalScope({
all,
hush,
cpm,
setCps,
setcps: setCps,
setCpm,
setcpm: setCpm,
});
};
const evaluate = async (code, autostart = true, shouldHush = true) => { const evaluate = async (code, autostart = true, shouldHush = true) => {
if (!code) { if (!code) {
throw new Error('no code to evaluate'); throw new Error('no code to evaluate');
} }
try { try {
updateState({ code, pending: true }); updateState({ code, pending: true });
await injectPatternMethods();
await beforeEval?.({ code }); await beforeEval?.({ code });
shouldHush && hush(); shouldHush && hush();
let { pattern, meta } = await _evaluate(code, transpiler); let { pattern, meta } = await _evaluate(code, transpiler);
@@ -166,27 +94,88 @@ export function repl({
afterEval?.({ code, pattern, meta }); afterEval?.({ code, pattern, meta });
return pattern; return pattern;
} catch (err) { } catch (err) {
// console.warn(`[repl] eval error: ${err.message}`);
logger(`[eval] error: ${err.message}`, 'error'); logger(`[eval] error: ${err.message}`, 'error');
console.error(err);
updateState({ evalError: err, pending: false }); updateState({ evalError: err, pending: false });
onEvalError?.(err); onEvalError?.(err);
} }
}; };
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);
// 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);
});
Pattern.prototype.p = function (id) {
pPatterns[id] = this;
return this;
};
Pattern.prototype.q = function (id) {
return silence;
};
const all = function (transform) {
allTransform = transform;
return silence;
};
try {
for (let i = 1; i < 10; ++i) {
Object.defineProperty(Pattern.prototype, `d${i}`, {
get() {
return this.p(i);
},
});
Object.defineProperty(Pattern.prototype, `p${i}`, {
get() {
return this.p(i);
},
});
Pattern.prototype[`q${i}`] = silence;
}
} catch (err) {
// already defined..
}
const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: scheduler.cps / hap.whole.duration, // overwrite speed completely?
unit: 'c',
})),
),
);
evalScope({
loopAt,
fit,
all,
hush,
setCps,
setcps: setCps,
setCpm,
setcpm: setCpm,
});
const setCode = (code) => updateState({ code }); const setCode = (code) => updateState({ code });
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state };
} }
export const getTrigger = export const getTrigger =
({ getTime, defaultOutput }) => ({ getTime, defaultOutput }) =>
async (hap, deadline, duration, cps, t) => { async (hap, deadline, duration, cps) => {
// TODO: get rid of deadline after https://github.com/tidalcycles/strudel/pull/1004
try { try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) { if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps, t); await defaultOutput(hap, deadline, duration, cps);
} }
if (hap.context.onTrigger) { if (hap.context.onTrigger) {
// call signature of output / onTrigger is different... // call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t); await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
} }
} catch (err) { } catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error'); logger(`[cyclist] error: ${err.message}`, 'error');
+41 -255
View File
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { Hap } from './hap.mjs'; import { Hap } from './hap.mjs';
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs'; import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
import Fraction from './fraction.mjs'; import Fraction from './fraction.mjs';
import { id, _mod, clamp, objectMap } from './util.mjs'; import { id, _mod, clamp } from './util.mjs';
export function steady(value) { export function steady(value) {
// A continuous value // A continuous value
@@ -27,11 +27,9 @@ export const isaw2 = isaw.toBipolar();
* *
* @return {Pattern} * @return {Pattern}
* @example * @example
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8") * "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
* .clip(saw.slow(2))
* @example * @example
* n(saw.range(0,8).segment(8)) * saw.range(0,8).segment(8).scale('C major').slow(4).note()
* .scale('C major')
* *
*/ */
export const saw = signal((t) => t % 1); export const saw = signal((t) => t % 1);
@@ -44,8 +42,7 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
* *
* @return {Pattern} * @return {Pattern}
* @example * @example
* n(sine.segment(16).range(0,15)) * sine.segment(16).range(0,15).slow(2).scale('C minor').note()
* .scale("C:minor")
* *
*/ */
export const sine = sine2.fromBipolar(); export const sine = sine2.fromBipolar();
@@ -55,8 +52,7 @@ export const sine = sine2.fromBipolar();
* *
* @return {Pattern} * @return {Pattern}
* @example * @example
* n(stack(sine,cosine).segment(16).range(0,15)) * stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor').note()
* .scale("C:minor")
* *
*/ */
export const cosine = sine._early(Fraction(1).div(4)); export const cosine = sine._early(Fraction(1).div(4));
@@ -67,7 +63,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
* *
* @return {Pattern} * @return {Pattern}
* @example * @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)); export const square = signal((t) => Math.floor((t * 2) % 2));
@@ -78,7 +74,7 @@ export const square2 = square.toBipolar();
* *
* @return {Pattern} * @return {Pattern}
* @example * @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); export const tri = fastcat(isaw, saw);
@@ -122,8 +118,8 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
/** /**
* A discrete pattern of numbers from 0 to n-1 * A discrete pattern of numbers from 0 to n-1
* @example * @example
* n(run(4)).scale("C4:pentatonic") * run(4).scale('C4 major').note()
* // n("0 1 2 3").scale("C4:pentatonic") * // "0 1 2 3".scale('C4 major').note()
*/ */
export const run = (n) => saw.range(0, n).floor().segment(n); export const run = (n) => saw.range(0, n).floor().segment(n);
@@ -133,7 +129,7 @@ export const run = (n) => saw.range(0, n).floor().segment(n);
* @name rand * @name rand
* @example * @example
* // randomly change the cutoff * // 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); export const rand = signal(timeToRand);
@@ -143,24 +139,7 @@ export const rand = signal(timeToRand);
export const rand2 = rand.toBipolar(); export const rand2 = rand.toBipolar();
export const _brandBy = (p) => rand.fmap((x) => x < p); export const _brandBy = (p) => rand.fmap((x) => x < p);
/**
* A continuous pattern of 0 or 1 (binary random), with a probability for the value being 1
*
* @name brandBy
* @param {number} probability - a number between 0 and 1
* @example
* s("hh*10").pan(brandBy(0.2))
*/
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin(); export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
/**
* A continuous pattern of 0 or 1 (binary random)
*
* @name brand
* @example
* s("hh*10").pan(brand)
*/
export const brand = _brandBy(0.5); export const brand = _brandBy(0.5);
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i)); export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
@@ -172,188 +151,36 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
* @param {number} n max value (exclusive) * @param {number} n max value (exclusive)
* @example * @example
* // randomly select scale notes from 0 - 7 (= C to C) * // 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(); export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
const _pick = function (lookup, pat, modulo = true) { /**
const array = Array.isArray(lookup); * pick from the list of values (or patterns of values) via the index using the given
const len = Object.keys(lookup).length; * pattern of integers
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note(pick("<0 1 [2!2] 3>", ["g a", "e f", "f g f g" , "g a c d"]))
*/
lookup = objectMap(lookup, reify); export const pick = (pat, xs) => {
xs = xs.map(reify);
if (len === 0) { if (xs.length == 0) {
return silence; return silence;
} }
return pat.fmap((i) => { return pat
let key = i; .fmap((i) => {
if (array) { const key = clamp(Math.round(i), 0, xs.length - 1);
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1); return xs[key];
} })
return lookup[key]; .innerJoin();
});
}; };
/** * 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));
});
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickOut = register('pickOut', function (lookup, pat) {
return _pick(lookup, pat, false).outerJoin();
});
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
return _pick(lookup, pat, true).outerJoin();
});
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickRestart = register('pickRestart', function (lookup, pat) {
return _pick(lookup, pat, false).restartJoin();
});
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
return _pick(lookup, pat, true).restartJoin();
});
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickReset = register('pickReset', function (lookup, pat) {
return _pick(lookup, pat, false).resetJoin();
});
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin();
});
/** /**
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name). * pick from the list of values (or patterns of values) via the index using the given
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @synonyms pickSqueeze
* @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, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, false).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.
* @name inhabitmod
* @synonyms pickmodSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, true).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 * pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
* @param {Pattern} pat * @param {Pattern} pat
* @param {*} xs * @param {*} xs
@@ -414,8 +241,6 @@ export const chooseInWith = (pat, xs) => {
* Chooses randomly from the given list of elements. * Chooses randomly from the given list of elements.
* @param {...any} xs values / patterns to choose from. * @param {...any} xs values / patterns to choose from.
* @returns {Pattern} - a continuous pattern. * @returns {Pattern} - a continuous pattern.
* @example
* note("c2 g2!2 d2 f1").s(choose("sine", "triangle", "bd:6"))
*/ */
export const choose = (...xs) => chooseWith(rand, xs); export const choose = (...xs) => chooseWith(rand, xs);
@@ -442,12 +267,11 @@ Pattern.prototype.choose2 = function (...xs) {
/** /**
* Picks one of the elements at random each cycle. * Picks one of the elements at random each cycle.
* @synonyms randcat
* @returns {Pattern} * @returns {Pattern}
* @example * @example
* chooseCycles("bd", "hh", "sd").s().fast(8) * chooseCycles("bd", "hh", "sd").s().fast(4)
* @example * @example
* s("bd | hh | sd").fast(8) * "bd | hh | sd".s().fast(4)
*/ */
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs); export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
@@ -471,27 +295,9 @@ const _wchooseWith = function (pat, ...pairs) {
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin(); const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
/**
* Chooses randomly from the given list of elements by giving a probability to each element
* @param {...any} pairs arrays of value and weight
* @returns {Pattern} - a continuous pattern.
* @example
* note("c2 g2!2 d2 f1").s(wchoose(["sine",10], ["triangle",1], ["bd:6",1]))
*/
export const wchoose = (...pairs) => wchooseWith(rand, ...pairs); export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
/** export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
* Picks one of the elements at random each cycle by giving a probability to each element
* @synonyms wrandcat
* @returns {Pattern}
* @example
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
* @example
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
*/
export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pairs).innerJoin();
export const wrandcat = wchooseCycles;
// this function expects pat to be a pattern of floats... // this function expects pat to be a pattern of floats...
export const perlinWith = (pat) => { export const perlinWith = (pat) => {
@@ -508,7 +314,7 @@ export const perlinWith = (pat) => {
* @name perlin * @name perlin
* @example * @example
* // randomly change the cutoff * // 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))); export const perlin = perlinWith(time.fmap((v) => Number(v)));
@@ -550,7 +356,7 @@ export const degradeBy = register('degradeBy', function (x, pat) {
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5)); 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 * 0 = 100% chance of removal
* 1 = 0% 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). * Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
@@ -561,11 +367,6 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
* @returns Pattern * @returns Pattern
* @example * @example
* s("hh*8").undegradeBy(0.2) * s("hh*8").undegradeBy(0.2)
* @example
* s("hh*10").layer(
* x => x.degradeBy(0.2).pan(0),
* x => x.undegradeBy(0.8).pan(1)
* )
*/ */
export const undegradeBy = register('undegradeBy', function (x, pat) { export const undegradeBy = register('undegradeBy', function (x, pat) {
return pat._degradeByWith( return pat._degradeByWith(
@@ -574,27 +375,12 @@ export const undegradeBy = register('undegradeBy', function (x, pat) {
); );
}); });
/**
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
* Events that would be removed by degrade are let through by undegrade and vice versa (see second example).
*
* @name undegrade
* @memberof Pattern
* @returns Pattern
* @example
* s("hh*8").undegrade()
* @example
* s("hh*10").layer(
* x => x.degrade().pan(0),
* x => x.undegrade().pan(1)
* )
*/
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5)); export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
/** /**
* *
* Randomly applies the given function by the given probability. * Randomly applies the given function by the given probability.
* Similar to `someCyclesBy` * Similar to {@link Pattern#someCyclesBy}
* *
* @name sometimesBy * @name sometimesBy
* @memberof Pattern * @memberof Pattern
@@ -602,7 +388,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
* @param {function} function - the transformation to apply * @param {function} function - the transformation to apply
* @returns Pattern * @returns Pattern
* @example * @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) { export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
@@ -620,7 +406,7 @@ export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
* @param {function} function - the transformation to apply * @param {function} function - the transformation to apply
* @returns Pattern * @returns Pattern
* @example * @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) { export const sometimes = register('sometimes', function (func, pat) {
return pat._sometimesBy(0.5, func); return pat._sometimesBy(0.5, func);
@@ -629,7 +415,7 @@ export const sometimes = register('sometimes', function (func, pat) {
/** /**
* *
* Randomly applies the given function by the given probability on a cycle by cycle basis. * 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 * @name someCyclesBy
* @memberof Pattern * @memberof Pattern
@@ -637,7 +423,7 @@ export const sometimes = register('sometimes', function (func, pat) {
* @param {function} function - the transformation to apply * @param {function} function - the transformation to apply
* @returns Pattern * @returns Pattern
* @example * @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) { export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
@@ -659,7 +445,7 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat)
* @memberof Pattern * @memberof Pattern
* @returns Pattern * @returns Pattern
* @example * @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) { export const someCycles = register('someCycles', function (func, pat) {
return pat._someCyclesBy(0.5, func); return pat._someCyclesBy(0.5, func);
@@ -1,5 +1,4 @@
import { Pattern } from '@strudel/core'; import { Pattern } from './index.mjs';
import { getTheme } from './draw.mjs';
// polar coords -> xy // polar coords -> xy
function fromPolar(angle, radius, cx, cy) { function fromPolar(angle, radius, cx, cy) {
@@ -20,7 +19,7 @@ function spiralSegment(options) {
cy = 100, cy = 100,
rotate = 0, rotate = 0,
thickness = margin / 2, thickness = margin / 2,
color = getTheme().foreground, color = '#0000ff30',
cap = 'round', cap = 'round',
stretch = 1, stretch = 1,
fromOpacity = 1, fromOpacity = 1,
@@ -50,81 +49,70 @@ function spiralSegment(options) {
ctx.stroke(); ctx.stroke();
} }
function drawSpiral(options) { Pattern.prototype.spiral = function (options = {}) {
let { const {
stretch = 1, stretch = 1,
size = 80, size = 80,
thickness = size / 2, thickness = size / 2,
cap = 'butt', // round butt squar, cap = 'butt', // round butt squar,
inset = 3, // start angl, inset = 3, // start angl,
playheadColor = '#ffffff', playheadColor = '#ffffff90',
playheadLength = 0.02, playheadLength = 0.02,
playheadThickness = thickness, playheadThickness = thickness,
padding = 0, padding = 0,
steady = 1, steady = 1,
activeColor = getTheme().foreground, inactiveColor = '#ffffff20',
inactiveColor = getTheme().gutterForeground,
colorizeInactive = 0, colorizeInactive = 0,
fade = true, fade = true,
// logSpiral = true, // logSpiral = true,
ctx,
time,
haps,
drawTime,
id,
} = options; } = options;
if (id) { function spiral({ ctx, time, haps, drawTime }) {
haps = haps.filter((hap) => hap.hasTag(id)); const [w, h] = [ctx.canvas.width, ctx.canvas.height];
} ctx.clearRect(0, 0, w * 2, h * 2);
const [cx, cy] = [w / 2, h / 2];
const settings = {
margin: size / stretch,
cx,
cy,
stretch,
cap,
thickness,
};
const [w, h] = [ctx.canvas.width, ctx.canvas.height]; const playhead = {
ctx.clearRect(0, 0, w * 2, h * 2); ...settings,
const [cx, cy] = [w / 2, h / 2]; thickness: playheadThickness,
const settings = { from: inset - playheadLength,
margin: size / stretch, to: inset,
cx, color: playheadColor,
cy, };
stretch,
cap,
thickness,
};
const playhead = { const [min] = drawTime;
...settings, const rotate = steady * time;
thickness: playheadThickness, haps.forEach((hap) => {
from: inset - playheadLength, const isActive = hap.whole.begin <= time && hap.endClipped > time;
to: inset, const from = hap.whole.begin - time + inset;
color: playheadColor, const to = hap.endClipped - time + inset - padding;
}; const { color } = hap.context;
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
const [min] = drawTime; spiralSegment({
const rotate = steady * time; ctx,
haps.forEach((hap) => { ...settings,
const isActive = hap.whole.begin <= time && hap.endClipped > time; from,
const from = hap.whole.begin - time + inset; to,
const to = hap.endClipped - time + inset - padding; rotate,
const hapColor = hap.value?.color || activeColor; color: colorizeInactive || isActive ? color : inactiveColor,
const color = colorizeInactive || isActive ? hapColor : inactiveColor; fromOpacity: opacity,
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1; toOpacity: opacity,
});
});
spiralSegment({ spiralSegment({
ctx, ctx,
...settings, ...playhead,
from,
to,
rotate, rotate,
color,
fromOpacity: opacity,
toOpacity: opacity,
}); });
}); }
spiralSegment({
ctx,
...playhead,
rotate,
});
}
Pattern.prototype.spiral = function (options = {}) { return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
return this.onPaint((ctx, time, haps, drawTime) => drawSpiral({ ctx, time, haps, drawTime, ...options }));
}; };
+5 -19
View File
@@ -4,39 +4,25 @@ Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { s, pan } from '../controls.mjs'; import controls from '../controls.mjs';
import { mini } from '../../mini/mini.mjs'; import { mini } from '../../mini/mini.mjs';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import Fraction from '../fraction.mjs';
describe('controls', () => { describe('controls', () => {
it('should support controls', () => { it('should support controls', () => {
expect(s('bd').firstCycleValues).toEqual([{ s: 'bd' }]); expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
}); });
it('should support compound controls', () => { it('should support compound controls', () => {
expect(s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]); expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
expect(s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([ expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
{ s: 'bd', n: 3 }, { s: 'bd', n: 3 },
{ s: 'sd', n: 4, gain: 1.4 }, { s: 'sd', n: 4, gain: 1.4 },
]); ]);
}); });
it('should support ignore extra elements in compound controls', () => { it('should support ignore extra elements in compound controls', () => {
expect(s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([ expect(controls.s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
{ s: 'bd', n: 3, gain: 0.4 }, { s: 'bd', n: 3, gain: 0.4 },
{ s: 'sd', n: 4, gain: 0.5 }, { s: 'sd', n: 4, gain: 0.5 },
]); ]);
}); });
it('should support nested controls', () => {
expect(s(mini('bd').pan(1)).firstCycleValues).toEqual([{ s: 'bd', pan: 1 }]);
expect(s(mini('bd:1').pan(1)).firstCycleValues).toEqual([{ s: 'bd', n: 1, pan: 1 }]);
});
it('preserves tactus of the left pattern', () => {
expect(s(mini('bd cp mt').pan(mini('1 2 3 4'))).tactus).toEqual(Fraction(3));
});
it('preserves tactus of the right pattern for .out', () => {
expect(s(mini('bd cp mt').set.out(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(4));
});
it('combines tactus of the pattern for .mix as lcm', () => {
expect(s(mini('bd cp mt').set.mix(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(12));
});
}); });
+20 -172
View File
@@ -21,8 +21,8 @@ import {
cat, cat,
sequence, sequence,
palindrome, palindrome,
s_polymeter, polymeter,
s_polymeterSteps, polymeterSteps,
polyrhythm, polyrhythm,
silence, silence,
fast, fast,
@@ -46,18 +46,13 @@ import {
rev, rev,
time, time,
run, run,
pick,
stackLeft,
stackRight,
stackCentre,
s_cat,
calculateTactus,
} from '../index.mjs'; } from '../index.mjs';
import { steady } from '../signal.mjs'; import { steady } from '../signal.mjs';
import { n, s } from '../controls.mjs'; import controls from '../controls.mjs';
const { n, s } = controls;
const st = (begin, end) => new State(ts(begin, end)); const st = (begin, end) => new State(ts(begin, end));
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end)); const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context); const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
@@ -186,18 +181,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7), new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
]); ]);
}); });
it('can Reset() structure', () => { it('can Trig() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.reset(20, 30) .add.trig(20, 30)
.early(2), .early(2),
sequence(26, 27, 36, 37), sequence(26, 27, 36, 37),
); );
}); });
it('can Restart() structure', () => { it('can Trigzero() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.restart(20, 30) .add.trigzero(20, 30)
.early(2), .early(2),
sequence(21, 22, 31, 32), sequence(21, 22, 31, 32),
); );
@@ -238,18 +233,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2), new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]); ]);
}); });
it('can Reset() structure', () => { it('can Trig() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.reset(20, 30) .keep.trig(20, 30)
.early(2), .early(2),
sequence(6, 7, 6, 7), sequence(6, 7, 6, 7),
); );
}); });
it('can Restart() structure', () => { it('can Trigzero() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.restart(20, 30) .keep.trigzero(20, 30)
.early(2), .early(2),
sequence(1, 2, 1, 2), sequence(1, 2, 1, 2),
); );
@@ -284,18 +279,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2), new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]); ]);
}); });
it('can Reset() structure', () => { it('can Trig() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.reset(false, true) .keepif.trig(false, true)
.early(2), .early(2),
sequence(silence, silence, 6, 7), sequence(silence, silence, 6, 7),
); );
}); });
it('can Restart() structure', () => { it('can Trigzero() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.restart(false, true) .keepif.trigzero(false, true)
.early(2), .early(2),
sequence(silence, silence, 1, 2), sequence(silence, silence, 1, 2),
); );
@@ -608,19 +603,16 @@ describe('Pattern', () => {
); );
}); });
}); });
describe('s_polymeter()', () => { describe('polymeter()', () => {
it('Can layer up cycles, stepwise, with lists', () => { it('Can layer up cycles, stepwise', () => {
expect(s_polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual( expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(), fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
); );
expect(s_polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual( expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(), stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
); );
}); });
it('Can layer up cycles, stepwise, with weighted patterns', () => {
sameFirst(s_polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
});
}); });
describe('firstOf()', () => { describe('firstOf()', () => {
@@ -1065,148 +1057,4 @@ describe('Pattern', () => {
expect(slowcat(0, 1).repeatCycles(2).fast(6).firstCycleValues).toStrictEqual([0, 0, 1, 1, 0, 0]); 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),
),
);
});
});
describe('tactus', () => {
it('Is correctly preserved/calculated through transformations', () => {
expect(sequence(0, 1, 2, 3).linger(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).iter(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).fast(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).rev().tactus).toStrictEqual(Fraction(4));
expect(sequence(1).segment(10).tactus).toStrictEqual(Fraction(10));
expect(sequence(1, 0, 1).invert().tactus).toStrictEqual(Fraction(3));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4).tactus).toStrictEqual(Fraction(8));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4).tactus).toStrictEqual(Fraction(8));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
Fraction(4),
);
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
Fraction(4),
);
expect(sequence({ n: 0 }, { n: 1 }, { n: 2 }).chop(4).tactus).toStrictEqual(Fraction(12));
expect(
pure((x) => x + 1)
.setTactus(3)
.appBoth(pure(1).setTactus(2)).tactus,
).toStrictEqual(Fraction(6));
expect(
pure((x) => x + 1)
.setTactus(undefined)
.appBoth(pure(1).setTactus(2)).tactus,
).toStrictEqual(Fraction(2));
expect(
pure((x) => x + 1)
.setTactus(3)
.appBoth(pure(1).setTactus(undefined)).tactus,
).toStrictEqual(Fraction(3));
expect(stack(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(6));
expect(stack(fastcat(0, 1, 2), fastcat(3, 4).setTactus(undefined)).tactus).toStrictEqual(Fraction(3));
expect(stackLeft(fastcat(0, 1, 2, 3), fastcat(3, 4)).tactus).toStrictEqual(Fraction(4));
expect(stackRight(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3));
// maybe this should double when they are either all even or all odd
expect(stackCentre(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3));
expect(fastcat(0, 1).ply(3).tactus).toStrictEqual(Fraction(6));
expect(fastcat(0, 1).setTactus(undefined).ply(3).tactus).toStrictEqual(undefined);
expect(fastcat(0, 1).fast(3).tactus).toStrictEqual(Fraction(2));
expect(fastcat(0, 1).setTactus(undefined).fast(3).tactus).toStrictEqual(undefined);
});
});
describe('s_cat', () => {
it('can cat', () => {
expect(sameFirst(s_cat(fastcat(0, 1, 2, 3), fastcat(4, 5)), fastcat(0, 1, 2, 3, 4, 5)));
expect(sameFirst(s_cat(pure(1), pure(2), pure(3)), fastcat(1, 2, 3)));
});
it('calculates undefined tactuses as the average', () => {
expect(sameFirst(s_cat(pure(1), pure(2), pure(3).setTactus(undefined)), fastcat(1, 2, 3)));
});
});
describe('s_taper', () => {
it('can taper', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_taper(1, 5), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0)));
});
it('can taper backwards', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_taper(-1, 5), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)));
});
});
describe('s_add and s_sub', () => {
it('can add from the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(2), sequence(0, 1)));
});
it('can sub to the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(2), sequence(0, 1, 2)));
});
it('can add from the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(-2), sequence(3, 4)));
});
it('can sub to the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(-2), sequence(2, 3, 4)));
});
it('can subtract nothing', () => {
expect(sameFirst(pure('a').s_sub(0), pure('a')));
});
it('can subtract nothing, repeatedly', () => {
expect(sameFirst(pure('a').s_sub(0, 0), fastcat('a', 'a')));
for (var i = 0; i < 100; ++i) {
expect(sameFirst(pure('a').s_sub(...Array(i).fill(0)), fastcat(...Array(i).fill('a'))));
}
});
});
}); });
+6 -1
View File
@@ -6,7 +6,8 @@ This program is free software: you can redistribute it and/or modify it under th
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { map, valued, mul } from '../value.mjs'; import { map, valued, mul } from '../value.mjs';
import { n } from '../controls.mjs'; import controls from '../controls.mjs';
const { n } = controls;
describe('Value', () => { describe('Value', () => {
it('unionWith', () => { it('unionWith', () => {
@@ -22,4 +23,8 @@ describe('Value', () => {
expect(valued(mul).ap(3).ap(3).value).toEqual(9); expect(valued(mul).ap(3).ap(3).value).toEqual(9);
expect(valued(3).mul(3).value).toEqual(9); expect(valued(3).mul(3).value).toEqual(9);
}); });
it('union bare numbers for numeral props', () => {
expect(n(3).cutoff(500).add(10).firstCycleValues).toEqual([{ n: 13, cutoff: 510 }]);
expect(n(3).cutoff(500).mul(2).firstCycleValues).toEqual([{ n: 6, cutoff: 1000 }]);
});
}); });
+18
View File
@@ -4,6 +4,19 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { getTime } from './time.mjs';
function frame(callback) {
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
const animate = (animationTime) => {
callback(animationTime, getTime());
window.strudelAnimation = requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}
export const backgroundImage = function (src, animateOptions = {}) { export const backgroundImage = function (src, animateOptions = {}) {
const container = document.getElementById('code'); const container = document.getElementById('code');
const bg = 'background-image:url(' + src + ');background-size:contain;'; const bg = 'background-image:url(' + src + ');background-size:contain;';
@@ -22,6 +35,11 @@ export const backgroundImage = function (src, animateOptions = {}) {
if (funcOptions.length === 0) { if (funcOptions.length === 0) {
return; return;
} }
frame((_, t) =>
funcOptions.forEach(([option, value]) => {
handleOption(option, value(t));
}),
);
}; };
export const cleanupUi = () => { export const cleanupUi = () => {
+1 -65
View File
@@ -61,11 +61,6 @@ export const valueToMidi = (value, fallbackValue) => {
return fallbackValue; return fallbackValue;
}; };
// used to schedule external event like midi and osc out
export const getEventOffsetMs = (targetTimeSeconds, currentTimeSeconds) => {
return (targetTimeSeconds - currentTimeSeconds) * 1000;
};
/** /**
* @deprecated does not appear to be referenced or invoked anywhere in the codebase * @deprecated does not appear to be referenced or invoked anywhere in the codebase
* @noAutocomplete * @noAutocomplete
@@ -91,7 +86,7 @@ export const midi2note = (n) => {
// modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does) // modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does)
export const _mod = (n, m) => ((n % m) + m) % m; export const _mod = (n, m) => ((n % m) + m) % m;
export function nanFallback(value, fallback = 0) { export function nanFallback(value, fallback) {
if (isNaN(Number(value))) { if (isNaN(Number(value))) {
logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning'); logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
return fallback; return fallback;
@@ -236,14 +231,6 @@ export const splitAt = function (index, value) {
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i])); export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
export const pairs = function (xs) {
const result = [];
for (let i = 0; i < xs.length - 1; ++i) {
result.push([xs[i], xs[i + 1]]);
}
return result;
};
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max); export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
/* solmization, not used yet */ /* solmization, not used yet */
@@ -302,30 +289,6 @@ export const sol2note = (n, notation = 'letters') => {
return note + oct; return note + oct;
}; };
// Remove duplicates from list
export function uniq(a) {
var seen = {};
return a.filter(function (item) {
return seen.hasOwn(item) ? false : (seen[item] = true);
});
}
// Remove duplicates from list, sorting in the process. Mutates argument!
export function uniqsort(a) {
return a.sort().filter(function (item, pos, ary) {
return !pos || item != ary[pos - 1];
});
}
// rational version
export function uniqsortr(a) {
return a
.sort((x, y) => x.compare(y))
.filter(function (item, pos, ary) {
return !pos || item.ne(ary[pos - 1]);
});
}
// code hashing helpers // code hashing helpers
export function unicodeToBase64(text) { export function unicodeToBase64(text) {
@@ -353,30 +316,3 @@ export function hash2code(hash) {
return base64ToUnicode(decodeURIComponent(hash)); return base64ToUnicode(decodeURIComponent(hash));
//return atob(decodeURIComponent(codeParam || '')); //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)]));
}
// Floating point versions, see Fraction for rational versions
// // greatest common divisor
// export const gcd = function (x, y, ...z) {
// if (!y && z.length > 0) {
// return gcd(x, ...z);
// }
// if (!y) {
// return x;
// }
// return gcd(y, x % y, ...z);
// };
// // lowest common multiple
// export const lcm = function (x, y, ...z) {
// if (z.length == 0) {
// return (x * y) / gcd(x, y);
// }
// return lcm((x * y) / gcd(x, y), ...z);
// };
+6 -5
View File
@@ -5,13 +5,14 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import { curry } from './util.mjs'; import { curry } from './util.mjs';
import { logger } from './logger.mjs';
export function unionWithObj(a, b, func) { export function unionWithObj(a, b, func) {
if (b?.value !== undefined && Object.keys(b).length === 1) { if (typeof b?.value === 'number') {
// https://github.com/tidalcycles/strudel/issues/1026 // https://github.com/tidalcycles/strudel/issues/262
logger(`[warn]: Can't do arithmetic on control pattern.`); const numKeys = Object.keys(a).filter((k) => typeof a[k] === 'number');
return a; const numerals = Object.fromEntries(numKeys.map((k) => [k, b.value]));
b = Object.assign(b, numerals);
delete b.value;
} }
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k)); const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])]))); return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])])));
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
formats: ['es'], formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs' })[ext], fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
}, },
rollupOptions: { rollupOptions: {
external: [...Object.keys(dependencies)], external: [...Object.keys(dependencies)],
+4 -9
View File
@@ -7,9 +7,6 @@ function createClock(
duration = 0.05, // duration of each cycle duration = 0.05, // duration of each cycle
interval = 0.1, // interval between callbacks interval = 0.1, // interval between callbacks
overlap = 0.1, // overlap between callbacks overlap = 0.1, // overlap between callbacks
setInterval = globalThis.setInterval,
clearInterval = globalThis.clearInterval,
round = true,
) { ) {
let tick = 0; // counts callbacks let tick = 0; // counts callbacks
let phase = 0; // next callback time let phase = 0; // next callback time
@@ -25,8 +22,9 @@ function createClock(
} }
// callback as long as we're inside the lookahead // callback as long as we're inside the lookahead
while (phase < lookahead) { while (phase < lookahead) {
phase = round ? Math.round(phase * precision) / precision : phase; phase = Math.round(phase * precision) / precision;
callback(phase, duration, tick, t); // callback has to skip / handle phase < t! phase >= t && callback(phase, duration, tick);
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
phase += duration; // increment phase by duration phase += duration; // increment phase by duration
tick++; tick++;
} }
@@ -37,10 +35,7 @@ function createClock(
onTick(); onTick();
intervalID = setInterval(onTick, interval * 1000); intervalID = setInterval(onTick, interval * 1000);
}; };
const clear = () => { const clear = () => intervalID !== undefined && clearInterval(intervalID);
intervalID !== undefined && clearInterval(intervalID);
intervalID = undefined;
};
const pause = () => clear(); const pause = () => clear();
const stop = () => { const stop = () => {
tick = 0; tick = 0;
-1
View File
@@ -1 +0,0 @@
# @strudel/csound
+5 -9
View File
@@ -1,5 +1,5 @@
import { getFrequency, logger, register } from '@strudel/core'; import { getFrequency, logger, register } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel/webaudio'; import { getAudioContext } from '@strudel.cycles/webaudio';
import csd from './project.csd?raw'; import csd from './project.csd?raw';
// import livecodeOrc from './livecode.orc?raw'; // import livecodeOrc from './livecode.orc?raw';
import presetsOrc from './presets.orc?raw'; import presetsOrc from './presets.orc?raw';
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
instrument = instrument || 'triangle'; instrument = instrument || 'triangle';
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
// TODO: find a alternative way to wait for csound to load (to wait with first time playback) // TODO: find a alternative way to wait for csound to load (to wait with first time playback)
return pat.onTrigger((time_deprecate, hap, currentTime, _cps, targetTime) => { return pat.onTrigger((time, hap) => {
if (!_csound) { if (!_csound) {
logger('[csound] not loaded yet', 'warning'); logger('[csound] not loaded yet', 'warning');
return; return;
@@ -38,11 +38,9 @@ export const csound = register('csound', (instrument, pat) => {
.join('/'); .join('/');
// TODO: find out how to send a precise ctx based time // TODO: find out how to send a precise ctx based time
// http://www.csounds.com/manual/html/i.html // http://www.csounds.com/manual/html/i.html
const timeOffset = targetTime - currentTime; // latency ?
//const timeOffset = time_deprecate - getAudioContext().currentTime
const params = [ const params = [
`"${instrument}"`, // p1: instrument name `"${instrument}"`, // p1: instrument name
timeOffset, // p2: starting time in arbitrary unit called beats time - getAudioContext().currentTime, //.toFixed(precision), // p2: starting time in arbitrary unit called beats
hap.duration + 0, // p3: duration in beats hap.duration + 0, // p3: duration in beats
// instrument specific params: // instrument specific params:
freq, //.toFixed(precision), // p4: frequency freq, //.toFixed(precision), // p4: frequency
@@ -154,14 +152,12 @@ export const csoundm = register('csoundm', (instrument, pat) => {
const p2 = tidal_time - getAudioContext().currentTime; const p2 = tidal_time - getAudioContext().currentTime;
const p3 = hap.duration.valueOf() + 0; const p3 = hap.duration.valueOf() + 0;
const frequency = getFrequency(hap); const frequency = getFrequency(hap);
let { gain = 1, velocity = 0.9 } = hap.value;
velocity = gain * velocity;
// Translate frequency to MIDI key number _without_ rounding. // Translate frequency to MIDI key number _without_ rounding.
const C4 = 261.62558; const C4 = 261.62558;
let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0; let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0;
const p4 = octave * 12.0 - 36.0; const p4 = octave * 12.0 - 36.0;
// We prefer floating point precision, but over the MIDI range [0, 127]. // We prefer floating point precision, but over the MIDI range [0, 127].
const p5 = 127 * velocity; const p5 = 127 * (hap.context?.velocity ?? 0.9);
// The Strudel controls as a string. // The Strudel controls as a string.
const p6 = Object.entries({ ...hap.value, frequency }) const p6 = Object.entries({ ...hap.value, frequency })
.flat() .flat()
+6 -6
View File
@@ -1,11 +1,11 @@
{ {
"name": "@strudel/csound", "name": "@strudel.cycles/csound",
"version": "1.0.1", "version": "0.9.0",
"description": "csound bindings for strudel", "description": "csound bindings for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module",
"publishConfig": { "publishConfig": {
"main": "dist/index.mjs" "main": "dist/index.js",
"module": "dist/index.mjs"
}, },
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
@@ -33,8 +33,8 @@
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@csound/browser": "6.18.7", "@csound/browser": "6.18.7",
"@strudel/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
"@strudel/webaudio": "workspace:*" "@strudel.cycles/webaudio": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"vite": "^5.0.10" "vite": "^5.0.10"
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
formats: ['es'], formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs' })[ext], fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
}, },
rollupOptions: { rollupOptions: {
external: [...Object.keys(dependencies)], external: [...Object.keys(dependencies)],
+6 -8
View File
@@ -1,18 +1,16 @@
import { Invoke } from './utils.mjs'; import { Invoke } from './utils.mjs';
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core'; import { Pattern, noteToMidi } from '@strudel.cycles/core';
const ON_MESSAGE = 0x90; const ON_MESSAGE = 0x90;
const OFF_MESSAGE = 0x80; const OFF_MESSAGE = 0x80;
const CC_MESSAGE = 0xb0; const CC_MESSAGE = 0xb0;
Pattern.prototype.midi = function (output) { Pattern.prototype.midi = function (output) {
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { return this.onTrigger((time, hap, currentTime) => {
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value; const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
//magic number to get audio engine to line up, can probably be calculated somehow const offset = (time - currentTime) * 1000;
const latencyMs = 34; const velocity = Math.floor((hap.context?.velocity ?? 0.9) * 100); // TODO: refactor velocity
const offset = getEventOffsetMs(targetTime, currentTime) + latencyMs; const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
velocity = Math.floor(gain * velocity * 100);
const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10);
const roundedOffset = Math.round(offset); const roundedOffset = Math.round(offset);
const midichan = (hap.value.midichan ?? 1) - 1; const midichan = (hap.value.midichan ?? 1) - 1;
const requestedport = output ?? 'IAC'; const requestedport = output ?? 'IAC';
+3 -3
View File
@@ -1,8 +1,8 @@
import { parseNumeral, Pattern, getEventOffsetMs } from '@strudel/core'; import { parseNumeral, Pattern } from '@strudel.cycles/core';
import { Invoke } from './utils.mjs'; import { Invoke } from './utils.mjs';
Pattern.prototype.osc = function () { Pattern.prototype.osc = function () {
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => { return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
hap.ensureObjectValue(); hap.ensureObjectValue();
const cycle = hap.wholeOrPart().begin.valueOf(); const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf(); const delta = hap.duration.valueOf();
@@ -13,7 +13,7 @@ Pattern.prototype.osc = function () {
const params = []; const params = [];
const timestamp = Math.round(Date.now() + getEventOffsetMs(targetTime, currentTime)); const timestamp = Math.round(Date.now() + (time - currentTime) * 1000);
Object.keys(controls).forEach((key) => { Object.keys(controls).forEach((key) => {
const val = controls[key]; const val = controls[key];
+1 -1
View File
@@ -22,7 +22,7 @@
"url": "https://github.com/tidalcycles/strudel/issues" "url": "https://github.com/tidalcycles/strudel/issues"
}, },
"dependencies": { "dependencies": {
"@strudel/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
"@tauri-apps/api": "^1.5.3" "@tauri-apps/api": "^1.5.3"
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme" "homepage": "https://github.com/tidalcycles/strudel#readme"
-9
View File
@@ -1,9 +0,0 @@
# @strudel/canvas
Helpers for drawing with the Canvas API and Strudel
## Install
```sh
npm i @strudel/canvas --save
```
-6
View File
@@ -1,6 +0,0 @@
export * from './animate.mjs';
export * from './color.mjs';
export * from './draw.mjs';
export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './pitchwheel.mjs';
-37
View File
@@ -1,37 +0,0 @@
{
"name": "@strudel/draw",
"version": "1.0.1",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"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:*"
},
"devDependencies": {
"vite": "^5.0.10"
}
}
-127
View File
@@ -1,127 +0,0 @@
import { Pattern, midiToFreq, getFrequency } from '@strudel/core';
import { getTheme, getDrawContext } from './draw.mjs';
const c = midiToFreq(36);
const circlePos = (cx, cy, radius, angle) => {
angle = angle * Math.PI * 2;
const x = Math.sin(angle) * radius + cx;
const y = Math.cos(angle) * radius + cy;
return [x, y];
};
const freq2angle = (freq, root) => {
return 0.5 - (Math.log2(freq / root) % 1);
};
export function pitchwheel({
haps,
ctx,
id,
hapcircles = 1,
circle = 0,
edo = 12,
root = c,
thickness = 3,
hapRadius = 6,
mode = 'flake',
margin = 10,
} = {}) {
const connectdots = mode === 'polygon';
const centerlines = mode === 'flake';
const w = ctx.canvas.width;
const h = ctx.canvas.height;
ctx.clearRect(0, 0, w, h);
const color = getTheme().foreground;
const size = Math.min(w, h);
const radius = size / 2 - thickness / 2 - hapRadius - margin;
const centerX = w / 2;
const centerY = h / 2;
if (id) {
haps = haps.filter((hap) => hap.hasTag(id));
}
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.globalAlpha = 1;
ctx.lineWidth = thickness;
if (circle) {
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.stroke();
}
if (edo) {
Array.from({ length: edo }, (_, i) => {
const angle = freq2angle(root * Math.pow(2, i / edo), root);
const [x, y] = circlePos(centerX, centerY, radius, angle);
ctx.beginPath();
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
ctx.fill();
});
ctx.stroke();
}
let shape = [];
ctx.lineWidth = hapRadius;
haps.forEach((hap) => {
let freq;
try {
freq = getFrequency(hap);
} catch (err) {
return;
}
const angle = freq2angle(freq, root);
const [x, y] = circlePos(centerX, centerY, radius, angle);
const hapColor = hap.value.color || color;
ctx.strokeStyle = hapColor;
ctx.fillStyle = hapColor;
const { velocity = 1, gain = 1 } = hap.value || {};
const alpha = velocity * gain;
ctx.globalAlpha = alpha;
shape.push([x, y, angle, hapColor, alpha]);
ctx.beginPath();
if (hapcircles) {
ctx.moveTo(x + hapRadius, y);
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
ctx.fill();
}
if (centerlines) {
ctx.moveTo(centerX, centerY);
ctx.lineTo(x, y);
}
ctx.stroke();
});
ctx.strokeStyle = color;
ctx.globalAlpha = 1;
if (connectdots && shape.length) {
shape = shape.sort((a, b) => a[2] - b[2]);
ctx.beginPath();
ctx.moveTo(shape[0][0], shape[0][1]);
shape.forEach(([x, y, _, color, alpha]) => {
ctx.strokeStyle = color;
ctx.globalAlpha = alpha;
ctx.lineTo(x, y);
});
ctx.lineTo(shape[0][0], shape[0][1]);
ctx.stroke();
}
return;
}
Pattern.prototype.pitchwheel = function (options = {}) {
let { ctx = getDrawContext(), id = 1 } = options;
return this.tag(id).onPaint((_, time, haps) =>
pitchwheel({
...options,
time,
ctx,
haps: haps.filter((hap) => hap.isActive(time)),
id,
}),
);
};
-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, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+22 -53
View File
@@ -1,64 +1,33 @@
# @strudel/embed # @strudel.cycles/embed
This package contains a embeddable web component for the Strudel REPL. This package contains a embeddable web component for the Strudel REPL.
## Usage via Script Tag ## Usage
Use this code in any HTML file: Either install with `npm i @strudel.cycles/embed` or just use a cdn to import the script:
```html ```html
<script src="https://unpkg.com/@strudel/embed@latest"></script> <script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
<strudel-repl> <strudel-repl>
<!-- <!--
setcps(1) note(`[[e5 [b4 c5] d5 [c5 b4]]
n("<0 1 2 3 4>*8").scale('G4 minor') [a4 [a4 c5] e5 [d5 c5]]
.s("gm_lead_6_voice") [b4 [~ c5] d5 e5]
.clip(sine.range(.2,.8).slow(8)) [c5 a4 a4 ~]
.jux(rev) [[~ d5] [~ f5] a5 [g5 f5]]
.room(2) [e5 [~ c5] e5 [d5 c5]]
.sometimes(add(note("12"))) [b4 [b4 c5] d5 e5]
.lpf(perlin.range(200,20000).slow(4)) [c5 a4 a4 ~]],
--> [[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]`).slow(16)
-->
</strudel-repl> </strudel-repl>
``` ```
This will load the strudel website in an iframe, using the code provided within the HTML comments `<!-- -->`. Note that the Code is placed inside HTML comments to prevent the browser from treating it as HTML.
The HTML comments are needed to make sure the browser won't interpret it as HTML.
Alternatively you can create a REPL from JavaScript like this:
```html
<script src="https://unpkg.com/@strudel/embed@1.0.2"></script>
<div id="strudel"></div>
<script>
let editor = document.createElement('strudel-repl');
editor.setAttribute(
'code',
`setcps(1)
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))`,
);
document.getElementById('strudel').append(editor);
</script>
```
When you're using JSX, you could also use the `code` attribute in your markup:
```html
<script src="https://unpkg.com/@strudel/embed@1.0.2"></script>
<strudel-repl code={`
setcps(1)
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))
`}></strudel-repl>
```
+1 -1
View File
@@ -4,7 +4,7 @@ class Strudel extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
setTimeout(() => { setTimeout(() => {
const code = this.getAttribute('code') || (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim(); const code = (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
const src = `https://strudel.cc/#${encodeURIComponent(btoa(code))}`; const src = `https://strudel.cc/#${encodeURIComponent(btoa(code))}`;
// const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`; // const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`;
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/embed", "name": "@strudel.cycles/embed",
"version": "1.0.0", "version": "0.2.0",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe", "description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js", "main": "embed.js",
"type": "module", "type": "module",
+1 -1
View File
@@ -27,7 +27,7 @@ npm i @strudel/hydra
Then add the import to your evalScope: Then add the import to your evalScope:
```js ```js
import { evalScope } from '@strudel/core'; import { evalScope } from '@strudel.cycles/core';
evalScope( evalScope(
import('@strudel/hydra') import('@strudel/hydra')
+15 -24
View File
@@ -1,49 +1,40 @@
import { getDrawContext } from '@strudel/draw'; import { getDrawContext } from '@strudel.cycles/core';
import { controls } from '@strudel/core';
let latestOptions; let latestOptions;
let hydra;
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 = {}) { export async function initHydra(options = {}) {
// reset if options have changed since last init // reset if options have changed since last init
if (latestOptions && JSON.stringify(latestOptions) !== JSON.stringify(options)) { if (latestOptions && JSON.stringify(latestOptions) !== JSON.stringify(options)) {
document.getElementById('hydra-canvas')?.remove(); document.getElementById('hydra-canvas').remove();
} }
latestOptions = options; latestOptions = options;
//load and init hydra //load and init hydra
if (!document.getElementById('hydra-canvas')) { if (!document.getElementById('hydra-canvas')) {
console.log('reinit..');
const { const {
src = 'https://unpkg.com/hydra-synth', src = 'https://unpkg.com/hydra-synth',
feedStrudel = false, feedStrudel = false,
contextType = 'webgl',
pixelRatio = 1,
pixelated = true,
...hydraConfig ...hydraConfig
} = { } = { detectAudio: false, ...options };
detectAudio: false,
...options,
};
const { canvas } = getDrawContext('hydra-canvas', { contextType, pixelRatio, pixelated });
hydraConfig.canvas = canvas;
await import(/* @vite-ignore */ src); await import(/* @vite-ignore */ src);
hydra = new Hydra(hydraConfig); const hydra = new Hydra(hydraConfig);
if (feedStrudel) { if (feedStrudel) {
const { canvas } = getDrawContext(); const { canvas } = getDrawContext();
canvas.style.display = 'none'; canvas.style.display = 'none';
hydra.synth.s0.init({ src: canvas }); hydra.synth.s0.init({ src: canvas });
} }
appendCanvas(hydra);
} }
} }
export function clearHydra() {
if (hydra) {
hydra.hush();
}
globalThis.s0?.clear();
document.getElementById('hydra-canvas')?.remove();
globalThis.speed = controls.speed;
globalThis.shape = controls.shape;
}
export const H = (p) => () => p.queryArc(getTime(), getTime())[0].value; export const H = (p) => () => p.queryArc(getTime(), getTime())[0].value;
+4 -5
View File
@@ -1,11 +1,11 @@
{ {
"name": "@strudel/hydra", "name": "@strudel/hydra",
"version": "1.0.1", "version": "0.9.0",
"description": "Hydra integration for strudel", "description": "Hydra integration for strudel",
"main": "hydra.mjs", "main": "hydra.mjs",
"type": "module",
"publishConfig": { "publishConfig": {
"main": "dist/index.mjs" "main": "dist/index.js",
"module": "dist/index.mjs"
}, },
"scripts": { "scripts": {
"server": "node server.js", "server": "node server.js",
@@ -33,8 +33,7 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@strudel/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
"@strudel/draw": "workspace:*",
"hydra-synth": "^1.3.29" "hydra-synth": "^1.3.29"
}, },
"devDependencies": { "devDependencies": {
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'hydra.mjs'), entry: resolve(__dirname, 'hydra.mjs'),
formats: ['es'], formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs' })[ext], fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
}, },
rollupOptions: { rollupOptions: {
external: [...Object.keys(dependencies)], external: [...Object.keys(dependencies)],
+2 -2
View File
@@ -1,9 +1,9 @@
# @strudel/midi # @strudel.cycles/midi
This package adds midi functionality to strudel Patterns. This package adds midi functionality to strudel Patterns.
## Install ## Install
```sh ```sh
npm i @strudel/midi --save npm i @strudel.cycles/midi --save
``` ```
+12 -18
View File
@@ -5,8 +5,8 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import * as _WebMidi from 'webmidi'; import * as _WebMidi from 'webmidi';
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core'; import { Pattern, isPattern, logger, ref } from '@strudel.cycles/core';
import { noteToMidi } from '@strudel/core'; import { noteToMidi } from '@strudel.cycles/core';
import { Note } from 'webmidi'; import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance: // if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi; export const { WebMidi } = _WebMidi;
@@ -112,24 +112,24 @@ Pattern.prototype.midi = function (output) {
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`), logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
}); });
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { return this.onTrigger((time, hap, currentTime, cps) => {
if (!WebMidi.enabled) { if (!WebMidi.enabled) {
console.log('not enabled'); console.log('not enabled');
return; return;
} }
const device = getDevice(output, WebMidi.outputs); const device = getDevice(output, WebMidi.outputs);
hap.ensureObjectValue(); hap.ensureObjectValue();
//magic number to get audio engine to line up, can probably be calculated somehow
const latencyMs = 34;
// 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 = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
// destructure value
let { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd, gain = 1, velocity = 0.9 } = hap.value;
velocity = gain * velocity; 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}`;
// destructure value
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd } = hap.value;
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length // note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
const duration = (hap.duration.valueOf() / cps) * 1000 - 10; const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
if (note != null) { if (note != null) {
const midiNumber = typeof note === 'number' ? note : noteToMidi(note); const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
const midiNote = new Note(midiNumber, { attack: velocity, duration }); const midiNote = new Note(midiNumber, { attack: velocity, duration });
@@ -167,15 +167,9 @@ let listeners = {};
const refs = {}; const refs = {};
export async function midin(input) { export async function midin(input) {
if (isPattern(input)) {
throw new Error(
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
}')`,
);
}
const initial = await enableWebMidi(); // only returns on first init const initial = await enableWebMidi(); // only returns on first init
const device = getDevice(input, WebMidi.inputs); const device = getDevice(input, WebMidi.inputs);
if (initial) { if (initial) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name); const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger( logger(
+6 -6
View File
@@ -1,11 +1,11 @@
{ {
"name": "@strudel/midi", "name": "@strudel.cycles/midi",
"version": "1.0.1", "version": "0.9.0",
"description": "Midi API for strudel", "description": "Midi API for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module",
"publishConfig": { "publishConfig": {
"main": "dist/index.mjs" "main": "dist/index.js",
"module": "dist/index.mjs"
}, },
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
@@ -29,8 +29,8 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@strudel/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
"@strudel/webaudio": "workspace:*", "@strudel.cycles/webaudio": "workspace:*",
"webmidi": "^3.1.8" "webmidi": "^3.1.8"
}, },
"devDependencies": { "devDependencies": {
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
formats: ['es'], formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs' })[ext], fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
}, },
rollupOptions: { rollupOptions: {
external: [...Object.keys(dependencies)], external: [...Object.keys(dependencies)],
+4 -4
View File
@@ -1,17 +1,17 @@
# @strudel/mini # @strudel.cycles/mini
This package contains the mini notation parser and pattern generator. This package contains the mini notation parser and pattern generator.
## Install ## Install
```sh ```sh
npm i @strudel/mini --save npm i @strudel.cycles/mini --save
``` ```
## Example ## Example
```js ```js
import { mini } from '@strudel/mini'; import { mini } from '@strudel.cycles/mini';
const pattern = mini('a [b c*2]'); const pattern = mini('a [b c*2]');
@@ -28,7 +28,7 @@ yields:
(7/8 -> 1/1, 7/8 -> 1/1, c) (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 ## Mini Notation API
+109 -226
View File
@@ -288,58 +288,48 @@ function peg$parse(input, options) {
var peg$f0 = function() { return parseFloat(text()); }; var peg$f0 = function() { return parseFloat(text()); };
var peg$f1 = function() { return parseInt(text()); }; var peg$f1 = function() { return parseInt(text()); };
var peg$f2 = function(chars) { const s = chars.join(""); return (s === ".") || (s === "_") }; var peg$f2 = function(chars) { return new AtomStub(chars.join("")) };
var peg$f3 = function(chars) { return new AtomStub(chars.join("")) }; var peg$f3 = function(s) { return s };
var peg$f4 = function(s) { return s }; var peg$f4 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; };
var peg$f5 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; }; var peg$f5 = function(a) { return a };
var peg$f6 = function(a) { return a }; var peg$f6 = function(s) { s.arguments_.alignment = 'slowcat'; return s; };
var peg$f7 = function(s) { s.arguments_.alignment = 'polymeter_slowcat'; return s; }; var peg$f7 = function(a) { return x => x.options_['weight'] = a };
var peg$f8 = function(a) { return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 }; var peg$f8 = function(a) { return x => x.options_['reps'] = a };
var peg$f9 = function(a) { return x => {const reps = (x.options_['reps'] ?? 1) + (a ?? 2) - 1; var peg$f9 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
x.options_['reps'] = reps; var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
console.log("reps: ", reps) var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
x.options_['ops'] = x.options_['ops'].filter(x => x.type_ !== "replicate"); var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
x.options_['ops'].push({ type_: "replicate", arguments_ :{ amount:reps }}); var peg$f13 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
x.options_['weight'] = reps; var peg$f14 = function(s) { return x => x.options_['ops'].push({ type_: "range", arguments_ :{ element:s } }) };
console.log("options: ", x.options_); var peg$f15 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
}
};
var peg$f10 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
var peg$f13 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
var peg$f14 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
var peg$f15 = function(s) { return x => x.options_['ops'].push({ type_: "range", arguments_ :{ element:s } }) };
var peg$f16 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
for (const op of ops) { for (const op of ops) {
op(result); op(result);
} }
return result; return result;
}; };
var peg$f17 = function(tactus, s) { return new PatternStub(s, 'fastcat', undefined, !!tactus); }; var peg$f16 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f18 = function(tail) { return { alignment: 'stack', list: tail }; }; var peg$f17 = function(tail) { return { alignment: 'stack', list: tail }; };
var peg$f19 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; }; var peg$f18 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
var peg$f20 = function(tail) { return { alignment: 'feet', list: tail, seed: seed++ }; }; var peg$f19 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
var peg$f21 = function(head, tail) {if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }; var peg$f20 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
var peg$f22 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }; var peg$f21 = function(sc) { return sc; };
var peg$f23 = function(sc) { return sc; }; var peg$f22 = function(s) { return { name: "struct", args: { mini:s }}};
var peg$f24 = function(s) { return { name: "struct", args: { mini:s }}}; var peg$f23 = function(s) { return { name: "target", args : { name:s}}};
var peg$f25 = function(s) { return { name: "target", args : { name:s}}}; var peg$f24 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
var peg$f26 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}}; var peg$f25 = function(a) { return { name: "stretch", args :{ amount: a}}};
var peg$f27 = function(a) { return { name: "stretch", args :{ amount: a}}}; var peg$f26 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
var peg$f28 = function(a) { return { name: "shift", args :{ amount: "-"+a}}}; var peg$f27 = function(a) { return { name: "shift", args :{ amount: a}}};
var peg$f29 = function(a) { return { name: "shift", args :{ amount: a}}}; var peg$f28 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
var peg$f30 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}}; var peg$f29 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
var peg$f31 = function(s) { return { name: "scale", args :{ scale: s.join("")}}}; var peg$f30 = function(s, v) { return v};
var peg$f32 = function(s, v) { return v}; var peg$f31 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
var peg$f33 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); }; var peg$f32 = function(sg) {return sg};
var peg$f34 = function(sg) {return sg}; var peg$f33 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
var peg$f35 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)}; var peg$f34 = function(sc) { return sc };
var peg$f36 = function(sc) { return sc }; var peg$f35 = function(c) { return c };
var peg$f37 = function(c) { return c }; var peg$f36 = function(v) { return new CommandStub("setcps", { value: v})};
var peg$f38 = function(v) { return new CommandStub("setcps", { value: v})}; var peg$f37 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
var peg$f39 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})}; var peg$f38 = function() { return new CommandStub("hush")};
var peg$f40 = function() { return new CommandStub("hush")};
var peg$currPos = 0; var peg$currPos = 0;
var peg$savedPos = 0; var peg$savedPos = 0;
var peg$posDetailsCache = [{ line: 1, column: 1 }]; var peg$posDetailsCache = [{ line: 1, column: 1 }];
@@ -831,30 +821,6 @@ function peg$parse(input, options) {
return s0; return s0;
} }
function peg$parsedot() {
var s0, s1, s2, s3;
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 46) {
s2 = peg$c0;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e1); }
}
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
s1 = [s1, s2, s3];
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parsequote() { function peg$parsequote() {
var s0; var s0;
@@ -947,7 +913,7 @@ function peg$parse(input, options) {
} }
function peg$parsestep() { function peg$parsestep() {
var s0, s1, s2, s3, s4; var s0, s1, s2, s3;
s0 = peg$currPos; s0 = peg$currPos;
s1 = peg$parsews(); s1 = peg$parsews();
@@ -963,20 +929,8 @@ function peg$parse(input, options) {
} }
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
s3 = peg$parsews(); s3 = peg$parsews();
peg$savedPos = peg$currPos; peg$savedPos = s0;
s4 = peg$f2(s2); s0 = peg$f2(s2);
if (s4) {
s4 = peg$FAILED;
} else {
s4 = undefined;
}
if (s4 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f3(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1012,7 +966,7 @@ function peg$parse(input, options) {
if (s6 !== peg$FAILED) { if (s6 !== peg$FAILED) {
s7 = peg$parsews(); s7 = peg$parsews();
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f4(s4); s0 = peg$f3(s4);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1060,7 +1014,7 @@ function peg$parse(input, options) {
} }
s8 = peg$parsews(); s8 = peg$parsews();
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f5(s4, s7); s0 = peg$f4(s4, s7);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1092,7 +1046,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice(); s2 = peg$parseslice();
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f6(s2); s0 = peg$f5(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1119,7 +1073,7 @@ function peg$parse(input, options) {
} }
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
s3 = peg$parsews(); s3 = peg$parsews();
s4 = peg$parsepolymeter_stack(); s4 = peg$parsesequence();
if (s4 !== peg$FAILED) { if (s4 !== peg$FAILED) {
s5 = peg$parsews(); s5 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 62) { if (input.charCodeAt(peg$currPos) === 62) {
@@ -1132,7 +1086,7 @@ function peg$parse(input, options) {
if (s6 !== peg$FAILED) { if (s6 !== peg$FAILED) {
s7 = peg$parsews(); s7 = peg$parsews();
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f7(s4); s0 = peg$f6(s4);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1196,33 +1150,25 @@ function peg$parse(input, options) {
} }
function peg$parseop_weight() { function peg$parseop_weight() {
var s0, s1, s2, s3; var s0, s1, s2;
s0 = peg$currPos; s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 64) { if (input.charCodeAt(peg$currPos) === 64) {
s2 = peg$c18; s1 = peg$c18;
peg$currPos++; peg$currPos++;
} else { } else {
s2 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e26); } if (peg$silentFails === 0) { peg$fail(peg$e26); }
} }
if (s2 === peg$FAILED) { if (s1 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 95) { s2 = peg$parsenumber();
s2 = peg$c10; if (s2 !== peg$FAILED) {
peg$currPos++; peg$savedPos = s0;
s0 = peg$f7(s2);
} else { } else {
s2 = peg$FAILED; peg$currPos = s0;
if (peg$silentFails === 0) { peg$fail(peg$e18); } s0 = peg$FAILED;
} }
}
if (s2 !== peg$FAILED) {
s3 = peg$parsenumber();
if (s3 === peg$FAILED) {
s3 = null;
}
peg$savedPos = s0;
s0 = peg$f8(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1232,24 +1178,25 @@ function peg$parse(input, options) {
} }
function peg$parseop_replicate() { function peg$parseop_replicate() {
var s0, s1, s2, s3; var s0, s1, s2;
s0 = peg$currPos; s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 33) { if (input.charCodeAt(peg$currPos) === 33) {
s2 = peg$c19; s1 = peg$c19;
peg$currPos++; peg$currPos++;
} else { } else {
s2 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e27); } if (peg$silentFails === 0) { peg$fail(peg$e27); }
} }
if (s2 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s3 = peg$parsenumber(); s2 = peg$parsenumber();
if (s3 === peg$FAILED) { if (s2 !== peg$FAILED) {
s3 = null; peg$savedPos = s0;
s0 = peg$f8(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
} }
peg$savedPos = s0;
s0 = peg$f9(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1299,7 +1246,7 @@ function peg$parse(input, options) {
} }
if (s13 !== peg$FAILED) { if (s13 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f10(s3, s7, s11); s0 = peg$f9(s3, s7, s11);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1339,7 +1286,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice(); s2 = peg$parseslice();
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f11(s2); s0 = peg$f10(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1367,7 +1314,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice(); s2 = peg$parseslice();
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f12(s2); s0 = peg$f11(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1397,7 +1344,7 @@ function peg$parse(input, options) {
s2 = null; s2 = null;
} }
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f13(s2); s0 = peg$f12(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1421,7 +1368,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice(); s2 = peg$parseslice();
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f14(s2); s0 = peg$f13(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1449,7 +1396,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice(); s2 = peg$parseslice();
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f15(s2); s0 = peg$f14(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1475,7 +1422,7 @@ function peg$parse(input, options) {
s3 = peg$parseslice_op(); s3 = peg$parseslice_op();
} }
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f16(s1, s2); s0 = peg$f15(s1, s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1485,36 +1432,24 @@ function peg$parse(input, options) {
} }
function peg$parsesequence() { function peg$parsesequence() {
var s0, s1, s2, s3; var s0, s1, s2;
s0 = peg$currPos; s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 94) { s1 = [];
s1 = peg$c9; s2 = peg$parseslice_with_ops();
peg$currPos++; if (s2 !== peg$FAILED) {
} else { while (s2 !== peg$FAILED) {
s1 = peg$FAILED; s1.push(s2);
if (peg$silentFails === 0) { peg$fail(peg$e17); } s2 = peg$parseslice_with_ops();
}
if (s1 === peg$FAILED) {
s1 = null;
}
s2 = [];
s3 = peg$parseslice_with_ops();
if (s3 !== peg$FAILED) {
while (s3 !== peg$FAILED) {
s2.push(s3);
s3 = peg$parseslice_with_ops();
} }
} else { } else {
s2 = peg$FAILED; s1 = peg$FAILED;
} }
if (s2 !== peg$FAILED) { if (s1 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f17(s1, s2); s1 = peg$f16(s1);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
} }
s0 = s1;
return s0; return s0;
} }
@@ -1561,7 +1496,7 @@ function peg$parse(input, options) {
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s1 = peg$f18(s1); s1 = peg$f17(s1);
} }
s0 = s1; s0 = s1;
@@ -1610,56 +1545,7 @@ function peg$parse(input, options) {
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s1 = peg$f19(s1); s1 = peg$f18(s1);
}
s0 = s1;
return s0;
}
function peg$parsedot_tail() {
var s0, s1, s2, s3, s4;
s0 = peg$currPos;
s1 = [];
s2 = peg$currPos;
s3 = peg$parsedot();
if (s3 !== peg$FAILED) {
s4 = peg$parsesequence();
if (s4 !== peg$FAILED) {
s2 = s4;
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = peg$currPos;
s3 = peg$parsedot();
if (s3 !== peg$FAILED) {
s4 = peg$parsesequence();
if (s4 !== peg$FAILED) {
s2 = s4;
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
}
} else {
s1 = peg$FAILED;
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f20(s1);
} }
s0 = s1; s0 = s1;
@@ -1675,15 +1561,12 @@ function peg$parse(input, options) {
s2 = peg$parsestack_tail(); s2 = peg$parsestack_tail();
if (s2 === peg$FAILED) { if (s2 === peg$FAILED) {
s2 = peg$parsechoose_tail(); s2 = peg$parsechoose_tail();
if (s2 === peg$FAILED) {
s2 = peg$parsedot_tail();
}
} }
if (s2 === peg$FAILED) { if (s2 === peg$FAILED) {
s2 = null; s2 = null;
} }
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f21(s1, s2); s0 = peg$f19(s1, s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1703,7 +1586,7 @@ function peg$parse(input, options) {
s2 = null; s2 = null;
} }
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f22(s1, s2); s0 = peg$f20(s1, s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1726,7 +1609,7 @@ function peg$parse(input, options) {
s6 = peg$parsequote(); s6 = peg$parsequote();
if (s6 !== peg$FAILED) { if (s6 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f23(s4); s0 = peg$f21(s4);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1788,7 +1671,7 @@ function peg$parse(input, options) {
s3 = peg$parsemini_or_operator(); s3 = peg$parsemini_or_operator();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f24(s3); s0 = peg$f22(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1821,7 +1704,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote(); s5 = peg$parsequote();
if (s5 !== peg$FAILED) { if (s5 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f25(s4); s0 = peg$f23(s4);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1866,7 +1749,7 @@ function peg$parse(input, options) {
s7 = null; s7 = null;
} }
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f26(s3, s5, s7); s0 = peg$f24(s3, s5, s7);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1899,7 +1782,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber(); s3 = peg$parsenumber();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f27(s3); s0 = peg$f25(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1928,7 +1811,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber(); s3 = peg$parsenumber();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f28(s3); s0 = peg$f26(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1957,7 +1840,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber(); s3 = peg$parsenumber();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f29(s3); s0 = peg$f27(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -1986,7 +1869,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber(); s3 = peg$parsenumber();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f30(s3); s0 = peg$f28(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -2028,7 +1911,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote(); s5 = peg$parsequote();
if (s5 !== peg$FAILED) { if (s5 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f31(s4); s0 = peg$f29(s4);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -2120,7 +2003,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator(); s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) { if (s9 !== peg$FAILED) {
peg$savedPos = s7; peg$savedPos = s7;
s7 = peg$f32(s5, s9); s7 = peg$f30(s5, s9);
} else { } else {
peg$currPos = s7; peg$currPos = s7;
s7 = peg$FAILED; s7 = peg$FAILED;
@@ -2137,7 +2020,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator(); s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) { if (s9 !== peg$FAILED) {
peg$savedPos = s7; peg$savedPos = s7;
s7 = peg$f32(s5, s9); s7 = peg$f30(s5, s9);
} else { } else {
peg$currPos = s7; peg$currPos = s7;
s7 = peg$FAILED; s7 = peg$FAILED;
@@ -2157,7 +2040,7 @@ function peg$parse(input, options) {
} }
if (s8 !== peg$FAILED) { if (s8 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f33(s5, s6); s0 = peg$f31(s5, s6);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -2203,7 +2086,7 @@ function peg$parse(input, options) {
s4 = peg$parsecomment(); s4 = peg$parsecomment();
} }
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f34(s1); s0 = peg$f32(s1);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -2225,7 +2108,7 @@ function peg$parse(input, options) {
s5 = peg$parsemini_or_operator(); s5 = peg$parsemini_or_operator();
if (s5 !== peg$FAILED) { if (s5 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f35(s1, s5); s0 = peg$f33(s1, s5);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -2250,7 +2133,7 @@ function peg$parse(input, options) {
s1 = peg$parsemini_or_operator(); s1 = peg$parsemini_or_operator();
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s1 = peg$f36(s1); s1 = peg$f34(s1);
} }
s0 = s1; s0 = s1;
if (s0 === peg$FAILED) { if (s0 === peg$FAILED) {
@@ -2283,7 +2166,7 @@ function peg$parse(input, options) {
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
s3 = peg$parsews(); s3 = peg$parsews();
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f37(s2); s0 = peg$f35(s2);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -2308,7 +2191,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber(); s3 = peg$parsenumber();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f38(s3); s0 = peg$f36(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -2337,7 +2220,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber(); s3 = peg$parsenumber();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f39(s3); s0 = peg$f37(s3);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;
@@ -2363,7 +2246,7 @@ function peg$parse(input, options) {
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s1 = peg$f40(); s1 = peg$f38();
} }
s0 = s1; s0 = s1;
@@ -2496,10 +2379,10 @@ function peg$parse(input, options) {
this.location_ = location(); this.location_ = location();
} }
var PatternStub = function(source, alignment, seed, tactus) var PatternStub = function(source, alignment, seed)
{ {
this.type_ = "pattern"; this.type_ = "pattern";
this.arguments_ = { alignment: alignment, tactus: tactus }; this.arguments_ = { alignment: alignment };
if (seed !== undefined) { if (seed !== undefined) {
this.arguments_.seed = seed; this.arguments_.seed = seed;
} }
+14 -27
View File
@@ -19,10 +19,10 @@ This program is free software: you can redistribute it and/or modify it under th
this.location_ = location(); this.location_ = location();
} }
var PatternStub = function(source, alignment, seed, tactus) var PatternStub = function(source, alignment, seed)
{ {
this.type_ = "pattern"; this.type_ = "pattern";
this.arguments_ = { alignment: alignment, tactus: tactus }; this.arguments_ = { alignment: alignment };
if (seed !== undefined) { if (seed !== undefined) {
this.arguments_.seed = seed; this.arguments_.seed = seed;
} }
@@ -98,7 +98,6 @@ DIGIT = [0-9]
ws "whitespace" = [ \n\r\t\u00A0]* ws "whitespace" = [ \n\r\t\u00A0]*
comma = ws "," ws comma = ws "," ws
pipe = ws "|" ws pipe = ws "|" ws
dot = ws "." ws
quote = '"' / "'" quote = '"' / "'"
// ------------------ steps and cycles --------------------------- // ------------------ steps and cycles ---------------------------
@@ -106,8 +105,7 @@ quote = '"' / "'"
// single step definition (e.g bd) // single step definition (e.g bd)
step_char "a letter, a number, \"-\", \"#\", \".\", \"^\", \"_\"" = step_char "a letter, a number, \"-\", \"#\", \".\", \"^\", \"_\"" =
unicode_letter / [0-9~] / "-" / "#" / "." / "^" / "_" unicode_letter / [0-9~] / "-" / "#" / "." / "^" / "_"
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
step = ws chars:step_char+ ws !{ const s = chars.join(""); return (s === ".") || (s === "_") } { return new AtomStub(chars.join("")) }
// define a sub cycle e.g. [1 2, 3 [4]] // define a sub cycle e.g. [1 2, 3 [4]]
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s } sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s }
@@ -121,8 +119,8 @@ polymeter_steps = "%"a:slice
// define a step-per-cycle timeline e.g <1 3 [3 5]>. We simply defer to a sequence and // define a step-per-cycle timeline e.g <1 3 [3 5]>. We simply defer to a sequence and
// change the alignment to slowcat // change the alignment to slowcat
slow_sequence = ws "<" ws s:polymeter_stack ws ">" ws slow_sequence = ws "<" ws s:sequence ws ">" ws
{ s.arguments_.alignment = 'polymeter_slowcat'; return s; } { s.arguments_.alignment = 'slowcat'; return s; }
// a slice is either a single step or a sub cycle // a slice is either a single step or a sub cycle
slice = step / sub_cycle / polymeter / slow_sequence slice = step / sub_cycle / polymeter / slow_sequence
@@ -131,18 +129,11 @@ slice = step / sub_cycle / polymeter / slow_sequence
// at this point, we assume we can represent them as regular sequence operators // at this point, we assume we can represent them as regular sequence operators
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail / op_range slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail / op_range
op_weight = ws ("@" / "_") a:number? op_weight = "@" a:number
{ return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 } { return x => x.options_['weight'] = a }
op_replicate = ws "!" a:number? op_replicate = "!"a:number
{ return x => {// A bit fiddly, to support both x!4 and x!!! as equivalent.. { return x => x.options_['reps'] = a }
const reps = (x.options_['reps'] ?? 1) + (a ?? 2) - 1;
x.options_['reps'] = reps;
x.options_['ops'] = x.options_['ops'].filter(x => x.type_ !== "replicate");
x.options_['ops'].push({ type_: "replicate", arguments_ :{ amount:reps }});
x.options_['weight'] = reps;
}
}
op_bjorklund = "(" ws p:slice_with_ops ws comma ws s:slice_with_ops ws comma? ws r:slice_with_ops? ws ")" op_bjorklund = "(" ws p:slice_with_ops ws comma ws s:slice_with_ops ws comma? ws r:slice_with_ops? ws ")"
{ return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) } { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }
@@ -172,8 +163,8 @@ slice_with_ops = s:slice ops:slice_op*
} }
// a sequence is a combination of one or more successive slices (as an array) // a sequence is a combination of one or more successive slices (as an array)
sequence = tactus:'^'? s:(slice_with_ops)+ sequence = s:(slice_with_ops)+
{ return new PatternStub(s, 'fastcat', undefined, !!tactus); } { return new PatternStub(s, 'fastcat'); }
// a stack is a series of vertically aligned sequence, separated by a comma // a stack is a series of vertically aligned sequence, separated by a comma
stack_tail = tail:(comma @sequence)+ stack_tail = tail:(comma @sequence)+
@@ -184,14 +175,10 @@ stack_tail = tail:(comma @sequence)+
choose_tail = tail:(pipe @sequence)+ choose_tail = tail:(pipe @sequence)+
{ return { alignment: 'rand', list: tail, seed: seed++ }; } { return { alignment: 'rand', list: tail, seed: seed++ }; }
// a foot separates subsequences, as an alternative to wrapping them in []
dot_tail = tail:(dot @sequence)+
{ return { alignment: 'feet', list: tail, seed: seed++ }; }
// if the stack contains only one element, we don't create a stack but return the // if the stack contains only one element, we don't create a stack but return the
// underlying element // underlying element
stack_or_choose = head:sequence tail:(stack_tail / choose_tail / dot_tail)? stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
{if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } } { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
polymeter_stack = head:sequence tail:stack_tail? polymeter_stack = head:sequence tail:stack_tail?
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); } { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
@@ -300,4 +287,4 @@ Lt = [\u01C5\u01C8\u01CB\u01F2\u1F88-\u1F8F\u1F98-\u1F9F\u1FA8-\u1FAF\u1FBC\u1FC
Lu = [\u0041-\u005A\u00C0-\u00D6\u00D8-\u00DE\u0100\u0102\u0104\u0106\u0108\u010A\u010C\u010E\u0110\u0112\u0114\u0116\u0118\u011A\u011C\u011E\u0120\u0122\u0124\u0126\u0128\u012A\u012C\u012E\u0130\u0132\u0134\u0136\u0139\u013B\u013D\u013F\u0141\u0143\u0145\u0147\u014A\u014C\u014E\u0150\u0152\u0154\u0156\u0158\u015A\u015C\u015E\u0160\u0162\u0164\u0166\u0168\u016A\u016C\u016E\u0170\u0172\u0174\u0176\u0178-\u0179\u017B\u017D\u0181-\u0182\u0184\u0186-\u0187\u0189-\u018B\u018E-\u0191\u0193-\u0194\u0196-\u0198\u019C-\u019D\u019F-\u01A0\u01A2\u01A4\u01A6-\u01A7\u01A9\u01AC\u01AE-\u01AF\u01B1-\u01B3\u01B5\u01B7-\u01B8\u01BC\u01C4\u01C7\u01CA\u01CD\u01CF\u01D1\u01D3\u01D5\u01D7\u01D9\u01DB\u01DE\u01E0\u01E2\u01E4\u01E6\u01E8\u01EA\u01EC\u01EE\u01F1\u01F4\u01F6-\u01F8\u01FA\u01FC\u01FE\u0200\u0202\u0204\u0206\u0208\u020A\u020C\u020E\u0210\u0212\u0214\u0216\u0218\u021A\u021C\u021E\u0220\u0222\u0224\u0226\u0228\u022A\u022C\u022E\u0230\u0232\u023A-\u023B\u023D-\u023E\u0241\u0243-\u0246\u0248\u024A\u024C\u024E\u0370\u0372\u0376\u037F\u0386\u0388-\u038A\u038C\u038E-\u038F\u0391-\u03A1\u03A3-\u03AB\u03CF\u03D2-\u03D4\u03D8\u03DA\u03DC\u03DE\u03E0\u03E2\u03E4\u03E6\u03E8\u03EA\u03EC\u03EE\u03F4\u03F7\u03F9-\u03FA\u03FD-\u042F\u0460\u0462\u0464\u0466\u0468\u046A\u046C\u046E\u0470\u0472\u0474\u0476\u0478\u047A\u047C\u047E\u0480\u048A\u048C\u048E\u0490\u0492\u0494\u0496\u0498\u049A\u049C\u049E\u04A0\u04A2\u04A4\u04A6\u04A8\u04AA\u04AC\u04AE\u04B0\u04B2\u04B4\u04B6\u04B8\u04BA\u04BC\u04BE\u04C0-\u04C1\u04C3\u04C5\u04C7\u04C9\u04CB\u04CD\u04D0\u04D2\u04D4\u04D6\u04D8\u04DA\u04DC\u04DE\u04E0\u04E2\u04E4\u04E6\u04E8\u04EA\u04EC\u04EE\u04F0\u04F2\u04F4\u04F6\u04F8\u04FA\u04FC\u04FE\u0500\u0502\u0504\u0506\u0508\u050A\u050C\u050E\u0510\u0512\u0514\u0516\u0518\u051A\u051C\u051E\u0520\u0522\u0524\u0526\u0528\u052A\u052C\u052E\u0531-\u0556\u10A0-\u10C5\u10C7\u10CD\u13A0-\u13F5\u1C90-\u1CBA\u1CBD-\u1CBF\u1E00\u1E02\u1E04\u1E06\u1E08\u1E0A\u1E0C\u1E0E\u1E10\u1E12\u1E14\u1E16\u1E18\u1E1A\u1E1C\u1E1E\u1E20\u1E22\u1E24\u1E26\u1E28\u1E2A\u1E2C\u1E2E\u1E30\u1E32\u1E34\u1E36\u1E38\u1E3A\u1E3C\u1E3E\u1E40\u1E42\u1E44\u1E46\u1E48\u1E4A\u1E4C\u1E4E\u1E50\u1E52\u1E54\u1E56\u1E58\u1E5A\u1E5C\u1E5E\u1E60\u1E62\u1E64\u1E66\u1E68\u1E6A\u1E6C\u1E6E\u1E70\u1E72\u1E74\u1E76\u1E78\u1E7A\u1E7C\u1E7E\u1E80\u1E82\u1E84\u1E86\u1E88\u1E8A\u1E8C\u1E8E\u1E90\u1E92\u1E94\u1E9E\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAA\u1EAC\u1EAE\u1EB0\u1EB2\u1EB4\u1EB6\u1EB8\u1EBA\u1EBC\u1EBE\u1EC0\u1EC2\u1EC4\u1EC6\u1EC8\u1ECA\u1ECC\u1ECE\u1ED0\u1ED2\u1ED4\u1ED6\u1ED8\u1EDA\u1EDC\u1EDE\u1EE0\u1EE2\u1EE4\u1EE6\u1EE8\u1EEA\u1EEC\u1EEE\u1EF0\u1EF2\u1EF4\u1EF6\u1EF8\u1EFA\u1EFC\u1EFE\u1F08-\u1F0F\u1F18-\u1F1D\u1F28-\u1F2F\u1F38-\u1F3F\u1F48-\u1F4D\u1F59\u1F5B\u1F5D\u1F5F\u1F68-\u1F6F\u1FB8-\u1FBB\u1FC8-\u1FCB\u1FD8-\u1FDB\u1FE8-\u1FEC\u1FF8-\u1FFB\u2102\u2107\u210B-\u210D\u2110-\u2112\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u2130-\u2133\u213E-\u213F\u2145\u2183\u2C00-\u2C2E\u2C60\u2C62-\u2C64\u2C67\u2C69\u2C6B\u2C6D-\u2C70\u2C72\u2C75\u2C7E-\u2C80\u2C82\u2C84\u2C86\u2C88\u2C8A\u2C8C\u2C8E\u2C90\u2C92\u2C94\u2C96\u2C98\u2C9A\u2C9C\u2C9E\u2CA0\u2CA2\u2CA4\u2CA6\u2CA8\u2CAA\u2CAC\u2CAE\u2CB0\u2CB2\u2CB4\u2CB6\u2CB8\u2CBA\u2CBC\u2CBE\u2CC0\u2CC2\u2CC4\u2CC6\u2CC8\u2CCA\u2CCC\u2CCE\u2CD0\u2CD2\u2CD4\u2CD6\u2CD8\u2CDA\u2CDC\u2CDE\u2CE0\u2CE2\u2CEB\u2CED\u2CF2\uA640\uA642\uA644\uA646\uA648\uA64A\uA64C\uA64E\uA650\uA652\uA654\uA656\uA658\uA65A\uA65C\uA65E\uA660\uA662\uA664\uA666\uA668\uA66A\uA66C\uA680\uA682\uA684\uA686\uA688\uA68A\uA68C\uA68E\uA690\uA692\uA694\uA696\uA698\uA69A\uA722\uA724\uA726\uA728\uA72A\uA72C\uA72E\uA732\uA734\uA736\uA738\uA73A\uA73C\uA73E\uA740\uA742\uA744\uA746\uA748\uA74A\uA74C\uA74E\uA750\uA752\uA754\uA756\uA758\uA75A\uA75C\uA75E\uA760\uA762\uA764\uA766\uA768\uA76A\uA76C\uA76E\uA779\uA77B\uA77D-\uA77E\uA780\uA782\uA784\uA786\uA78B\uA78D\uA790\uA792\uA796\uA798\uA79A\uA79C\uA79E\uA7A0\uA7A2\uA7A4\uA7A6\uA7A8\uA7AA-\uA7AE\uA7B0-\uA7B4\uA7B6\uA7B8\uFF21-\uFF3A] Lu = [\u0041-\u005A\u00C0-\u00D6\u00D8-\u00DE\u0100\u0102\u0104\u0106\u0108\u010A\u010C\u010E\u0110\u0112\u0114\u0116\u0118\u011A\u011C\u011E\u0120\u0122\u0124\u0126\u0128\u012A\u012C\u012E\u0130\u0132\u0134\u0136\u0139\u013B\u013D\u013F\u0141\u0143\u0145\u0147\u014A\u014C\u014E\u0150\u0152\u0154\u0156\u0158\u015A\u015C\u015E\u0160\u0162\u0164\u0166\u0168\u016A\u016C\u016E\u0170\u0172\u0174\u0176\u0178-\u0179\u017B\u017D\u0181-\u0182\u0184\u0186-\u0187\u0189-\u018B\u018E-\u0191\u0193-\u0194\u0196-\u0198\u019C-\u019D\u019F-\u01A0\u01A2\u01A4\u01A6-\u01A7\u01A9\u01AC\u01AE-\u01AF\u01B1-\u01B3\u01B5\u01B7-\u01B8\u01BC\u01C4\u01C7\u01CA\u01CD\u01CF\u01D1\u01D3\u01D5\u01D7\u01D9\u01DB\u01DE\u01E0\u01E2\u01E4\u01E6\u01E8\u01EA\u01EC\u01EE\u01F1\u01F4\u01F6-\u01F8\u01FA\u01FC\u01FE\u0200\u0202\u0204\u0206\u0208\u020A\u020C\u020E\u0210\u0212\u0214\u0216\u0218\u021A\u021C\u021E\u0220\u0222\u0224\u0226\u0228\u022A\u022C\u022E\u0230\u0232\u023A-\u023B\u023D-\u023E\u0241\u0243-\u0246\u0248\u024A\u024C\u024E\u0370\u0372\u0376\u037F\u0386\u0388-\u038A\u038C\u038E-\u038F\u0391-\u03A1\u03A3-\u03AB\u03CF\u03D2-\u03D4\u03D8\u03DA\u03DC\u03DE\u03E0\u03E2\u03E4\u03E6\u03E8\u03EA\u03EC\u03EE\u03F4\u03F7\u03F9-\u03FA\u03FD-\u042F\u0460\u0462\u0464\u0466\u0468\u046A\u046C\u046E\u0470\u0472\u0474\u0476\u0478\u047A\u047C\u047E\u0480\u048A\u048C\u048E\u0490\u0492\u0494\u0496\u0498\u049A\u049C\u049E\u04A0\u04A2\u04A4\u04A6\u04A8\u04AA\u04AC\u04AE\u04B0\u04B2\u04B4\u04B6\u04B8\u04BA\u04BC\u04BE\u04C0-\u04C1\u04C3\u04C5\u04C7\u04C9\u04CB\u04CD\u04D0\u04D2\u04D4\u04D6\u04D8\u04DA\u04DC\u04DE\u04E0\u04E2\u04E4\u04E6\u04E8\u04EA\u04EC\u04EE\u04F0\u04F2\u04F4\u04F6\u04F8\u04FA\u04FC\u04FE\u0500\u0502\u0504\u0506\u0508\u050A\u050C\u050E\u0510\u0512\u0514\u0516\u0518\u051A\u051C\u051E\u0520\u0522\u0524\u0526\u0528\u052A\u052C\u052E\u0531-\u0556\u10A0-\u10C5\u10C7\u10CD\u13A0-\u13F5\u1C90-\u1CBA\u1CBD-\u1CBF\u1E00\u1E02\u1E04\u1E06\u1E08\u1E0A\u1E0C\u1E0E\u1E10\u1E12\u1E14\u1E16\u1E18\u1E1A\u1E1C\u1E1E\u1E20\u1E22\u1E24\u1E26\u1E28\u1E2A\u1E2C\u1E2E\u1E30\u1E32\u1E34\u1E36\u1E38\u1E3A\u1E3C\u1E3E\u1E40\u1E42\u1E44\u1E46\u1E48\u1E4A\u1E4C\u1E4E\u1E50\u1E52\u1E54\u1E56\u1E58\u1E5A\u1E5C\u1E5E\u1E60\u1E62\u1E64\u1E66\u1E68\u1E6A\u1E6C\u1E6E\u1E70\u1E72\u1E74\u1E76\u1E78\u1E7A\u1E7C\u1E7E\u1E80\u1E82\u1E84\u1E86\u1E88\u1E8A\u1E8C\u1E8E\u1E90\u1E92\u1E94\u1E9E\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAA\u1EAC\u1EAE\u1EB0\u1EB2\u1EB4\u1EB6\u1EB8\u1EBA\u1EBC\u1EBE\u1EC0\u1EC2\u1EC4\u1EC6\u1EC8\u1ECA\u1ECC\u1ECE\u1ED0\u1ED2\u1ED4\u1ED6\u1ED8\u1EDA\u1EDC\u1EDE\u1EE0\u1EE2\u1EE4\u1EE6\u1EE8\u1EEA\u1EEC\u1EEE\u1EF0\u1EF2\u1EF4\u1EF6\u1EF8\u1EFA\u1EFC\u1EFE\u1F08-\u1F0F\u1F18-\u1F1D\u1F28-\u1F2F\u1F38-\u1F3F\u1F48-\u1F4D\u1F59\u1F5B\u1F5D\u1F5F\u1F68-\u1F6F\u1FB8-\u1FBB\u1FC8-\u1FCB\u1FD8-\u1FDB\u1FE8-\u1FEC\u1FF8-\u1FFB\u2102\u2107\u210B-\u210D\u2110-\u2112\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u2130-\u2133\u213E-\u213F\u2145\u2183\u2C00-\u2C2E\u2C60\u2C62-\u2C64\u2C67\u2C69\u2C6B\u2C6D-\u2C70\u2C72\u2C75\u2C7E-\u2C80\u2C82\u2C84\u2C86\u2C88\u2C8A\u2C8C\u2C8E\u2C90\u2C92\u2C94\u2C96\u2C98\u2C9A\u2C9C\u2C9E\u2CA0\u2CA2\u2CA4\u2CA6\u2CA8\u2CAA\u2CAC\u2CAE\u2CB0\u2CB2\u2CB4\u2CB6\u2CB8\u2CBA\u2CBC\u2CBE\u2CC0\u2CC2\u2CC4\u2CC6\u2CC8\u2CCA\u2CCC\u2CCE\u2CD0\u2CD2\u2CD4\u2CD6\u2CD8\u2CDA\u2CDC\u2CDE\u2CE0\u2CE2\u2CEB\u2CED\u2CF2\uA640\uA642\uA644\uA646\uA648\uA64A\uA64C\uA64E\uA650\uA652\uA654\uA656\uA658\uA65A\uA65C\uA65E\uA660\uA662\uA664\uA666\uA668\uA66A\uA66C\uA680\uA682\uA684\uA686\uA688\uA68A\uA68C\uA68E\uA690\uA692\uA694\uA696\uA698\uA69A\uA722\uA724\uA726\uA728\uA72A\uA72C\uA72E\uA732\uA734\uA736\uA738\uA73A\uA73C\uA73E\uA740\uA742\uA744\uA746\uA748\uA74A\uA74C\uA74E\uA750\uA752\uA754\uA756\uA758\uA75A\uA75C\uA75E\uA760\uA762\uA764\uA766\uA768\uA76A\uA76C\uA76E\uA779\uA77B\uA77D-\uA77E\uA780\uA782\uA784\uA786\uA78B\uA78D\uA790\uA792\uA796\uA798\uA79A\uA79C\uA79E\uA7A0\uA7A2\uA7A4\uA7A6\uA7A8\uA7AA-\uA7AE\uA7B0-\uA7B4\uA7B6\uA7B8\uFF21-\uFF3A]
// Number, Letter // Number, Letter
Nl = [\u16EE-\u16F0\u2160-\u2182\u2185-\u2188\u3007\u3021-\u3029\u3038-\u303A\uA6E6-\uA6EF] Nl = [\u16EE-\u16F0\u2160-\u2182\u2185-\u2188\u3007\u3021-\u3029\u3038-\u303A\uA6E6-\uA6EF]
+44 -74
View File
@@ -5,8 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import * as krill from './krill-parser.js'; import * as krill from './krill-parser.js';
import * as strudel from '@strudel/core'; import * as strudel from '@strudel.cycles/core';
import Fraction, { lcm } from '@strudel/core/fraction.mjs';
const randOffset = 0.0003; const randOffset = 0.0003;
@@ -27,12 +26,6 @@ const applyOptions = (parent, enter) => (pat, i) => {
pat = strudel.reify(pat)[type](enter(amount)); pat = strudel.reify(pat)[type](enter(amount));
break; break;
} }
case 'replicate': {
const { amount } = op.arguments_;
pat = strudel.reify(pat);
pat = pat._repeatCycles(amount)._fast(amount);
break;
}
case 'bjorklund': { case 'bjorklund': {
if (op.arguments_.rotation) { if (op.arguments_.rotation) {
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation)); pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
@@ -73,88 +66,65 @@ const applyOptions = (parent, enter) => (pat, i) => {
return pat; return pat;
}; };
function resolveReplications(ast) {
ast.source_ = strudel.flatten(
ast.source_.map((child) => {
const { reps } = child.options_ || {};
if (!reps) {
return [child];
}
delete child.options_.reps;
return Array(reps).fill(child);
}),
);
}
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered // expects ast from mini2ast + quoted mini string + optional callback when a node is entered
export function patternifyAST(ast, code, onEnter, offset = 0) { export function patternifyAST(ast, code, onEnter, offset = 0) {
onEnter?.(ast); onEnter?.(ast);
const enter = (node) => patternifyAST(node, code, onEnter, offset); const enter = (node) => patternifyAST(node, code, onEnter, offset);
switch (ast.type_) { switch (ast.type_) {
case 'pattern': { case 'pattern': {
// resolveReplications(ast); resolveReplications(ast);
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter)); const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
const alignment = ast.arguments_.alignment; const alignment = ast.arguments_.alignment;
const with_tactus = children.filter((child) => child.__tactus_source); if (alignment === 'stack') {
let pat; return strudel.stack(...children);
switch (alignment) { }
case 'stack': { if (alignment === 'polymeter') {
pat = strudel.stack(...children); // polymeter
if (with_tactus.length) { const stepsPerCycle = ast.arguments_.stepsPerCycle
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus))); ? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
} : strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
break;
}
case 'polymeter_slowcat': {
pat = strudel.stack(...children.map((child) => child._slow(child.__weight)));
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'polymeter': {
// polymeter
const stepsPerCycle = ast.arguments_.stepsPerCycle
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight)))); const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
pat = strudel.stack(...aligned); return strudel.stack(...aligned);
break;
}
case 'rand': {
pat = strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'feet': {
pat = strudel.fastcat(...children);
break;
}
default: {
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (weightedChildren) {
const weightSum = ast.source_.reduce(
(sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)),
strudel.Fraction(0),
);
pat = strudel.timeCat(
...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]),
);
pat.__weight = weightSum; // for polymeter
pat.tactus = weightSum;
if (with_tactus.length) {
pat.tactus = pat.tactus.mul(lcm(...with_tactus.map((x) => Fraction(x.tactus))));
}
} else {
pat = strudel.sequence(...children);
pat.tactus = children.length;
}
if (ast.arguments_.tactus) {
pat.__tactus_source = true;
}
}
} }
if (with_tactus.length) { if (alignment === 'rand') {
pat.__tactus_source = true; return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
} }
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (!weightedChildren && alignment === 'slowcat') {
return strudel.slowcat(...children);
}
if (weightedChildren) {
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
if (alignment === 'slowcat') {
return pat._slow(weightSum); // timecat + slow
}
pat.__weight = weightSum;
return pat;
}
const pat = strudel.sequence(...children);
pat.__weight = children.length;
return pat; return pat;
} }
case 'element': { case 'element': {
1;
return enter(ast.source_); return enter(ast.source_);
} }
case 'atom': { case 'atom': {
if (ast.source_ === '~' || ast.source_ === '-') { if (ast.source_ === '~') {
return strudel.silence; return strudel.silence;
} }
if (!ast.location_) { if (!ast.location_) {
@@ -190,7 +160,7 @@ export const getLeafLocation = (code, leaf, globalOffset = 0) => {
}; };
// takes quoted mini string, returns ast // takes quoted mini string, returns ast
export const mini2ast = (code, start = 0, userCode = code) => { export const mini2ast = (code, start, userCode) => {
try { try {
return krill.parse(code); return krill.parse(code);
} catch (error) { } catch (error) {
+5 -4
View File
@@ -1,11 +1,12 @@
{ {
"name": "@strudel/mini", "name": "@strudel.cycles/mini",
"version": "1.0.1", "version": "0.9.0",
"description": "Mini notation for strudel", "description": "Mini notation for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
"publishConfig": { "publishConfig": {
"main": "dist/index.mjs" "main": "dist/index.js",
"module": "dist/index.mjs"
}, },
"scripts": { "scripts": {
"test": "vitest run", "test": "vitest run",
@@ -31,7 +32,7 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@strudel/core": "workspace:*" "@strudel.cycles/core": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"peggy": "^3.0.2", "peggy": "^3.0.2",
+1 -28
View File
@@ -5,8 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs'; import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs';
import '@strudel/core/euclid.mjs'; import '@strudel.cycles/core/euclid.mjs';
import { Fraction } from '@strudel/core/index.mjs';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
describe('mini', () => { describe('mini', () => {
@@ -74,10 +73,6 @@ describe('mini', () => {
expect(minS('a!3 b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']); expect(minS('a!3 b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
expect(minS('[<a b c>]!3 d')).toEqual(minS('<a b c> <a b c> <a b c> d')); expect(minS('[<a b c>]!3 d')).toEqual(minS('<a b c> <a b c> <a b c> d'));
}); });
it('supports replication via repeated !', () => {
expect(minS('a ! ! b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
expect(minS('[<a b c>]!! d')).toEqual(minS('<a b c> <a b c> <a b c> d'));
});
it('supports euclidean rhythms', () => { it('supports euclidean rhythms', () => {
expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']); expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
}); });
@@ -118,9 +113,6 @@ describe('mini', () => {
checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~'); checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~');
checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~'); checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~');
}); });
it('supports the - alias for ~', () => {
expect(minS('a - b [- c]')).toEqual(minS('a ~ b [~ c]'));
});
it('supports the ? operator', () => { it('supports the ? operator', () => {
expect( expect(
mini('a?') mini('a?')
@@ -198,25 +190,6 @@ describe('mini', () => {
it('supports patterned ranges', () => { it('supports patterned ranges', () => {
expect(minS('[<0 1> .. <2 4>]*2')).toEqual(minS('[0 1 2] [1 2 3 4]')); expect(minS('[<0 1> .. <2 4>]*2')).toEqual(minS('[0 1 2] [1 2 3 4]'));
}); });
it('supports the . operator', () => {
expect(minS('a . b c')).toEqual(minS('a [b c]'));
expect(minS('a . b c . [d e f . g h]')).toEqual(minS('a [b c] [[d e f] [g h]]'));
});
it('supports the _ operator', () => {
expect(minS('a _ b _ _')).toEqual(minS('a@2 b@3'));
});
it('_ and @ are almost interchangeable', () => {
expect(minS('a @ b @ @')).toEqual(minS('a _2 b _3'));
});
it('supports ^ tactus marking', () => {
expect(mini('a [^b c]').tactus).toEqual(Fraction(4));
expect(mini('[a b c] [d [e f]]').tactus).toEqual(Fraction(2));
expect(mini('^[a b c] [d [e f]]').tactus).toEqual(Fraction(2));
expect(mini('[a b c] [d [^e f]]').tactus).toEqual(Fraction(8));
expect(mini('[a b c] [^d [e f]]').tactus).toEqual(Fraction(4));
expect(mini('[^a b c] [^d [e f]]').tactus).toEqual(Fraction(12));
expect(mini('[^a b c] [d [^e f]]').tactus).toEqual(Fraction(24));
});
}); });
describe('getLeafLocation', () => { describe('getLeafLocation', () => {
+2 -2
View File
@@ -8,8 +8,8 @@ export default defineConfig({
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
formats: ['es'], formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs' })[ext], fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
}, },
rollupOptions: { rollupOptions: {
external: [...Object.keys(dependencies)], external: [...Object.keys(dependencies)],
-19
View File
@@ -1,19 +0,0 @@
# @strudel/node
This is an experiment to run strudel in node.
## Usage
```sh
# Setup
# - make sure node >= 20 is installed
# - make sure pnpm is installed
cd packages/node
pnpm i
# Usage
pnpm start
```
Then run `sclang` with superdirt in another terminal.
You can now edit and save the file `pattern.mjs` to update your pattern!
-64
View File
@@ -1,64 +0,0 @@
import { createClock, evalScope } from '@strudel/core';
import { evaluate } from '@strudel/transpiler';
import watch from 'node-watch';
import fs from 'node:fs/promises';
import { AudioContext, OscillatorNode, GainNode } from 'node-web-audio-api';
const audioContext = new AudioContext();
let file = 'pattern.mjs';
let pattern;
async function evaluateFile() {
try {
console.log('// file evaluated:');
const code = await fs.readFile(file, { encoding: 'utf8' });
console.log(code);
const res = await evaluate(code);
pattern = res.pattern;
} catch (err) {
console.error(err);
}
}
// const getTime = () => performance.now() / 1000;
const getTime = () => audioContext.currentTime;
let minLatency = 50;
async function main() {
await evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/tonal'));
await evaluateFile();
watch(file, { recursive: true }, () => evaluateFile());
let lastEnd;
const clock = createClock(getTime, (phase) => {
if (!lastEnd) {
lastEnd = phase;
return;
}
const haps = pattern.queryArc(lastEnd, phase);
lastEnd = phase;
const cps = 1;
const cycle = Math.floor(phase);
haps
.filter((h) => h.hasOnset())
.forEach((hap) => {
const env = new GainNode(audioContext, { gain: 0 });
const { attack = 0.01, gain = 1 } = hap.value;
env.connect(audioContext.destination);
const now = hap.whole.begin;
const duration = hap.duration;
env.gain
.setValueAtTime(0, now)
.linearRampToValueAtTime(gain * 0.2, now + attack)
.exponentialRampToValueAtTime(0.0001, now + duration);
const frequency = hap.value.freq;
const osc = new OscillatorNode(audioContext, { frequency });
osc.connect(env);
osc.start(now);
osc.stop(now + duration);
});
});
clock.start();
}
main();

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