Compare commits
208 Commits
v0.6.0
...
iclc2023-paper
| Author | SHA1 | Date | |
|---|---|---|---|
| 44064fea55 | |||
| 1a56d8a923 | |||
| a5e9cc77a5 | |||
| 9ed9ba33c5 | |||
| d046e01684 | |||
| b217471d2a | |||
| e4c249e568 | |||
| b4ef80fcae | |||
| 703e516458 | |||
| 220342154b | |||
| ef7182cd76 | |||
| 892d8ba152 | |||
| da11069f75 | |||
| bf72908dc9 | |||
| 81ab5c78e2 | |||
| de503c7d63 | |||
| 487ccb90a1 | |||
| 4bfeaa47bf | |||
| 9a4d8036a6 | |||
| a50bd6375d | |||
| 14508e91a9 | |||
| 2d5064bd5b | |||
| fdc4819e39 | |||
| efddbb616b | |||
| db4d0f26a8 | |||
| 06cffbc31b | |||
| 3cfb0f4c76 | |||
| 436ba067bc | |||
| f8724bf47e | |||
| 1f7e293204 | |||
| 86d2652258 | |||
| f5c0cc2e2d | |||
| 4463d4a417 | |||
| df35a5c9b1 | |||
| dda93b3c13 | |||
| 5de40b6bf5 | |||
| dcbe799d25 | |||
| a5641aef03 | |||
| 668f97a4c8 | |||
| e9d577496b | |||
| 66fba947ac | |||
| 18c7e02609 | |||
| 7e1351507a | |||
| 032acbd27c | |||
| 8b51f0fbd2 | |||
| ead5942ef0 | |||
| cfe369d9fe | |||
| 2750a3c130 | |||
| a7f6d6ecac | |||
| 3680282450 | |||
| 3b3435c999 | |||
| e2ae6278c2 | |||
| 37d6723e3c | |||
| 961bbf6f55 | |||
| fc71d349ed | |||
| 97c8ab0f71 | |||
| 28534ea547 | |||
| 1a75b79a33 | |||
| 75dfb0af35 | |||
| 073fa09648 | |||
| 3015e7b203 | |||
| 7ac0cdc0f9 | |||
| 49234183a3 | |||
| ed7763df92 | |||
| b818a02f76 | |||
| 886f8449fd | |||
| 5de6643604 | |||
| 6283273d81 | |||
| 5506d18bc5 | |||
| c8f8f02a46 | |||
| 4c5d6f1d6c | |||
| f349e36345 | |||
| 1574c4ab13 | |||
| 0dc9bc3ab6 | |||
| 0006d57a4a | |||
| 70f7e73b9a | |||
| b67b049802 | |||
| ff99dbcd22 | |||
| 0198697737 | |||
| a742a71f67 | |||
| 8f6c1531fa | |||
| d9f56f11cb | |||
| 7ff877464f | |||
| 7c367eb1e8 | |||
| 20848aac09 | |||
| 7716574076 | |||
| 89cd0c769b | |||
| a21b3d788f | |||
| 7994ba8b38 | |||
| 81b4cb2f16 | |||
| 14af1df6a3 | |||
| ac3bd7d7db | |||
| 632e8e9634 | |||
| 1e3f09f69b | |||
| f3f18ffca7 | |||
| ceb3aa0627 | |||
| 014555fe5d | |||
| fdb76867a7 | |||
| 57157fde9c | |||
| 4a3540cf2b | |||
| 540bd938f2 | |||
| c6fbebd996 | |||
| 18d3a7e23a | |||
| cbae355896 | |||
| 924e8a764a | |||
| 40e629cb0e | |||
| 99ec261f79 | |||
| 9daea5bbc2 | |||
| 4704c03d48 | |||
| 9dbf52bedb | |||
| cfd04ab7fa | |||
| 33dbf75e3b | |||
| 8ffaeeb171 | |||
| 451828e5d4 | |||
| 176877f2b8 | |||
| 61b0d08b5f | |||
| f50a6ce1ae | |||
| 8549d6ea8b | |||
| 90a90a87a5 | |||
| 760bdea521 | |||
| 0fd783e83f | |||
| 42caa9058b | |||
| 9ab315ac2c | |||
| a08ad07537 | |||
| c6ce763f7c | |||
| 70d1eb17a7 | |||
| 0393205eda | |||
| 5c05a5a63e | |||
| b01d9f849b | |||
| 0ab904275c | |||
| 5f82a86801 | |||
| 13a5d8de86 | |||
| f024b71808 | |||
| cd35a17904 | |||
| be3f68ed06 | |||
| 64b8fd9584 | |||
| b001d1d0f1 | |||
| 64ad75c79e | |||
| 12eb361d69 | |||
| dd51bd0a2f | |||
| 0fb478a7c8 | |||
| 9694ecf55b | |||
| fe80478b8a | |||
| a253c26bee | |||
| a500f983f5 | |||
| 3579b6f8f3 | |||
| 14cb954213 | |||
| 6170eef441 | |||
| ddb9af4d57 | |||
| 6db2c0bf93 | |||
| 46f70e6a5e | |||
| 0b994a1b0f | |||
| 0326244956 | |||
| ea1b2252fc | |||
| 0da25ea7cc | |||
| 3209afa4ff | |||
| 33e2631e5d | |||
| f02a41b675 | |||
| f497d7b212 | |||
| 0f18f5eb81 | |||
| 8da81104aa | |||
| 97cbdc6b0f | |||
| 7f53f4e36b | |||
| c350f262e6 | |||
| 3e727893bf | |||
| 2a2c7437e4 | |||
| 4f0885d3af | |||
| 5656cd2fc6 | |||
| b6e42876ad | |||
| 3671344867 | |||
| 30b149053c | |||
| cdd40df9d3 | |||
| 9d661808ba | |||
| 6a5d849b3c | |||
| 25bbd73a90 | |||
| 3d27a5f8c7 | |||
| 44ef372399 | |||
| fd09dff381 | |||
| 10580a0bec | |||
| f3f87b0a46 | |||
| ac606bcf71 | |||
| bb50df07e7 | |||
| f7ac033e1c | |||
| a1043352c1 | |||
| e15fecf6cf | |||
| 3e99f345ef | |||
| 0a77770ef3 | |||
| 379b8392dd | |||
| 0fdae70461 | |||
| 087423c754 | |||
| ce64c98fb7 | |||
| e11a8a9dbb | |||
| ef4a68a24d | |||
| 0ec65c85d3 | |||
| 74326f226d | |||
| 554e0d7a39 | |||
| 1b99ac92dd | |||
| ff0b77908a | |||
| 83d19638ba | |||
| a7d484b1a8 | |||
| d87beb37b8 | |||
| dcdb383960 | |||
| 478a7e95ea | |||
| f7b3b582cc | |||
| d1719dc277 | |||
| 9d8c812aa3 | |||
| 4121fc926e | |||
| fd9ab987e6 |
@@ -15,3 +15,6 @@ vite.config.js
|
||||
!**/*.mjs
|
||||
**/*.tsx
|
||||
**/*.ts
|
||||
**/*.json
|
||||
**/dev-dist
|
||||
**/dist
|
||||
@@ -37,4 +37,6 @@ talk/public/EmuSP12
|
||||
talk/public/samples
|
||||
server/samples/old
|
||||
repl/stats.html
|
||||
coverage
|
||||
coverage
|
||||
public/icons/apple-splash-*
|
||||
dev-dist
|
||||
@@ -8,4 +8,5 @@ packages/mini/krill-parser.js
|
||||
packages/xen/tunejs.js
|
||||
paper
|
||||
pnpm-lock.yaml
|
||||
pnpm-workspace.yaml
|
||||
pnpm-workspace.yaml
|
||||
**/dev-dist
|
||||
@@ -7,7 +7,7 @@
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"proseWrap": "preserve",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
|
||||
@@ -82,6 +82,10 @@ Please report any problems you've had with the setup instructions!
|
||||
## Code Style
|
||||
|
||||
To make sure the code changes only where it should, we are using prettier to unify the code style.
|
||||
|
||||
- You can format all files at once by running `pnpm prettier` from the project root
|
||||
- Run `pnpm format-check` from the project root to check if all files are well formatted
|
||||
|
||||
If you use VSCode, you can
|
||||
|
||||
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||
@@ -89,6 +93,24 @@ If you use VSCode, you can
|
||||
3. Choose "Configure Default Formatter..."
|
||||
4. Select prettier
|
||||
|
||||
## ESLint
|
||||
|
||||
To prevent unwanted runtime errors, this project uses [eslint](https://eslint.org/).
|
||||
|
||||
- You can check for lint errors by running `pnpm lint`
|
||||
|
||||
There are also eslint extensions / plugins for most editors.
|
||||
|
||||
## Running Tests
|
||||
|
||||
- Run all tests with `pnpm test`
|
||||
- Run all tests with UI using `pnpm test-ui`
|
||||
|
||||
## Running all CI Checks
|
||||
|
||||
When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests.
|
||||
You can run the same check with `pnpm check`
|
||||
|
||||
## Package Workflow
|
||||
|
||||
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
|
||||
|
||||
@@ -7,14 +7,15 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
|
||||
- Try it here: <https://strudel.tidalcycles.org/>
|
||||
- Docs: <https://strudel.tidalcycles.org/learn/>
|
||||
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
||||
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
||||
|
||||
## Running Locally
|
||||
|
||||
After cloning the project, you can run the REPL locally:
|
||||
|
||||
```bash
|
||||
npm run setup
|
||||
npm run repl
|
||||
pnpm run setup
|
||||
pnpm run repl
|
||||
```
|
||||
|
||||
## Using Strudel In Your Project
|
||||
|
||||
@@ -20,11 +20,8 @@ Example: <https://felixroos.github.io/strudel/swatch/>
|
||||
### 6. edit `website/astro.config.mjs` to use site: `https://<your-username>.github.io` and base `/strudel`, like this
|
||||
|
||||
```js
|
||||
export default defineConfig({
|
||||
/* ... rest of config ... */
|
||||
site: 'https://<your-username>.github.io',
|
||||
base: '/strudel',
|
||||
});
|
||||
const site = 'https://<your-username>.github.io';
|
||||
const base = '/strudel';
|
||||
```
|
||||
|
||||
### 7. commit & push the changes
|
||||
|
||||
@@ -46,14 +46,15 @@
|
||||
},
|
||||
"homepage": "https://strudel.tidalcycles.org",
|
||||
"dependencies": {
|
||||
"dependency-tree": "^9.0.0",
|
||||
"vitest": "^0.25.7",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/mini": "workspace:*",
|
||||
"@strudel.cycles/tonal": "workspace:*",
|
||||
"@strudel.cycles/transpiler": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"@strudel.cycles/xen": "workspace:*"
|
||||
"@strudel.cycles/xen": "workspace:*",
|
||||
"acorn": "^8.8.1",
|
||||
"dependency-tree": "^9.0.0",
|
||||
"vitest": "^0.25.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/ui": "^0.25.7",
|
||||
|
||||
@@ -10,44 +10,49 @@ import { logger } from './logger.mjs';
|
||||
export class Cyclist {
|
||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||
this.started = false;
|
||||
this.cps = 1; // TODO
|
||||
this.phase = 0;
|
||||
this.getTime = getTime;
|
||||
this.cps = 1;
|
||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||
this.lastBegin = 0; // query begin of last tick
|
||||
this.lastEnd = 0; // query end of last tick
|
||||
this.getTime = getTime; // get absolute time
|
||||
this.onToggle = onToggle;
|
||||
this.latency = latency;
|
||||
this.latency = latency; // fixed trigger time offset
|
||||
const round = (x) => Math.round(x * 1000) / 1000;
|
||||
this.clock = createClock(
|
||||
getTime,
|
||||
// called slightly before each cycle
|
||||
(phase, duration, tick) => {
|
||||
if (tick === 0) {
|
||||
this.origin = phase;
|
||||
}
|
||||
const begin = round(phase - this.origin);
|
||||
this.phase = begin - latency;
|
||||
const end = round(begin + duration);
|
||||
const time = getTime();
|
||||
try {
|
||||
const haps = this.pattern.queryArc(begin, end); // get Haps
|
||||
const time = getTime();
|
||||
const begin = this.lastEnd;
|
||||
this.lastBegin = begin;
|
||||
const end = round(begin + duration * this.cps);
|
||||
this.lastEnd = end;
|
||||
const haps = this.pattern.queryArc(begin, end);
|
||||
const tickdeadline = phase - time; // time left till phase begins
|
||||
this.lastTick = time + tickdeadline;
|
||||
|
||||
haps.forEach((hap) => {
|
||||
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||
const deadline = hap.whole.begin + this.origin - time + latency;
|
||||
const duration = hap.duration * 1;
|
||||
onTrigger?.(hap, deadline, duration);
|
||||
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
|
||||
const duration = hap.duration / this.cps;
|
||||
onTrigger?.(hap, deadline, duration, this.cps);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger(`[cyclist] error: ${e.message}`);
|
||||
onError?.(e);
|
||||
}
|
||||
}, // called slightly before each cycle
|
||||
},
|
||||
interval, // duration of each cycle
|
||||
);
|
||||
}
|
||||
getPhase() {
|
||||
return this.getTime() - this.origin - this.latency;
|
||||
}
|
||||
now() {
|
||||
return this.getTime() - this.origin + this.clock.minLatency;
|
||||
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
|
||||
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
|
||||
}
|
||||
setStarted(v) {
|
||||
this.started = v;
|
||||
|
||||
@@ -11,15 +11,9 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { Pattern, timeCat, register, silence } from './pattern.mjs';
|
||||
import { rotate, flatten } from './util.mjs';
|
||||
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
|
||||
const splitAt = function (index, value) {
|
||||
return [value.slice(0, index), value.slice(index)];
|
||||
};
|
||||
|
||||
const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||
|
||||
const left = function (n, x) {
|
||||
const [ons, offs] = n;
|
||||
const [xs, ys] = x;
|
||||
|
||||
@@ -6,12 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import { isPattern } from './index.mjs';
|
||||
|
||||
let scoped = false;
|
||||
export const evalScope = async (...args) => {
|
||||
if (scoped) {
|
||||
console.warn('evalScope was called more than once.');
|
||||
}
|
||||
scoped = true;
|
||||
const results = await Promise.allSettled(args);
|
||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||
results.forEach((result, i) => {
|
||||
@@ -42,9 +37,6 @@ function safeEval(str, options = {}) {
|
||||
}
|
||||
|
||||
export const evaluate = async (code, transpiler) => {
|
||||
if (!scoped) {
|
||||
await evalScope(); // at least scope Pattern.prototype.boostrap
|
||||
}
|
||||
if (transpiler) {
|
||||
code = transpiler(code); // transform syntactically correct js code to semantically usable code
|
||||
}
|
||||
|
||||
@@ -126,6 +126,19 @@ export class Hap {
|
||||
setContext(context) {
|
||||
return new Hap(this.whole, this.part, this.value, context);
|
||||
}
|
||||
|
||||
ensureObjectValue() {
|
||||
/* if (isNote(hap.value)) {
|
||||
// supports primitive hap values that look like notes
|
||||
hap.value = { note: hap.value };
|
||||
} */
|
||||
if (typeof this.value !== 'object') {
|
||||
throw new Error(
|
||||
`expected hap.value to be an object, but got "${this.value}". Hint: append .note() or .s() to the end`,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Hap;
|
||||
|
||||
@@ -27,6 +27,7 @@ export class Pattern {
|
||||
* Create a pattern. As an end user, you will most likely not create a Pattern directly.
|
||||
*
|
||||
* @param {function} query - The function that maps a {@link State} to an array of {@link Hap}.
|
||||
* @noAutocomplete
|
||||
*/
|
||||
constructor(query) {
|
||||
this.query = query;
|
||||
@@ -51,6 +52,7 @@ export class Pattern {
|
||||
|
||||
/**
|
||||
* see {@link Pattern#withValue}
|
||||
* @noAutocomplete
|
||||
*/
|
||||
fmap(func) {
|
||||
return this.withValue(func);
|
||||
@@ -62,6 +64,7 @@ export class Pattern {
|
||||
* pattern of functions.
|
||||
* @param {Function} whole_func
|
||||
* @param {Function} func
|
||||
* @noAutocomplete
|
||||
* @returns Pattern
|
||||
*/
|
||||
appWhole(whole_func, pat_val) {
|
||||
@@ -97,6 +100,7 @@ export class Pattern {
|
||||
* are not the same but do intersect, the resulting hap has a timespan of the
|
||||
* intersection. This applies to both the part and the whole timespan.
|
||||
* @param {Pattern} pat_val
|
||||
* @noAutocomplete
|
||||
* @returns Pattern
|
||||
*/
|
||||
appBoth(pat_val) {
|
||||
@@ -117,6 +121,7 @@ export class Pattern {
|
||||
* are preserved from the pattern of functions (often referred to as the left
|
||||
* hand or inner pattern).
|
||||
* @param {Pattern} pat_val
|
||||
* @noAutocomplete
|
||||
* @returns Pattern
|
||||
*/
|
||||
appLeft(pat_val) {
|
||||
@@ -147,6 +152,7 @@ export class Pattern {
|
||||
* pattern of values, i.e. structure is preserved from the right hand/outer
|
||||
* pattern.
|
||||
* @param {Pattern} pat_val
|
||||
* @noAutocomplete
|
||||
* @returns Pattern
|
||||
*/
|
||||
appRight(pat_val) {
|
||||
@@ -332,6 +338,7 @@ export class Pattern {
|
||||
* const haps = pattern.queryArc(0, 1)
|
||||
* console.log(haps)
|
||||
* silence
|
||||
* @noAutocomplete
|
||||
*/
|
||||
queryArc(begin, end) {
|
||||
try {
|
||||
@@ -347,6 +354,7 @@ export class Pattern {
|
||||
* some calculations easier to express, as all haps are then constrained to
|
||||
* happen within a cycle.
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
splitQueries() {
|
||||
const pat = this;
|
||||
@@ -361,6 +369,7 @@ export class Pattern {
|
||||
* timespan before passing it to the original pattern.
|
||||
* @param {Function} func the function to apply
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
withQuerySpan(func) {
|
||||
return new Pattern((state) => this.query(state.withSpan(func)));
|
||||
@@ -382,6 +391,7 @@ export class Pattern {
|
||||
* begin and end time of the query timespan.
|
||||
* @param {Function} func the function to apply
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
withQueryTime(func) {
|
||||
return new Pattern((state) => this.query(state.withSpan((span) => span.withTime(func))));
|
||||
@@ -393,6 +403,7 @@ export class Pattern {
|
||||
* present, `whole` timespans).
|
||||
* @param {Function} func
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
withHapSpan(func) {
|
||||
return new Pattern((state) => this.query(state).map((hap) => hap.withSpan(func)));
|
||||
@@ -403,6 +414,7 @@ export class Pattern {
|
||||
* begin and end time of the hap timespans.
|
||||
* @param {Function} func the function to apply
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
withHapTime(func) {
|
||||
return this.withHapSpan((span) => span.withTime(func));
|
||||
@@ -412,6 +424,7 @@ export class Pattern {
|
||||
* Returns a new pattern with the given function applied to the list of haps returned by every query.
|
||||
* @param {Function} func
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
withHaps(func) {
|
||||
return new Pattern((state) => func(this.query(state)));
|
||||
@@ -421,6 +434,7 @@ export class Pattern {
|
||||
* As with {@link Pattern#withHaps}, but applies the function to every hap, rather than every list of haps.
|
||||
* @param {Function} func
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
withHap(func) {
|
||||
return this.withHaps((haps) => haps.map(func));
|
||||
@@ -430,6 +444,7 @@ export class Pattern {
|
||||
* Returns a new pattern with the context field set to every hap set to the given value.
|
||||
* @param {*} context
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
setContext(context) {
|
||||
return this.withHap((hap) => hap.setContext(context));
|
||||
@@ -439,6 +454,7 @@ export class Pattern {
|
||||
* Returns a new pattern with the given function applied to the context field of every hap.
|
||||
* @param {Function} func
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
withContext(func) {
|
||||
return this.withHap((hap) => hap.setContext(func(hap.context)));
|
||||
@@ -447,6 +463,7 @@ export class Pattern {
|
||||
/**
|
||||
* Returns a new pattern with the context field of every hap set to an empty object.
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
stripContext() {
|
||||
return this.withHap((hap) => hap.setContext({}));
|
||||
@@ -458,6 +475,7 @@ export class Pattern {
|
||||
* @param {Number} start
|
||||
* @param {Number} end
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
withLocation(start, end) {
|
||||
const location = {
|
||||
@@ -500,6 +518,7 @@ export class Pattern {
|
||||
* Returns a new Pattern, which only returns haps that meet the given test.
|
||||
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
filterHaps(hap_test) {
|
||||
return new Pattern((state) => this.query(state).filter(hap_test));
|
||||
@@ -510,6 +529,7 @@ export class Pattern {
|
||||
* inside haps.
|
||||
* @param {Function} value_test
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
filterValues(value_test) {
|
||||
return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value)));
|
||||
@@ -519,6 +539,7 @@ export class Pattern {
|
||||
* Returns a new pattern, with haps containing undefined values removed from
|
||||
* query results.
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
removeUndefineds() {
|
||||
return this.filterValues((val) => val != undefined);
|
||||
@@ -529,6 +550,7 @@ export class Pattern {
|
||||
* with an onset is one with a `whole` timespan that begins at the same time
|
||||
* as its `part` timespan.
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
onsetsOnly() {
|
||||
// Returns a new pattern that will only return haps where the start
|
||||
@@ -541,6 +563,7 @@ export class Pattern {
|
||||
* Returns a new pattern, with 'continuous' haps (those without 'whole'
|
||||
* timespans) removed from query results.
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
discreteOnly() {
|
||||
// removes continuous haps that don't have a 'whole' timespan
|
||||
@@ -550,6 +573,7 @@ export class Pattern {
|
||||
/**
|
||||
* Combines adjacent haps with the same value and whole. Only
|
||||
* intended for use in tests.
|
||||
* @noAutocomplete
|
||||
*/
|
||||
defragmentHaps() {
|
||||
// remove continuous haps
|
||||
@@ -604,6 +628,7 @@ export class Pattern {
|
||||
* @param {Boolean} with_context - set to true, otherwise the context field
|
||||
* will be stripped from the resulting haps.
|
||||
* @returns [Hap]
|
||||
* @noAutocomplete
|
||||
*/
|
||||
firstCycle(with_context = false) {
|
||||
var self = this;
|
||||
@@ -615,6 +640,7 @@ export class Pattern {
|
||||
|
||||
/**
|
||||
* Accessor for a list of values returned by querying the first cycle.
|
||||
* @noAutocomplete
|
||||
*/
|
||||
get firstCycleValues() {
|
||||
return this.firstCycle().map((hap) => hap.value);
|
||||
@@ -622,6 +648,7 @@ export class Pattern {
|
||||
|
||||
/**
|
||||
* More human-readable version of the {@link Pattern#firstCycleValues} accessor.
|
||||
* @noAutocomplete
|
||||
*/
|
||||
get showFirstCycle() {
|
||||
return this.firstCycle().map(
|
||||
@@ -633,6 +660,7 @@ export class Pattern {
|
||||
* Returns a new pattern, which returns haps sorted in temporal order. Mainly
|
||||
* of use when comparing two patterns for equality, in tests.
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
sortHapsByPart() {
|
||||
return this.withHaps((haps) =>
|
||||
@@ -812,7 +840,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// functions relating to chords/patterns of lists
|
||||
// functions relating to chords/patterns of lists/lists of patterns
|
||||
|
||||
// returns Array<Hap[]> where each list of haps satisfies eq
|
||||
function groupHapsBy(eq, haps) {
|
||||
@@ -861,6 +889,35 @@ Pattern.prototype.arp = function (pat) {
|
||||
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
|
||||
};
|
||||
|
||||
/*
|
||||
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
|
||||
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
|
||||
* @name weave
|
||||
* @memberof Pattern
|
||||
* @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd"))
|
||||
* @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd"))
|
||||
|
||||
addToPrototype('weave', function (t, ...pats) {
|
||||
return this.weaveWith(t, ...pats.map((x) => set.out(x)));
|
||||
});
|
||||
|
||||
*/
|
||||
/*
|
||||
* Like 'weave', but accepts functions rather than patterns, which are applied to the pattern.
|
||||
* @name weaveWith
|
||||
* @memberof Pattern
|
||||
|
||||
addToPrototype('weaveWith', function (t, ...funcs) {
|
||||
const pat = this;
|
||||
const l = funcs.length;
|
||||
t = Fraction(t);
|
||||
if (l == 0) {
|
||||
return silence;
|
||||
}
|
||||
return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t);
|
||||
});
|
||||
*/
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// compose matrix functions
|
||||
|
||||
@@ -1103,6 +1160,7 @@ export const silence = new Pattern(() => []);
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* pure('e4') // "e4"
|
||||
* @noAutocomplete
|
||||
*/
|
||||
export function pure(value) {
|
||||
function query(state) {
|
||||
@@ -1226,6 +1284,20 @@ export function timeCat(...timepats) {
|
||||
return stack(...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to arrange multiple patterns together over multiple cycles.
|
||||
* Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* arrange([4, "<c a f e>(3,8)"],[2, "<g a>(5,8)"]).note()
|
||||
*/
|
||||
export function arrange(...sections) {
|
||||
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
|
||||
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
|
||||
return timeCat(...sections).slow(total);
|
||||
}
|
||||
|
||||
export function fastcat(...pats) {
|
||||
return slowcat(...pats)._fast(pats.length);
|
||||
}
|
||||
@@ -1343,41 +1415,48 @@ export const func = curry((a, b) => reify(b).func(a));
|
||||
*
|
||||
* @param {string} name name of the function
|
||||
* @param {function} func function with 1 or more params, where last is the current pattern
|
||||
* @noAutocomplete
|
||||
*
|
||||
*/
|
||||
export function register(name, func) {
|
||||
export function register(name, func, patternify = true) {
|
||||
if (Array.isArray(name)) {
|
||||
const result = {};
|
||||
for (const name_item of name) {
|
||||
result[name_item] = register(name_item, func);
|
||||
result[name_item] = register(name_item, func, patternify);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
const arity = func.length;
|
||||
var pfunc; // the patternified function
|
||||
|
||||
pfunc = function (...args) {
|
||||
args = args.map(reify);
|
||||
const pat = args[args.length - 1];
|
||||
if (arity === 1) {
|
||||
return func(pat);
|
||||
}
|
||||
const [left, ...right] = args.slice(0, -1);
|
||||
let mapFn = (...args) => {
|
||||
// make sure to call func with the correct argument count
|
||||
// args.length is expected to be <= arity-1
|
||||
// so we set undefined args explicitly undefined
|
||||
Array(arity - 1)
|
||||
.fill()
|
||||
.map((_, i) => args[i] ?? undefined);
|
||||
return func(...args, pat);
|
||||
if (patternify) {
|
||||
pfunc = function (...args) {
|
||||
args = args.map(reify);
|
||||
const pat = args[args.length - 1];
|
||||
if (arity === 1) {
|
||||
return func(pat);
|
||||
}
|
||||
const [left, ...right] = args.slice(0, -1);
|
||||
let mapFn = (...args) => {
|
||||
// make sure to call func with the correct argument count
|
||||
// args.length is expected to be <= arity-1
|
||||
// so we set undefined args explicitly undefined
|
||||
Array(arity - 1)
|
||||
.fill()
|
||||
.map((_, i) => args[i] ?? undefined);
|
||||
return func(...args, pat);
|
||||
};
|
||||
mapFn = curry(mapFn, null, arity - 1);
|
||||
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
|
||||
};
|
||||
mapFn = curry(mapFn, null, arity - 1);
|
||||
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
|
||||
};
|
||||
} else {
|
||||
pfunc = function (...args) {
|
||||
args = args.map(reify);
|
||||
return func(...args);
|
||||
};
|
||||
}
|
||||
|
||||
Pattern.prototype[name] = function (...args) {
|
||||
args = args.map(reify);
|
||||
// For methods that take a single argument (plus 'this'), allow
|
||||
// multiple arguments but sequence them
|
||||
if (arity === 2 && args.length !== 1) {
|
||||
@@ -1385,6 +1464,7 @@ export function register(name, func) {
|
||||
} else if (arity !== args.length + 1) {
|
||||
throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`);
|
||||
}
|
||||
args = args.map(reify);
|
||||
return pfunc(...args, this);
|
||||
};
|
||||
|
||||
@@ -1448,15 +1528,17 @@ export const ceil = register('ceil', function (pat) {
|
||||
* Assumes a numerical pattern, containing unipolar values in the range 0 ..
|
||||
* 1. Returns a new pattern with values scaled to the bipolar range -1 .. 1
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
export const toBipolar = register('toBipolar', function (pat) {
|
||||
return pat.fmap((x) => x * 2 - 1);
|
||||
});
|
||||
|
||||
/**
|
||||
* Assumes a numerical pattern, containing bipolar values in the range -1 ..
|
||||
* 1. Returns a new pattern with values scaled to the unipolar range 0 .. 1
|
||||
* Assumes a numerical pattern, containing bipolar values in the range -1 .. 1
|
||||
* Returns a new pattern with values scaled to the unipolar range 0 .. 1
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
export const fromBipolar = register('fromBipolar', function (pat) {
|
||||
return pat.fmap((x) => (x + 1) / 2);
|
||||
@@ -2162,6 +2244,61 @@ const _loopAt = function (factor, pat, cps = 1) {
|
||||
.slow(factor);
|
||||
};
|
||||
|
||||
/*
|
||||
* Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers.
|
||||
* @name slice
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
|
||||
*/
|
||||
|
||||
const slice = register(
|
||||
'slice',
|
||||
function (npat, ipat, opat) {
|
||||
return npat.innerBind((n) =>
|
||||
ipat.outerBind((i) =>
|
||||
opat.outerBind((o) => {
|
||||
// If it's not an object, assume it's a string and make it a 's' control parameter
|
||||
o = o instanceof Object ? o : { s: o };
|
||||
// Remember we must stay pure and avoid editing the object directly
|
||||
const toAdd = { begin: i / n, end: (i + 1) / n, _slices: n };
|
||||
return pure({ ...toAdd, ...o });
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
false, // turns off auto-patternification
|
||||
);
|
||||
|
||||
/*
|
||||
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
|
||||
* @name splice
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
|
||||
*/
|
||||
|
||||
const splice = register(
|
||||
'splice',
|
||||
function (npat, ipat, opat) {
|
||||
const sliced = slice(npat, ipat, opat);
|
||||
return sliced.withHap(function (hap) {
|
||||
return hap.withValue((v) => ({
|
||||
...{
|
||||
speed: (1 / v._slices / hap.whole.duration) * (v.speed || 1),
|
||||
unit: 'c',
|
||||
},
|
||||
...v,
|
||||
}));
|
||||
});
|
||||
},
|
||||
false, // turns off auto-patternification
|
||||
);
|
||||
|
||||
const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
|
||||
return _loopAt(factor, pat, 1);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Cyclist } from './cyclist.mjs';
|
||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { setTime } from './time.mjs';
|
||||
import { evalScope } from './evaluate.mjs';
|
||||
|
||||
export function repl({
|
||||
interval,
|
||||
@@ -17,13 +18,12 @@ export function repl({
|
||||
}) {
|
||||
const scheduler = new Cyclist({
|
||||
interval,
|
||||
onTrigger: async (hap, deadline, duration) => {
|
||||
onTrigger: async (hap, deadline, duration, cps) => {
|
||||
try {
|
||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||
await defaultOutput(hap, deadline, duration);
|
||||
await defaultOutput(hap, deadline, duration, cps);
|
||||
}
|
||||
if (hap.context.onTrigger) {
|
||||
const cps = 1;
|
||||
// call signature of output / onTrigger is different...
|
||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
||||
}
|
||||
@@ -42,6 +42,17 @@ export function repl({
|
||||
}
|
||||
try {
|
||||
beforeEval?.({ code });
|
||||
scheduler.setCps(1); // reset cps in case the code does not contain a setCps call
|
||||
// problem: when the code does contain a setCps after an awaited promise,
|
||||
// the cps will be 1 until the promise resolves
|
||||
// example:
|
||||
/*
|
||||
await new Promise(resolve => setTimeout(resolve,1000))
|
||||
setCps(.5)
|
||||
note("c a f e")
|
||||
*/
|
||||
// to make sure the setCps inside the code is called immediately,
|
||||
// it has to be placed first
|
||||
let { pattern } = await _evaluate(code, transpiler);
|
||||
|
||||
logger(`[eval] code updated`);
|
||||
@@ -58,5 +69,10 @@ export function repl({
|
||||
const stop = () => scheduler.stop();
|
||||
const start = () => scheduler.start();
|
||||
const pause = () => scheduler.pause();
|
||||
return { scheduler, evaluate, start, stop, pause };
|
||||
const setCps = (cps) => scheduler.setCps(cps);
|
||||
evalScope({
|
||||
setCps,
|
||||
setcps: setCps,
|
||||
});
|
||||
return { scheduler, evaluate, start, stop, pause, setCps };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
controls.test.mjs - <short description TODO>
|
||||
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/controls.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import controls from '../controls.mjs';
|
||||
import { mini } from '../../mini/mini.mjs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('controls', () => {
|
||||
it('should support controls', () => {
|
||||
expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
|
||||
});
|
||||
it('should support compound controls', () => {
|
||||
expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
|
||||
expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
|
||||
{ s: 'bd', n: 3 },
|
||||
{ s: 'sd', n: 4, gain: 1.4 },
|
||||
]);
|
||||
});
|
||||
it('should support ignore extra elements in compound controls', () => {
|
||||
expect(controls.s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
|
||||
{ s: 'bd', n: 3, gain: 0.4 },
|
||||
{ s: 'sd', n: 4, gain: 0.5 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -959,4 +959,48 @@ describe('Pattern', () => {
|
||||
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
|
||||
});
|
||||
});
|
||||
/*describe('composable functions', () => {
|
||||
it('Can compose functions', () => {
|
||||
sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
|
||||
});
|
||||
it('Can compose by method chaining operators with controls', () => {
|
||||
sameFirst(s('bd').apply(set.n(3).fast(2)), s('bd').set.n(3).fast(2));
|
||||
});
|
||||
it('Can compose by method chaining operators and alignments with controls', () => {
|
||||
sameFirst(s('bd').apply(set.in.n(3).fast(2)), s('bd').set.n(3).fast(2));
|
||||
// sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2));
|
||||
});
|
||||
});
|
||||
describe('weave', () => {
|
||||
it('Can distribute patterns along a pattern', () => {
|
||||
sameFirst(n(0, 1).weave(2, s('bd', silence), s(silence, 'sd')), sequence(s('bd').n(0), s('sd').n(1)));
|
||||
});
|
||||
});
|
||||
*/
|
||||
describe('slice', () => {
|
||||
it('Can slice a sample', () => {
|
||||
sameFirst(
|
||||
s('break').slice(4, sequence(0, 1, 2, 3)),
|
||||
sequence(
|
||||
{ begin: 0, end: 0.25, s: 'break', _slices: 4 },
|
||||
{ begin: 0.25, end: 0.5, s: 'break', _slices: 4 },
|
||||
{ begin: 0.5, end: 0.75, s: 'break', _slices: 4 },
|
||||
{ begin: 0.75, end: 1, s: 'break', _slices: 4 },
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('splice', () => {
|
||||
it('Can splice a sample', () => {
|
||||
sameFirst(
|
||||
s('break').splice(4, sequence(0, 1, 2, 3)),
|
||||
sequence(
|
||||
{ begin: 0, end: 0.25, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||
{ begin: 0.25, end: 0.5, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||
{ begin: 0.5, end: 0.75, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||
{ begin: 0.75, end: 1, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,6 +58,7 @@ export const valueToMidi = (value, fallbackValue) => {
|
||||
|
||||
/**
|
||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||
* @noAutocomplete
|
||||
*/
|
||||
export const getFreq = (noteOrMidi) => {
|
||||
if (typeof noteOrMidi === 'number') {
|
||||
@@ -68,6 +69,7 @@ export const getFreq = (noteOrMidi) => {
|
||||
|
||||
/**
|
||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||
* @noAutocomplete
|
||||
*/
|
||||
export const midi2note = (n) => {
|
||||
const oct = Math.floor(n / 12) - 1;
|
||||
@@ -204,3 +206,9 @@ export function parseFractional(numOrString) {
|
||||
}
|
||||
|
||||
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
|
||||
|
||||
export const splitAt = function (index, value) {
|
||||
return [value.slice(0, index), value.slice(index)];
|
||||
};
|
||||
|
||||
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||
|
||||
@@ -44,6 +44,6 @@ function createClock(
|
||||
};
|
||||
const getPhase = () => phase;
|
||||
// setCallback
|
||||
return { setDuration, start, stop, pause, duration, getPhase, minLatency };
|
||||
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
|
||||
}
|
||||
export default createClock;
|
||||
|
||||
@@ -28,9 +28,7 @@ export const csound = register('csound', (instrument, pat) => {
|
||||
logger('[csound] not loaded yet', 'warning');
|
||||
return;
|
||||
}
|
||||
if (typeof hap.value !== 'object') {
|
||||
throw new Error('csound only support objects as hap values');
|
||||
}
|
||||
hap.ensureObjectValue();
|
||||
let { gain = 0.8 } = hap.value;
|
||||
gain *= 0.2;
|
||||
|
||||
@@ -92,6 +90,7 @@ async function load() {
|
||||
['message'].forEach((k) => _csound.on(k, (...args) => eventLogger(k, args)));
|
||||
await _csound.setOption('-m0d'); // see -m flag https://csound.com/docs/manual/CommandFlags.html
|
||||
await _csound.setOption('--sample-accurate');
|
||||
await _csound.setOption('-odac');
|
||||
await _csound.compileCsdText(csd);
|
||||
// await _csound.compileOrc(livecodeOrc);
|
||||
await _csound.compileOrc(presetsOrc);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/csound",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.2",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@csound/browser": "^6.18.3",
|
||||
"@csound/browser": "6.18.5",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -5,8 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import * as _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern, isNote, getPlayableNoteValue, logger } from '@strudel.cycles/core';
|
||||
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
|
||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||
import { toMidi } from '@strudel.cycles/core';
|
||||
|
||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||
export const { WebMidi } = _WebMidi;
|
||||
@@ -63,7 +64,7 @@ function getDevice(output, outputs) {
|
||||
}
|
||||
|
||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||
Pattern.prototype.midi = function (output, channel = 1) {
|
||||
Pattern.prototype.midi = function (output) {
|
||||
if (!supportsMidi()) {
|
||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||
}
|
||||
@@ -90,11 +91,6 @@ Pattern.prototype.midi = function (output, channel = 1) {
|
||||
);
|
||||
}
|
||||
return this.onTrigger((time, hap) => {
|
||||
let note = getPlayableNoteValue(hap);
|
||||
const velocity = hap.context?.velocity ?? 0.9;
|
||||
if (!isNote(note)) {
|
||||
throw new Error('not a note: ' + note);
|
||||
}
|
||||
if (!midiReady) {
|
||||
return;
|
||||
}
|
||||
@@ -106,15 +102,34 @@ Pattern.prototype.midi = function (output, channel = 1) {
|
||||
.join(' | ')}`,
|
||||
);
|
||||
}
|
||||
// console.log('midi', value, output);
|
||||
hap.ensureObjectValue();
|
||||
|
||||
// calculate time
|
||||
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
|
||||
time = time * 1000 + timingOffset;
|
||||
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
||||
// await enableWebMidi()
|
||||
device.playNote(note, channel, {
|
||||
time,
|
||||
duration: hap.duration.valueOf() * 1000 - 5,
|
||||
attack: velocity,
|
||||
});
|
||||
|
||||
// destructure value
|
||||
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
|
||||
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
|
||||
const duration = hap.duration.valueOf() * 1000 - 5;
|
||||
|
||||
if (note) {
|
||||
const midiNumber = toMidi(note);
|
||||
device.playNote(midiNumber, midichan, {
|
||||
time,
|
||||
duration,
|
||||
attack: velocity,
|
||||
});
|
||||
}
|
||||
if (ccv && ccn) {
|
||||
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
||||
throw new Error('expected ccv to be a number between 0 and 1');
|
||||
}
|
||||
if (!['string', 'number'].includes(typeof ccn)) {
|
||||
throw new Error('expected ccn to be a number or a string');
|
||||
}
|
||||
const scaled = Math.round(ccv * 127);
|
||||
device.sendControlChange(ccn, scaled, midichan, { time });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -182,21 +182,21 @@ function peg$parse(input, options) {
|
||||
var peg$c8 = "#";
|
||||
var peg$c9 = "^";
|
||||
var peg$c10 = "_";
|
||||
var peg$c11 = ":";
|
||||
var peg$c12 = "[";
|
||||
var peg$c13 = "]";
|
||||
var peg$c14 = "{";
|
||||
var peg$c15 = "}";
|
||||
var peg$c16 = "%";
|
||||
var peg$c17 = "<";
|
||||
var peg$c18 = ">";
|
||||
var peg$c19 = "@";
|
||||
var peg$c20 = "!";
|
||||
var peg$c21 = "(";
|
||||
var peg$c22 = ")";
|
||||
var peg$c23 = "/";
|
||||
var peg$c24 = "*";
|
||||
var peg$c25 = "?";
|
||||
var peg$c11 = "[";
|
||||
var peg$c12 = "]";
|
||||
var peg$c13 = "{";
|
||||
var peg$c14 = "}";
|
||||
var peg$c15 = "%";
|
||||
var peg$c16 = "<";
|
||||
var peg$c17 = ">";
|
||||
var peg$c18 = "@";
|
||||
var peg$c19 = "!";
|
||||
var peg$c20 = "(";
|
||||
var peg$c21 = ")";
|
||||
var peg$c22 = "/";
|
||||
var peg$c23 = "*";
|
||||
var peg$c24 = "?";
|
||||
var peg$c25 = ":";
|
||||
var peg$c26 = "struct";
|
||||
var peg$c27 = "target";
|
||||
var peg$c28 = "euclid";
|
||||
@@ -237,21 +237,21 @@ function peg$parse(input, options) {
|
||||
var peg$e15 = peg$literalExpectation("#", false);
|
||||
var peg$e16 = peg$literalExpectation("^", false);
|
||||
var peg$e17 = peg$literalExpectation("_", false);
|
||||
var peg$e18 = peg$literalExpectation(":", false);
|
||||
var peg$e19 = peg$literalExpectation("[", false);
|
||||
var peg$e20 = peg$literalExpectation("]", false);
|
||||
var peg$e21 = peg$literalExpectation("{", false);
|
||||
var peg$e22 = peg$literalExpectation("}", false);
|
||||
var peg$e23 = peg$literalExpectation("%", false);
|
||||
var peg$e24 = peg$literalExpectation("<", false);
|
||||
var peg$e25 = peg$literalExpectation(">", false);
|
||||
var peg$e26 = peg$literalExpectation("@", false);
|
||||
var peg$e27 = peg$literalExpectation("!", false);
|
||||
var peg$e28 = peg$literalExpectation("(", false);
|
||||
var peg$e29 = peg$literalExpectation(")", false);
|
||||
var peg$e30 = peg$literalExpectation("/", false);
|
||||
var peg$e31 = peg$literalExpectation("*", false);
|
||||
var peg$e32 = peg$literalExpectation("?", false);
|
||||
var peg$e18 = peg$literalExpectation("[", false);
|
||||
var peg$e19 = peg$literalExpectation("]", false);
|
||||
var peg$e20 = peg$literalExpectation("{", false);
|
||||
var peg$e21 = peg$literalExpectation("}", false);
|
||||
var peg$e22 = peg$literalExpectation("%", false);
|
||||
var peg$e23 = peg$literalExpectation("<", false);
|
||||
var peg$e24 = peg$literalExpectation(">", false);
|
||||
var peg$e25 = peg$literalExpectation("@", false);
|
||||
var peg$e26 = peg$literalExpectation("!", false);
|
||||
var peg$e27 = peg$literalExpectation("(", false);
|
||||
var peg$e28 = peg$literalExpectation(")", false);
|
||||
var peg$e29 = peg$literalExpectation("/", false);
|
||||
var peg$e30 = peg$literalExpectation("*", false);
|
||||
var peg$e31 = peg$literalExpectation("?", false);
|
||||
var peg$e32 = peg$literalExpectation(":", false);
|
||||
var peg$e33 = peg$literalExpectation("struct", false);
|
||||
var peg$e34 = peg$literalExpectation("target", false);
|
||||
var peg$e35 = peg$literalExpectation("euclid", false);
|
||||
@@ -280,35 +280,36 @@ function peg$parse(input, options) {
|
||||
var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
|
||||
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
|
||||
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) };
|
||||
var peg$f12 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||
var peg$f12 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
|
||||
var peg$f13 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||
for (const op of ops) {
|
||||
op(result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
var peg$f13 = function(s) { return new PatternStub(s, 'fastcat'); };
|
||||
var peg$f14 = function(tail) { return { alignment: 'stack', list: tail }; };
|
||||
var peg$f15 = function(tail) { return { alignment: 'rand', list: tail }; };
|
||||
var peg$f16 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } };
|
||||
var peg$f17 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
||||
var peg$f18 = function(sc) { return sc; };
|
||||
var peg$f19 = function(s) { return { name: "struct", args: { mini:s }}};
|
||||
var peg$f20 = function(s) { return { name: "target", args : { name:s}}};
|
||||
var peg$f21 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
|
||||
var peg$f22 = function(a) { return { name: "stretch", args :{ amount: a}}};
|
||||
var peg$f23 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
|
||||
var peg$f24 = function(a) { return { name: "shift", args :{ amount: a}}};
|
||||
var peg$f25 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
|
||||
var peg$f26 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
|
||||
var peg$f27 = function(s, v) { return v};
|
||||
var peg$f28 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
|
||||
var peg$f29 = function(sg) {return sg};
|
||||
var peg$f30 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
|
||||
var peg$f31 = function(sc) { return sc };
|
||||
var peg$f32 = function(c) { return c };
|
||||
var peg$f33 = function(v) { return new CommandStub("setcps", { value: v})};
|
||||
var peg$f34 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
|
||||
var peg$f35 = function() { return new CommandStub("hush")};
|
||||
var peg$f14 = function(s) { return new PatternStub(s, 'fastcat'); };
|
||||
var peg$f15 = function(tail) { return { alignment: 'stack', list: tail }; };
|
||||
var peg$f16 = function(tail) { return { alignment: 'rand', list: tail }; };
|
||||
var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } };
|
||||
var peg$f18 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
||||
var peg$f19 = function(sc) { return sc; };
|
||||
var peg$f20 = function(s) { return { name: "struct", args: { mini:s }}};
|
||||
var peg$f21 = function(s) { return { name: "target", args : { name:s}}};
|
||||
var peg$f22 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
|
||||
var peg$f23 = function(a) { return { name: "stretch", args :{ amount: a}}};
|
||||
var peg$f24 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
|
||||
var peg$f25 = function(a) { return { name: "shift", args :{ amount: a}}};
|
||||
var peg$f26 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
|
||||
var peg$f27 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
|
||||
var peg$f28 = function(s, v) { return v};
|
||||
var peg$f29 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
|
||||
var peg$f30 = function(sg) {return sg};
|
||||
var peg$f31 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
|
||||
var peg$f32 = function(sc) { return sc };
|
||||
var peg$f33 = function(c) { return c };
|
||||
var peg$f34 = function(v) { return new CommandStub("setcps", { value: v})};
|
||||
var peg$f35 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
|
||||
var peg$f36 = function() { return new CommandStub("hush")};
|
||||
var peg$currPos = 0;
|
||||
var peg$savedPos = 0;
|
||||
var peg$posDetailsCache = [{ line: 1, column: 1 }];
|
||||
@@ -848,15 +849,6 @@ function peg$parse(input, options) {
|
||||
s0 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e17); }
|
||||
}
|
||||
if (s0 === peg$FAILED) {
|
||||
if (input.charCodeAt(peg$currPos) === 58) {
|
||||
s0 = peg$c11;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s0 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -899,11 +891,11 @@ function peg$parse(input, options) {
|
||||
s0 = peg$currPos;
|
||||
s1 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 91) {
|
||||
s2 = peg$c12;
|
||||
s2 = peg$c11;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s2 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||
}
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
@@ -911,11 +903,11 @@ function peg$parse(input, options) {
|
||||
if (s4 !== peg$FAILED) {
|
||||
s5 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 93) {
|
||||
s6 = peg$c13;
|
||||
s6 = peg$c12;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s6 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
}
|
||||
if (s6 !== peg$FAILED) {
|
||||
s7 = peg$parsews();
|
||||
@@ -943,11 +935,11 @@ function peg$parse(input, options) {
|
||||
s0 = peg$currPos;
|
||||
s1 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 123) {
|
||||
s2 = peg$c14;
|
||||
s2 = peg$c13;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s2 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e21); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
||||
}
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
@@ -955,11 +947,11 @@ function peg$parse(input, options) {
|
||||
if (s4 !== peg$FAILED) {
|
||||
s5 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 125) {
|
||||
s6 = peg$c15;
|
||||
s6 = peg$c14;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s6 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e22); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e21); }
|
||||
}
|
||||
if (s6 !== peg$FAILED) {
|
||||
s7 = peg$parsepolymeter_steps();
|
||||
@@ -990,11 +982,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 37) {
|
||||
s1 = peg$c16;
|
||||
s1 = peg$c15;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e22); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
@@ -1019,11 +1011,11 @@ function peg$parse(input, options) {
|
||||
s0 = peg$currPos;
|
||||
s1 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 60) {
|
||||
s2 = peg$c17;
|
||||
s2 = peg$c16;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s2 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
||||
}
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
@@ -1031,11 +1023,11 @@ function peg$parse(input, options) {
|
||||
if (s4 !== peg$FAILED) {
|
||||
s5 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 62) {
|
||||
s6 = peg$c18;
|
||||
s6 = peg$c17;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s6 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e25); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
||||
}
|
||||
if (s6 !== peg$FAILED) {
|
||||
s7 = peg$parsews();
|
||||
@@ -1088,6 +1080,9 @@ function peg$parse(input, options) {
|
||||
s0 = peg$parseop_replicate();
|
||||
if (s0 === peg$FAILED) {
|
||||
s0 = peg$parseop_degrade();
|
||||
if (s0 === peg$FAILED) {
|
||||
s0 = peg$parseop_tail();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1102,11 +1097,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 64) {
|
||||
s1 = peg$c19;
|
||||
s1 = peg$c18;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e25); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
@@ -1130,11 +1125,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 33) {
|
||||
s1 = peg$c20;
|
||||
s1 = peg$c19;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e27); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
@@ -1158,11 +1153,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 40) {
|
||||
s1 = peg$c21;
|
||||
s1 = peg$c20;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e27); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
@@ -1186,11 +1181,11 @@ function peg$parse(input, options) {
|
||||
}
|
||||
s12 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 41) {
|
||||
s13 = peg$c22;
|
||||
s13 = peg$c21;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s13 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||
}
|
||||
if (s13 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
@@ -1224,11 +1219,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 47) {
|
||||
s1 = peg$c23;
|
||||
s1 = peg$c22;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e30); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
@@ -1252,11 +1247,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 42) {
|
||||
s1 = peg$c24;
|
||||
s1 = peg$c23;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e31); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e30); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
@@ -1280,11 +1275,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 63) {
|
||||
s1 = peg$c25;
|
||||
s1 = peg$c24;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e32); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e31); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
@@ -1301,6 +1296,34 @@ function peg$parse(input, options) {
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parseop_tail() {
|
||||
var s0, s1, s2;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 58) {
|
||||
s1 = peg$c25;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e32); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f12(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parseslice_with_ops() {
|
||||
var s0, s1, s2, s3;
|
||||
|
||||
@@ -1314,7 +1337,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parseslice_op();
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f12(s1, s2);
|
||||
s0 = peg$f13(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1339,7 +1362,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f13(s1);
|
||||
s1 = peg$f14(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1388,7 +1411,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f14(s1);
|
||||
s1 = peg$f15(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1437,7 +1460,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f15(s1);
|
||||
s1 = peg$f16(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1458,7 +1481,7 @@ function peg$parse(input, options) {
|
||||
s2 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f16(s1, s2);
|
||||
s0 = peg$f17(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1478,7 +1501,7 @@ function peg$parse(input, options) {
|
||||
s2 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f17(s1, s2);
|
||||
s0 = peg$f18(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1499,7 +1522,7 @@ function peg$parse(input, options) {
|
||||
s4 = peg$parsequote();
|
||||
if (s4 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f18(s3);
|
||||
s0 = peg$f19(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1561,7 +1584,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsemini_or_operator();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f19(s3);
|
||||
s0 = peg$f20(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1594,7 +1617,7 @@ function peg$parse(input, options) {
|
||||
s5 = peg$parsequote();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f20(s4);
|
||||
s0 = peg$f21(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1639,7 +1662,7 @@ function peg$parse(input, options) {
|
||||
s7 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f21(s3, s5, s7);
|
||||
s0 = peg$f22(s3, s5, s7);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1672,7 +1695,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f22(s3);
|
||||
s0 = peg$f23(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1701,7 +1724,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f23(s3);
|
||||
s0 = peg$f24(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1730,7 +1753,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f24(s3);
|
||||
s0 = peg$f25(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1759,7 +1782,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f25(s3);
|
||||
s0 = peg$f26(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1801,7 +1824,7 @@ function peg$parse(input, options) {
|
||||
s5 = peg$parsequote();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f26(s4);
|
||||
s0 = peg$f27(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1876,11 +1899,11 @@ function peg$parse(input, options) {
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 91) {
|
||||
s3 = peg$c12;
|
||||
s3 = peg$c11;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s3 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||
}
|
||||
if (s3 !== peg$FAILED) {
|
||||
s4 = peg$parsews();
|
||||
@@ -1893,7 +1916,7 @@ function peg$parse(input, options) {
|
||||
s9 = peg$parsemini_or_operator();
|
||||
if (s9 !== peg$FAILED) {
|
||||
peg$savedPos = s7;
|
||||
s7 = peg$f27(s5, s9);
|
||||
s7 = peg$f28(s5, s9);
|
||||
} else {
|
||||
peg$currPos = s7;
|
||||
s7 = peg$FAILED;
|
||||
@@ -1910,7 +1933,7 @@ function peg$parse(input, options) {
|
||||
s9 = peg$parsemini_or_operator();
|
||||
if (s9 !== peg$FAILED) {
|
||||
peg$savedPos = s7;
|
||||
s7 = peg$f27(s5, s9);
|
||||
s7 = peg$f28(s5, s9);
|
||||
} else {
|
||||
peg$currPos = s7;
|
||||
s7 = peg$FAILED;
|
||||
@@ -1922,15 +1945,15 @@ function peg$parse(input, options) {
|
||||
}
|
||||
s7 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 93) {
|
||||
s8 = peg$c13;
|
||||
s8 = peg$c12;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s8 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
}
|
||||
if (s8 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f28(s5, s6);
|
||||
s0 = peg$f29(s5, s6);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1976,7 +1999,7 @@ function peg$parse(input, options) {
|
||||
s4 = peg$parsecomment();
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f29(s1);
|
||||
s0 = peg$f30(s1);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1998,7 +2021,7 @@ function peg$parse(input, options) {
|
||||
s5 = peg$parsemini_or_operator();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f30(s1, s5);
|
||||
s0 = peg$f31(s1, s5);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2023,7 +2046,7 @@ function peg$parse(input, options) {
|
||||
s1 = peg$parsemini_or_operator();
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f31(s1);
|
||||
s1 = peg$f32(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
if (s0 === peg$FAILED) {
|
||||
@@ -2056,7 +2079,7 @@ function peg$parse(input, options) {
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f32(s2);
|
||||
s0 = peg$f33(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2081,7 +2104,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f33(s3);
|
||||
s0 = peg$f34(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2110,7 +2133,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f34(s3);
|
||||
s0 = peg$f35(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2136,7 +2159,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f35();
|
||||
s1 = peg$f36();
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ quote = '"' / "'"
|
||||
// ------------------ steps and cycles ---------------------------
|
||||
|
||||
// single step definition (e.g bd)
|
||||
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
|
||||
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_"
|
||||
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
|
||||
|
||||
// define a sub cycle e.g. [1 2, 3 [4]]
|
||||
@@ -119,7 +119,7 @@ slice = step / sub_cycle / polymeter / slow_sequence
|
||||
|
||||
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
||||
// at this point, we assume we can represent them as regular sequence operators
|
||||
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade
|
||||
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail
|
||||
|
||||
op_weight = "@" a:number
|
||||
{ return x => x.options_['weight'] = a }
|
||||
@@ -139,6 +139,9 @@ op_fast = "*"a:slice
|
||||
op_degrade = "?"a:number?
|
||||
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
|
||||
|
||||
op_tail = ":" s:slice
|
||||
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
|
||||
|
||||
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
||||
slice_with_ops = s:slice ops:slice_op*
|
||||
{ const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||
|
||||
@@ -18,6 +18,7 @@ const applyOptions = (parent, code) => (pat, i) => {
|
||||
const ast = parent.source_[i];
|
||||
const options = ast.options_;
|
||||
const ops = options?.ops;
|
||||
|
||||
if (ops) {
|
||||
for (const op of ops) {
|
||||
switch (op.type_) {
|
||||
@@ -66,6 +67,11 @@ const applyOptions = (parent, code) => (pat, i) => {
|
||||
pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
|
||||
break;
|
||||
}
|
||||
case 'tail': {
|
||||
const friend = patternifyAST(op.arguments_.element, code);
|
||||
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.warn(`operator "${op.type_}" not implemented`);
|
||||
}
|
||||
|
||||
@@ -140,6 +140,9 @@ describe('mini', () => {
|
||||
expect(haps.length < 230).toBe(true);
|
||||
// 'Had too many cycles remaining after degradeBy 0.8');
|
||||
});
|
||||
it('supports lists', () => {
|
||||
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
|
||||
});
|
||||
/*it('supports the random choice operator ("|") with nesting', () => {
|
||||
const numCycles = 900;
|
||||
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
|
||||
|
||||
@@ -39,14 +39,16 @@ let startedAt = -1;
|
||||
/**
|
||||
*
|
||||
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
|
||||
* For more info, read [MIDI & OSC in the docs](https://strudel.tidalcycles.org/learn/input-output)
|
||||
*
|
||||
* @name osc
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
*/
|
||||
Pattern.prototype.osc = async function () {
|
||||
const osc = await connect();
|
||||
return this.onTrigger((time, hap, currentTime, cps = 1) => {
|
||||
Pattern.prototype.osc = function () {
|
||||
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
|
||||
hap.ensureObjectValue();
|
||||
const osc = await connect();
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
// time should be audio time of onset
|
||||
|
||||
@@ -13,24 +13,43 @@ npm i @strudel.cycles/react
|
||||
Here is a minimal example of how to set up a MiniRepl:
|
||||
|
||||
```jsx
|
||||
import { evalScope, controls } from '@strudel.cycles/core';
|
||||
import * as React from 'react';
|
||||
import '@strudel.cycles/react/dist/style.css';
|
||||
import { MiniRepl } from '@strudel.cycles/react';
|
||||
import { prebake } from '../repl/src/prebake.mjs';
|
||||
import { evalScope, controls } from '@strudel.cycles/core';
|
||||
import { samples, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
||||
|
||||
evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
/* probably import other strudel packages */
|
||||
);
|
||||
async function prebake() {
|
||||
await samples(
|
||||
'https://strudel.tidalcycles.org/tidal-drum-machines.json',
|
||||
'github:ritchse/tidal-drum-machines/main/machines/'
|
||||
);
|
||||
await samples(
|
||||
'https://strudel.tidalcycles.org/EmuSP12.json',
|
||||
'https://strudel.tidalcycles.org/EmuSP12/'
|
||||
);
|
||||
}
|
||||
|
||||
prebake();
|
||||
async function init() {
|
||||
await evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/tonal')
|
||||
);
|
||||
await prebake();
|
||||
initAudioOnFirstClick();
|
||||
}
|
||||
|
||||
export function Repl({ tune }) {
|
||||
return <MiniRepl tune={tune} hideOutsideView={true} />;
|
||||
if (typeof window !== 'undefined') {
|
||||
init();
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <MiniRepl tune={`s("bd sd,hh*4")`} />;
|
||||
}
|
||||
```
|
||||
|
||||
For a more sophisticated example, check out the [nano-repl](./examples/nano-repl/)!
|
||||
- Open [example on stackblitz](https://stackblitz.com/edit/react-ts-saaair?file=tune.tsx,App.tsx)
|
||||
- Also check out the [nano-repl](./examples/nano-repl/) for a more sophisticated example
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# nano-repl
|
||||
|
||||
this is an example of how to create a repl with strudel and react.
|
||||
|
||||
## Usage
|
||||
|
||||
after cloning the strudel repo:
|
||||
|
||||
```sh
|
||||
pnpm i
|
||||
cd packages/react/examples/nano-repl
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
you should now have a repl running at `http://localhost:5173/`
|
||||
@@ -66,7 +66,7 @@ function App() {
|
||||
const [code, setCode] = useState(defaultTune);
|
||||
const [view, setView] = useState();
|
||||
// const [code, setCode] = useState(`"c3".note().slow(2)`);
|
||||
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern } = useStrudel({
|
||||
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern, started } = useStrudel({
|
||||
code,
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime,
|
||||
@@ -75,8 +75,8 @@ function App() {
|
||||
useHighlighting({
|
||||
view,
|
||||
pattern,
|
||||
active: !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.getPhase(),
|
||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.now(),
|
||||
});
|
||||
|
||||
const error = evalError || schedulerError;
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: #222;
|
||||
--lineBackground: #22222250;
|
||||
--foreground: #fff;
|
||||
--caret: #ffcc00;
|
||||
--selection: rgba(128, 203, 196, 0.5);
|
||||
--selectionMatch: #036dd626;
|
||||
--lineHighlight: #00000050;
|
||||
--gutterBackground: transparent;
|
||||
--gutterForeground: #8a919966;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #123;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,23 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
module.exports = {
|
||||
// TODO: find out if leaving out tutorial path works now
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}', '../../src/**/*.{html,js,jsx,md,mdx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
colors: {
|
||||
// codemirror-theme settings
|
||||
background: 'var(--background)',
|
||||
lineBackground: 'var(--lineBackground)',
|
||||
foreground: 'var(--foreground)',
|
||||
caret: 'var(--caret)',
|
||||
selection: 'var(--selection)',
|
||||
selectionMatch: 'var(--selectionMatch)',
|
||||
gutterBackground: 'var(--gutterBackground)',
|
||||
gutterForeground: 'var(--gutterForeground)',
|
||||
gutterBorder: 'var(--gutterBorder)',
|
||||
lineHighlight: 'var(--lineHighlight)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/react",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.4",
|
||||
"description": "React components for strudel",
|
||||
"main": "src/index.js",
|
||||
"publishConfig": {
|
||||
@@ -32,10 +32,13 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.4.0",
|
||||
"@codemirror/lang-javascript": "^6.1.1",
|
||||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/view": "^6.7.3",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@replit/codemirror-emacs": "^6.0.0",
|
||||
"@replit/codemirror-vim": "^6.0.6",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/transpiler": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
@@ -44,17 +47,17 @@
|
||||
"react-hook-inview": "^4.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"postcss": "^8.4.18",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"vite": "^3.2.2"
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ evalScope(
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/midi'),
|
||||
import('@strudel.cycles/xen'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import jsdoc from '../../../../doc.json';
|
||||
|
||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||
const getInnerText = (html) => {
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
return div.textContent || div.innerText || '';
|
||||
};
|
||||
|
||||
export function Autocomplete({ doc }) {
|
||||
return (
|
||||
<div className="prose dark:prose-invert max-h-[400px] overflow-auto">
|
||||
<h3 className="pt-0 mt-0">{getDocLabel(doc)}</h3>
|
||||
<div dangerouslySetInnerHTML={{ __html: doc.description }} />
|
||||
<ul>
|
||||
{doc.params?.map(({ name, type, description }, i) => (
|
||||
<li key={i}>
|
||||
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
{doc.examples?.map((example, i) => (
|
||||
<div key={i}>
|
||||
<pre
|
||||
className="cursor-pointer"
|
||||
onMouseDown={(e) => {
|
||||
console.log('ola!');
|
||||
navigator.clipboard.writeText(example);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{example}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const jsdocCompletions = jsdoc.docs
|
||||
.filter(
|
||||
(doc) =>
|
||||
getDocLabel(doc) &&
|
||||
!getDocLabel(doc).startsWith('_') &&
|
||||
!['package'].includes(doc.kind) &&
|
||||
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
|
||||
)
|
||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
||||
.map((doc) /*: Completion */ => ({
|
||||
label: getDocLabel(doc),
|
||||
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
||||
info: () => {
|
||||
const node = document.createElement('div');
|
||||
// if Autocomplete is non-interactive, it could also be rendered at build time..
|
||||
// .. using renderToStaticMarkup
|
||||
createRoot(node).render(<Autocomplete doc={doc} />);
|
||||
return node;
|
||||
},
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
}));
|
||||
|
||||
export const strudelAutocomplete = (context /* : CompletionContext */) => {
|
||||
let word = context.matchBefore(/\w*/);
|
||||
if (word.from == word.to && !context.explicit) return null;
|
||||
return {
|
||||
from: word.from,
|
||||
options: jsdocCompletions,
|
||||
/* options: [
|
||||
{ label: 'match', type: 'keyword' },
|
||||
{ label: 'hello', type: 'variable', info: '(World)' },
|
||||
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
|
||||
], */
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import _CodeMirror from '@uiw/react-codemirror';
|
||||
import { EditorView, Decoration } from '@codemirror/view';
|
||||
import { StateField, StateEffect } from '@codemirror/state';
|
||||
@@ -6,6 +6,10 @@ import { javascript } from '@codemirror/lang-javascript';
|
||||
import strudelTheme from '../themes/strudel-theme';
|
||||
import './style.css';
|
||||
import { useCallback } from 'react';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
//import { strudelAutocomplete } from './Autocomplete';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
|
||||
export const setFlash = StateEffect.define();
|
||||
const flashField = StateField.define({
|
||||
@@ -49,19 +53,20 @@ const highlightField = StateField.define({
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setHighlights)) {
|
||||
const { haps } = e.value;
|
||||
const marks =
|
||||
e.value
|
||||
haps
|
||||
.map((hap) =>
|
||||
(hap.context.locations || []).map(({ start, end }) => {
|
||||
const color = hap.context.color || '#FFCA28';
|
||||
// const color = hap.context.color || e.value.color || '#FFCA28';
|
||||
let from = tr.newDoc.line(start.line).from + start.column;
|
||||
let to = tr.newDoc.line(end.line).from + end.column;
|
||||
const l = tr.newDoc.length;
|
||||
if (from > l || to > l) {
|
||||
return; // dont mark outside of range, as it will throw an error
|
||||
}
|
||||
// const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
|
||||
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
|
||||
//const mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
|
||||
const mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
|
||||
return mark.range(from, to);
|
||||
}),
|
||||
)
|
||||
@@ -79,9 +84,27 @@ const highlightField = StateField.define({
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
const extensions = [javascript(), strudelTheme, highlightField, flashField];
|
||||
const staticExtensions = [
|
||||
javascript(),
|
||||
highlightField,
|
||||
flashField,
|
||||
// javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
|
||||
// autocompletion({ override: [strudelAutocomplete] }),
|
||||
autocompletion({ override: [] }), // wait for https://github.com/uiwjs/react-codemirror/pull/458
|
||||
];
|
||||
|
||||
export default function CodeMirror({ value, onChange, onViewChanged, onSelectionChange, options, editorDidMount }) {
|
||||
export default function CodeMirror({
|
||||
value,
|
||||
onChange,
|
||||
onViewChanged,
|
||||
onSelectionChange,
|
||||
theme,
|
||||
keybindings,
|
||||
fontSize = 18,
|
||||
fontFamily = 'monospace',
|
||||
options,
|
||||
editorDidMount,
|
||||
}) {
|
||||
const handleOnChange = useCallback(
|
||||
(value) => {
|
||||
onChange?.(value);
|
||||
@@ -102,16 +125,27 @@ export default function CodeMirror({ value, onChange, onViewChanged, onSelection
|
||||
},
|
||||
[onSelectionChange],
|
||||
);
|
||||
const extensions = useMemo(() => {
|
||||
let bindings = {
|
||||
vim,
|
||||
emacs,
|
||||
};
|
||||
if (bindings[keybindings]) {
|
||||
return [...staticExtensions, bindings[keybindings]()];
|
||||
}
|
||||
return staticExtensions;
|
||||
}, [keybindings]);
|
||||
return (
|
||||
<>
|
||||
<div style={{ fontSize, fontFamily }} className="w-full">
|
||||
<_CodeMirror
|
||||
value={value}
|
||||
theme={theme || strudelTheme}
|
||||
onChange={handleOnChange}
|
||||
onCreateEditor={handleOnCreateEditor}
|
||||
onUpdate={handleOnUpdate}
|
||||
extensions={extensions}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,22 @@ import useHighlighting from '../hooks/useHighlighting.mjs';
|
||||
import useStrudel from '../hooks/useStrudel.mjs';
|
||||
import CodeMirror6, { flash } from './CodeMirror6';
|
||||
import { Icon } from './Icon';
|
||||
import styles from './MiniRepl.module.css';
|
||||
import './style.css';
|
||||
import { logger } from '@strudel.cycles/core';
|
||||
import useEvent from '../hooks/useEvent.mjs';
|
||||
import useKeydown from '../hooks/useKeydown.mjs';
|
||||
|
||||
const getTime = () => getAudioContext().currentTime;
|
||||
|
||||
export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTime, punchcard, canvasHeight = 200 }) {
|
||||
export function MiniRepl({
|
||||
tune,
|
||||
hideOutsideView = false,
|
||||
enableKeyboard,
|
||||
drawTime,
|
||||
punchcard,
|
||||
canvasHeight = 200,
|
||||
theme,
|
||||
}) {
|
||||
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
|
||||
const evalOnMount = !!drawTime;
|
||||
const drawContext = useCallback(
|
||||
@@ -63,6 +72,27 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTi
|
||||
getTime: () => scheduler.now(),
|
||||
});
|
||||
|
||||
// keyboard shortcuts
|
||||
useKeydown(
|
||||
useCallback(
|
||||
async (e) => {
|
||||
if (view?.hasFocus) {
|
||||
if (e.ctrlKey || e.altKey) {
|
||||
if (e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
flash(view);
|
||||
await activateCode();
|
||||
} else if (e.code === 'Period') {
|
||||
stop();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[activateCode, stop, view],
|
||||
),
|
||||
);
|
||||
|
||||
// set active pattern on ctrl+enter
|
||||
useLayoutEffect(() => {
|
||||
if (enableKeyboard) {
|
||||
@@ -98,20 +128,32 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTi
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={ref}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.buttons}>
|
||||
<button className={cx(styles.button, started ? 'animate-pulse' : '')} onClick={() => togglePlay()}>
|
||||
<div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight" ref={ref}>
|
||||
<div className="flex justify-between bg-lineHighlight">
|
||||
<div className="flex">
|
||||
<button
|
||||
className={cx(
|
||||
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
|
||||
started ? 'animate-pulse' : '',
|
||||
)}
|
||||
onClick={() => togglePlay()}
|
||||
>
|
||||
<Icon type={started ? 'stop' : 'play'} />
|
||||
</button>
|
||||
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
||||
<button
|
||||
className={cx(
|
||||
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
|
||||
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
onClick={() => activateCode()}
|
||||
>
|
||||
<Icon type="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
{error && <div className={styles.error}>{error.message}</div>}
|
||||
{error && <div className="text-right p-1 text-sm text-red-200">{error.message}</div>}
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
|
||||
<div className="overflow-auto relative">
|
||||
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} theme={theme} />}
|
||||
</div>
|
||||
{drawTime && (
|
||||
<canvas
|
||||
@@ -140,13 +182,3 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTi
|
||||
function useLogger(onTrigger) {
|
||||
useEvent(logger.key, onTrigger);
|
||||
}
|
||||
|
||||
// TODO: dedupe
|
||||
function useEvent(name, onTrigger, useCapture = false) {
|
||||
useEffect(() => {
|
||||
document.addEventListener(name, onTrigger, useCapture);
|
||||
return () => {
|
||||
document.removeEventListener(name, onTrigger, useCapture);
|
||||
};
|
||||
}, [onTrigger]);
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
.container {
|
||||
@apply rounded-md overflow-hidden bg-[#222222];
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply flex justify-between bg-slate-700 border-t border-slate-500;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
@apply flex;
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply cursor-pointer w-16 flex items-center justify-center p-1 bg-slate-700 border-r border-slate-500 text-white hover:bg-slate-600;
|
||||
}
|
||||
|
||||
.buttonDisabled {
|
||||
@apply w-16 flex items-center justify-center p-1 bg-slate-600 text-slate-400 cursor-not-allowed;
|
||||
}
|
||||
|
||||
.error {
|
||||
@apply text-right p-1 text-sm text-red-200;
|
||||
}
|
||||
|
||||
.body {
|
||||
@apply overflow-auto relative;
|
||||
}
|
||||
@@ -1,14 +1,26 @@
|
||||
:root {
|
||||
--background: #222;
|
||||
--lineBackground: #22222250;
|
||||
--foreground: #fff;
|
||||
--caret: #ffcc00;
|
||||
--selection: rgba(128, 203, 196, 0.5);
|
||||
--selectionMatch: #036dd626;
|
||||
--lineHighlight: #00000050;
|
||||
--gutterBackground: transparent;
|
||||
--gutterForeground: #8a919966;
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
background-color: transparent !important;
|
||||
height: 100%;
|
||||
z-index: 11;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.cm-theme {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cm-theme-light {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cm-line > * {
|
||||
background: #00000095;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
function useEvent(name, onTrigger, useCapture = false) {
|
||||
useEffect(() => {
|
||||
document.addEventListener(name, onTrigger, useCapture);
|
||||
return () => {
|
||||
document.removeEventListener(name, onTrigger, useCapture);
|
||||
};
|
||||
}, [onTrigger]);
|
||||
}
|
||||
|
||||
export default useEvent;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { setHighlights } from '../components/CodeMirror6';
|
||||
const round = (x) => Math.round(x * 1000) / 1000;
|
||||
|
||||
function useHighlighting({ view, pattern, active, getTime }) {
|
||||
const highlights = useRef([]);
|
||||
@@ -14,14 +15,14 @@ function useHighlighting({ view, pattern, active, getTime }) {
|
||||
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
|
||||
// see https://github.com/tidalcycles/strudel/issues/108
|
||||
const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy
|
||||
const span = [begin, audioTime + 1 / 60];
|
||||
const span = [round(begin), round(audioTime + 1 / 60)];
|
||||
lastEnd.current = span[1];
|
||||
highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
|
||||
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
||||
highlights.current = highlights.current.concat(haps); // add potential new onsets
|
||||
view.dispatch({ effects: setHighlights.of(highlights.current) }); // highlight all still active + new active haps
|
||||
view.dispatch({ effects: setHighlights.of({ haps: highlights.current }) }); // highlight all still active + new active haps
|
||||
} catch (err) {
|
||||
view.dispatch({ effects: setHighlights.of([]) });
|
||||
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
|
||||
}
|
||||
frame = requestAnimationFrame(updateHighlights);
|
||||
});
|
||||
@@ -30,7 +31,7 @@ function useHighlighting({ view, pattern, active, getTime }) {
|
||||
};
|
||||
} else {
|
||||
highlights.current = [];
|
||||
view.dispatch({ effects: setHighlights.of([]) });
|
||||
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
|
||||
}
|
||||
}
|
||||
}, [pattern, active, view]);
|
||||
|
||||
@@ -10,7 +10,6 @@ function useStrudel({
|
||||
getTime,
|
||||
evalOnMount = false,
|
||||
initialCode = '',
|
||||
autolink = false,
|
||||
beforeEval,
|
||||
afterEval,
|
||||
editPattern,
|
||||
@@ -33,7 +32,7 @@ function useStrudel({
|
||||
const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
|
||||
|
||||
// TODO: make sure this hook reruns when scheduler.started changes
|
||||
const { scheduler, evaluate, start, stop, pause } = useMemo(
|
||||
const { scheduler, evaluate, start, stop, pause, setCps } = useMemo(
|
||||
() =>
|
||||
repl({
|
||||
interval,
|
||||
@@ -51,15 +50,13 @@ function useStrudel({
|
||||
setCode(code);
|
||||
beforeEval?.();
|
||||
},
|
||||
afterEval: ({ pattern: _pattern, code }) => {
|
||||
afterEval: (res) => {
|
||||
const { pattern: _pattern, code } = res;
|
||||
setActiveCode(code);
|
||||
setPattern(_pattern);
|
||||
setEvalError();
|
||||
setSchedulerError();
|
||||
if (autolink) {
|
||||
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
||||
}
|
||||
afterEval?.();
|
||||
afterEval?.(res);
|
||||
},
|
||||
onToggle: (v) => {
|
||||
setStarted(v);
|
||||
@@ -156,6 +153,7 @@ function useStrudel({
|
||||
stop,
|
||||
pause,
|
||||
togglePlay,
|
||||
setCps,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// import 'tailwindcss/tailwind.css';
|
||||
|
||||
export { default as CodeMirror, flash } from './components/CodeMirror6';
|
||||
export * from './components/MiniRepl';
|
||||
export { default as useHighlighting } from './hooks/useHighlighting';
|
||||
export { default as CodeMirror, flash } from './components/CodeMirror6'; // !SSR
|
||||
export * from './components/MiniRepl'; // !SSR
|
||||
export { default as useHighlighting } from './hooks/useHighlighting'; // !SSR
|
||||
export { default as useStrudel } from './hooks/useStrudel'; // !SSR
|
||||
export { default as usePostMessage } from './hooks/usePostMessage';
|
||||
export { default as useStrudel } from './hooks/useStrudel';
|
||||
export { default as useKeydown } from './hooks/useKeydown';
|
||||
export { default as useEvent } from './hooks/useEvent';
|
||||
export { default as strudelTheme } from './themes/strudel-theme';
|
||||
export { default as cx } from './cx';
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
ReactDOM.render(
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from '@uiw/codemirror-themes';
|
||||
export const settings = {
|
||||
background: '#9bbc0f',
|
||||
foreground: '#0f380f', // whats that?
|
||||
caret: '#0f380f',
|
||||
selection: '#306230',
|
||||
selectionMatch: '#ffffff26',
|
||||
lineHighlight: '#8bac0f',
|
||||
lineBackground: '#9bbc0f50',
|
||||
//lineBackground: 'transparent',
|
||||
gutterBackground: 'transparent',
|
||||
gutterForeground: '#0f380f',
|
||||
light: true,
|
||||
};
|
||||
export default createTheme({
|
||||
theme: 'light',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.keyword, color: '#0f380f' },
|
||||
{ tag: t.operator, color: '#0f380f' },
|
||||
{ tag: t.special(t.variableName), color: '#0f380f' },
|
||||
{ tag: t.typeName, color: '#0f380f' },
|
||||
{ tag: t.atom, color: '#0f380f' },
|
||||
{ tag: t.number, color: '#0f380f' },
|
||||
{ tag: t.definition(t.variableName), color: '#0f380f' },
|
||||
{ tag: t.string, color: '#0f380f' },
|
||||
{ tag: t.special(t.string), color: '#0f380f' },
|
||||
{ tag: t.comment, color: '#0f380f' },
|
||||
{ tag: t.variableName, color: '#0f380f' },
|
||||
{ tag: t.tagName, color: '#0f380f' },
|
||||
{ tag: t.bracket, color: '#0f380f' },
|
||||
{ tag: t.meta, color: '#0f380f' },
|
||||
{ tag: t.attributeName, color: '#0f380f' },
|
||||
{ tag: t.propertyName, color: '#0f380f' },
|
||||
{ tag: t.className, color: '#0f380f' },
|
||||
{ tag: t.invalid, color: '#0f380f' },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from '@uiw/codemirror-themes';
|
||||
export const settings = {
|
||||
background: 'black',
|
||||
foreground: 'white', // whats that?
|
||||
caret: 'white',
|
||||
selection: '#ffffff20',
|
||||
selectionMatch: '#036dd626',
|
||||
lineHighlight: '#ffffff10',
|
||||
lineBackground: '#00000050',
|
||||
gutterBackground: 'transparent',
|
||||
gutterForeground: '#8a919966',
|
||||
};
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.keyword, color: 'white' },
|
||||
{ tag: t.operator, color: 'white' },
|
||||
{ tag: t.special(t.variableName), color: 'white' },
|
||||
{ tag: t.typeName, color: 'white' },
|
||||
{ tag: t.atom, color: 'white' },
|
||||
{ tag: t.number, color: 'white' },
|
||||
{ tag: t.definition(t.variableName), color: 'white' },
|
||||
{ tag: t.string, color: 'white' },
|
||||
{ tag: t.special(t.string), color: 'white' },
|
||||
{ tag: t.comment, color: 'white' },
|
||||
{ tag: t.variableName, color: 'white' },
|
||||
{ tag: t.tagName, color: 'white' },
|
||||
{ tag: t.bracket, color: 'white' },
|
||||
{ tag: t.meta, color: 'white' },
|
||||
{ tag: t.attributeName, color: 'white' },
|
||||
{ tag: t.propertyName, color: 'white' },
|
||||
{ tag: t.className, color: 'white' },
|
||||
{ tag: t.invalid, color: 'white' },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from '@uiw/codemirror-themes';
|
||||
export const settings = {
|
||||
background: '#051DB5',
|
||||
lineBackground: '#051DB550',
|
||||
foreground: 'white', // whats that?
|
||||
caret: 'white',
|
||||
selection: 'rgba(128, 203, 196, 0.5)',
|
||||
selectionMatch: '#036dd626',
|
||||
// lineHighlight: '#8a91991a', // original
|
||||
lineHighlight: '#00000050',
|
||||
gutterBackground: 'transparent',
|
||||
// gutterForeground: '#8a919966',
|
||||
gutterForeground: '#8a919966',
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.keyword, color: 'white' },
|
||||
{ tag: t.operator, color: 'white' },
|
||||
{ tag: t.special(t.variableName), color: 'white' },
|
||||
{ tag: t.typeName, color: 'white' },
|
||||
{ tag: t.atom, color: 'white' },
|
||||
{ tag: t.number, color: 'white' },
|
||||
{ tag: t.definition(t.variableName), color: 'white' },
|
||||
{ tag: t.string, color: 'white' },
|
||||
{ tag: t.special(t.string), color: 'white' },
|
||||
{ tag: t.comment, color: 'white' },
|
||||
{ tag: t.variableName, color: 'white' },
|
||||
{ tag: t.tagName, color: 'white' },
|
||||
{ tag: t.bracket, color: 'white' },
|
||||
{ tag: t.meta, color: 'white' },
|
||||
{ tag: t.attributeName, color: 'white' },
|
||||
{ tag: t.propertyName, color: 'white' },
|
||||
{ tag: t.className, color: 'white' },
|
||||
{ tag: t.invalid, color: 'white' },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from '@uiw/codemirror-themes';
|
||||
export const settings = {
|
||||
background: 'black',
|
||||
foreground: '#41FF00', // whats that?
|
||||
caret: '#41FF00',
|
||||
selection: '#ffffff20',
|
||||
selectionMatch: '#036dd626',
|
||||
lineHighlight: '#ffffff10',
|
||||
gutterBackground: 'transparent',
|
||||
gutterForeground: '#8a919966',
|
||||
};
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.keyword, color: '#41FF00' },
|
||||
{ tag: t.operator, color: '#41FF00' },
|
||||
{ tag: t.special(t.variableName), color: '#41FF00' },
|
||||
{ tag: t.typeName, color: '#41FF00' },
|
||||
{ tag: t.atom, color: '#41FF00' },
|
||||
{ tag: t.number, color: '#41FF00' },
|
||||
{ tag: t.definition(t.variableName), color: '#41FF00' },
|
||||
{ tag: t.string, color: '#41FF00' },
|
||||
{ tag: t.special(t.string), color: '#41FF00' },
|
||||
{ tag: t.comment, color: '#41FF00' },
|
||||
{ tag: t.variableName, color: '#41FF00' },
|
||||
{ tag: t.tagName, color: '#41FF00' },
|
||||
{ tag: t.bracket, color: '#41FF00' },
|
||||
{ tag: t.meta, color: '#41FF00' },
|
||||
{ tag: t.attributeName, color: '#41FF00' },
|
||||
{ tag: t.propertyName, color: '#41FF00' },
|
||||
{ tag: t.className, color: '#41FF00' },
|
||||
{ tag: t.invalid, color: '#41FF00' },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from '@uiw/codemirror-themes';
|
||||
export const settings = {
|
||||
background: 'white',
|
||||
foreground: 'black', // whats that?
|
||||
caret: 'black',
|
||||
selection: 'rgba(128, 203, 196, 0.5)',
|
||||
selectionMatch: '#ffffff26',
|
||||
lineHighlight: '#cccccc50',
|
||||
lineBackground: '#ffffff50',
|
||||
gutterBackground: 'transparent',
|
||||
gutterForeground: 'black',
|
||||
light: true,
|
||||
};
|
||||
export default createTheme({
|
||||
theme: 'light',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.keyword, color: 'black' },
|
||||
{ tag: t.operator, color: 'black' },
|
||||
{ tag: t.special(t.variableName), color: 'black' },
|
||||
{ tag: t.typeName, color: 'black' },
|
||||
{ tag: t.atom, color: 'black' },
|
||||
{ tag: t.number, color: 'black' },
|
||||
{ tag: t.definition(t.variableName), color: 'black' },
|
||||
{ tag: t.string, color: 'black' },
|
||||
{ tag: t.special(t.string), color: 'black' },
|
||||
{ tag: t.comment, color: 'black' },
|
||||
{ tag: t.variableName, color: 'black' },
|
||||
{ tag: t.tagName, color: 'black' },
|
||||
{ tag: t.bracket, color: 'black' },
|
||||
{ tag: t.meta, color: 'black' },
|
||||
{ tag: t.attributeName, color: 'black' },
|
||||
{ tag: t.propertyName, color: 'black' },
|
||||
{ tag: t.className, color: 'black' },
|
||||
{ tag: t.invalid, color: 'black' },
|
||||
],
|
||||
});
|
||||
@@ -1,7 +1,21 @@
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
colors: {
|
||||
// codemirror-theme settings
|
||||
background: 'var(--background)',
|
||||
lineBackground: 'var(--lineBackground)',
|
||||
foreground: 'var(--foreground)',
|
||||
caret: 'var(--caret)',
|
||||
selection: 'var(--selection)',
|
||||
selectionMatch: 'var(--selectionMatch)',
|
||||
gutterBackground: 'var(--gutterBackground)',
|
||||
gutterForeground: 'var(--gutterForeground)',
|
||||
gutterBorder: 'var(--gutterBorder)',
|
||||
lineHighlight: 'var(--lineHighlight)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
corePlugins: {
|
||||
|
||||
@@ -9,6 +9,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
import '../tonal.mjs'; // need to import this to add prototypes
|
||||
import { pure, controls, seq } from '@strudel.cycles/core';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mini } from '../../mini/mini.mjs';
|
||||
const { n } = controls;
|
||||
|
||||
describe('tonal', () => {
|
||||
@@ -30,4 +31,18 @@ describe('tonal', () => {
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('scale with colon', () => {
|
||||
expect(
|
||||
n(0, 1, 2)
|
||||
.scale('C:major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('scale with mininotation colon', () => {
|
||||
expect(
|
||||
n(0, 1, 2)
|
||||
.scale(mini('C:major'))
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,28 +123,37 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
|
||||
/**
|
||||
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
||||
*
|
||||
* The scale name has the form "TO? N" wher
|
||||
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
|
||||
*
|
||||
* - T = Tonic
|
||||
* - O = Octave (optional, defaults to 3)
|
||||
* - N = Name of scale, available names can be found [here](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
|
||||
* The root note defaults to octave 3, if no octave number is given.
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name scale
|
||||
* @param {string} scale Name of scale
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "0 2 4 6 4 2".scale("C2:major").note()
|
||||
* @example
|
||||
* "0 2 4 6 4 2"
|
||||
* .scale(seq('C2 major', 'C2 minor').slow(2))
|
||||
* .scale("C2:<major minor>")
|
||||
* .note()
|
||||
* @example
|
||||
* "0 1 2 3 4 5 6 7".rev().scale("C2:<major minor>").note()
|
||||
* .s("folkharp")
|
||||
*/
|
||||
|
||||
export const scale = register('scale', function (scale /* : string */, pat) {
|
||||
export const scale = register('scale', function (scale, pat) {
|
||||
// Supports ':' list syntax in mininotation
|
||||
if (Array.isArray(scale)) {
|
||||
scale = scale.join(' ');
|
||||
}
|
||||
return pat.withHap((hap) => {
|
||||
const isObject = typeof hap.value === 'object';
|
||||
let note = isObject ? hap.value.n : hap.value;
|
||||
const asNumber = Number(note);
|
||||
if (!isNaN(asNumber)) {
|
||||
// TODO: worth keeping for supporting ':' in (non-mininotation) strings?
|
||||
scale = scale.replaceAll(':', ' ');
|
||||
let [tonic, scaleName] = Scale.tokenize(scale);
|
||||
const { pc, oct = 3 } = Note.get(tonic);
|
||||
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
|
||||
|
||||
@@ -111,7 +111,9 @@ let sampleCache = { current: undefined };
|
||||
|
||||
/**
|
||||
* Loads a collection of samples to use with `s`
|
||||
*
|
||||
* @example
|
||||
* samples('github:tidalcycles/Dirt-Samples/master');
|
||||
* s("[bd ~]*2, [~ hh]*2, ~ sd")
|
||||
* @example
|
||||
* samples({
|
||||
* bd: '808bd/BD0000.WAV',
|
||||
|
||||
@@ -97,17 +97,6 @@ const getSoundfontKey = (s) => {
|
||||
return;
|
||||
};
|
||||
|
||||
const splitSN = (s, n) => {
|
||||
if (!s.includes(':')) {
|
||||
return [s, n];
|
||||
}
|
||||
let [s2, n2] = s.split(':');
|
||||
if (isNaN(Number(n2))) {
|
||||
return [s, n];
|
||||
}
|
||||
return [s2, n2];
|
||||
};
|
||||
|
||||
let workletsLoading;
|
||||
function loadWorklets() {
|
||||
if (workletsLoading) {
|
||||
@@ -191,23 +180,11 @@ function effectSend(input, effect, wet) {
|
||||
}
|
||||
|
||||
// export const webaudioOutput = async (t, hap, ct, cps) => {
|
||||
export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
||||
export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
|
||||
const ac = getAudioContext();
|
||||
/* if (isNote(hap.value)) {
|
||||
// supports primitive hap values that look like notes
|
||||
hap.value = { note: hap.value };
|
||||
} */
|
||||
if (typeof hap.value !== 'object') {
|
||||
logger(
|
||||
`hap.value "${hap.value}" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
|
||||
'error',
|
||||
);
|
||||
/* throw new Error(
|
||||
`hap.value "${hap.value}"" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
|
||||
); */
|
||||
return;
|
||||
}
|
||||
// calculate correct time (tone.js workaround)
|
||||
hap.ensureObjectValue();
|
||||
|
||||
// calculate absolute time
|
||||
let t = ac.currentTime + deadline;
|
||||
// destructure value
|
||||
let {
|
||||
@@ -220,20 +197,14 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
||||
note,
|
||||
gain = 0.8,
|
||||
// low pass
|
||||
lpf,
|
||||
cutoff = lpf,
|
||||
lpq = 1,
|
||||
resonance = lpq,
|
||||
cutoff,
|
||||
resonance = 1,
|
||||
// high pass
|
||||
hpf,
|
||||
hcutoff = hpf,
|
||||
hpq = 1,
|
||||
hresonance = hpq,
|
||||
hcutoff,
|
||||
hresonance = 1,
|
||||
// band pass
|
||||
bpf,
|
||||
bandf = bpf,
|
||||
bpq = 1,
|
||||
bandq = bpq,
|
||||
bandf,
|
||||
bandq = 1,
|
||||
//
|
||||
coarse,
|
||||
crush,
|
||||
@@ -253,7 +224,6 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
||||
orbit = 1,
|
||||
room,
|
||||
size = 2,
|
||||
roomsize = size,
|
||||
} = hap.value;
|
||||
const { velocity = 1 } = hap.context;
|
||||
gain *= velocity; // legacy fix for velocity
|
||||
@@ -262,12 +232,6 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
||||
if (bank && s) {
|
||||
s = `${bank}_${s}`;
|
||||
}
|
||||
if (typeof s === 'string') {
|
||||
[s, n] = splitSN(s, n);
|
||||
}
|
||||
if (typeof note === 'string') {
|
||||
[note, n] = splitSN(note, n);
|
||||
}
|
||||
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
|
||||
// destructure adsr here, because the default should be different for synths and samples
|
||||
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = hap.value;
|
||||
@@ -324,7 +288,7 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
||||
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
|
||||
if (unit === 'c') {
|
||||
// are there other units?
|
||||
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration;
|
||||
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * cps;
|
||||
}
|
||||
let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
||||
@@ -385,8 +349,8 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
||||
}
|
||||
// reverb
|
||||
let reverbSend;
|
||||
if (room > 0 && roomsize > 0) {
|
||||
const reverbNode = getReverb(orbit, roomsize);
|
||||
if (room > 0 && size > 0) {
|
||||
const reverbNode = getReverb(orbit, size);
|
||||
reverbSend = effectSend(post, reverbNode, room);
|
||||
}
|
||||
|
||||
@@ -397,7 +361,7 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
||||
chain[0].onended = () => chain.concat([delaySend, reverbSend]).forEach((n) => n?.disconnect());
|
||||
};
|
||||
|
||||
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps);
|
||||
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps, cps);
|
||||
|
||||
Pattern.prototype.webaudio = function () {
|
||||
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
|
||||
|
||||
@@ -59,6 +59,7 @@ export function loadWebDirt(config) {
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("bd*2 hh sd hh").n("<0 1>").webdirt()
|
||||
* @noAutocomplete
|
||||
*/
|
||||
Pattern.prototype.webdirt = function () {
|
||||
// create a WebDirt object and initialize Web Audio context
|
||||
|
||||
@@ -0,0 +1,597 @@
|
||||
[
|
||||
{
|
||||
"id": "toussaintEuclideanAlgorithmGenerates2005",
|
||||
"type": "paper-conference",
|
||||
"abstract": "The Euclidean algorithm (which comes down to us from Euclid's Elements) computes the greatest common divisor of two given integers. It is shown here that the structure of the Euclidean algorithm may be used to automatically generate, very efficiently, a large family of rhythms used as timelines (ostinatos), in traditional world music. These rhythms, here dubbed Euclidean rhythms, have the property that their onset patterns are distributed as evenly as possible in a mathematically precise sense, and optimal manner.",
|
||||
"container-title": "In Proceedings of BRIDGES: Mathematical Connections in Art, Music and Science",
|
||||
"page": "47–56",
|
||||
"title": "The Euclidean Algorithm Generates Traditional Musical Rhythms",
|
||||
"URL": "http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231",
|
||||
"author": [
|
||||
{
|
||||
"family": "Toussaint",
|
||||
"given": "Godfried"
|
||||
}
|
||||
],
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2005"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "roberts2015",
|
||||
"type": "paper-conference",
|
||||
"abstract": "We describe research extending the interactive affordances of textual code fragments in creative coding environments. In particular we examine the potential of source code both to display the state of running processes and also to alter state using means other than traditional text editing. In contrast to previous research that has focused on the inclusion of additional interactive widgets inside or alongside text editors, our research adds a parsing stage to the runtime evaluation of code fragments and imparts additional interactive capabilities on the source code itself. After implementing various techniques in the creative coding environment Gibber, we evaluate our research through a survey on the various methods of visual feedback provided by our research. In addition to results quantifying preferences for certain techniques over others, we found near unanimous support among survey respondents for including similar techniques in other live coding environments.",
|
||||
"collection-title": "NIME 2015",
|
||||
"container-title": "Proceedings of the international conference on New Interfaces for Musical Expression",
|
||||
"event-place": "Baton Rouge, Louisiana, USA",
|
||||
"ISBN": "978-0-692-49547-6",
|
||||
"page": "126–131",
|
||||
"publisher": "The School of Music and the Center for Computation and Technology (CCT), Louisiana State University",
|
||||
"publisher-place": "Baton Rouge, Louisiana, USA",
|
||||
"source": "ACM Digital Library",
|
||||
"title": "Beyond Editing: Extended Interaction with Textual Code Fragments",
|
||||
"title-short": "Beyond Editing",
|
||||
"author": [
|
||||
{
|
||||
"family": "Roberts",
|
||||
"given": "Charles"
|
||||
},
|
||||
{
|
||||
"family": "Wright",
|
||||
"given": "Matthew"
|
||||
},
|
||||
{
|
||||
"family": "Kuchera-Morin",
|
||||
"given": "JoAnn"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2021",
|
||||
1,
|
||||
27
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2015",
|
||||
5,
|
||||
30
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rohrhuberSuperDirt2022",
|
||||
"type": "software",
|
||||
"abstract": "Tidal Audio Engine",
|
||||
"genre": "SuperCollider",
|
||||
"license": "GPL-2.0",
|
||||
"note": "original-date: 2015-11-03T20:18:44Z",
|
||||
"publisher": "musikinformatik",
|
||||
"source": "GitHub",
|
||||
"title": "SuperDirt",
|
||||
"URL": "https://github.com/musikinformatik/SuperDirt",
|
||||
"author": [
|
||||
{
|
||||
"family": "Rohrhuber",
|
||||
"given": "Julian"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
6,
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
6,
|
||||
24
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mcleanAlgorithmicPattern2020",
|
||||
"type": "paper-conference",
|
||||
"abstract": "This paper brings together two main perspectives on algorithmic pattern. First, the writing of musical patterns in live coding performance, and second, the weaving of patterns in textiles. In both cases, algorithmic pattern is an interface between the human and the outcome, where small changes have far-reaching impact on the results. By bringing contemporary live coding and ancient textile approaches together, we reach a common view of pattern as algorithmic movement (e.g. looping, shifting, reflecting, interfering) in the making of things. This works beyond the usual definition of pattern used in musical interfaces, of mere repeating sequences. We conclude by considering the place of algorithmic pattern in a wider activity of making.",
|
||||
"container-title": "Proceedings of the International Conference on New Interfaces for Musical Expression",
|
||||
"event-place": "Birmingham, UK",
|
||||
"event-title": "NIME2020",
|
||||
"page": "265--270",
|
||||
"publisher-place": "Birmingham, UK",
|
||||
"title": "Algorithmic Pattern",
|
||||
"URL": "https://zenodo.org/record/4813352",
|
||||
"author": [
|
||||
{
|
||||
"family": "Mclean",
|
||||
"given": "Alex"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
1,
|
||||
9
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2020",
|
||||
6,
|
||||
1
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mcleanFeedforward2020",
|
||||
"type": "paper-conference",
|
||||
"abstract": "This is an improvised, from-scratch live coding performance. The NIME interface which this performance showcases is the new Feedfoward editor for the TidalCycles live coding environment. Feedforward is written in Haskell using the ncurses library for terminal-based user interfaces. It runs on low-powered hardware including the Raspberry Pi Zero, with formative testing of prototypes conducted with several groups of children between the ages of 8 and 14. Feedforward has a number of features designed to support improvised, multi-pattern live coding. Individual Tidal patterns are addressable with hotkeys for fast mute and unmuting. Each pattern has a stereo VU meter, to aid the quick matching of sound to pattern within a mix. In addition, TidalCycles has been extended to store context with each event, so that source code positions in its polyrhythmic sequence mini-notation are tracked. This allows steps to be highlighted in the source code when- ever they are active. This works even when Tidal combinators have been applied to manipulate the timeline. Formal evaluation has yet to take place, but this feature appears to support learning of how pattern manipulations work in Tidal. Feedforward and TidalCycles is free/open source software under a GPL licence version 3.0.",
|
||||
"container-title": "Proceedings of New Interfaces for Musical Expression",
|
||||
"event-place": "Birmingham",
|
||||
"event-title": "NIME2020",
|
||||
"publisher-place": "Birmingham",
|
||||
"title": "Feedforward",
|
||||
"URL": "https://zenodo.org/record/6353969",
|
||||
"author": [
|
||||
{
|
||||
"family": "McLean",
|
||||
"given": "Alex"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
6,
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2020",
|
||||
7,
|
||||
21
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "robertsGibberLiveCoding2012",
|
||||
"type": "paper-conference",
|
||||
"abstract": "We present Gibber: a live coding environment for web browsers. Gibber performances are written in pure Java-Script with no syntactical additions or modifications; this enables Gibber code to be executed in any web page viewed inside a browser implementing a realtime audio API. Gib-ber offers an array of synthesis options (FM, granular, subtractive, physical modeling), audio effects and sequenc-ing objects to control them. The Gibber environment en-ables simple networked performances where multiple users simultaneously control a remote instance of Gibber. We strove to make the syntax of Gibber clear and concise; when coupled with the ability to run examples in any web page this gives Gibber interesting possibilities as an edu-cational tool. 1.",
|
||||
"container-title": "In Proceedings of the 2012 International Computer Music Conference",
|
||||
"source": "CiteSeer",
|
||||
"title": "Gibber: Live coding audio in the browser",
|
||||
"title-short": "Gibber",
|
||||
"author": [
|
||||
{
|
||||
"family": "Roberts",
|
||||
"given": "Charles"
|
||||
},
|
||||
{
|
||||
"family": "Kuchera-morin",
|
||||
"given": "Joann"
|
||||
}
|
||||
],
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2012"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ogbornEstuaryBrowserbasedCollaborative2017",
|
||||
"type": "paper-conference",
|
||||
"abstract": "This paper describes the initial design and development of Estuary, a browser-based collaborative projectional editing environment built on top of the popular TidalCycles language for the live coding of musical pattern. Key features of Estuary include a strict form of structure editing (making syntactical errors impossible), a click-only border-free approach to interface design, explicit notations to modulate the liveness of different parts of the code, and a server-based network collaboration system that can be used for many simultaneous collaborative live coding performances, as well as to present different views of the same live coding activity. Estuary has been developed using Reflex-DOM, a Haskell-based framework for web development whose strictness promises robustness and security advantages.",
|
||||
"container-title": "Proceedings of the International Conference on Live Coding",
|
||||
"event-place": "Morelia",
|
||||
"event-title": "ICLC2017",
|
||||
"language": "en",
|
||||
"page": "11",
|
||||
"publisher-place": "Morelia",
|
||||
"source": "Zotero",
|
||||
"title": "Estuary: Browser-based Collaborative Projectional Live Coding of Musical Patterns",
|
||||
"author": [
|
||||
{
|
||||
"family": "Ogborn",
|
||||
"given": "David"
|
||||
},
|
||||
{
|
||||
"family": "Beverley",
|
||||
"given": "Jamie"
|
||||
},
|
||||
{
|
||||
"family": "Navarro del Angel",
|
||||
"given": "Luis"
|
||||
},
|
||||
{
|
||||
"family": "Tsabary",
|
||||
"given": "Eldad"
|
||||
},
|
||||
{
|
||||
"family": "McLean",
|
||||
"given": "Alex"
|
||||
}
|
||||
],
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2017"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mcleanTidalVortexZero2022",
|
||||
"type": "paper-conference",
|
||||
"abstract": "In this paper we introduce ‘version zero’ of TidalVortex, an alternative implementation of the TidalCycles live coding system, using the Python programming language. This is open-ended work, exploring what happens when we try to extract the 'essence' of a system like TidalCycles and translate it into another programming language, while taking advantage of the affordance of its new host. First, we review the substantial prior art in porting TidalCycles, and in representing musical patterns in Python. We then compare equivalent patterns written in Haskell (TidalCycles) and Python (TidalVortex), and relate implementation details of how functional reactive paradigms have translated from the pure functional, strongly typed Haskell to the more multi-paradigm, dynamically typed Python. Finally, we conclude with reflections and generalisable outcomes.",
|
||||
"container-title": "Proceedings of the 7th International Conference on Live Coding",
|
||||
"DOI": "10.5281/zenodo.6456380",
|
||||
"event-place": "Limerick, Ireland",
|
||||
"event-title": "ICMC2022",
|
||||
"note": "event-title: International Conference on Computer Music (ICMC)",
|
||||
"publisher": "Zenodo",
|
||||
"publisher-place": "Limerick, Ireland",
|
||||
"title": "TidalVortex Zero",
|
||||
"URL": "https://zenodo.org/record/6456380/export/hx",
|
||||
"author": [
|
||||
{
|
||||
"family": "McLean",
|
||||
"given": "Alex"
|
||||
},
|
||||
{
|
||||
"family": "Forment",
|
||||
"given": "Raphaël"
|
||||
},
|
||||
{
|
||||
"family": "Le Beux",
|
||||
"given": "Sylvain"
|
||||
},
|
||||
{
|
||||
"family": "Silvani",
|
||||
"given": "Damián"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
6,
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
4,
|
||||
12
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solomonPurescriptocarina2022",
|
||||
"type": "software",
|
||||
"abstract": "Web audio graphs as a stream",
|
||||
"genre": "PureScript",
|
||||
"license": "Apache-2.0",
|
||||
"note": "original-date: 2021-03-25T12:19:55Z",
|
||||
"source": "GitHub",
|
||||
"title": "purescript-ocarina",
|
||||
"URL": "https://github.com/mikesol/purescript-ocarina",
|
||||
"author": [
|
||||
{
|
||||
"family": "Solomon",
|
||||
"given": "Mike"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
6,
|
||||
24
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
6,
|
||||
20
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "orlareyFaustProgrammingLanguage2023",
|
||||
"type": "software",
|
||||
"abstract": "Functional programming language for signal processing and sound synthesis",
|
||||
"genre": "C++",
|
||||
"note": "original-date: 2016-11-05T13:09:11Z",
|
||||
"publisher": "GRAME",
|
||||
"source": "GitHub",
|
||||
"title": "Faust - Programming Language for Audio Applications and Plugins",
|
||||
"URL": "https://github.com/grame-cncm/faust",
|
||||
"author": [
|
||||
{
|
||||
"family": "Orlarey",
|
||||
"given": "Yann"
|
||||
},
|
||||
{
|
||||
"family": "Fober",
|
||||
"given": "Dominique"
|
||||
},
|
||||
{
|
||||
"family": "Letz",
|
||||
"given": "Stéphane"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
13
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "jackHydra2023",
|
||||
"type": "software",
|
||||
"abstract": "Livecoding networked visuals in the browser",
|
||||
"genre": "JavaScript",
|
||||
"license": "AGPL-3.0",
|
||||
"note": "original-date: 2022-06-13T16:57:34Z",
|
||||
"source": "GitHub",
|
||||
"title": "Hydra",
|
||||
"URL": "https://github.com/ojack/hydra",
|
||||
"author": [
|
||||
{
|
||||
"family": "Jack",
|
||||
"given": "Olivia"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
8
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lanGlicol2023",
|
||||
"type": "software",
|
||||
"abstract": "Graph-oriented live coding language and music DSP library written in Rust",
|
||||
"genre": "Rust",
|
||||
"license": "MIT",
|
||||
"note": "original-date: 2020-07-13T22:13:33Z",
|
||||
"source": "GitHub",
|
||||
"title": "Glicol",
|
||||
"URL": "https://github.com/chaosprint/glicol",
|
||||
"author": [
|
||||
{
|
||||
"family": "Lan",
|
||||
"given": "Qichao"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ogbornWebDirt2022",
|
||||
"type": "software",
|
||||
"abstract": "Sampling engine implemented in Web Audio API (rough re-creation of Dirt)",
|
||||
"genre": "JavaScript",
|
||||
"license": "GPL-3.0",
|
||||
"note": "original-date: 2016-05-04T21:19:04Z",
|
||||
"source": "GitHub",
|
||||
"title": "WebDirt",
|
||||
"URL": "https://github.com/dktr0/WebDirt",
|
||||
"author": [
|
||||
{
|
||||
"family": "Ogborn",
|
||||
"given": "David"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
12,
|
||||
14
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "roosStrudelAlgorithmicPatterns2022",
|
||||
"type": "paper-conference",
|
||||
"abstract": "This paper introduces Strudel (or sometimes StrudelCycles), an alternative implementation of the Tidal (or Tidal-Cycles) live coding system, using the JavaScript programming language. Strudel is an attempt to make live coding more accessible, by creating a system that runs entirely in the browser, while opening Tidals approach to algorithmic patterns (Mclean 2020) up to modern audio/visual web technologies. The Strudel REPL is a live code editor dedicated to manipulating Strudel patterns while they play, with builtin visual feedback. While Strudel is written in JavaScript, the API is optimized for simplicity and readability by applying code transformations on the syntax tree level, allowing language operations that would otherwise be impossible. The application supports multiple ways to output sound, including Tone.js, Web Audio nodes, OSC (Open Sound Control) messages, Web Serial andWeb MIDI. The project is split into multiple packages, allowing granular reuse in other applications. Apart from TidalCycles, Strudel draws inspiration from many prior existing projects like TidalVortex (McLean et al. 2022), Gibber (Roberts and Kuchera-morin 2012), Estuary (Ogborn et al. 2017), Hydra (Jack [2022] 2022), Ocarina (Solomon [2021] 2022) and Feedforward (McLean 2020).",
|
||||
"container-title": "Proceedings of Web Audio Conference",
|
||||
"event-place": "Cannes, France",
|
||||
"event-title": "WAC2022",
|
||||
"note": "DOI: 10.5281/zenodo.6821023\npublisher: Zenodo",
|
||||
"publisher-place": "Cannes, France",
|
||||
"title": "Strudel: Algorithmic Patterns for the Web",
|
||||
"URL": "https://zenodo.org/record/6821023",
|
||||
"author": [
|
||||
{
|
||||
"family": "Roos",
|
||||
"given": "Felix"
|
||||
},
|
||||
{
|
||||
"family": "McLean",
|
||||
"given": "Alex"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
11,
|
||||
29
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2022",
|
||||
6,
|
||||
28
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "yiWebAssemblyAudioWorkletCsound2018",
|
||||
"type": "paper-conference",
|
||||
"abstract": "This paper describes WebAssembly AudioWorklet (WAAW)\nCsound, one of the implementations of Web Audio Csound.\nWe begin by introducing the background to this current implementation, stemming from the two first ports of Csound\nto the web platform using Native Clients and asm.js. The\ntechnology of WebAssembly is then introduced and discussed in its more relevant aspects. The AudioWorklet interface of Web Audio API is explored, together with its use in\nWAAW Csound. We complement this discussion by considering the overarching question of support for multiple platforms, which implement different versions of Web Audio.\nSome initial examples of the system are presented to illustrate various potential applications. Finally, we complement\nthe paper by discussing current issues that are fundamental\nfor this project and others that rely on the development of\na robust support for WASM-based audio computing.",
|
||||
"event-place": "Berlin, Germany",
|
||||
"event-title": "Web Audio Conference WAC-2018",
|
||||
"language": "en",
|
||||
"publisher-place": "Berlin, Germany",
|
||||
"source": "mural.maynoothuniversity.ie",
|
||||
"title": "WebAssembly AudioWorklet Csound",
|
||||
"URL": "https://mural.maynoothuniversity.ie/16018/",
|
||||
"author": [
|
||||
{
|
||||
"family": "Yi",
|
||||
"given": "Steven"
|
||||
},
|
||||
{
|
||||
"family": "Lazzarini",
|
||||
"given": "Victor"
|
||||
},
|
||||
{
|
||||
"family": "Costello",
|
||||
"given": "Edward"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2018"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "majdaPeggy2023",
|
||||
"type": "software",
|
||||
"abstract": "Peggy: Parser generator for JavaScript",
|
||||
"genre": "JavaScript",
|
||||
"license": "MIT",
|
||||
"note": "original-date: 2020-11-21T11:52:39Z",
|
||||
"publisher": "peggyjs",
|
||||
"source": "GitHub",
|
||||
"title": "Peggy",
|
||||
"URL": "https://github.com/peggyjs/peggy",
|
||||
"author": [
|
||||
{
|
||||
"family": "Majda",
|
||||
"given": "David"
|
||||
}
|
||||
],
|
||||
"accessed": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
},
|
||||
"issued": {
|
||||
"date-parts": [
|
||||
[
|
||||
"2023",
|
||||
3,
|
||||
14
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,97 @@
|
||||
**Submission ID:** 8
|
||||
|
||||
**Title:** Strudel: live coding patterns on the Web
|
||||
|
||||
**Status:** Accept
|
||||
|
||||
**Reviewer 1:**
|
||||
|
||||
Author Comments: * Summary of the contribution:\
|
||||
This paper describes Strudel, a Web-based implementation of Tidal Cycles. It is generally a good overview of the system which could help prospective users understand how it works beyond the manual, and it provides many code examples. The most interesting contribution is the comparison between the Haskell and JavaScript features for this case.
|
||||
|
||||
* On theme (1-5):\
|
||||
1
|
||||
|
||||
* Citation quality (1-5):\
|
||||
5
|
||||
|
||||
* Missing references:\
|
||||
not answered
|
||||
|
||||
* Suggested improvements:\
|
||||
This paper is a good introduction to Strudel. The style is mostly illustrative, but it is hard to understand where the Tidal syntax is being introduced vs where the specific Strudel features are used. Thus, the best way to improve it would be to focus on Strudel. Also, it would be good to give a better hint on what output method would be preferable. Also the title of Section 9 should compare Strudel to Tidal or Haskell to JavaScript.
|
||||
|
||||
The paper is driven by examples, but many are a bit difficult to follow, here are some issues:\
|
||||
Section 4 - the mini-notation example does not look terser despite the text.\
|
||||
Section 5 - echo funciton is not used.\
|
||||
Sections 7.11 and 7.12 - the same (mini-notation) example is used so it is not clear what relation there is between transpilation and using mini-notation. Also please reference peggy\
|
||||
Sectin 7.3.2 The sawtooth waveform has been used in all examples, so it would be clearer if it was used here (maybe cutoff as well).
|
||||
|
||||
Generally, beyond the language differences, what does Strudel contribute with respect to Haskell-based Tidal? I would say that there is a lot to be gained in terms of installation, availability and potential for collaboration.
|
||||
|
||||
* Clarity (1-5):\
|
||||
4
|
||||
|
||||
* Corrections required:\
|
||||
No contentious / incorrect claims
|
||||
|
||||
* Other Changes if Published:\
|
||||
not answered
|
||||
|
||||
* Correct Template used?\
|
||||
Yes
|
||||
|
||||
* All materials provided?\
|
||||
Yes
|
||||
|
||||
* Wordcount respected?\
|
||||
Yes
|
||||
|
||||
* Have any deviations, errors or omissions impacted your ability to give a fair review?\
|
||||
No
|
||||
|
||||
**Reviewer 2:**
|
||||
|
||||
Author Comments: * Summary of the contribution:\
|
||||
This proposal presents Strudel, an online, JavaScript-native approach to algorithmic pattern composition that in many ways mirrors TidalCycles. TidalCycles, developed by one of the authors, is a key software piece for live coding. Perhaps the most difficult aspects of Tidal are related to Haskell and Haskell's dependency system that makes it very difficult to install. Haskell in itself is a difficult language to grasp; if Tidal users would like to expand and 'personalise' the possibilities of the software, they would have to get familiar with the particularities of Haskell. JavaScript is a multi-paradigm language that is used for web development can make pattern-based, algorithmic composition more popular. The process of bringing Tidal's paradigm to a dynamic type system seems to be an important part of this research. I enjoy and find very interesting the dialectical process that occurs between Strudel and Tidal, where the implementation of Tidal concepts in Strudel simultaneously transforms Tidal, either by introspection (finding bugs, etc.) or by bringing back to Tidal new insights that are clear in JavaScript. I think moving to TypeScript makes sense; perhaps a small section where the decision to use JavaScript is discussed a little further could be helpful for readers (beyond accessibility). Overall, this paper is clear and presents software that is a key development for live coding and will be a great addition to the next ICLC.
|
||||
|
||||
* On theme (1-5):\
|
||||
4
|
||||
|
||||
* Citation quality (1-5):\
|
||||
5
|
||||
|
||||
* Missing references:\
|
||||
not answered
|
||||
|
||||
* Suggested improvements:\
|
||||
Tidal-Strudel parity seems to be on the immediate horizon, but I wonder if this parity is transient and if the ultimate goal of Strudel is to ultimately diverge from Tidal. Perhaps the project is in its early stages, and this is not clear, but it would be interesting to get some insights on the authors' intentions. Personally, I think that a key discussion that needs to happen in live coding communities and development nodes is that of what is understood as multilingual live coding. Tidal's paradigm is implemented in a number of programming languages (as well as platforms such as Estuary), each with its own set of features and challenges: some are functional reactive programming, type strict oriented, while others are multi-paradigm, dynamic type oriented, etc. This seems to suggest that it is possible for many programming languages to adopt one single understanding of how to make music. Perhaps further considerations should be made as to whether we need one single grammar to name and express all music structures or whether we need environments and software that help difference to proliferate in terms of music/sound art/art idiosyncrasies. Perhaps questions in this direction are beyond the scope of this paper and I hope this comment is taken as productive feedback for the authors.
|
||||
|
||||
* Clarity (1-5):\
|
||||
4
|
||||
|
||||
* Corrections required:\
|
||||
Nothing contentious or incorrect to note.
|
||||
|
||||
* Other Changes if Published:\
|
||||
The example shown on page 3 under "Pattern Example" seems to be different from its explanation. The explanations do not mention scale but mention echo, which is not present in the example. I would double check all the code examples inc ase I missed something like this.
|
||||
|
||||
* Correct Template used?\
|
||||
Yes
|
||||
|
||||
* All materials provided?\
|
||||
Yes
|
||||
|
||||
* Wordcount respected?\
|
||||
Yes
|
||||
|
||||
* Have any deviations, errors or omissions impacted your ability to give a fair review?\
|
||||
no
|
||||
|
||||
* General remarks:\
|
||||
This proposal presents Strudel, an online, JavaScript-native approach to algorithmic pattern composition that in many ways mirrors TidalCycles. TidalCycles, developed by one of the authors, is a key software piece for live coding. Perhaps the most difficult aspects of Tidal are related to Haskell and Haskell's dependency system that makes it very difficult to install. Haskell in itself is a difficult language to grasp; if Tidal users would like to expand and 'personalise' the possibilities of the software, they would have to get familiar with the particularities of Haskell. JavaScript is a multi-paradigm language that can make pattern-based, algorithmic composition more popular. The process of bringing Tidal's paradigm to a dynamic type system seems to be an important part of this research. I enjoy and find very interesting the dialectical process that occurs between Strudel and Tidal, where the implementation of Tidal concepts in Strudel simultaneously transforms Tidal, either by introspection (finding bugs, etc.) or by bringing back to Tidal new insights that are clear in JavaScript. I think moving to TypeScript makes sense; perhaps a small section where the decision to use JavaScript is discussed a little further could be helpful for readers (beyond accessibility). I am curious, did the authors consider PureScript? I can see how, in terms of popularity, PureScript is a lateral move coming from Haskell, but an explicit explanation about the (maybe) obvious candidates would be interesting.\
|
||||
Tidal-Strudel parity seems to be on the immediate horizon, but I wonder if this parity is transient and if the ultimate goal of Strudel is to ultimately diverge from Tidal. Perhaps the project is in its early stages, and this is not clear, but it would be interesting to get some insights on the authors' intentions. Personally, I think that a key discussion that needs to happen in live coding communities and development nodes is that of what is understood as multilingual live coding. Tidal's paradigm is implemented in a number of programming languages (as well as platforms such as Estuary), each with its own set of features and challenges: some are functional reactive programming, type strict oriented, while others are multi-paradigm, dynamic type oriented, etc. This seems to suggest that it is possible for many programming languages to adopt one single understanding of how to make music. Perhaps further considerations should be made as to whether we need one single grammar to name and express all music structures or whether we need environments and software that help difference to proliferate in terms of music/sound art/art idiosyncrasies. Perhaps questions in this direction are beyond the scope of this paper and I hope this last comment is taken as productive feedback for the authors.
|
||||
|
||||
Overall, this paper is clear and presents software that is a key development for live coding and will be a great addition to the next ICLC.
|
||||
|
||||
PS. The example shown on page 3 under "Pattern Example" seems to be different from its explanation. The explanations do not mention scale but mention echo, which is not present in the example.
|
||||
@@ -8,7 +8,7 @@ author:
|
||||
affiliation: Then Try This
|
||||
email: alex@slab.org
|
||||
abstract: |
|
||||
This paper introduces Strudel, which brings the TidalCycles approach to live coding algorithmic patterns to native JavaScript and the web. We begin by giving a little background of the first year of development, before sharing some detail about its implementation and examples of use. We go on to outline the wide range of synthesis and other outputs available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), WebSerial and CSound, and introduce Strudel's REPL live editor, including its built-in visualisations. We then compare Strudel with Tidal, the trade-offs involved between JavaScript and Haskell, and the unique capabilities offered by Strudel for aligning patterns, before concluding with some thoughts about the future.
|
||||
This paper introduces Strudel, which faithfully ports the TidalCycles approach to live coding algorithmic patterns to native JavaScript and the web. We begin by giving a little background of the first year of development, before sharing some detail about its implementation and examples of use. We go on to outline the wide range of synthesis and other outputs available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), WebSerial and CSound, and introduce Strudel's REPL live editor, including its built-in visualisations. We then compare Strudel with Tidal, the trade-offs involved between JavaScript and Haskell, and the unique capabilities offered by Strudel for aligning patterns, before concluding with some thoughts about the future.
|
||||
bibliography: citations.json
|
||||
fontsize: 11pt
|
||||
geometry: margin=2cm
|
||||
@@ -23,37 +23,36 @@ date: '2022-12-14'
|
||||
|
||||
In the following paper, we introduce *Strudel*, an alternative
|
||||
implementation of the TidalCycles (or 'Tidal' for short) live coding
|
||||
system, using the JavaScript programming language. Strudel is an
|
||||
system. Whereas Tidal is implemented in the pure functional Haskell programming language, Strudel brings it to the multi-paradigm, web-centric JavaScript language. Strudel is an
|
||||
attempt to make live coding more accessible, by creating a system that
|
||||
runs entirely in the browser, while opening Tidal's approach to
|
||||
algorithmic patterns [@mcleanAlgorithmicPattern2020a] up to modern
|
||||
can run entirely in the browser, while opening Tidal's approach to
|
||||
algorithmic patterns [@mcleanAlgorithmicPattern2020] up to modern
|
||||
audio/visual web technologies. The Strudel REPL is a live code editor
|
||||
dedicated to manipulating patterns while they play, with builtin
|
||||
visual feedback. While Strudel is written in JavaScript, the API is
|
||||
optimized for simplicity and readability by applying code
|
||||
visual feedback. While Strudel is written in standard JavaScript, it does support some optimisations for simplicity, readability and editability by applying code
|
||||
transformations on the syntax tree level, allowing language operations
|
||||
that would otherwise be impossible. The application supports multiple
|
||||
ways to output sound, including Tone.js, Web Audio Nodes, OSC (Open
|
||||
Sound Control) messages, Web Serial, Web MIDI and Csound. The project
|
||||
is split into multiple packages, allowing granular reuse in other
|
||||
applications. Apart from TidalCycles, Strudel draws inspiration from
|
||||
many prior existing projects like TidalVortex
|
||||
several other existing projects like TidalVortex
|
||||
[@mcleanTidalVortexZero2022], Gibber [@robertsGibberLiveCoding2012],
|
||||
Estuary [@ogbornEstuaryBrowserbasedCollaborative2017], Hydra
|
||||
[@jackHydra2022], Ocarina [@solomonPurescriptocarina2022] and
|
||||
Feedforward [@mcleanFeedforward2020]. This paper expands the Strudel
|
||||
Demo paper for the Web Audio Conference 2022 [@StrudelWAC2022].
|
||||
[@jackHydra2023], Ocarina [@solomonPurescriptocarina2022] and
|
||||
Feedforward [@mcleanFeedforward2020]. This paper builds upon a
|
||||
demo paper presented at Web Audio Conference 2022 [@roosStrudelAlgorithmicPatterns2022].
|
||||
|
||||
The first tentative commit to the Strudel project was on 22nd January
|
||||
2022 by Alex McLean, with the core representation implemented over the
|
||||
following few days. Although this was his first attempt at a
|
||||
JavaScript-based application, by 27th January, Alex had managed to
|
||||
upload the initial version to the 'npm' javascript package database,
|
||||
upload an initial version to the 'npm' JavaScript package database,
|
||||
sharing with the wider community for comment. By 4th February, Felix
|
||||
Roos had discovered Strudel and contributed a 'REPL' user interface to
|
||||
it, and then contributed a scheduler the next day, so that Strudel
|
||||
could already make sound. At this point, Alex and Felix shared
|
||||
ownership to the repository, and the project has since proved to be a
|
||||
it, and a scheduler the following day, so that Strudel
|
||||
could already begin to make sound. At this point, Alex and Felix shared
|
||||
ownership of the repository, and the project has since proved to be a
|
||||
productive confluence of Felix's own work into music representation
|
||||
and visualisation, with Alex's experience with making Tidal. Felix has
|
||||
since become the primary contributor to Strudel, with Alex continuing
|
||||
@@ -61,24 +60,24 @@ to jump between developing both Strudel and Tidal. Aspects of
|
||||
Strudel's development have therefore fed back into TidalCycles, and
|
||||
both systems have maintained a shared conceptual underpinning. We plan
|
||||
to continue working towards feature parity between these systems,
|
||||
although within the syntactical trade-offs and library ecosystems of
|
||||
although within the different syntactical trade-offs and library ecosystems of
|
||||
JavaScript and Haskell, some divergence is inevitable and healthy.
|
||||
|
||||
Over the first year of its life, Strudel is now a fully-fledged live
|
||||
coding environment, porting Tidal's core represention of patterns,
|
||||
Now celebrating the first year of its life, Strudel is now a fully-fledged live
|
||||
coding environment, porting Tidal's core representation of patterns,
|
||||
pattern transformations, and mini-notation for polymetric sequences,
|
||||
combined with a wealth of features for synthesising and visualising
|
||||
those patterns.
|
||||
|
||||
# From Tidal to Strudel and back
|
||||
|
||||
As mentioned above, the original Tidal is implemented as a domain specific language (DSL) embedded in the Haskell pure functional programming language, and takes advantage of Haskell's terse syntax and advanced, 'strong' type system. JavaScript on the other hand, is a multi-paradigm programming language, with a dynamic type system. Because Tidal leans heavily on many of Haskell's more unique features, it was not always clear that it could meaningfully be ported to a multi-paradigm scripting language. However, this possibility was already demonstrated with an earlier port to Python [TidalVortex; @mcleanTidalVortexZero2022], and we have now successfully implemented Tidal's pure functional representation of patterns in Strudel, including partial application, currying, and the functor, applicative and monadic structures that underlie Tidal's expressive pattern transformations. The result is a terse and highly composable system, where everything is either a pattern, or a function for combining and manipulating patterns, offering a rich creative ground for exploration.
|
||||
As mentioned earlier, the original Tidal is implemented as a domain specific language (DSL) embedded in the Haskell pure functional programming language, and takes advantage of Haskell's terse syntax and advanced, 'strong' type system. JavaScript on the other hand, is a multi-paradigm programming language, with a dynamic type system. Because Tidal leans heavily on many of Haskell's more unique features, it was not always clear that it could meaningfully be ported to a multi-paradigm scripting language. However, this possibility was already demonstrated with an earlier port to Python [TidalVortex; @mcleanTidalVortexZero2022], and we have now successfully implemented Tidal's pure functional representation of patterns in Strudel, including partial application, currying, and the functor, applicative and monadic structures that underlie Tidal's expressive pattern transformations. The result is a terse and highly composable system, where everything is either a pattern, or a function for combining and manipulating patterns, offering a rich creative ground for exploration.
|
||||
|
||||
This development process has been far from a one-way port, however. The process of porting Tidal's concepts has also opened up new possibilities, some just from revisiting every design decision, and some from the particular affordances and constraints offered by JavaScript. This has lead to new features (and indeed bugfixes) that have found their way back to Tidal where appropriate, and ongoing work that we will return to in the conclusion of this paper.
|
||||
|
||||
# Representing Patterns
|
||||
|
||||
Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern can output a set of events that happen within that time span. It depends on the structure of the Pattern how the events are located in time.
|
||||
Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern will output a set of events that happen within that time span. It depends on the structure of the Pattern how the events are located in time.
|
||||
From now on, this process of generating events from a time span will be called **querying**.
|
||||
Example:
|
||||
|
||||
@@ -101,7 +100,7 @@ The resulting events are:
|
||||
|
||||
Each event has a value, a begin time and an end time, where time is represented as a fraction. In the above case, the events are placed in sequential order, where c3 takes the first half, and e3 and g3 together take the second half. This temporal placement is the result of the `sequence` function, which divides its arguments equally over one cycle. If an argument is an array, the same rule applies to that part of the cycle. In the example, e3 and g3 are divided equally over the second half of the whole cycle.
|
||||
|
||||
Note that the query function is not just a way to access a pattern, but true to the principles of functional programming, is the pattern itself. This means that in theory there is no way to change a pattern, it is opaque as a pure function. In practice though, Strudel and Tidal are all about transforming patterns, so how is this done? The answer is, by replacing the pattern with a new one, that calls the old one. This new one is only able to manipulate the query before passing it to the old pattern, and manipulate the results from it before returning them to caller. But, this is enough to support all the temporal and structural manipulations provided by Strudel (and Tidal's) extensive library of functions.
|
||||
Note that the query function is not just a way to access a pattern, but true to the principles of functional programming, is the pattern itself. This means that in theory there is no way to change a pattern, it is opaque as a pure function. In practice though, Strudel and Tidal are all about transforming patterns, so how is this done? The answer is, by replacing the pattern with a new one, that calls the old one. This new function is only able to manipulate the query before passing it to the old pattern, and manipulate the results from it before returning them to caller. But, this is enough to support all the temporal and structural manipulations provided by Strudel (and Tidal's) extensive library of functions.
|
||||
|
||||
The above examples do not represent how Strudel is used in practice. In the live coding editor, the user only has to type in the pattern itself, the querying will be handled by the scheduler. The scheduler will repeatedly query the pattern for events, which are then scheduled as sound synthesis or other event triggers.
|
||||
Also, the above event data structure has been simplified for readability.
|
||||
@@ -110,7 +109,7 @@ Also, the above event data structure has been simplified for readability.
|
||||
|
||||
# Making Patterns
|
||||
|
||||
In practice, the end-user live coder will not deal with constructing patterns directly, but will rather build patterns using Strudel's extensive combinator library to create, combine and transform patterns.
|
||||
In practice, the end-user live coder will not deal with constructing patterns directly, but will rather build patterns using Strudel's extensive combinator library, to create, combine and transform patterns.
|
||||
|
||||
The live coder will rarely use the `sequence` function as seen above, as sequencing is implicit in many functions. For example in the following, the `note` function constructs a pattern of notes, sequencing its arguments in the same manner as the previous example.
|
||||
|
||||
@@ -118,29 +117,29 @@ The live coder will rarely use the `sequence` function as seen above, as sequenc
|
||||
note(c3, [e3, g3])
|
||||
```
|
||||
|
||||
Perhaps more often, they will use the mini-notation for even terser notation of rhythmic sequences: ^[This last example is also valid Tidal code, albeit the parenthesis is not required in its Haskell syntax in this case. Tidal does not support passing sequences as lists directly to the `note` function, however.].
|
||||
Perhaps more often, they will use the mini-notation for even terser notation of rhythmic sequences: ^[This example is also valid Tidal code, albeit the parenthesis is not required in its Haskell syntax in this case. Tidal does not support passing sequences as lists directly to the `note` function, however.].
|
||||
|
||||
```js
|
||||
note("c3 [e3 g3]")
|
||||
```
|
||||
|
||||
Such sequences are often treated only as a starting point for manipulation, where they then undergo pattern transformations such as repetition, symmetry, interference/combination or randomisation, potentially at multiple timescales. Because Strudel patterns are represented as pure functions of time rather than as data structures, very long and complex generative results can be represented and manipulated without having to store the resulting sequences in memory.
|
||||
Such sequences are often treated only as a starting point for manipulation, where functions are then applied which represent pattern transformations such as repetition, symmetry, interference/combination or randomisation, potentially at multiple timescales. Because Strudel patterns are represented as pure functions of time rather than as data structures, very long and complex generative results can be represented and manipulated without having to store the resulting sequences in memory.
|
||||
|
||||
# Pattern Example
|
||||
|
||||
The following example showcases how patterns can be utilized to create musical complexity from simple parts, using repetition and interference:
|
||||
The following example showcases how patterns can be utilised to create musical complexity from simple parts, using repetition and interference:
|
||||
|
||||
```js
|
||||
"<0 2 [4 6](3,4,1) 3>"
|
||||
.off(1/4, add(2))
|
||||
.off(1/2, add(6))
|
||||
.scale('D minor')
|
||||
.legato(.25)
|
||||
.scale('D3 minor')
|
||||
.note().s("sawtooth square")
|
||||
.legato(.25)
|
||||
.delay(.8).delaytime(.125)
|
||||
```
|
||||
|
||||
The pattern starts with a rhythm of numbers in mini-notation, which are later interpreted inside the scale of D minor.
|
||||
The pattern starts with a rhythmic pattern of numbers expressed in mini-notation, which are later interpreted inside the scale of D minor.
|
||||
The first line could also be expressed without mini-notation:
|
||||
|
||||
```js
|
||||
@@ -153,16 +152,16 @@ These numbers then undergo various pattern transformations. Here is a short desc
|
||||
- `brackets`: elements inside brackets are divided equally over the time of their parent
|
||||
- `.euclid(p, s, o)`: place p pulses evenly over s steps, with offset o [@toussaintEuclideanAlgorithmGenerates2005]
|
||||
- `.off(n, f)`: layers a pattern on top of itself, with the new layer offset by n cycles, and with function f applied
|
||||
- `.legato(n)`: multiply the duration of all events in a pattern by a factor of n
|
||||
- `.echo(t, n, v)`: copy each event t times, with n cycles in between each copy, decreasing velocity by v
|
||||
- `.note()`: interpretes values as notes
|
||||
- `.scale(name)`: interpretes numbers as indices inside the given scale
|
||||
- `.note()`: interprets values as notes
|
||||
- `.s(name)`: play back each event with the given sound
|
||||
- `.delay(wet)`: add delay
|
||||
- `.legato(n)`: multiply the duration of all events in a pattern by a factor of n
|
||||
- `.delay(amount)`: delay effect send amount
|
||||
- `.delaytime(t)`: set delay time
|
||||
|
||||
Much of the above will be familiar to Tidal users.
|
||||
|
||||
<!-- This example shows some of Strudel's unique support for chords and transposition familiar to students of Western music theory. This differs a little from Tidal's approach and thanks to the integration of the javascript library XXX (*TODO* ? or is this all your work Felix?), Strudel's support for tonal transformations such as voice leading is perhaps respects more advanced than Tidal. -->
|
||||
<!-- This example shows some of Strudel's unique support for chords and transposition familiar to students of Western music theory. This differs a little from Tidal's approach and thanks to the integration of the JavaScript library XXX (*TODO* ? or is this all your work Felix?), Strudel's support for tonal transformations such as voice leading is perhaps respects more advanced than Tidal. -->
|
||||
|
||||
# Ways to make Sound (and other events)
|
||||
|
||||
@@ -181,11 +180,11 @@ While the Web Audio API takes a *fire-and-forget* approach, creating a lot of To
|
||||
|
||||
Strudel's new default output uses the Web Audio API to create a new audio graph for each event. It currently supports basic oscillators, sample playback, various effects and an experimental support for soundfonts.
|
||||
|
||||
WebDirt [@ogbornDktr0WebDirt2022] was created as part of the Estuary Live Coding System [@ogbornEstuaryBrowserbasedCollaborative2017], and proved to be a solid choice for handling samples in Strudel as well. We are however focused on working more directly with the Web Audio API to be able to integrate new features more tightly.
|
||||
WebDirt [@ogbornWebDirt2022] was created as part of the Estuary Live Coding System [@ogbornEstuaryBrowserbasedCollaborative2017], and proved to be a solid choice for handling samples in Strudel as well. We are however focused on working more directly with the Web Audio API to be able to integrate new features more tightly.
|
||||
|
||||
Using the OSC protocol via Strudel's provided Node.js-based OSC proxy server, it is possible to send network messages to trigger events. This is mainly used to render sound using SuperDirt [@SuperDirt2022], which is the well-developed Supercollider-based synthesis framework that Tidal live coders generally use as standard.
|
||||
Using the OSC protocol via Strudel's provided Node.js-based OSC proxy server, it is possible to send network messages to trigger events. This is mainly used to render sound using SuperDirt [@rohrhuberSuperDirt2022], which is the well-developed Supercollider-based synthesis framework used by Tidal live coders as standard.
|
||||
|
||||
Recently, the experimental integration of Csound proved to bring a new dimension of sound design capabilities to Strudel. Thanks to the WebAssembly distribution of this classic system [@CsoundWebAssembly], Csound 'orchestra' synthesisers can be embedded in and then patterned with Strudel code.
|
||||
Recently, the experimental integration of Csound proved to bring a new dimension of sound design capabilities to Strudel. Thanks to the WebAssembly distribution of this classic system [@yiWebAssemblyAudioWorkletCsound2018], Csound 'orchestra' synthesisers can be embedded in and then patterned with Strudel code.
|
||||
|
||||
MIDI output can also be used to send MIDI messages to either external instruments or to other programs on the same device. Unlike OSC, Strudel is able to send MIDI directly without requiring additional proxy software, but only from web browsers that support it (at the time of writing, this means Chromium-based browsers).
|
||||
|
||||
@@ -231,17 +230,13 @@ mini("c3 [e3 g3]*2").withMiniLocation([1,0,0],[1,14,14])
|
||||
|
||||
Here, the string is wrapped in `mini`, which will create a pattern from a mini-notation string. Additionally, the `withMiniLocation` method passes the original source code location of the string to the pattern, which enables highlighting active events.
|
||||
|
||||
Other convenient features like pseudo variables, operator overloading and top level await are possible with transpilation.
|
||||
|
||||
After the transpilation, the code is ready to be evaluated into a `Pattern`.
|
||||
|
||||
Behind the scenes, the user code string is parsed with `acorn`, turning it into an Abstract Syntax Tree (AST). The AST allows changing the structure of the code before generating the transpiled version using `escodegen`.
|
||||
Other convenient features like pseudo variables, operator overloading and top level await are possible with transpilation. After the transpilation, the code is ready to be evaluated into a `Pattern`. Behind the scenes, the user code string is parsed with `acorn`, turning it into an Abstract Syntax Tree (AST). The AST allows changing the structure of the code before generating the transpiled version using `escodegen`.
|
||||
|
||||
### Mini-notation
|
||||
|
||||
While the transpilation allows JavaScript to express Patterns in a less verbose way, it is still preferable to use the mini-notation as a more compact way to express rhythm. Strudel aims to provide the same mini-notation features and syntax as used in Tidal.
|
||||
While the transpilation allows us to express patterns in JavaScript in a less verbose way, it is still preferable to use the mini-notation as a more compact way to express rhythm. Strudel aims to provide the same mini-notation features and syntax as used in Tidal.
|
||||
|
||||
The mini-notation parser is implemented using `peggy`, which allows generating performant parsers for Domain Specific Languages (DSLs) using a concise grammar notation. The generated parser turns the mini-notation string into an AST which is used to call the respective Strudel functions with the given structure. For example, `"c3 [e3 g3]*2"` will result in the following calls:
|
||||
The mini-notation parser is implemented using `peggy` [@majdaPeggy2023], which allows generating performant parsers for Domain Specific Languages (DSLs) using a concise grammar notation. The generated parser turns the mini-notation string into an AST which is used to call the respective Strudel functions with the required structure. For example, `"c3 [e3 g3]*2"` will result in the following calls:
|
||||
|
||||
```js
|
||||
seq(
|
||||
@@ -255,12 +250,12 @@ seq(
|
||||
|
||||
### Highlighting Locations
|
||||
|
||||
As seen in the examples above, both the JS and the mini-notation parser add source code locations using `withMiniLocation` and `withLocation` methods. While the JS parser adds locations relative to the user code as a whole, the mini-notation adds locations relative to the position of the mini-notation string. The absolute location of elements within mini-notation can be calculated by simply adding both locations together. This absolute location can be used to highlight active events in real time.
|
||||
As seen in the examples above, both the JS and the mini-notation parser add source code locations using `withMiniLocation` and `withLocation` methods. While the JS parser adds locations relative to the user code as a whole, the mini-notation adds locations relative to the position of the mini-notation string. The absolute location of elements within mini-notation can be calculated by simply adding both locations together. This absolute location is then used to highlight active events in real time.
|
||||
|
||||
## Scheduling Events
|
||||
|
||||
After an instance of `Pattern` is obtained from the user code,
|
||||
it is used by the scheduler to get queried for events. Once started, the scheduler runs at a fixed interval to query the active pattern for events within the current interval's time span. A simplified implementation looks like this:
|
||||
the scheduler queries it for events. Once started, the scheduler runs at a fixed interval to query the active pattern for events within the current interval's time span. A simplified implementation looks like this:
|
||||
|
||||
```js
|
||||
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
|
||||
@@ -277,7 +272,7 @@ setInterval(() => {
|
||||
}, interval * 1000); // query each "interval" seconds
|
||||
```
|
||||
|
||||
Note that the above code is simplified for illustrative purposes. The actual implementation has to work around imprecise callbacks of `setInterval`. More about the implementation details can be read in [this blog post](https://loophole-letters.vercel.app/web-audio-scheduling).
|
||||
Note that the above code is simplified for illustrative purposes, for example the actual implementation has to work around imprecise callbacks of `setInterval`. ^[Further implementation details of the Strudel scheduler can be read in this blog post: [loophole-letters.vercel.app/web-audio-scheduling](https://loophole-letters.vercel.app/web-audio-scheduling).]
|
||||
|
||||
The fact that `Pattern.queryArc` is a pure function that maps a time span to a set of events allows us to choose any interval we like without changing the resulting output. It also means that when the pattern is changed from outside, the next scheduling callback will work with the new pattern, keeping its clock running.
|
||||
|
||||
@@ -310,7 +305,7 @@ const { x, y } = createParams('x', 'y')
|
||||
x(sine.range(0, 200)).y(cosine.range(0,200))
|
||||
```
|
||||
|
||||
This example creates the custom control parameters `x` and `y` which are then used to form a pattern that descibes the coordinates of a circle.
|
||||
This example creates the custom control parameters `x` and `y` which are then used to form a pattern that describes the coordinates of a circle.
|
||||
|
||||
### Outputs
|
||||
|
||||
@@ -328,13 +323,13 @@ function onTrigger(hap, deadline, duration) {
|
||||
}
|
||||
```
|
||||
|
||||
The above example will create an `OscillatorNode` for each event, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
|
||||
The above example will create an `OscillatorNode` for each event, where the frequency is controlled by the `note` parameter. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
|
||||
|
||||
# Pattern alignment and combination
|
||||
|
||||
One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective of their structure. Its declarative approach means a live coder does not have to think about the details of *how* this is done, only *what* is to be done.
|
||||
|
||||
As a simple example, consider two number patterns `"0 [1 2] 3"`, and `"10 20"`. The first has three contiguous steps of equal lengths, with the second step broken down into two substeps, giving four events in total. There are a very large number of ways in which the structure of these two patterns could be combined, but the default method in both Strudel and Tidal is to line up the cycles of the two patterns, and then take events from the first pattern and match them with those in the second pattern. Therefore, the following two lines are equivalent:
|
||||
As a simple example, consider two number patterns `"0 [1 2] 3"`, and `"10 20"`. The first has three contiguous steps of equal lengths, the second of which is broken down into two substeps, giving four events in total. The second pattern simply has two steps, taking up half a cycle each. There are a very large number of ways in which the structure of these two patterns could be combined, but the default method in both Strudel and Tidal is to line up the cycles of the two patterns, and then take events from the first pattern and match them with those in the second pattern. Therefore, the following two lines are equivalent:
|
||||
|
||||
```js
|
||||
"0 [1 2] 3".add("10 20")
|
||||
@@ -389,20 +384,20 @@ versus Strudel's simplicity.
|
||||
To demonstrate this, consider the following Tidal pattern:
|
||||
|
||||
```haskell
|
||||
iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4
|
||||
iter 4 $ every 3 (||+ n "10 20") $ n "20 21 23" # s "triangle" # crush 4
|
||||
```
|
||||
|
||||
This can be directly translated to the Strudel equivalent:
|
||||
|
||||
```js
|
||||
iter(4, every(3, add.squeeze("10 20"), n("0 1 3").s("triangle").crush(4)))
|
||||
iter(4, every(3, x => x.add.squeeze(n("10 20")), n("20 21 23").s("triangle").crush(4)))
|
||||
```
|
||||
|
||||
Although for a more canonical Strudel expression, we would reorder it
|
||||
as:
|
||||
|
||||
```js
|
||||
n("0 1 3").every(3, add.squeeze("10 20")).iter(4).s("triangle").crush(4)
|
||||
n("20 21 23").every(3, x => x.add.squeeze(n("10 20"))).iter(4).s("triangle").crush(4)
|
||||
```
|
||||
|
||||
The Strudel example uses the `.` method call operator for all
|
||||
@@ -426,17 +421,17 @@ features.
|
||||
|
||||
With Strudel, we have little choice but to embrace the affordances and
|
||||
constraints offered by JavaScript, and while designing a
|
||||
domain-specific language based on method calls is a
|
||||
domain-specific language based on chaining method calls is a
|
||||
challenge, through creative adoption of functional programming
|
||||
techniques like partial application, we are so far very happy with the
|
||||
results. Tidal's functional reactive approach to pattern-making has in
|
||||
general translated well to JavaScript, and opportunities and
|
||||
constraints have overall traded off to create a very approachable and
|
||||
useable live coding environment.
|
||||
constraints have traded off to create a very approachable and
|
||||
usable live coding environment.
|
||||
|
||||
## The trade-off of flexible typing
|
||||
|
||||
We have identified one problem with porting Tidal to JavaScript where we have missed Haskell's strict typing and type inference. In both Tidal and Strudel, time is rational, where any point in time is represented as the ratio of two integers. This allows representation of musical ratios such that are impossible to represent accurately using the more common floating point numbers. However while libraries are available that support rational numbers in JavaScript, the lack of strict typing means that it is easy to implement pattern methods where computationally expensive conversion from floating point to rational numbers are performed late, and therefore often enough to overload the CPUs, due to the large number of iterative calculations required to estimate a ratio for a given floating point number. To mitigate this problem, we might consider moving to TypeScript in the future.
|
||||
We have identified one problem with porting Tidal to JavaScript where we have missed Haskell's strict typing and type inference. In both Tidal and Strudel, time is rational, where any point in time is represented as the ratio of two integers. This allows representation of musical ratios such that are impossible to represent accurately using the more common floating point numbers. However while libraries are available that support rational numbers in JavaScript, the lack of strict typing means that computationally expensive conversion from floating point to rational numbers may be performed late, and therefore often enough to overload the CPUs, due to the large number of iterative calculations required to estimate a ratio for a given floating point number. To mitigate this problem, we might consider moving to TypeScript in the future.
|
||||
|
||||
# Future Outlook
|
||||
|
||||
@@ -446,14 +441,14 @@ The project is still young, with many features on the horizon. As general guidin
|
||||
2. consistent with Tidal's approach to pattern
|
||||
3. modular and extensible
|
||||
|
||||
While Haskell's type system makes it a great language for the ongoing development of Tidal's inner representation of pattern, JavaScript's vibrant ecosystem, flexibility and accessibility makes it a great host for more ad-hoc experiments, including interface design. For the future, it is planned to integrate additional alternative sound engines such as Glicol [@lanChaosprintGlicol2022] and Faust [@FaustProgrammingLanguage2022]. Strudel is already approaching feature parity with Tidal, but there are more Tidal functions to be ported, and work to be done to improve compatibility with Tidal's mini-notation. Tidal version 2.0 is under development, which brings a new representation for sequences to its patterns, which will then be brought to Strudel. Besides sound, other ways to render events are being explored, such as graphical, and choreographic output. We are also looking into alternative ways of editing patterns, including multi-user editing for network music, parsing a novel syntax to escape the constraints of JavaScript, and developing hardware/e-textile interfaces. In summary, there is a lot of fun ahead.
|
||||
While Haskell's type system makes it a great language for the ongoing development of Tidal's inner representation of pattern, JavaScript's vibrant ecosystem, flexibility and accessibility makes it a great host for more ad-hoc experiments, including interface design. For the future, it is planned to integrate additional alternative sound engines such as Glicol [@lanGlicol2023] and Faust [@orlareyFaustProgrammingLanguage2023]. Strudel is already approaching feature parity with Tidal, but there are more Tidal functions to be ported, and work to be done to improve compatibility with Tidal's mini-notation. Tidal version 2.0 is under development, which brings a new representation for sequences to its patterns, which will then be brought to Strudel. Besides sound, other ways to render events are being explored, such as graphical, and choreographic output. We are also looking into alternative ways of editing patterns, including multi-user editing for network music, parsing a novel syntax to escape the constraints of JavaScript, and developing hardware/e-textile interfaces. In summary, there is a lot of fun ahead.
|
||||
|
||||
# Links
|
||||
|
||||
The Strudel REPL is available at <https://strudel.tidalcycles.org>, including an interactive tutorial.
|
||||
The repository is at <https://github.com/tidalcycles/strudel>, all the code is open source under the AGPL-3.0 License.
|
||||
|
||||
# Acknowledgments
|
||||
# Acknowledgements
|
||||
|
||||
Thanks to the Strudel and wider Tidal, live coding, WebAudio and free/open source software communities for inspiration and support. Alex McLean's work on this project is supported by a UKRI Future Leaders Fellowship [grant number MR/V025260/1].
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
@@ -1,818 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||
<meta name="generator" content="pandoc" />
|
||||
<meta name="date" content="2022-12-14" />
|
||||
<title>Strudel: live coding patterns on the Web</title>
|
||||
<style type="text/css">code{white-space: pre;}</style>
|
||||
<style type="text/css">
|
||||
pre > code.sourceCode { white-space: pre; position: relative; }
|
||||
pre > code.sourceCode > span { display: inline-block; line-height: 1.25; }
|
||||
pre > code.sourceCode > span:empty { height: 1.2em; }
|
||||
.sourceCode { overflow: visible; }
|
||||
code.sourceCode > span { color: inherit; text-decoration: inherit; }
|
||||
div.sourceCode { margin: 1em 0; }
|
||||
pre.sourceCode { margin: 0; }
|
||||
@media screen {
|
||||
div.sourceCode { overflow: auto; }
|
||||
}
|
||||
@media print {
|
||||
pre > code.sourceCode { white-space: pre-wrap; }
|
||||
pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
|
||||
}
|
||||
pre.numberSource code
|
||||
{ counter-reset: source-line 0; }
|
||||
pre.numberSource code > span
|
||||
{ position: relative; left: -4em; counter-increment: source-line; }
|
||||
pre.numberSource code > span > a:first-child::before
|
||||
{ content: counter(source-line);
|
||||
position: relative; left: -1em; text-align: right; vertical-align: baseline;
|
||||
border: none; display: inline-block;
|
||||
-webkit-touch-callout: none; -webkit-user-select: none;
|
||||
-khtml-user-select: none; -moz-user-select: none;
|
||||
-ms-user-select: none; user-select: none;
|
||||
padding: 0 4px; width: 4em;
|
||||
color: #aaaaaa;
|
||||
}
|
||||
pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; }
|
||||
div.sourceCode
|
||||
{ }
|
||||
@media screen {
|
||||
pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
|
||||
}
|
||||
code span.al { color: #ff0000; font-weight: bold; } /* Alert */
|
||||
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
|
||||
code span.at { color: #7d9029; } /* Attribute */
|
||||
code span.bn { color: #40a070; } /* BaseN */
|
||||
code span.bu { color: #008000; } /* BuiltIn */
|
||||
code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
|
||||
code span.ch { color: #4070a0; } /* Char */
|
||||
code span.cn { color: #880000; } /* Constant */
|
||||
code span.co { color: #60a0b0; font-style: italic; } /* Comment */
|
||||
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
|
||||
code span.do { color: #ba2121; font-style: italic; } /* Documentation */
|
||||
code span.dt { color: #902000; } /* DataType */
|
||||
code span.dv { color: #40a070; } /* DecVal */
|
||||
code span.er { color: #ff0000; font-weight: bold; } /* Error */
|
||||
code span.ex { } /* Extension */
|
||||
code span.fl { color: #40a070; } /* Float */
|
||||
code span.fu { color: #06287e; } /* Function */
|
||||
code span.im { color: #008000; font-weight: bold; } /* Import */
|
||||
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
|
||||
code span.kw { color: #007020; font-weight: bold; } /* Keyword */
|
||||
code span.op { color: #666666; } /* Operator */
|
||||
code span.ot { color: #007020; } /* Other */
|
||||
code span.pp { color: #bc7a00; } /* Preprocessor */
|
||||
code span.sc { color: #4070a0; } /* SpecialChar */
|
||||
code span.ss { color: #bb6688; } /* SpecialString */
|
||||
code span.st { color: #4070a0; } /* String */
|
||||
code span.va { color: #19177c; } /* Variable */
|
||||
code span.vs { color: #4070a0; } /* VerbatimString */
|
||||
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
|
||||
</style>
|
||||
<link rel="stylesheet" href="css/iclc.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<h1 class="title">Strudel: live coding patterns on the Web</h1>
|
||||
<ul id="authorlist">
|
||||
<li>true</li>
|
||||
<li>true</li>
|
||||
</ul>
|
||||
<h3 class="date">2022-12-14</h3>
|
||||
</div>
|
||||
|
||||
<h2 class="abstract">Abstract</h2>
|
||||
<div id="abstract">
|
||||
<p>This paper introduces Strudel, which brings the TidalCycles approach
|
||||
to live coding algorithmic patterns to native JavaScript and the web. We
|
||||
begin by giving a little background of the first year of development,
|
||||
before sharing some detail about its implementation and examples of use.
|
||||
We go on to outline the wide range of synthesis and other outputs
|
||||
available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt),
|
||||
WebSerial and CSound, and introduce Strudel’s REPL live editor,
|
||||
including its built-in visualisations. We then compare Strudel with
|
||||
Tidal, the trade-offs involved between JavaScript and Haskell, and the
|
||||
unique capabilities offered by Strudel for aligning patterns.</p>
|
||||
</div>
|
||||
|
||||
<h1 data-number="1" id="introduction"><span
|
||||
class="header-section-number">1</span> Introduction</h1>
|
||||
<p>In the following paper, we introduce <em>Strudel</em>, an alternative
|
||||
implementation of the TidalCycles (or ‘Tidal’ for short) live coding
|
||||
system, using the JavaScript programming language. Strudel is an attempt
|
||||
to make live coding more accessible, by creating a system that runs
|
||||
entirely in the browser, while opening Tidal’s approach to algorithmic
|
||||
patterns <span class="citation"
|
||||
data-cites="mcleanAlgorithmicPattern2020a">(Mclean 2020)</span> up to
|
||||
modern audio/visual web technologies. The Strudel REPL is a live code
|
||||
editor dedicated to manipulating patterns while they play, with builtin
|
||||
visual feedback. While Strudel is written in JavaScript, the API is
|
||||
optimized for simplicity and readability by applying code
|
||||
transformations on the syntax tree level, allowing language operations
|
||||
that would otherwise be impossible. The application supports multiple
|
||||
ways to output sound, including Tone.js, Web Audio Nodes, OSC (Open
|
||||
Sound Control) messages, Web Serial, Web MIDI and Csound. The project is
|
||||
split into multiple packages, allowing granular reuse in other
|
||||
applications. Apart from TidalCycles, Strudel draws inspiration from
|
||||
many prior existing projects like TidalVortex <span class="citation"
|
||||
data-cites="mcleanTidalVortexZero2022">(McLean et al. 2022)</span>,
|
||||
Gibber <span class="citation"
|
||||
data-cites="robertsGibberLiveCoding2012">(Roberts and Kuchera-morin
|
||||
2012)</span>, Estuary <span class="citation"
|
||||
data-cites="ogbornEstuaryBrowserbasedCollaborative2017">(Ogborn et al.
|
||||
2017)</span>, Hydra <span class="citation"
|
||||
data-cites="jackHydra2022">(Jack [2022] 2022)</span>, Ocarina <span
|
||||
class="citation" data-cites="solomonPurescriptocarina2022">(Solomon
|
||||
[2021] 2022)</span> and Feedforward <span class="citation"
|
||||
data-cites="mcleanFeedforward2020">(McLean 2020)</span>. This paper
|
||||
expands the Strudel Demo paper for the Web Audio Conference 2022 <span
|
||||
class="citation" data-cites="StrudelWAC2022">(Roos and McLean
|
||||
2022)</span>.</p>
|
||||
<p>The first tentative commit to the Strudel project was on 22nd January
|
||||
2022 by Alex McLean, with the core representation implemented over the
|
||||
following few days. Although this was his first attempt at a
|
||||
JavaScript-based application, by 27th January, Alex had managed to
|
||||
upload the initial version to the ‘npm’ javascript package database,
|
||||
sharing with the wider community for comment. By 4th February, Felix
|
||||
Roos had discovered Strudel and contributed a ‘REPL’ user interface to
|
||||
it, and then contributed a scheduler the next day, so that Strudel could
|
||||
already make sound. At this point, Alex and Felix shared ownership to
|
||||
the repository, and the project has since proved to be a productive
|
||||
confluence of Felix’s own work into music representation and
|
||||
visualisation, with Alex’s experience with making Tidal. Felix has since
|
||||
become the primary contributor to Strudel, with Alex continuing to jump
|
||||
between developing both Strudel and Tidal. Aspects of Strudel’s
|
||||
development have therefore fed back into TidalCycles, and both systems
|
||||
have maintained a shared conceptual underpinning. We plan to continue
|
||||
working towards feature parity between these systems, although within
|
||||
the syntactical trade-offs and library ecosystems of JavaScript and
|
||||
Haskell, some divergence is inevitable and healthy.</p>
|
||||
<p>Over the first year of its life, Strudel is now a fully-fledged live
|
||||
coding environment, porting Tidal’s core represention of patterns,
|
||||
pattern transformations, and mininotation for polymetric sequences,
|
||||
combined with a wealth of features for synthesising and visualising
|
||||
those patterns.</p>
|
||||
<h1 data-number="2" id="from-tidal-to-strudel-and-back"><span
|
||||
class="header-section-number">2</span> From Tidal to Strudel and
|
||||
back</h1>
|
||||
<p>As mentioned above, the original Tidal is implemented as a domain
|
||||
specific language (DSL) embedded in the Haskell pure functional
|
||||
programming language, and takes advantage of Haskell’s terse syntax and
|
||||
advanced, ‘strong’ type system. JavaScript on the other hand, is a
|
||||
multi-paradigm programming language, with a dynamic type system. Because
|
||||
Tidal leans heavily on many of Haskell’s more unique features, it was
|
||||
not always clear that it could meaningfully be ported to a
|
||||
multi-paradigm scripting language. However, this possibility was already
|
||||
demonstrated with an earlier port to Python [TidalVortex; <span
|
||||
class="citation" data-cites="mcleanTidalVortexZero2022">McLean et al.
|
||||
(2022)</span>], and we have now successfully implemented Tidal’s pure
|
||||
functional representation of patterns in Strudel, including partial
|
||||
application, currying, and the functor, applicative and monadic
|
||||
structures that underlie Tidal’s expressive pattern transformations. The
|
||||
result is a terse and highly composable system, where everything is
|
||||
either a pattern, or a function for combining and manipulating patterns,
|
||||
offering a rich creative ground for exploration.</p>
|
||||
<p>This development process has been far from a one-way port, however.
|
||||
The process of porting Tidal’s concepts has also opened up new
|
||||
possibilities, some just from revisiting every design decision, and some
|
||||
from the particular affordances and constraints offered by JavaScript.
|
||||
This has lead to new features (and indeed bugfixes) that have found
|
||||
their way back to Tidal where appropriate, and ongoing work that we will
|
||||
return to in the conclusion of this paper.</p>
|
||||
<h1 data-number="3" id="representing-patterns"><span
|
||||
class="header-section-number">3</span> Representing Patterns</h1>
|
||||
<p>Patterns are the essence of Tidal. Its patterns are abstract entities
|
||||
that represent flows of time as functions, adapting a technique called
|
||||
pure functional reactive programming. Taking a time span as its input, a
|
||||
Pattern can output a set of events that happen within that time span. It
|
||||
depends on the structure of the Pattern how the events are located in
|
||||
time. From now on, this process of generating events from a time span
|
||||
will be called <strong>querying</strong>. Example:</p>
|
||||
<div class="sourceCode" id="cb1"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> pattern <span class="op">=</span> <span class="fu">sequence</span>(c3<span class="op">,</span> [e3<span class="op">,</span> g3])</span>
|
||||
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> events <span class="op">=</span> pattern<span class="op">.</span><span class="fu">queryArc</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">1</span>)</span>
|
||||
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="bu">console</span><span class="op">.</span><span class="fu">log</span>(events<span class="op">.</span><span class="fu">map</span>(e <span class="kw">=></span> e<span class="op">.</span><span class="fu">show</span>()))</span></code></pre></div>
|
||||
<p>In this example, we create a pattern using the <code>sequence</code>
|
||||
function and <strong>query</strong> it for the time span from
|
||||
<code>0</code> to <code>1</code>. Those numbers represent units of time
|
||||
called <strong>cycles</strong>. The length of one cycle depends on the
|
||||
tempo, which defaults to one cycle per second. The resulting events
|
||||
are:</p>
|
||||
<div class="sourceCode" id="cb2"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>[{ <span class="dt">value</span><span class="op">:</span> <span class="st">'c3'</span><span class="op">,</span> <span class="dt">begin</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">end</span><span class="op">:</span> <span class="dv">1</span><span class="op">/</span><span class="dv">2</span> }<span class="op">,</span></span>
|
||||
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>{ <span class="dt">value</span><span class="op">:</span> <span class="st">'e3'</span><span class="op">,</span> <span class="dt">begin</span><span class="op">:</span> <span class="dv">1</span><span class="op">/</span><span class="dv">2</span><span class="op">,</span> <span class="dt">end</span><span class="op">:</span> <span class="dv">3</span><span class="op">/</span><span class="dv">4</span> }<span class="op">,</span></span>
|
||||
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>{ <span class="dt">value</span><span class="op">:</span> <span class="st">'g3'</span><span class="op">,</span> <span class="dt">begin</span><span class="op">:</span> <span class="dv">3</span><span class="op">/</span><span class="dv">4</span><span class="op">,</span> <span class="dt">end</span><span class="op">:</span> <span class="dv">1</span> }]</span></code></pre></div>
|
||||
<p>Each event has a value, a begin time and an end time, where time is
|
||||
represented as a fraction. In the above case, the events are placed in
|
||||
sequential order, where c3 takes the first half, and e3 and g3 together
|
||||
take the second half. This temporal placement is the result of the
|
||||
<code>sequence</code> function, which divides its arguments equally over
|
||||
one cycle. If an argument is an array, the same rule applies to that
|
||||
part of the cycle. In the example, e3 and g3 are divided equally over
|
||||
the second half of the whole cycle.</p>
|
||||
<p>The above examples do not represent how Strudel is used in practice.
|
||||
In the live coding editor, the user only has to type in the pattern
|
||||
itself, the querying will be handled by the scheduler. The scheduler
|
||||
will repeatedly query the pattern for events, which are then scheduled
|
||||
as sound synthesis or other event triggers. Also, the above event data
|
||||
structure has been simplified for readability.</p>
|
||||
<figure>
|
||||
<img src="images/strudel-screenshot2.png" style="width:60.0%"
|
||||
alt="Screenshot of the Strudel ‘REPL’ live coding editor, including piano-roll visualisation." />
|
||||
<figcaption aria-hidden="true">Screenshot of the Strudel ‘REPL’ live
|
||||
coding editor, including piano-roll visualisation.</figcaption>
|
||||
</figure>
|
||||
<h1 data-number="4" id="making-patterns"><span
|
||||
class="header-section-number">4</span> Making Patterns</h1>
|
||||
<p>In practice, the end-user live coder will not deal with constructing
|
||||
patterns directly, but will rather build patterns using Strudel’s
|
||||
extensive combinator library to create, combine and transform
|
||||
patterns.</p>
|
||||
<p>The live coder will rarely use the <code>sequence</code> function as
|
||||
seen above, as sequencing is implicit in many functions. For example in
|
||||
the following, the <code>note</code> function constructs a pattern of
|
||||
notes, sequencing its arguments in the same manner as the previous
|
||||
example.</p>
|
||||
<div class="sourceCode" id="cb3"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="fu">note</span>(c3<span class="op">,</span> [e3<span class="op">,</span> g3])</span></code></pre></div>
|
||||
<p>Perhaps more often, they will use the mini-notation for even terser
|
||||
notation of rhythmic sequences: [^This last example is also valid Tidal
|
||||
code, albeit the parenthesis is not required in its Haskell syntax in
|
||||
this case. Tidal does not support passing sequences as lists directly to
|
||||
the <code>note</code> function, however.].</p>
|
||||
<div class="sourceCode" id="cb4"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="fu">note</span>(<span class="st">"c3 [e3 g3]"</span>)</span></code></pre></div>
|
||||
<p>Such sequences are often treated only a starting point for
|
||||
manipulation, where they then undergo pattern transformations such as
|
||||
repetition, symmetry, interference/combination or randomisation,
|
||||
potentially at multiple timescales. Because Strudel patterns are
|
||||
represented as pure functions of time rather than as data structures,
|
||||
very long and complex generative results can be represented and
|
||||
manipulated without having to store the resulting sequences in
|
||||
memory.</p>
|
||||
<h1 data-number="5" id="pattern-example"><span
|
||||
class="header-section-number">5</span> Pattern Example</h1>
|
||||
<p>The following example showcases how patterns can be utilized to
|
||||
create musical complexity from simple parts, using repetition and
|
||||
interference:</p>
|
||||
<div class="sourceCode" id="cb5"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="st">"<0 2 [4 6](3,4,1) 3>"</span></span>
|
||||
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">off</span>(<span class="dv">1</span><span class="op">/</span><span class="dv">4</span><span class="op">,</span> <span class="fu">add</span>(<span class="dv">2</span>))</span>
|
||||
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">off</span>(<span class="dv">1</span><span class="op">/</span><span class="dv">2</span><span class="op">,</span> <span class="fu">add</span>(<span class="dv">6</span>))</span>
|
||||
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">scale</span>(<span class="st">'D minor'</span>)</span>
|
||||
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">legato</span>(<span class="op">.</span><span class="dv">25</span>)</span>
|
||||
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">note</span>()<span class="op">.</span><span class="fu">s</span>(<span class="st">"sawtooth square"</span>)</span>
|
||||
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">delay</span>(<span class="op">.</span><span class="dv">8</span>)<span class="op">.</span><span class="fu">delaytime</span>(<span class="op">.</span><span class="dv">125</span>)</span></code></pre></div>
|
||||
<p>The pattern starts with a rhythm of numbers in mini notation, which
|
||||
are later interpreted inside the scale of D minor. The first line could
|
||||
also be expressed without mini notation:</p>
|
||||
<div class="sourceCode" id="cb6"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="fu">cat</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">2</span><span class="op">,</span> [<span class="dv">4</span><span class="op">,</span> <span class="dv">6</span>]<span class="op">.</span><span class="fu">euclid</span>(<span class="dv">3</span><span class="op">,</span> <span class="dv">4</span><span class="op">,</span> <span class="dv">1</span>)<span class="op">,</span> <span class="dv">3</span>)</span></code></pre></div>
|
||||
<p>These numbers then undergo various pattern transformations. Here is a
|
||||
short description of all the functions used:</p>
|
||||
<ul>
|
||||
<li><code>cat</code>: play elements sequentially, where each lasts one
|
||||
cycle</li>
|
||||
<li><code>brackets</code>: elements inside brackets are divided equally
|
||||
over the time of their parent</li>
|
||||
<li><code>.euclid(p, s, o)</code>: place p pulses evenly over s steps,
|
||||
with offset o <span class="citation"
|
||||
data-cites="toussaintEuclideanAlgorithmGenerates2005">(Toussaint
|
||||
2005)</span></li>
|
||||
<li><code>.off(n, f)</code>: layers a pattern on top of itself, with the
|
||||
new layer offset by n cycles, and with function f applied</li>
|
||||
<li><code>.legato(n)</code>: multiply the duration of all events in a
|
||||
pattern by a factor of n</li>
|
||||
<li><code>.echo(t, n, v)</code>: copy each event t times, with n cycles
|
||||
in between each copy, decreasing velocity by v</li>
|
||||
<li><code>.note()</code>: interpretes values as notes</li>
|
||||
<li><code>.s(name)</code>: play back each event with the given
|
||||
sound</li>
|
||||
<li><code>.delay(wet)</code>: add delay</li>
|
||||
<li><code>.delaytime(t)</code>: set delay time</li>
|
||||
</ul>
|
||||
<p>Much of the above will be familiar to Tidal users.</p>
|
||||
<!-- This example shows some of Strudel's unique support for chords and transposition familiar to students of Western music theory. This differs a little from Tidal's approach and thanks to the integration of the javascript library XXX (*TODO* ? or is this all your work Felix?), Strudel's support for tonal transformations such as voice leading is perhaps respects more advanced than Tidal. -->
|
||||
<h1 data-number="6" id="ways-to-make-sound-and-other-events"><span
|
||||
class="header-section-number">6</span> Ways to make Sound (and other
|
||||
events)</h1>
|
||||
<p>To generate sound, Strudel supports bindings for different
|
||||
outputs:</p>
|
||||
<ul>
|
||||
<li>Tone.js (deprecated)</li>
|
||||
<li>Web Audio API</li>
|
||||
<li>WebDirt, a js recreation of Tidal’s <em>Dirt</em> sample engine
|
||||
(deprecated)</li>
|
||||
<li>OSC via osc-js, compatible with superdirt</li>
|
||||
<li>Csound via the Csound WebAssembly build</li>
|
||||
<li>MIDI via WebMIDI</li>
|
||||
<li>Serial via WebSerial</li>
|
||||
</ul>
|
||||
<p>At first, we used Tone.js as sound output, but it proved to be
|
||||
limited for the use case of Strudel, where each individual event could
|
||||
potentially have a completely different audio graph. While the Web Audio
|
||||
API takes a <em>fire-and-forget</em> approach, creating a lot of Tone.js
|
||||
instruments and effects causes performance issues quickly. For that
|
||||
reason, we chose to search for alternatives.</p>
|
||||
<p>Strudel’s new default output uses the Web Audio API to create a new
|
||||
audio graph for each event. It currently supports basic oscillators,
|
||||
sample playback, various effects and an experimental support for
|
||||
soundfonts.</p>
|
||||
<p>WebDirt <span class="citation"
|
||||
data-cites="ogbornDktr0WebDirt2022">(Ogborn [2016] 2022)</span> was
|
||||
created as part of the Estuary Live Coding System <span class="citation"
|
||||
data-cites="ogbornEstuaryBrowserbasedCollaborative2017">(Ogborn et al.
|
||||
2017)</span>, and proved to be a solid choice for handling samples in
|
||||
Strudel as well. We are however focused on working more directly with
|
||||
the Web Audio API to be able to integrate new features more tightly.</p>
|
||||
<p>Using the OSC protocol via Strudel’s provided Node.js-based OSC proxy
|
||||
server, it is possible to send network messages to trigger events. This
|
||||
is mainly used to render sound using SuperDirt <span class="citation"
|
||||
data-cites="SuperDirt2022">(<em>SuperDirt</em> [2015] 2022)</span>,
|
||||
which is the well-developed Supercollider-based synthesis framework that
|
||||
Tidal live coders generally use as standard.</p>
|
||||
<p>Recently, the experimental integration of Csound proved to bring a
|
||||
new dimension of sound design capabilities to Strudel. Thanks to the
|
||||
WebAssembly distribution of this classic system <span class="citation"
|
||||
data-cites="CsoundWebAssembly">(Yi, Lazzarini, and Costello
|
||||
2018)</span>, Csound ‘orchestra’ synthesisers can be embedded in and
|
||||
then patterned with Strudel code.</p>
|
||||
<p>MIDI output can also be used to send MIDI messages to either external
|
||||
instruments or to other programs on the same device. Unlike OSC, Strudel
|
||||
is able to send MIDI directly without requiring additional proxy
|
||||
software, but only from web browsers that support it (at the time of
|
||||
writing, this means Chromium-based browsers).</p>
|
||||
<p>Finally, Strudel supports Serial output, for example to trigger
|
||||
events via microcontrollers. This has already been explored for robot
|
||||
choreography by Kate Sicchio and Alex McLean, via a performance
|
||||
presented at the International Conference on Live Interfaces 2022.</p>
|
||||
<h1 data-number="7" id="the-strudel-repl"><span
|
||||
class="header-section-number">7</span> The Strudel REPL</h1>
|
||||
<p>While Strudel can be used as a library in any JavaScript codebase,
|
||||
its main, reference user interface is the Strudel REPL[^REPL stands for
|
||||
read, evaluate, print/play, loop. It is friendly jargon for an
|
||||
interactive programming interface from computing heritage, usually for a
|
||||
commandline interface but also applied to live coding editors.], which
|
||||
is a browser-based live coding environment. This live code editor is
|
||||
dedicated to manipulating Strudel patterns while they play. The REPL
|
||||
features built-in visual feedback, which highlights which elements in
|
||||
the patterned (mini-notation) sequences are influencing the event that
|
||||
is currently being played. This feedback is designed to support both
|
||||
learning and live use of Strudel.</p>
|
||||
<p>Besides a UI for playback control and meta information, the main part
|
||||
of the REPL interface is the code editor powered by CodeMirror. In it,
|
||||
the user can edit and evaluate pattern code live, using one of the
|
||||
available synthesis outputs to create music and/or sound art. The
|
||||
control flow of the REPL follows 3 basic steps:</p>
|
||||
<ol type="1">
|
||||
<li>The user writes and updates code. Each update transpiles and
|
||||
evaluates it to create a <code>Pattern</code> instance</li>
|
||||
<li>While the REPL is running, the <code>Scheduler</code> queries the
|
||||
active <code>Pattern</code> by a regular interval, generating
|
||||
<code>Events</code> (also known as <code>Haps</code> in Strudel) for the
|
||||
next time span.</li>
|
||||
<li>For each scheduling tick, all generated <code>Events</code> are
|
||||
triggered by calling their <code>onTrigger</code> method, which is set
|
||||
by the output.</li>
|
||||
</ol>
|
||||
<figure>
|
||||
<img
|
||||
src="https://github.com/tidalcycles/strudel/raw/talk/talk/public/strudelflow.png?raw=true"
|
||||
style="width:43.0%" alt="REPL control flow" />
|
||||
<figcaption aria-hidden="true">REPL control flow</figcaption>
|
||||
</figure>
|
||||
<h2 data-number="7.1" id="user-code"><span
|
||||
class="header-section-number">7.1</span> User Code</h2>
|
||||
<p>To create a <code>Pattern</code> from the user code, two steps are
|
||||
needed:</p>
|
||||
<ol type="1">
|
||||
<li>Transpile the JS input code to make it functional</li>
|
||||
<li>Evaluate the transpiled code</li>
|
||||
</ol>
|
||||
<h3 data-number="7.1.1" id="transpilation-evaluation"><span
|
||||
class="header-section-number">7.1.1</span> Transpilation &
|
||||
Evaluation</h3>
|
||||
<p>In the JavaScript world, using transpilation is a common practise to
|
||||
be able to use language features that are not supported by the base
|
||||
language. Tools like <code>babel</code> will transpile code that
|
||||
contains unsupported language features into a version of the code
|
||||
without those features.</p>
|
||||
<p>In the same tradition, Strudel can add a transpilation step to
|
||||
simplify the user code in the context of live coding. For example, the
|
||||
Strudel REPL lets the user create mini notation patterns using just
|
||||
double quoted strings, while single quoted strings remain what they
|
||||
are:</p>
|
||||
<div class="sourceCode" id="cb7"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="st">"c3 [e3 g3]*2"</span></span></code></pre></div>
|
||||
<p>is transpiled to:</p>
|
||||
<div class="sourceCode" id="cb8"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="fu">mini</span>(<span class="st">"c3 [e3 g3]*2"</span>)<span class="op">.</span><span class="fu">withMiniLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span>]<span class="op">,</span>[<span class="dv">1</span><span class="op">,</span><span class="dv">14</span><span class="op">,</span><span class="dv">14</span>])</span></code></pre></div>
|
||||
<p>Here, the string is wrapped in <code>mini</code>, which will create a
|
||||
pattern from a mini notation string. Additionally, the
|
||||
<code>withMiniLocation</code> method passes the original source code
|
||||
location of the string to the pattern, which enables highlighting active
|
||||
events.</p>
|
||||
<p>Other convenient features like pseudo variables, operator overloading
|
||||
and top level await are possible with transpilation.</p>
|
||||
<p>After the transpilation, the code is ready to be evaluated into a
|
||||
<code>Pattern</code>.</p>
|
||||
<p>Behind the scenes, the user code string is parsed with
|
||||
<code>acorn</code>, turning it into an Abstract Syntax Tree (AST). The
|
||||
AST allows changing the structure of the code before generating the
|
||||
transpiled version using <code>escodegen</code>.</p>
|
||||
<h3 data-number="7.1.2" id="mini-notation"><span
|
||||
class="header-section-number">7.1.2</span> Mini Notation</h3>
|
||||
<p>While the transpilation allows JavaScript to express Patterns in a
|
||||
less verbose way, it is still preferable to use the Mini Notation as a
|
||||
more compact way to express rhythm. Strudel aims to provide the same
|
||||
Mini Notation features and syntax as used in Tidal.</p>
|
||||
<p>The Mini Notation parser is implemented using <code>peggy</code>,
|
||||
which allows generating performant parsers for Domain Specific Languages
|
||||
(DSLs) using a concise grammar notation. The generated parser turns the
|
||||
Mini Notation string into an AST which is used to call the respective
|
||||
Strudel functions with the given structure. For example,
|
||||
<code>"c3 [e3 g3]*2"</code> will result in the following calls:</p>
|
||||
<div class="sourceCode" id="cb9"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="fu">seq</span>(</span>
|
||||
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a> <span class="fu">reify</span>(<span class="st">'c3'</span>)<span class="op">.</span><span class="fu">withLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">1</span><span class="op">,</span><span class="dv">1</span>]<span class="op">,</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">4</span><span class="op">,</span><span class="dv">4</span>])<span class="op">,</span></span>
|
||||
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a> <span class="fu">seq</span>(</span>
|
||||
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">reify</span>(<span class="st">'e3'</span>)<span class="op">.</span><span class="fu">withLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">5</span><span class="op">,</span><span class="dv">5</span>]<span class="op">,</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">8</span><span class="op">,</span><span class="dv">8</span>])<span class="op">,</span></span>
|
||||
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">reify</span>(<span class="st">'g3'</span>)<span class="op">.</span><span class="fu">withLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">8</span><span class="op">,</span><span class="dv">8</span>]<span class="op">,</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">10</span><span class="op">,</span><span class="dv">10</span>])<span class="op">,</span></span>
|
||||
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a> )<span class="op">.</span><span class="fu">fast</span>(<span class="dv">2</span>)</span>
|
||||
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a>)</span></code></pre></div>
|
||||
<h3 data-number="7.1.3" id="highlighting-locations"><span
|
||||
class="header-section-number">7.1.3</span> Highlighting Locations</h3>
|
||||
<p>As seen in the examples above, both the JS and the Mini Notation
|
||||
parser add source code locations using <code>withMiniLocation</code> and
|
||||
<code>withLocation</code> methods. While the JS parser adds locations
|
||||
relative to the user code as a whole, the Mini Notation adds locations
|
||||
relative to the position of the mini notation string. The absolute
|
||||
location of elements within Mini Notation can be calculated by simply
|
||||
adding both locations together. This absolute location can be used to
|
||||
highlight active events in real time.</p>
|
||||
<h2 data-number="7.2" id="scheduling-events"><span
|
||||
class="header-section-number">7.2</span> Scheduling Events</h2>
|
||||
<p>After an instance of <code>Pattern</code> is obtained from the user
|
||||
code, it is used by the scheduler to get queried for events. Once
|
||||
started, the scheduler runs at a fixed interval to query active pattern
|
||||
for events withing the current interval’s time span. A simplified
|
||||
implementation looks like this:</p>
|
||||
<div class="sourceCode" id="cb10"><pre
|
||||
class="sourceCode js"><code class="sourceCode javascript"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> pattern <span class="op">=</span> <span class="fu">seq</span>(<span class="st">'c3'</span><span class="op">,</span> [<span class="st">'e3'</span><span class="op">,</span> <span class="st">'g3'</span>])<span class="op">;</span> <span class="co">// pattern from user</span></span>
|
||||
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> interval <span class="op">=</span> <span class="fl">0.5</span><span class="op">;</span> <span class="co">// query interval in seconds</span></span>
|
||||
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> time <span class="op">=</span> <span class="dv">0</span><span class="op">;</span> <span class="co">// beginning of current time span</span></span>
|
||||
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> minLatency <span class="op">=</span> <span class="op">.</span><span class="dv">1</span><span class="op">;</span> <span class="co">// min time before a hap should trigger</span></span>
|
||||
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a><span class="pp">setInterval</span>(() <span class="kw">=></span> {</span>
|
||||
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> haps <span class="op">=</span> pattern<span class="op">.</span><span class="fu">queryArc</span>(time<span class="op">,</span> time <span class="op">+</span> interval)<span class="op">;</span></span>
|
||||
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a> time <span class="op">+=</span> interval<span class="op">;</span> <span class="co">// increment time</span></span>
|
||||
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a> haps<span class="op">.</span><span class="fu">forEach</span>((hap) <span class="kw">=></span> {</span>
|
||||
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> deadline <span class="op">=</span> hap<span class="op">.</span><span class="at">whole</span><span class="op">.</span><span class="at">begin</span> <span class="op">-</span> time <span class="op">+</span> minLatency<span class="op">;</span></span>
|
||||
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a> <span class="fu">onTrigger</span>(hap<span class="op">,</span> deadline<span class="op">,</span> duration)<span class="op">;</span></span>
|
||||
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a> })<span class="op">;</span></span>
|
||||
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a>}<span class="op">,</span> interval <span class="op">*</span> <span class="dv">1000</span>)<span class="op">;</span> <span class="co">// query each "interval" seconds</span></span></code></pre></div>
|
||||
<p>Note that the above code is simplified for illustrative purposes. The
|
||||
actual implementation has to work around imprecise callbacks of
|
||||
<code>setInterval</code>. More about the implementation details can be
|
||||
read in <a
|
||||
href="https://loophole-letters.vercel.app/web-audio-scheduling">this
|
||||
blog post</a>.</p>
|
||||
<p>The fact that <code>Pattern.queryArc</code> is a pure function that
|
||||
maps a time span to a set of events allows us to choose any interval we
|
||||
like without changing the resulting output. It also means that when the
|
||||
pattern is changed from outside, the next scheduling callback will work
|
||||
with the new pattern, keeping its clock running.</p>
|
||||
<p>The latency between the time the pattern is evaluated and the change
|
||||
is heard is between <code>minLatency</code> and
|
||||
<code>interval + minLatency</code>, in our example between 100ms and
|
||||
600ms. In Strudel, the current query interval is 50ms with a minLatency
|
||||
of 100ms, meaning the latency is between 50ms and 150ms.</p>
|
||||
<h2 data-number="7.3" id="output"><span
|
||||
class="header-section-number">7.3</span> Output</h2>
|
||||
<p>The last step is to trigger each event in the chosen output. This is
|
||||
where the given time and value of each event is used to generate audio
|
||||
or any other form of time based output. The default output of the
|
||||
Strudel REPL is the WebAudio output. To understand what an output does,
|
||||
we first have to understand what control parameters are.</p>
|
||||
<h3 data-number="7.3.1" id="control-parameters"><span
|
||||
class="header-section-number">7.3.1</span> Control Parameters</h3>
|
||||
<p>To be able to manipulate multiple aspects of sound in parallel, so
|
||||
called control parameters are used to shape the value of each event.
|
||||
Example:</p>
|
||||
<div class="sourceCode" id="cb11"><pre
|
||||
class="sourceCode js"><code class="sourceCode javascript"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="fu">note</span>(<span class="st">"c3 e3"</span>)<span class="op">.</span><span class="fu">cutoff</span>(<span class="dv">1000</span>)<span class="op">.</span><span class="fu">s</span>(<span class="st">'sawtooth'</span>)</span>
|
||||
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="fu">queryArc</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">1</span>)<span class="op">.</span><span class="fu">map</span>(hap <span class="kw">=></span> hap<span class="op">.</span><span class="at">value</span>)</span>
|
||||
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a><span class="co">/* [</span></span>
|
||||
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a><span class="co"> { note: 'c3', cutoff: 1000, s: 'sawtooth' }</span></span>
|
||||
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a><span class="co"> { note: 'e3', cutoff: 1000, s: 'sawtooth' }</span></span>
|
||||
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a><span class="co">] */</span></span></code></pre></div>
|
||||
<p>Here, the control parameter functions <code>note</code>,
|
||||
<code>cutoff</code> and <code>s</code> are used, where each controls a
|
||||
different property in the value object. Each control parameter function
|
||||
accepts a primitive value, a list of values to be sequenced into a
|
||||
<code>Pattern</code>, or a <code>Pattern</code>. In the example,
|
||||
<code>note</code> gets a <code>Pattern</code> from a Mini Notation
|
||||
expression (double quoted), while <code>cutoff</code> and <code>s</code>
|
||||
are given a <code>Number</code> and a (single quoted)
|
||||
<code>String</code> respectively.</p>
|
||||
<p>Strudel comes with a large default set of control parameter functions
|
||||
that are based on the ones used by Tidal and SuperDirt, focusing on
|
||||
music and audio terminology. It is however possible to create custom
|
||||
control paramters for any purpose:</p>
|
||||
<div class="sourceCode" id="cb12"><pre
|
||||
class="sourceCode js"><code class="sourceCode javascript"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> { x<span class="op">,</span> y } <span class="op">=</span> <span class="fu">createParams</span>(<span class="st">'x'</span><span class="op">,</span> <span class="st">'y'</span>)</span>
|
||||
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a><span class="fu">x</span>(sine<span class="op">.</span><span class="fu">range</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">200</span>))<span class="op">.</span><span class="fu">y</span>(cosine<span class="op">.</span><span class="fu">range</span>(<span class="dv">0</span><span class="op">,</span><span class="dv">200</span>))</span></code></pre></div>
|
||||
<p>This example creates the custom control parameters <code>x</code> and
|
||||
<code>y</code> which are then used to form a pattern that descibes the
|
||||
coordinates of a circle.</p>
|
||||
<h3 data-number="7.3.2" id="outputs"><span
|
||||
class="header-section-number">7.3.2</span> Outputs</h3>
|
||||
<p>Now that we know how the value of an event is manipulated using
|
||||
control parameters, we can look at how outputs can use that value to
|
||||
generate anything. The scheduler above was calling the
|
||||
<code>onTrigger</code> function which is used to implement the output. A
|
||||
very simple version of the web audio output could look like this:</p>
|
||||
<div class="sourceCode" id="cb13"><pre
|
||||
class="sourceCode js"><code class="sourceCode javascript"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">onTrigger</span>(hap<span class="op">,</span> deadline<span class="op">,</span> duration) {</span>
|
||||
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> { note } <span class="op">=</span> hap<span class="op">.</span><span class="at">value</span><span class="op">;</span></span>
|
||||
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> time <span class="op">=</span> <span class="fu">getAudioContext</span>()<span class="op">.</span><span class="at">currentTime</span> <span class="op">+</span> deadline<span class="op">;</span></span>
|
||||
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> o <span class="op">=</span> <span class="fu">getAudioContext</span>()<span class="op">.</span><span class="fu">createOscillator</span>()<span class="op">;</span></span>
|
||||
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="at">frequency</span><span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="fu">getFreq</span>(note)<span class="op">;</span></span>
|
||||
<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="fu">start</span>(time)<span class="op">;</span></span>
|
||||
<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="fu">stop</span>(time <span class="op">+</span> <span class="bu">event</span><span class="op">.</span><span class="at">duration</span>)<span class="op">;</span></span>
|
||||
<span id="cb13-8"><a href="#cb13-8" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="fu">connect</span>(<span class="fu">getAudioContext</span>()<span class="op">.</span><span class="at">destination</span>)<span class="op">;</span></span>
|
||||
<span id="cb13-9"><a href="#cb13-9" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
|
||||
<p>The above example will create an <code>OscillatorNode</code> for each
|
||||
event, where the frequency is controlled by the <code>note</code> param.
|
||||
In essence, this is how the WebAudio API output of Strudel works, only
|
||||
with many more parameters to control synths, samples and effects.</p>
|
||||
<h1 data-number="8" id="pattern-alignment-and-combination"><span
|
||||
class="header-section-number">8</span> Pattern alignment and
|
||||
combination</h1>
|
||||
<p>One core aspect of Strudel, inherited from Tidal, is the flexible way
|
||||
that patterns can be combined, irrespective of their structure. Its
|
||||
declarative approach means a live coder does not have to think about the
|
||||
details of <em>how</em> this is done, only <em>what</em> is to be
|
||||
done.</p>
|
||||
<p>As a simple example, consider two number patterns
|
||||
<code>"0 [1 2] 3"</code>, and <code>"10 20"</code>. The first has three
|
||||
contiguous steps of equal lengths, with the second step broken down into
|
||||
two substeps, giving four events in total. There are a very large number
|
||||
of ways in which the structure of these two patterns could be combined,
|
||||
but the default method in both Strudel and Tidal is to line up the
|
||||
cycles of the two patterns, and then take events from the first pattern
|
||||
and match them with those in the second pattern. Therefore, the
|
||||
following two lines are equivalent:</p>
|
||||
<div class="sourceCode" id="cb14"><pre
|
||||
class="sourceCode js"><code class="sourceCode javascript"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="st">"0 [1 2] 3"</span><span class="op">.</span><span class="fu">add</span>(<span class="st">"10 20"</span>)</span>
|
||||
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a><span class="st">"10 [11 22] 23"</span></span></code></pre></div>
|
||||
<p>Where the events only partially overlap, they are treated as
|
||||
fragments of the event in the first pattern. This is a little difficult
|
||||
to conceptualise, but lets start by comparing the two patterns in the
|
||||
following example:</p>
|
||||
<div class="sourceCode" id="cb15"><pre
|
||||
class="sourceCode js"><code class="sourceCode javascript"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="st">"0 1 2"</span><span class="op">.</span><span class="fu">add</span>(<span class="st">"10 20"</span>)</span>
|
||||
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a><span class="st">"10 [11 21] 20"</span></span></code></pre></div>
|
||||
<p>They are similar to the previous example in that the number
|
||||
<code>1</code> is split in two, with its two halves added to
|
||||
<code>10</code> and <code>20</code> respectively. However, the
|
||||
<code>11</code> ‘remembers’ that it is a fragment of that original
|
||||
<code>1</code> event, and so is treated as having a duration of a third
|
||||
of a cycle, despite only being active for a sixth of a cycle. Likewise,
|
||||
the <code>21</code> is also a fragment of that original <code>1</code>
|
||||
event, but a fragment of its second half. Because the start of its event
|
||||
is missing, it wouldn’t actually trigger a sound (unless it underwent
|
||||
further pattern transformations/combinations).</p>
|
||||
<p>In practice, the effect of this default, implicit method for
|
||||
combining two patterns is that the second pattern is added <em>in</em>
|
||||
to the first one, and indeed this can be made explicit:</p>
|
||||
<div class="sourceCode" id="cb16"><pre
|
||||
class="sourceCode js"><code class="sourceCode javascript"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a><span class="st">"0 1 2"</span><span class="op">.</span><span class="at">add</span><span class="op">.</span><span class="fu">in</span>(<span class="st">"10 20"</span>)</span></code></pre></div>
|
||||
<p>This makes way for other ways to align the pattern, and several are
|
||||
already defined, in particular:</p>
|
||||
<ul>
|
||||
<li><code>in</code> - as explained above, aligns cycles, and applies
|
||||
values from the pattern on the right <em>in</em> to the pattern on the
|
||||
left.</li>
|
||||
<li><code>out</code> - as with <code>in</code>, but values are applied
|
||||
<em>out</em> of the pattern on the left (i.e. <em>in</em> to the one on
|
||||
the right).</li>
|
||||
<li><code>mix</code> - structures from both patterns are combined, so
|
||||
that the new events are not fragments but are created at intersections
|
||||
of events from both sides.</li>
|
||||
<li><code>squeeze</code> - cycles from the pattern on the right are
|
||||
squeezed into events on the left. So that
|
||||
e.g. <code>"0 1 2".add.squeeze("10 20")</code> is equivalent to
|
||||
<code>"[10 20] [11 21] [12 22]"</code>.</li>
|
||||
<li><code>squeezeout</code> - as with <code>squeeze</code>, but cycles
|
||||
from the left are squeezed into events on the right. So,
|
||||
<code>"0 1 2".add.squeezeout("10 20")</code> is equivalent to
|
||||
<code>[10 11 12] [20 21 22]</code>.</li>
|
||||
<li><code>trig</code> is similar to <code>squeezeout</code> in that
|
||||
cycles from the right are aligned with events on the left. However those
|
||||
cycles are not ‘squeezed’, rather they are truncated to fit the event.
|
||||
So <code>"0 1 2 3 4 5 6 7".add.trig("10 [20 30]")</code> would be
|
||||
equivalent to <code>10 11 12 13 20 21 30 31</code>. In effect, events on
|
||||
the right ‘trigger’ cycles on the left.</li>
|
||||
<li><code>trigzero</code> is similar to <code>trig</code>, but the
|
||||
pattern is ‘triggered’ from its very first cycle, rather than from the
|
||||
current cycle. <code>trig</code> and <code>trigzero</code> therefore
|
||||
only give different results where the leftmost pattern differs from one
|
||||
cycle to the next.</li>
|
||||
</ul>
|
||||
<p>We will save going deeper into the background, design and
|
||||
practicalities of these alignment functions for future publications.
|
||||
However in the next section, we take them as a case study for looking at
|
||||
the different design affordances offered by Haskell to Tidal, and
|
||||
JavaScript to Strudel.</p>
|
||||
<h1 data-number="9" id="comparing-strudel-and-haskell-in-use"><span
|
||||
class="header-section-number">9</span> Comparing Strudel and Haskell in
|
||||
use</h1>
|
||||
<p>Unlike Haskell, JavaScript lacks the ability to define custom infix
|
||||
operators, or change the meaning of existing ones. So the above Strudel
|
||||
example of <code>"0 1 2".add.out("10 20")</code> is equivalent to the
|
||||
Tidal expression <code>"0 1 2" +| "10 20"</code>, where the vertical bar
|
||||
in the operator <code>+|</code> stands for <code>out</code> (where
|
||||
<code>a |+ b</code> would be equivalent of
|
||||
<code>a.add.in(b)</code>).</p>
|
||||
<p>From this we can already see that Tidal tends towards brevity through
|
||||
mixing infix operators with functions, and Strudel tends towards
|
||||
spelling out operations which are joined together with the
|
||||
<code>.</code> operator. This then is the design trade-off of Tidal’s
|
||||
tersity, versus Strudel’s simplicity.</p>
|
||||
<p>To demonstrate this, consider the following Tidal pattern:</p>
|
||||
<pre class="tidal"><code>iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4</code></pre>
|
||||
<p>This can be directly translated to the Strudel equivalent:</p>
|
||||
<div class="sourceCode" id="cb18"><pre
|
||||
class="sourceCode js"><code class="sourceCode javascript"><span id="cb18-1"><a href="#cb18-1" aria-hidden="true" tabindex="-1"></a><span class="fu">iter</span>(<span class="dv">4</span><span class="op">,</span> <span class="fu">every</span>(<span class="dv">3</span><span class="op">,</span> add<span class="op">.</span><span class="fu">squeeze</span>(<span class="st">"10 20"</span>)<span class="op">,</span> <span class="fu">n</span>(<span class="st">"0 1 3"</span>)<span class="op">.</span><span class="fu">s</span>(<span class="st">"triangle"</span>)<span class="op">.</span><span class="fu">crush</span>(<span class="dv">4</span>)))</span></code></pre></div>
|
||||
<p>Although for a more canonical Strudel expression, we would reorder it
|
||||
as:</p>
|
||||
<div class="sourceCode" id="cb19"><pre
|
||||
class="sourceCode js"><code class="sourceCode javascript"><span id="cb19-1"><a href="#cb19-1" aria-hidden="true" tabindex="-1"></a><span class="fu">n</span>(<span class="st">"0 1 3"</span>)<span class="op">.</span><span class="fu">every</span>(<span class="dv">3</span><span class="op">,</span> add<span class="op">.</span><span class="fu">squeeze</span>(<span class="st">"10 20"</span>))<span class="op">.</span><span class="fu">iter</span>(<span class="dv">4</span>)<span class="op">.</span><span class="fu">s</span>(<span class="st">"triangle"</span>)<span class="op">.</span><span class="fu">crush</span>(<span class="dv">4</span>)</span></code></pre></div>
|
||||
<p>The Strudel example uses the <code>.</code> method call operator for
|
||||
all operations and combinations, whereas the Tidal example has
|
||||
<code>#</code> for the default method for combining patterns and uses
|
||||
infix operators for other methods. The lack of parenthesis in the Tidal
|
||||
example is partly due to the way that arguments are applied to Haskell’s
|
||||
functions, and partly due to the use of the <code>$</code> operator as
|
||||
an alternative way to establish precedence and control the order of
|
||||
evaluation.</p>
|
||||
<p>Considering the above, we argue that the Haskell syntax is a little
|
||||
cleaner, but that the Strudel syntax is easier to learn. Our informal
|
||||
observation is that while Haskell’s dollar <code>$</code> operator is
|
||||
very useful in making code easier to work with, it is one of the most
|
||||
difficult aspects of Tidal use for beginners to learn. On the other
|
||||
hand, the deeper levels of parenthesis in Strudel code can be difficult
|
||||
to keep track of, especially while coding under pressure of live musical
|
||||
performance. However this difficulty can be largely be mitigated by
|
||||
reordering expressions, and further mitigated by supporting editor
|
||||
features.</p>
|
||||
<p>With Strudel, we have little choice but to embrace the affordances
|
||||
and constraints offered by JavaScript, and while designing a
|
||||
domain-specific language entirely based on method calls is a challenge,
|
||||
through creative adoption of functional programming techniques like
|
||||
partial application, we are so far very happy with the results. Tidal’s
|
||||
functional reactive approach to pattern-making has in general translated
|
||||
well to JavaScript, and opportunities and constraints have overall
|
||||
traded off to create a very approachable and useable live coding
|
||||
environment.</p>
|
||||
<h2 data-number="9.1" id="the-trade-off-of-flexible-typing"><span
|
||||
class="header-section-number">9.1</span> The trade-off of flexible
|
||||
typing</h2>
|
||||
<p>We have identified one problem with porting Tidal to JavaScript where
|
||||
we have missed Haskell’s strict typing and type inference. In both Tidal
|
||||
and Strudel, time is rational, where any point in time is represented as
|
||||
the ratio of two integers. This allows representation of musical ratios
|
||||
such that are impossible to represent accurately using the more common
|
||||
floating point numbers. However while libraries are available that
|
||||
support rational numbers in JavaScript, the lack of strict typing means
|
||||
that it is easy to implement pattern methods where computationally
|
||||
expensive conversion from floating point to rational numbers are
|
||||
performed late, and therefore often enough to overload the CPUs, due to
|
||||
the large number of iterative calculations required to estimate a ratio
|
||||
for a given floating point number. To mitigate this problem, we might
|
||||
consider moving to TypeScript in the future.</p>
|
||||
<h1 data-number="10" id="future-outlook"><span
|
||||
class="header-section-number">10</span> Future Outlook</h1>
|
||||
<p>The project is still young, with many features on the horizon. As
|
||||
general guiding principles, Strudel aims to be</p>
|
||||
<ol type="1">
|
||||
<li>accessible</li>
|
||||
<li>consistent with Tidal’s approach to pattern</li>
|
||||
<li>modular and extensible</li>
|
||||
</ol>
|
||||
<p>While Haskell’s type system makes it a great language for the ongoing
|
||||
development of Tidal’s inner representation of pattern, JavaScript’s
|
||||
vibrant ecosystem, flexibility and accessibility makes it a great host
|
||||
for more ad-hoc experiments, including interface design. For the future,
|
||||
it is planned to integrate additional alternative sound engines such as
|
||||
Glicol <span class="citation" data-cites="lanChaosprintGlicol2022">(Lan
|
||||
[2020] 2022)</span> and Faust <span class="citation"
|
||||
data-cites="FaustProgrammingLanguage2022">(<em>Faust - Programming
|
||||
Language for Audio Applications and Plugins</em> [2016] 2022)</span>.
|
||||
Strudel is already approaching feature parity with Tidal, but there are
|
||||
more Tidal functions to be ported, and work to be done to improve
|
||||
compatibility with Tidal’s mininotation. Tidal version 2.0 is under
|
||||
development, which brings a new representation for sequences to its
|
||||
patterns, which will then be brought to Strudel. Besides sound, other
|
||||
ways to render events are being explored, such as graphical, and
|
||||
choreographic output. We are also looking into alternative ways of
|
||||
editing patterns, including multi-user editing for network music,
|
||||
parsing a novel syntax to escape the constraints of javascript, and
|
||||
developing hardware/e-textile interfaces. In summary, there is a lot of
|
||||
fun ahead.</p>
|
||||
<h1 data-number="11" id="links"><span
|
||||
class="header-section-number">11</span> Links</h1>
|
||||
<p>The Strudel REPL is available at <a
|
||||
href="https://strudel.tidalcycles.org"
|
||||
class="uri">https://strudel.tidalcycles.org</a>, including an
|
||||
interactive tutorial. The repository is at <a
|
||||
href="https://github.com/tidalcycles/strudel"
|
||||
class="uri">https://github.com/tidalcycles/strudel</a>, all the code is
|
||||
open source under the AGPL-3.0 License.</p>
|
||||
<h1 data-number="12" id="acknowledgments"><span
|
||||
class="header-section-number">12</span> Acknowledgments</h1>
|
||||
<p>Thanks to the Strudel and wider Tidal, live coding, WebAudio and
|
||||
free/open source software communities for inspiration and support. Alex
|
||||
McLean’s work on this project is supported by a UKRI Future Leaders
|
||||
Fellowship [grant number MR/V025260/1].</p>
|
||||
<h1 class="unnumbered" id="references">References</h1>
|
||||
<div id="refs" class="references csl-bib-body hanging-indent"
|
||||
role="doc-bibliography">
|
||||
<div id="ref-FaustProgrammingLanguage2022" class="csl-entry"
|
||||
role="doc-biblioentry">
|
||||
<em>Faust - Programming Language for Audio Applications and
|
||||
Plugins</em>. (2016) 2022. C++. GRAME. <a
|
||||
href="https://github.com/grame-cncm/faust">https://github.com/grame-cncm/faust</a>.
|
||||
</div>
|
||||
<div id="ref-jackHydra2022" class="csl-entry" role="doc-biblioentry">
|
||||
Jack, Olivia. (2022) 2022. <em>Hydra</em>. <a
|
||||
href="https://github.com/ojack/hydra">https://github.com/ojack/hydra</a>.
|
||||
</div>
|
||||
<div id="ref-lanChaosprintGlicol2022" class="csl-entry"
|
||||
role="doc-biblioentry">
|
||||
Lan, Qichao. (2020) 2022. <em>Chaosprint/Glicol</em>. Rust. <a
|
||||
href="https://github.com/chaosprint/glicol">https://github.com/chaosprint/glicol</a>.
|
||||
</div>
|
||||
<div id="ref-mcleanAlgorithmicPattern2020a" class="csl-entry"
|
||||
role="doc-biblioentry">
|
||||
Mclean, Alex. 2020. <span>“Algorithmic Pattern.”</span> In
|
||||
<em>Proceedings of the International Conference on New Interfaces for
|
||||
Musical Expression</em>, 265--270. Birmingham, UK. <a
|
||||
href="https://zenodo.org/record/4813352">https://zenodo.org/record/4813352</a>.
|
||||
</div>
|
||||
<div id="ref-mcleanFeedforward2020" class="csl-entry"
|
||||
role="doc-biblioentry">
|
||||
McLean, Alex. 2020. <span>“Feedforward.”</span> In <em>Proceedings of
|
||||
New Interfaces for Musical Expression</em>. Birmingham. <a
|
||||
href="https://zenodo.org/record/6353969">https://zenodo.org/record/6353969</a>.
|
||||
</div>
|
||||
<div id="ref-mcleanTidalVortexZero2022" class="csl-entry"
|
||||
role="doc-biblioentry">
|
||||
McLean, Alex, Raphaël Forment, Sylvain Le Beux, and Damián Silvani.
|
||||
2022. <span>“TidalVortex Zero.”</span> In <em>Proceedings of the 7th
|
||||
International Conference on Live Coding</em>. Limerick, Ireland: Zenodo.
|
||||
<a
|
||||
href="https://doi.org/10.5281/zenodo.6456380">https://doi.org/10.5281/zenodo.6456380</a>.
|
||||
</div>
|
||||
<div id="ref-ogbornDktr0WebDirt2022" class="csl-entry"
|
||||
role="doc-biblioentry">
|
||||
Ogborn, David. (2016) 2022. <em>Dktr0/WebDirt</em>. JavaScript. <a
|
||||
href="https://github.com/dktr0/WebDirt">https://github.com/dktr0/WebDirt</a>.
|
||||
</div>
|
||||
<div id="ref-ogbornEstuaryBrowserbasedCollaborative2017"
|
||||
class="csl-entry" role="doc-biblioentry">
|
||||
Ogborn, David, Jamie Beverley, Luis Navarro del Angel, Eldad Tsabary,
|
||||
and Alex McLean. 2017. <span>“Estuary: Browser-Based Collaborative
|
||||
Projectional Live Coding of Musical Patterns.”</span> In <em>Proceedings
|
||||
of the International Conference on Live Coding</em>, 11. Morelia.
|
||||
</div>
|
||||
<div id="ref-robertsGibberLiveCoding2012" class="csl-entry"
|
||||
role="doc-biblioentry">
|
||||
Roberts, Charles, and Joann Kuchera-morin. 2012. <span>“Gibber: Live
|
||||
Coding Audio in the Browser.”</span> In <em>In Proceedings of the 2012
|
||||
International Computer Music Conference</em>.
|
||||
</div>
|
||||
<div id="ref-StrudelWAC2022" class="csl-entry" role="doc-biblioentry">
|
||||
Roos, Felix, and Alex McLean. 2022. <span>“Strudel: Algorithmic Patterns
|
||||
for the Web.”</span> In. Zenodo. <a
|
||||
href="https://doi.org/10.5281/zenodo.6768844">https://doi.org/10.5281/zenodo.6768844</a>.
|
||||
</div>
|
||||
<div id="ref-solomonPurescriptocarina2022" class="csl-entry"
|
||||
role="doc-biblioentry">
|
||||
Solomon, Mike. (2021) 2022. <em>Purescript-Ocarina</em>. PureScript. <a
|
||||
href="https://github.com/mikesol/purescript-ocarina">https://github.com/mikesol/purescript-ocarina</a>.
|
||||
</div>
|
||||
<div id="ref-SuperDirt2022" class="csl-entry" role="doc-biblioentry">
|
||||
<em>SuperDirt</em>. (2015) 2022. SuperCollider. musikinformatik. <a
|
||||
href="https://github.com/musikinformatik/SuperDirt">https://github.com/musikinformatik/SuperDirt</a>.
|
||||
</div>
|
||||
<div id="ref-toussaintEuclideanAlgorithmGenerates2005" class="csl-entry"
|
||||
role="doc-biblioentry">
|
||||
Toussaint, Godfried. 2005. <span>“The Euclidean Algorithm Generates
|
||||
Traditional Musical Rhythms.”</span> In <em>In Proceedings of BRIDGES:
|
||||
Mathematical Connections in Art, Music and Science</em>, 47–56. <a
|
||||
href="http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231">http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231</a>.
|
||||
</div>
|
||||
<div id="ref-CsoundWebAssembly" class="csl-entry"
|
||||
role="doc-biblioentry">
|
||||
Yi, Steven, Victor Lazzarini, and Edward Costello. 2018.
|
||||
<span>“WebAssembly AudioWorklet Csound.”</span> In. Berlin, Germany. <a
|
||||
href="https://mural.maynoothuniversity.ie/16018/">https://mural.maynoothuniversity.ie/16018/</a>.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -848,6 +848,23 @@ exports[`runs examples > example "arpWith" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "arrange" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:c ]",
|
||||
"[ 3/8 → 1/2 | note:c ]",
|
||||
"[ 3/4 → 7/8 | note:c ]",
|
||||
"[ 1/1 → 9/8 | note:a ]",
|
||||
"[ 11/8 → 3/2 | note:a ]",
|
||||
"[ 7/4 → 15/8 | note:a ]",
|
||||
"[ 2/1 → 17/8 | note:f ]",
|
||||
"[ 19/8 → 5/2 | note:f ]",
|
||||
"[ 11/4 → 23/8 | note:f ]",
|
||||
"[ 3/1 → 25/8 | note:e ]",
|
||||
"[ 27/8 → 7/2 | note:e ]",
|
||||
"[ 15/4 → 31/8 | note:e ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "attack" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | note:c3 attack:0 ]",
|
||||
@@ -861,7 +878,29 @@ exports[`runs examples > example "attack" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "bandf" example index 0 1`] = `
|
||||
exports[`runs examples > example "bank" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd bank:RolandTR909 ]",
|
||||
"[ 1/2 → 1/1 | s:sd bank:RolandTR909 ]",
|
||||
"[ 1/1 → 3/2 | s:bd bank:RolandTR909 ]",
|
||||
"[ 3/2 → 2/1 | s:sd bank:RolandTR909 ]",
|
||||
"[ 2/1 → 5/2 | s:bd bank:RolandTR909 ]",
|
||||
"[ 5/2 → 3/1 | s:sd bank:RolandTR909 ]",
|
||||
"[ 3/1 → 7/2 | s:bd bank:RolandTR909 ]",
|
||||
"[ 7/2 → 4/1 | s:sd bank:RolandTR909 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "begin" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:rave begin:0 ]",
|
||||
"[ 1/1 → 2/1 | s:rave begin:0.25 ]",
|
||||
"[ 2/1 → 3/1 | s:rave begin:0.5 ]",
|
||||
"[ 3/1 → 4/1 | s:rave begin:0.75 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "bpf" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd bandf:1000 ]",
|
||||
"[ 1/2 → 1/1 | s:sd bandf:1000 ]",
|
||||
@@ -886,7 +925,7 @@ exports[`runs examples > example "bandf" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "bandq" example index 0 1`] = `
|
||||
exports[`runs examples > example "bpq" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd bandf:500 bandq:0 ]",
|
||||
"[ 1/2 → 1/1 | s:sd bandf:500 bandq:0 ]",
|
||||
@@ -899,28 +938,6 @@ exports[`runs examples > example "bandq" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "bank" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd bank:RolandTR909 ]",
|
||||
"[ 1/2 → 1/1 | s:sd bank:RolandTR909 ]",
|
||||
"[ 1/1 → 3/2 | s:bd bank:RolandTR909 ]",
|
||||
"[ 3/2 → 2/1 | s:sd bank:RolandTR909 ]",
|
||||
"[ 2/1 → 5/2 | s:bd bank:RolandTR909 ]",
|
||||
"[ 5/2 → 3/1 | s:sd bank:RolandTR909 ]",
|
||||
"[ 3/1 → 7/2 | s:bd bank:RolandTR909 ]",
|
||||
"[ 7/2 → 4/1 | s:sd bank:RolandTR909 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "begin" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:rave begin:0 ]",
|
||||
"[ 1/1 → 2/1 | s:rave begin:0.25 ]",
|
||||
"[ 2/1 → 3/1 | s:rave begin:0.5 ]",
|
||||
"[ 3/1 → 4/1 | s:rave begin:0.75 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "cat" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:hh ]",
|
||||
@@ -1060,6 +1077,27 @@ exports[`runs examples > example "chunkBack" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "clip" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/5 | note:c s:piano clip:1 ]",
|
||||
"[ 1/5 → 2/5 | note:a s:piano clip:1 ]",
|
||||
"[ 2/5 → 3/5 | note:f s:piano clip:1 ]",
|
||||
"[ 3/5 → 4/5 | note:e s:piano clip:1 ]",
|
||||
"[ 1/1 → 6/5 | note:c s:piano clip:1 ]",
|
||||
"[ 6/5 → 7/5 | note:a s:piano clip:1 ]",
|
||||
"[ 7/5 → 8/5 | note:f s:piano clip:1 ]",
|
||||
"[ 8/5 → 9/5 | note:e s:piano clip:1 ]",
|
||||
"[ 2/1 → 11/5 | note:c s:piano clip:1 ]",
|
||||
"[ 11/5 → 12/5 | note:a s:piano clip:1 ]",
|
||||
"[ 12/5 → 13/5 | note:f s:piano clip:1 ]",
|
||||
"[ 13/5 → 14/5 | note:e s:piano clip:1 ]",
|
||||
"[ 3/1 → 16/5 | note:c s:piano clip:1 ]",
|
||||
"[ 16/5 → 17/5 | note:a s:piano clip:1 ]",
|
||||
"[ 17/5 → 18/5 | note:f s:piano clip:1 ]",
|
||||
"[ 18/5 → 19/5 | note:e s:piano clip:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "coarse" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd coarse:1 ]",
|
||||
@@ -1254,31 +1292,6 @@ exports[`runs examples > example "cut" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "cutoff" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd cutoff:4000 ]",
|
||||
"[ 1/2 → 1/1 | s:sd cutoff:4000 ]",
|
||||
"[ 0/1 → 1/3 | s:hh cutoff:4000 ]",
|
||||
"[ 1/3 → 2/3 | s:hh cutoff:4000 ]",
|
||||
"[ 2/3 → 1/1 | s:hh cutoff:4000 ]",
|
||||
"[ 1/1 → 3/2 | s:bd cutoff:2000 ]",
|
||||
"[ 3/2 → 2/1 | s:sd cutoff:2000 ]",
|
||||
"[ 1/1 → 4/3 | s:hh cutoff:2000 ]",
|
||||
"[ 4/3 → 5/3 | s:hh cutoff:2000 ]",
|
||||
"[ 5/3 → 2/1 | s:hh cutoff:2000 ]",
|
||||
"[ 2/1 → 5/2 | s:bd cutoff:1000 ]",
|
||||
"[ 5/2 → 3/1 | s:sd cutoff:1000 ]",
|
||||
"[ 2/1 → 7/3 | s:hh cutoff:1000 ]",
|
||||
"[ 7/3 → 8/3 | s:hh cutoff:1000 ]",
|
||||
"[ 8/3 → 3/1 | s:hh cutoff:1000 ]",
|
||||
"[ 3/1 → 7/2 | s:bd cutoff:500 ]",
|
||||
"[ 7/2 → 4/1 | s:sd cutoff:500 ]",
|
||||
"[ 3/1 → 10/3 | s:hh cutoff:500 ]",
|
||||
"[ 10/3 → 11/3 | s:hh cutoff:500 ]",
|
||||
"[ 11/3 → 4/1 | s:hh cutoff:500 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "decay" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | note:c3 decay:0.1 sustain:0 ]",
|
||||
@@ -1403,6 +1416,19 @@ exports[`runs examples > example "delay" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "delay" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd delay:0.65 delaytime:0.25 delayfeedback:0.9 ]",
|
||||
"[ 1/2 → 1/1 | s:bd delay:0.65 delaytime:0.125 delayfeedback:0.7 ]",
|
||||
"[ 1/1 → 3/2 | s:bd delay:0.65 delaytime:0.25 delayfeedback:0.9 ]",
|
||||
"[ 3/2 → 2/1 | s:bd delay:0.65 delaytime:0.125 delayfeedback:0.7 ]",
|
||||
"[ 2/1 → 5/2 | s:bd delay:0.65 delaytime:0.25 delayfeedback:0.9 ]",
|
||||
"[ 5/2 → 3/1 | s:bd delay:0.65 delaytime:0.125 delayfeedback:0.7 ]",
|
||||
"[ 3/1 → 7/2 | s:bd delay:0.65 delaytime:0.25 delayfeedback:0.9 ]",
|
||||
"[ 7/2 → 4/1 | s:bd delay:0.65 delaytime:0.125 delayfeedback:0.7 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "delayfeedback" example index 0 1`] = `
|
||||
[
|
||||
"[ (0/1 → 1/1) ⇝ 2/1 | s:bd delay:0.25 delayfeedback:0.25 ]",
|
||||
@@ -1898,7 +1924,7 @@ exports[`runs examples > example "gain" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "hcutoff" example index 0 1`] = `
|
||||
exports[`runs examples > example "hpf" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd hcutoff:4000 ]",
|
||||
"[ 1/2 → 1/1 | s:sd hcutoff:4000 ]",
|
||||
@@ -1927,7 +1953,36 @@ exports[`runs examples > example "hcutoff" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "hresonance" example index 0 1`] = `
|
||||
exports[`runs examples > example "hpf" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd hcutoff:2000 ]",
|
||||
"[ 1/2 → 1/1 | s:sd hcutoff:2000 ]",
|
||||
"[ 0/1 → 1/4 | s:hh hcutoff:2000 ]",
|
||||
"[ 1/4 → 1/2 | s:hh hcutoff:2000 ]",
|
||||
"[ 1/2 → 3/4 | s:hh hcutoff:2000 ]",
|
||||
"[ 3/4 → 1/1 | s:hh hcutoff:2000 ]",
|
||||
"[ 1/1 → 3/2 | s:bd hcutoff:2000 hresonance:25 ]",
|
||||
"[ 3/2 → 2/1 | s:sd hcutoff:2000 hresonance:25 ]",
|
||||
"[ 1/1 → 5/4 | s:hh hcutoff:2000 hresonance:25 ]",
|
||||
"[ 5/4 → 3/2 | s:hh hcutoff:2000 hresonance:25 ]",
|
||||
"[ 3/2 → 7/4 | s:hh hcutoff:2000 hresonance:25 ]",
|
||||
"[ 7/4 → 2/1 | s:hh hcutoff:2000 hresonance:25 ]",
|
||||
"[ 2/1 → 5/2 | s:bd hcutoff:2000 ]",
|
||||
"[ 5/2 → 3/1 | s:sd hcutoff:2000 ]",
|
||||
"[ 2/1 → 9/4 | s:hh hcutoff:2000 ]",
|
||||
"[ 9/4 → 5/2 | s:hh hcutoff:2000 ]",
|
||||
"[ 5/2 → 11/4 | s:hh hcutoff:2000 ]",
|
||||
"[ 11/4 → 3/1 | s:hh hcutoff:2000 ]",
|
||||
"[ 3/1 → 7/2 | s:bd hcutoff:2000 hresonance:25 ]",
|
||||
"[ 7/2 → 4/1 | s:sd hcutoff:2000 hresonance:25 ]",
|
||||
"[ 3/1 → 13/4 | s:hh hcutoff:2000 hresonance:25 ]",
|
||||
"[ 13/4 → 7/2 | s:hh hcutoff:2000 hresonance:25 ]",
|
||||
"[ 7/2 → 15/4 | s:hh hcutoff:2000 hresonance:25 ]",
|
||||
"[ 15/4 → 4/1 | s:hh hcutoff:2000 hresonance:25 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "hpq" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd hcutoff:2000 hresonance:0 ]",
|
||||
"[ 1/2 → 1/1 | s:sd hcutoff:2000 hresonance:0 ]",
|
||||
@@ -1959,19 +2014,19 @@ exports[`runs examples > example "hresonance" example index 0 1`] = `
|
||||
exports[`runs examples > example "hurry" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 3/4 | s:bd speed:1 ]",
|
||||
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd:2 speed:1 ]",
|
||||
"[ 3/4 ⇜ (1/1 → 3/2) | s:sd:2 speed:1 ]",
|
||||
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd n:2 speed:1 ]",
|
||||
"[ 3/4 ⇜ (1/1 → 3/2) | s:sd n:2 speed:1 ]",
|
||||
"[ 3/2 → 15/8 | s:bd speed:2 ]",
|
||||
"[ (15/8 → 2/1) ⇝ 9/4 | s:sd:2 speed:2 ]",
|
||||
"[ 15/8 ⇜ (2/1 → 9/4) | s:sd:2 speed:2 ]",
|
||||
"[ (15/8 → 2/1) ⇝ 9/4 | s:sd n:2 speed:2 ]",
|
||||
"[ 15/8 ⇜ (2/1 → 9/4) | s:sd n:2 speed:2 ]",
|
||||
"[ 9/4 → 21/8 | s:bd speed:2 ]",
|
||||
"[ 21/8 → 3/1 | s:sd:2 speed:2 ]",
|
||||
"[ 21/8 → 3/1 | s:sd n:2 speed:2 ]",
|
||||
"[ 3/1 → 51/16 | s:bd speed:4 ]",
|
||||
"[ 51/16 → 27/8 | s:sd:2 speed:4 ]",
|
||||
"[ 51/16 → 27/8 | s:sd n:2 speed:4 ]",
|
||||
"[ 27/8 → 57/16 | s:bd speed:4 ]",
|
||||
"[ 57/16 → 15/4 | s:sd:2 speed:4 ]",
|
||||
"[ 57/16 → 15/4 | s:sd n:2 speed:4 ]",
|
||||
"[ 15/4 → 63/16 | s:bd speed:4 ]",
|
||||
"[ (63/16 → 4/1) ⇝ 33/8 | s:sd:2 speed:4 ]",
|
||||
"[ (63/16 → 4/1) ⇝ 33/8 | s:sd n:2 speed:4 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -2385,6 +2440,97 @@ exports[`runs examples > example "loopAtCps" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lpf" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd cutoff:4000 ]",
|
||||
"[ 1/2 → 1/1 | s:sd cutoff:4000 ]",
|
||||
"[ 0/1 → 1/3 | s:hh cutoff:4000 ]",
|
||||
"[ 1/3 → 2/3 | s:hh cutoff:4000 ]",
|
||||
"[ 2/3 → 1/1 | s:hh cutoff:4000 ]",
|
||||
"[ 1/1 → 3/2 | s:bd cutoff:2000 ]",
|
||||
"[ 3/2 → 2/1 | s:sd cutoff:2000 ]",
|
||||
"[ 1/1 → 4/3 | s:hh cutoff:2000 ]",
|
||||
"[ 4/3 → 5/3 | s:hh cutoff:2000 ]",
|
||||
"[ 5/3 → 2/1 | s:hh cutoff:2000 ]",
|
||||
"[ 2/1 → 5/2 | s:bd cutoff:1000 ]",
|
||||
"[ 5/2 → 3/1 | s:sd cutoff:1000 ]",
|
||||
"[ 2/1 → 7/3 | s:hh cutoff:1000 ]",
|
||||
"[ 7/3 → 8/3 | s:hh cutoff:1000 ]",
|
||||
"[ 8/3 → 3/1 | s:hh cutoff:1000 ]",
|
||||
"[ 3/1 → 7/2 | s:bd cutoff:500 ]",
|
||||
"[ 7/2 → 4/1 | s:sd cutoff:500 ]",
|
||||
"[ 3/1 → 10/3 | s:hh cutoff:500 ]",
|
||||
"[ 10/3 → 11/3 | s:hh cutoff:500 ]",
|
||||
"[ 11/3 → 4/1 | s:hh cutoff:500 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lpf" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:bd cutoff:1000 resonance:0 ]",
|
||||
"[ 1/8 → 1/4 | s:bd cutoff:1000 resonance:0 ]",
|
||||
"[ 1/4 → 3/8 | s:bd cutoff:1000 resonance:10 ]",
|
||||
"[ 3/8 → 1/2 | s:bd cutoff:1000 resonance:10 ]",
|
||||
"[ 1/2 → 5/8 | s:bd cutoff:1000 resonance:20 ]",
|
||||
"[ 5/8 → 3/4 | s:bd cutoff:1000 resonance:20 ]",
|
||||
"[ 3/4 → 7/8 | s:bd cutoff:1000 resonance:30 ]",
|
||||
"[ 7/8 → 1/1 | s:bd cutoff:1000 resonance:30 ]",
|
||||
"[ 1/1 → 9/8 | s:bd cutoff:1000 resonance:0 ]",
|
||||
"[ 9/8 → 5/4 | s:bd cutoff:1000 resonance:0 ]",
|
||||
"[ 5/4 → 11/8 | s:bd cutoff:1000 resonance:10 ]",
|
||||
"[ 11/8 → 3/2 | s:bd cutoff:1000 resonance:10 ]",
|
||||
"[ 3/2 → 13/8 | s:bd cutoff:1000 resonance:20 ]",
|
||||
"[ 13/8 → 7/4 | s:bd cutoff:1000 resonance:20 ]",
|
||||
"[ 7/4 → 15/8 | s:bd cutoff:1000 resonance:30 ]",
|
||||
"[ 15/8 → 2/1 | s:bd cutoff:1000 resonance:30 ]",
|
||||
"[ 2/1 → 17/8 | s:bd cutoff:1000 resonance:0 ]",
|
||||
"[ 17/8 → 9/4 | s:bd cutoff:1000 resonance:0 ]",
|
||||
"[ 9/4 → 19/8 | s:bd cutoff:1000 resonance:10 ]",
|
||||
"[ 19/8 → 5/2 | s:bd cutoff:1000 resonance:10 ]",
|
||||
"[ 5/2 → 21/8 | s:bd cutoff:1000 resonance:20 ]",
|
||||
"[ 21/8 → 11/4 | s:bd cutoff:1000 resonance:20 ]",
|
||||
"[ 11/4 → 23/8 | s:bd cutoff:1000 resonance:30 ]",
|
||||
"[ 23/8 → 3/1 | s:bd cutoff:1000 resonance:30 ]",
|
||||
"[ 3/1 → 25/8 | s:bd cutoff:1000 resonance:0 ]",
|
||||
"[ 25/8 → 13/4 | s:bd cutoff:1000 resonance:0 ]",
|
||||
"[ 13/4 → 27/8 | s:bd cutoff:1000 resonance:10 ]",
|
||||
"[ 27/8 → 7/2 | s:bd cutoff:1000 resonance:10 ]",
|
||||
"[ 7/2 → 29/8 | s:bd cutoff:1000 resonance:20 ]",
|
||||
"[ 29/8 → 15/4 | s:bd cutoff:1000 resonance:20 ]",
|
||||
"[ 15/4 → 31/8 | s:bd cutoff:1000 resonance:30 ]",
|
||||
"[ 31/8 → 4/1 | s:bd cutoff:1000 resonance:30 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lpq" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd cutoff:2000 resonance:0 ]",
|
||||
"[ 1/2 → 1/1 | s:sd cutoff:2000 resonance:0 ]",
|
||||
"[ 0/1 → 1/4 | s:hh cutoff:2000 resonance:0 ]",
|
||||
"[ 1/4 → 1/2 | s:hh cutoff:2000 resonance:0 ]",
|
||||
"[ 1/2 → 3/4 | s:hh cutoff:2000 resonance:0 ]",
|
||||
"[ 3/4 → 1/1 | s:hh cutoff:2000 resonance:0 ]",
|
||||
"[ 1/1 → 3/2 | s:bd cutoff:2000 resonance:10 ]",
|
||||
"[ 3/2 → 2/1 | s:sd cutoff:2000 resonance:10 ]",
|
||||
"[ 1/1 → 5/4 | s:hh cutoff:2000 resonance:10 ]",
|
||||
"[ 5/4 → 3/2 | s:hh cutoff:2000 resonance:10 ]",
|
||||
"[ 3/2 → 7/4 | s:hh cutoff:2000 resonance:10 ]",
|
||||
"[ 7/4 → 2/1 | s:hh cutoff:2000 resonance:10 ]",
|
||||
"[ 2/1 → 5/2 | s:bd cutoff:2000 resonance:20 ]",
|
||||
"[ 5/2 → 3/1 | s:sd cutoff:2000 resonance:20 ]",
|
||||
"[ 2/1 → 9/4 | s:hh cutoff:2000 resonance:20 ]",
|
||||
"[ 9/4 → 5/2 | s:hh cutoff:2000 resonance:20 ]",
|
||||
"[ 5/2 → 11/4 | s:hh cutoff:2000 resonance:20 ]",
|
||||
"[ 11/4 → 3/1 | s:hh cutoff:2000 resonance:20 ]",
|
||||
"[ 3/1 → 7/2 | s:bd cutoff:2000 resonance:30 ]",
|
||||
"[ 7/2 → 4/1 | s:sd cutoff:2000 resonance:30 ]",
|
||||
"[ 3/1 → 13/4 | s:hh cutoff:2000 resonance:30 ]",
|
||||
"[ 13/4 → 7/2 | s:hh cutoff:2000 resonance:30 ]",
|
||||
"[ 7/2 → 15/4 | s:hh cutoff:2000 resonance:30 ]",
|
||||
"[ 15/4 → 4/1 | s:hh cutoff:2000 resonance:30 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lrate" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | n:0 s:supersquare leslie:1 lrate:1 ]",
|
||||
@@ -2456,40 +2602,26 @@ exports[`runs examples > example "mul" example index 0 1`] = `
|
||||
|
||||
exports[`runs examples > example "n" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:superpiano n:0 ]",
|
||||
"[ 1/1 → 2/1 | s:superpiano n:1 ]",
|
||||
"[ 2/1 → 3/1 | s:superpiano n:2 ]",
|
||||
"[ 3/1 → 4/1 | s:superpiano n:3 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "n" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:superpiano n:c4 ]",
|
||||
"[ 1/1 → 2/1 | s:superpiano n:d4 ]",
|
||||
"[ 2/1 → 3/1 | s:superpiano n:e4 ]",
|
||||
"[ 3/1 → 4/1 | s:superpiano n:g4 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "n" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | n:0 s:east ]",
|
||||
"[ 1/4 → 1/2 | n:1 s:east ]",
|
||||
"[ 1/2 → 3/4 | n:2 s:east ]",
|
||||
"[ 3/4 → 1/1 | n:3 s:east ]",
|
||||
"[ 1/1 → 5/4 | n:0 s:east ]",
|
||||
"[ 5/4 → 3/2 | n:1 s:east ]",
|
||||
"[ 3/2 → 7/4 | n:2 s:east ]",
|
||||
"[ 7/4 → 2/1 | n:3 s:east ]",
|
||||
"[ 2/1 → 9/4 | n:0 s:east ]",
|
||||
"[ 9/4 → 5/2 | n:1 s:east ]",
|
||||
"[ 5/2 → 11/4 | n:2 s:east ]",
|
||||
"[ 11/4 → 3/1 | n:3 s:east ]",
|
||||
"[ 3/1 → 13/4 | n:0 s:east ]",
|
||||
"[ 13/4 → 7/2 | n:1 s:east ]",
|
||||
"[ 7/2 → 15/4 | n:2 s:east ]",
|
||||
"[ 15/4 → 4/1 | n:3 s:east ]",
|
||||
"[ 0/1 → 1/2 | s:bd n:0 ]",
|
||||
"[ 1/2 → 1/1 | s:sd n:0 ]",
|
||||
"[ 0/1 → 1/3 | s:hh n:0 ]",
|
||||
"[ 1/3 → 2/3 | s:hh n:0 ]",
|
||||
"[ 2/3 → 1/1 | s:hh n:0 ]",
|
||||
"[ 1/1 → 3/2 | s:bd n:1 ]",
|
||||
"[ 3/2 → 2/1 | s:sd n:1 ]",
|
||||
"[ 1/1 → 4/3 | s:hh n:1 ]",
|
||||
"[ 4/3 → 5/3 | s:hh n:1 ]",
|
||||
"[ 5/3 → 2/1 | s:hh n:1 ]",
|
||||
"[ 2/1 → 5/2 | s:bd n:0 ]",
|
||||
"[ 5/2 → 3/1 | s:sd n:0 ]",
|
||||
"[ 2/1 → 7/3 | s:hh n:0 ]",
|
||||
"[ 7/3 → 8/3 | s:hh n:0 ]",
|
||||
"[ 8/3 → 3/1 | s:hh n:0 ]",
|
||||
"[ 3/1 → 7/2 | s:bd n:1 ]",
|
||||
"[ 7/2 → 4/1 | s:sd n:1 ]",
|
||||
"[ 3/1 → 10/3 | s:hh n:1 ]",
|
||||
"[ 10/3 → 11/3 | s:hh n:1 ]",
|
||||
"[ 11/3 → 4/1 | s:hh n:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -2530,6 +2662,69 @@ exports[`runs examples > example "never" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "note" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c ]",
|
||||
"[ 1/4 → 1/2 | note:a ]",
|
||||
"[ 1/2 → 3/4 | note:f ]",
|
||||
"[ 3/4 → 1/1 | note:e ]",
|
||||
"[ 1/1 → 5/4 | note:c ]",
|
||||
"[ 5/4 → 3/2 | note:a ]",
|
||||
"[ 3/2 → 7/4 | note:f ]",
|
||||
"[ 7/4 → 2/1 | note:e ]",
|
||||
"[ 2/1 → 9/4 | note:c ]",
|
||||
"[ 9/4 → 5/2 | note:a ]",
|
||||
"[ 5/2 → 11/4 | note:f ]",
|
||||
"[ 11/4 → 3/1 | note:e ]",
|
||||
"[ 3/1 → 13/4 | note:c ]",
|
||||
"[ 13/4 → 7/2 | note:a ]",
|
||||
"[ 7/2 → 15/4 | note:f ]",
|
||||
"[ 15/4 → 4/1 | note:e ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "note" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c4 ]",
|
||||
"[ 1/4 → 1/2 | note:a4 ]",
|
||||
"[ 1/2 → 3/4 | note:f4 ]",
|
||||
"[ 3/4 → 1/1 | note:e4 ]",
|
||||
"[ 1/1 → 5/4 | note:c4 ]",
|
||||
"[ 5/4 → 3/2 | note:a4 ]",
|
||||
"[ 3/2 → 7/4 | note:f4 ]",
|
||||
"[ 7/4 → 2/1 | note:e4 ]",
|
||||
"[ 2/1 → 9/4 | note:c4 ]",
|
||||
"[ 9/4 → 5/2 | note:a4 ]",
|
||||
"[ 5/2 → 11/4 | note:f4 ]",
|
||||
"[ 11/4 → 3/1 | note:e4 ]",
|
||||
"[ 3/1 → 13/4 | note:c4 ]",
|
||||
"[ 13/4 → 7/2 | note:a4 ]",
|
||||
"[ 7/2 → 15/4 | note:f4 ]",
|
||||
"[ 15/4 → 4/1 | note:e4 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "note" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:60 ]",
|
||||
"[ 1/4 → 1/2 | note:69 ]",
|
||||
"[ 1/2 → 3/4 | note:65 ]",
|
||||
"[ 3/4 → 1/1 | note:64 ]",
|
||||
"[ 1/1 → 5/4 | note:60 ]",
|
||||
"[ 5/4 → 3/2 | note:69 ]",
|
||||
"[ 3/2 → 7/4 | note:65 ]",
|
||||
"[ 7/4 → 2/1 | note:64 ]",
|
||||
"[ 2/1 → 9/4 | note:60 ]",
|
||||
"[ 9/4 → 5/2 | note:69 ]",
|
||||
"[ 5/2 → 11/4 | note:65 ]",
|
||||
"[ 11/4 → 3/1 | note:64 ]",
|
||||
"[ 3/1 → 13/4 | note:60 ]",
|
||||
"[ 13/4 → 7/2 | note:69 ]",
|
||||
"[ 7/2 → 15/4 | note:65 ]",
|
||||
"[ 15/4 → 4/1 | note:64 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "octave" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | n:0 s:supersquare octave:3 ]",
|
||||
@@ -3048,35 +3243,6 @@ exports[`runs examples > example "reset" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "resonance" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd cutoff:2000 resonance:0 ]",
|
||||
"[ 1/2 → 1/1 | s:sd cutoff:2000 resonance:0 ]",
|
||||
"[ 0/1 → 1/4 | s:hh cutoff:2000 resonance:0 ]",
|
||||
"[ 1/4 → 1/2 | s:hh cutoff:2000 resonance:0 ]",
|
||||
"[ 1/2 → 3/4 | s:hh cutoff:2000 resonance:0 ]",
|
||||
"[ 3/4 → 1/1 | s:hh cutoff:2000 resonance:0 ]",
|
||||
"[ 1/1 → 3/2 | s:bd cutoff:2000 resonance:10 ]",
|
||||
"[ 3/2 → 2/1 | s:sd cutoff:2000 resonance:10 ]",
|
||||
"[ 1/1 → 5/4 | s:hh cutoff:2000 resonance:10 ]",
|
||||
"[ 5/4 → 3/2 | s:hh cutoff:2000 resonance:10 ]",
|
||||
"[ 3/2 → 7/4 | s:hh cutoff:2000 resonance:10 ]",
|
||||
"[ 7/4 → 2/1 | s:hh cutoff:2000 resonance:10 ]",
|
||||
"[ 2/1 → 5/2 | s:bd cutoff:2000 resonance:20 ]",
|
||||
"[ 5/2 → 3/1 | s:sd cutoff:2000 resonance:20 ]",
|
||||
"[ 2/1 → 9/4 | s:hh cutoff:2000 resonance:20 ]",
|
||||
"[ 9/4 → 5/2 | s:hh cutoff:2000 resonance:20 ]",
|
||||
"[ 5/2 → 11/4 | s:hh cutoff:2000 resonance:20 ]",
|
||||
"[ 11/4 → 3/1 | s:hh cutoff:2000 resonance:20 ]",
|
||||
"[ 3/1 → 7/2 | s:bd cutoff:2000 resonance:30 ]",
|
||||
"[ 7/2 → 4/1 | s:sd cutoff:2000 resonance:30 ]",
|
||||
"[ 3/1 → 13/4 | s:hh cutoff:2000 resonance:30 ]",
|
||||
"[ 13/4 → 7/2 | s:hh cutoff:2000 resonance:30 ]",
|
||||
"[ 7/2 → 15/4 | s:hh cutoff:2000 resonance:30 ]",
|
||||
"[ 15/4 → 4/1 | s:hh cutoff:2000 resonance:30 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "restart" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd ]",
|
||||
@@ -3161,16 +3327,29 @@ exports[`runs examples > example "room" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "room" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd room:0.9 size:1 ]",
|
||||
"[ 1/2 → 1/1 | s:sd room:0.9 size:1 ]",
|
||||
"[ 1/1 → 3/2 | s:bd room:0.9 size:4 ]",
|
||||
"[ 3/2 → 2/1 | s:sd room:0.9 size:4 ]",
|
||||
"[ 2/1 → 5/2 | s:bd room:0.9 size:1 ]",
|
||||
"[ 5/2 → 3/1 | s:sd room:0.9 size:1 ]",
|
||||
"[ 3/1 → 7/2 | s:bd room:0.9 size:4 ]",
|
||||
"[ 7/2 → 4/1 | s:sd room:0.9 size:4 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "roomsize" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd room:0.8 roomsize:0 ]",
|
||||
"[ 1/2 → 1/1 | s:sd room:0.8 roomsize:0 ]",
|
||||
"[ 1/1 → 3/2 | s:bd room:0.8 roomsize:1 ]",
|
||||
"[ 3/2 → 2/1 | s:sd room:0.8 roomsize:1 ]",
|
||||
"[ 2/1 → 5/2 | s:bd room:0.8 roomsize:2 ]",
|
||||
"[ 5/2 → 3/1 | s:sd room:0.8 roomsize:2 ]",
|
||||
"[ 3/1 → 7/2 | s:bd room:0.8 roomsize:4 ]",
|
||||
"[ 7/2 → 4/1 | s:sd room:0.8 roomsize:4 ]",
|
||||
"[ 0/1 → 1/2 | s:bd room:0.8 size:0 ]",
|
||||
"[ 1/2 → 1/1 | s:sd room:0.8 size:0 ]",
|
||||
"[ 1/1 → 3/2 | s:bd room:0.8 size:1 ]",
|
||||
"[ 3/2 → 2/1 | s:sd room:0.8 size:1 ]",
|
||||
"[ 2/1 → 5/2 | s:bd room:0.8 size:2 ]",
|
||||
"[ 5/2 → 3/1 | s:sd room:0.8 size:2 ]",
|
||||
"[ 3/1 → 7/2 | s:bd room:0.8 size:4 ]",
|
||||
"[ 7/2 → 4/1 | s:sd room:0.8 size:4 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -3234,6 +3413,27 @@ exports[`runs examples > example "s" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "s" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:bd n:0 ]",
|
||||
"[ 1/4 → 1/2 | s:bd n:1 ]",
|
||||
"[ 1/2 → 3/4 | s:bd n:0 gain:0.3 ]",
|
||||
"[ 3/4 → 1/1 | s:bd n:1 gain:1.4 ]",
|
||||
"[ 1/1 → 5/4 | s:bd n:0 ]",
|
||||
"[ 5/4 → 3/2 | s:bd n:1 ]",
|
||||
"[ 3/2 → 7/4 | s:bd n:0 gain:0.3 ]",
|
||||
"[ 7/4 → 2/1 | s:bd n:1 gain:1.4 ]",
|
||||
"[ 2/1 → 9/4 | s:bd n:0 ]",
|
||||
"[ 9/4 → 5/2 | s:bd n:1 ]",
|
||||
"[ 5/2 → 11/4 | s:bd n:0 gain:0.3 ]",
|
||||
"[ 11/4 → 3/1 | s:bd n:1 gain:1.4 ]",
|
||||
"[ 3/1 → 13/4 | s:bd n:0 ]",
|
||||
"[ 13/4 → 7/2 | s:bd n:1 ]",
|
||||
"[ 7/2 → 15/4 | s:bd n:0 gain:0.3 ]",
|
||||
"[ 15/4 → 4/1 | s:bd n:1 gain:1.4 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "samples" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:bd ]",
|
||||
@@ -3259,6 +3459,31 @@ exports[`runs examples > example "samples" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "samples" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:bd ]",
|
||||
"[ 1/2 → 3/4 | s:bd ]",
|
||||
"[ 1/4 → 1/2 | s:hh ]",
|
||||
"[ 3/4 → 1/1 | s:hh ]",
|
||||
"[ 1/2 → 1/1 | s:sd ]",
|
||||
"[ 1/1 → 5/4 | s:bd ]",
|
||||
"[ 3/2 → 7/4 | s:bd ]",
|
||||
"[ 5/4 → 3/2 | s:hh ]",
|
||||
"[ 7/4 → 2/1 | s:hh ]",
|
||||
"[ 3/2 → 2/1 | s:sd ]",
|
||||
"[ 2/1 → 9/4 | s:bd ]",
|
||||
"[ 5/2 → 11/4 | s:bd ]",
|
||||
"[ 9/4 → 5/2 | s:hh ]",
|
||||
"[ 11/4 → 3/1 | s:hh ]",
|
||||
"[ 5/2 → 3/1 | s:sd ]",
|
||||
"[ 3/1 → 13/4 | s:bd ]",
|
||||
"[ 7/2 → 15/4 | s:bd ]",
|
||||
"[ 13/4 → 7/2 | s:hh ]",
|
||||
"[ 15/4 → 4/1 | s:hh ]",
|
||||
"[ 7/2 → 4/1 | s:sd ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "saw" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/32 | note:c3 ]",
|
||||
@@ -3302,6 +3527,35 @@ exports[`runs examples > example "saw" example index 1 1`] = `
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "scale" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | note:C2 ]",
|
||||
"[ 1/6 → 1/3 | note:E2 ]",
|
||||
"[ 1/3 → 1/2 | note:G2 ]",
|
||||
"[ 1/2 → 2/3 | note:B2 ]",
|
||||
"[ 2/3 → 5/6 | note:G2 ]",
|
||||
"[ 5/6 → 1/1 | note:E2 ]",
|
||||
"[ 1/1 → 7/6 | note:C2 ]",
|
||||
"[ 7/6 → 4/3 | note:E2 ]",
|
||||
"[ 4/3 → 3/2 | note:G2 ]",
|
||||
"[ 3/2 → 5/3 | note:B2 ]",
|
||||
"[ 5/3 → 11/6 | note:G2 ]",
|
||||
"[ 11/6 → 2/1 | note:E2 ]",
|
||||
"[ 2/1 → 13/6 | note:C2 ]",
|
||||
"[ 13/6 → 7/3 | note:E2 ]",
|
||||
"[ 7/3 → 5/2 | note:G2 ]",
|
||||
"[ 5/2 → 8/3 | note:B2 ]",
|
||||
"[ 8/3 → 17/6 | note:G2 ]",
|
||||
"[ 17/6 → 3/1 | note:E2 ]",
|
||||
"[ 3/1 → 19/6 | note:C2 ]",
|
||||
"[ 19/6 → 10/3 | note:E2 ]",
|
||||
"[ 10/3 → 7/2 | note:G2 ]",
|
||||
"[ 7/2 → 11/3 | note:B2 ]",
|
||||
"[ 11/3 → 23/6 | note:G2 ]",
|
||||
"[ 23/6 → 4/1 | note:E2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "scale" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | note:C2 ]",
|
||||
"[ 1/6 → 1/3 | note:E2 ]",
|
||||
@@ -3330,6 +3584,43 @@ exports[`runs examples > example "scale" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "scale" example index 2 1`] = `
|
||||
[
|
||||
"[ 7/8 → 1/1 | note:C2 s:folkharp ]",
|
||||
"[ 3/4 → 7/8 | note:D2 s:folkharp ]",
|
||||
"[ 5/8 → 3/4 | note:E2 s:folkharp ]",
|
||||
"[ 1/2 → 5/8 | note:F2 s:folkharp ]",
|
||||
"[ 3/8 → 1/2 | note:G2 s:folkharp ]",
|
||||
"[ 1/4 → 3/8 | note:A2 s:folkharp ]",
|
||||
"[ 1/8 → 1/4 | note:B2 s:folkharp ]",
|
||||
"[ 0/1 → 1/8 | note:C3 s:folkharp ]",
|
||||
"[ 15/8 → 2/1 | note:C2 s:folkharp ]",
|
||||
"[ 7/4 → 15/8 | note:D2 s:folkharp ]",
|
||||
"[ 13/8 → 7/4 | note:Eb2 s:folkharp ]",
|
||||
"[ 3/2 → 13/8 | note:F2 s:folkharp ]",
|
||||
"[ 11/8 → 3/2 | note:G2 s:folkharp ]",
|
||||
"[ 5/4 → 11/8 | note:Ab2 s:folkharp ]",
|
||||
"[ 9/8 → 5/4 | note:Bb2 s:folkharp ]",
|
||||
"[ 1/1 → 9/8 | note:C3 s:folkharp ]",
|
||||
"[ 23/8 → 3/1 | note:C2 s:folkharp ]",
|
||||
"[ 11/4 → 23/8 | note:D2 s:folkharp ]",
|
||||
"[ 21/8 → 11/4 | note:E2 s:folkharp ]",
|
||||
"[ 5/2 → 21/8 | note:F2 s:folkharp ]",
|
||||
"[ 19/8 → 5/2 | note:G2 s:folkharp ]",
|
||||
"[ 9/4 → 19/8 | note:A2 s:folkharp ]",
|
||||
"[ 17/8 → 9/4 | note:B2 s:folkharp ]",
|
||||
"[ 2/1 → 17/8 | note:C3 s:folkharp ]",
|
||||
"[ 31/8 → 4/1 | note:C2 s:folkharp ]",
|
||||
"[ 15/4 → 31/8 | note:D2 s:folkharp ]",
|
||||
"[ 29/8 → 15/4 | note:Eb2 s:folkharp ]",
|
||||
"[ 7/2 → 29/8 | note:F2 s:folkharp ]",
|
||||
"[ 27/8 → 7/2 | note:G2 s:folkharp ]",
|
||||
"[ 13/4 → 27/8 | note:Ab2 s:folkharp ]",
|
||||
"[ 25/8 → 13/4 | note:Bb2 s:folkharp ]",
|
||||
"[ 3/1 → 25/8 | note:C3 s:folkharp ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | note:C3 ]",
|
||||
|
||||
@@ -298,11 +298,11 @@ exports[`renders tunes > tune: bassFuge 1`] = `
|
||||
"[ -3/4 ⇜ (0/1 → 3/4) ⇝ 5/4 | note:C5 s:flbass n:0 gain:0.3 cutoff:2924.3791043233605 resonance:10 clip:1 ]",
|
||||
"[ -3/4 ⇜ (3/4 → 1/1) ⇝ 5/4 | note:A4 s:flbass n:0 gain:0.3 cutoff:2924.3791043233605 resonance:10 clip:1 ]",
|
||||
"[ -3/4 ⇜ (3/4 → 1/1) ⇝ 5/4 | note:C5 s:flbass n:0 gain:0.3 cutoff:2924.3791043233605 resonance:10 clip:1 ]",
|
||||
"[ 0/1 → 1/2 | s:bd:1 ]",
|
||||
"[ 1/2 → 1/1 | s:bd:1 ]",
|
||||
"[ 1/2 → 1/1 | s:sd:0 ]",
|
||||
"[ 1/4 → 1/2 | s:hh:0 ]",
|
||||
"[ 3/4 → 1/1 | s:hh:0 ]",
|
||||
"[ 0/1 → 1/2 | s:bd n:1 ]",
|
||||
"[ 1/2 → 1/1 | s:bd n:1 ]",
|
||||
"[ 1/2 → 1/1 | s:sd n:0 ]",
|
||||
"[ 1/4 → 1/2 | s:hh n:0 ]",
|
||||
"[ 3/4 → 1/1 | s:hh n:0 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -313,7 +313,7 @@ exports[`renders tunes > tune: belldub 1`] = `
|
||||
"[ (5/8 → 1/1) ⇝ 5/4 | s:hh room:0 end:0.04483079938329212 ]",
|
||||
"[ 0/1 → 5/16 | s:mt gain:0.5 room:0.5 ]",
|
||||
"[ (15/16 → 1/1) ⇝ 5/4 | s:lt gain:0.5 room:0.5 ]",
|
||||
"[ (0/1 → 1/1) ⇝ 5/1 | s:misc:2 speed:1 delay:0.5 delaytime:0.3333333333333333 gain:0.4 ]",
|
||||
"[ (0/1 → 1/1) ⇝ 5/1 | s:misc n:2 speed:1 delay:0.5 delaytime:0.3333333333333333 gain:0.4 ]",
|
||||
"[ (5/8 → 1/1) ⇝ 5/4 | note:F3 s:sawtooth gain:0.5 cutoff:400.16785462816676 decay:0.05380063255866716 sustain:0 delay:0.9 room:1 ]",
|
||||
"[ (5/8 → 1/1) ⇝ 5/4 | note:A3 s:sawtooth gain:0.5 cutoff:400.16785462816676 decay:0.05380063255866716 sustain:0 delay:0.9 room:1 ]",
|
||||
"[ (5/8 → 1/1) ⇝ 5/4 | note:Bb3 s:sawtooth gain:0.5 cutoff:400.16785462816676 decay:0.05380063255866716 sustain:0 delay:0.9 room:1 ]",
|
||||
@@ -6974,16 +6974,16 @@ exports[`renders tunes > tune: flatrave 1`] = `
|
||||
"[ 1/2 → 1/1 | s:bd bank:RolandTR909 ]",
|
||||
"[ 1/2 → 1/1 | s:cp bank:RolandTR909 ]",
|
||||
"[ 1/2 → 1/1 | s:sd bank:RolandTR909 ]",
|
||||
"[ 0/1 → 1/4 | s:hh:1 end:0.02000058072071123 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 0/1 ⇜ (1/8 → 1/4) | s:hh:1 end:0.02000058072071123 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/4 → 3/8 | s:hh:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/4 → 3/8 | s:hh:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 3/8 → 1/2 | s:hh:1 end:0.020023446730265706 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 5/8 → 3/4 | s:hh:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 5/8 → 3/4 | s:hh:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 3/4 → 7/8 | s:hh:1 end:0.02013941880355398 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/8 → 1/4 | s:hh:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/8 → 1/4 | s:hh:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 0/1 → 1/4 | s:hh n:1 end:0.02000058072071123 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 0/1 ⇜ (1/8 → 1/4) | s:hh n:1 end:0.02000058072071123 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/4 → 3/8 | s:hh n:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/4 → 3/8 | s:hh n:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 3/8 → 1/2 | s:hh n:1 end:0.020023446730265706 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 5/8 → 3/4 | s:hh n:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 5/8 → 3/4 | s:hh n:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 3/4 → 7/8 | s:hh n:1 end:0.02013941880355398 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/8 → 1/4 | s:hh n:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/8 → 1/4 | s:hh n:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/8 → 1/4 | note:G1 s:sawtooth decay:0.1 sustain:0 ]",
|
||||
"[ 1/4 → 3/8 | note:G1 s:sawtooth decay:0.1 sustain:0 ]",
|
||||
"[ 1/2 → 5/8 | note:G1 s:sawtooth decay:0.1 sustain:0 ]",
|
||||
@@ -8127,8 +8127,8 @@ exports[`renders tunes > tune: loungeSponge 1`] = `
|
||||
|
||||
exports[`renders tunes > tune: meltingsubmarine 1`] = `
|
||||
[
|
||||
"[ (0/1 → 1/1) ⇝ 3/2 | s:bd:5 speed:0.7519542165100574 ]",
|
||||
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd:1 speed:0.7931522866332671 ]",
|
||||
"[ (0/1 → 1/1) ⇝ 3/2 | s:bd n:5 speed:0.7519542165100574 ]",
|
||||
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd n:1 speed:0.7931522866332671 ]",
|
||||
"[ 3/8 → 3/4 | s:hh27 speed:0.7285963821098448 ]",
|
||||
"[ (3/4 → 1/1) ⇝ 9/8 | s:hh27 speed:0.77531205091027 ]",
|
||||
"[ (0/1 → 1/1) ⇝ 3/2 | note:33.129885541275144 decay:0.15 sustain:0 s:sawtooth gain:0.4 cutoff:3669.6267869262615 ]",
|
||||
@@ -8364,15 +8364,15 @@ exports[`renders tunes > tune: randomBells 1`] = `
|
||||
|
||||
exports[`renders tunes > tune: sampleDemo 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:woodblock:1 ]",
|
||||
"[ 1/4 → 3/8 | s:woodblock:2 ]",
|
||||
"[ 0/1 → 1/8 | s:brakedrum:1 ]",
|
||||
"[ 3/4 → 7/8 | s:brakedrum:1 ]",
|
||||
"[ 3/8 → 1/2 | s:woodblock:2 speed:2 ]",
|
||||
"[ 1/2 → 1/1 | s:snare_rim:0 speed:2 ]",
|
||||
"[ 0/1 → 1/4 | s:woodblock n:1 ]",
|
||||
"[ 1/4 → 3/8 | s:woodblock n:2 ]",
|
||||
"[ 0/1 → 1/8 | s:brakedrum n:1 ]",
|
||||
"[ 3/4 → 7/8 | s:brakedrum n:1 ]",
|
||||
"[ 3/8 → 1/2 | s:woodblock n:2 speed:2 ]",
|
||||
"[ 1/2 → 1/1 | s:snare_rim n:0 speed:2 ]",
|
||||
"[ (0/1 → 1/1) ⇝ 8/1 | s:gong speed:2 ]",
|
||||
"[ 3/8 → 1/2 | s:brakedrum:1 speed:2 ]",
|
||||
"[ 3/4 → 1/1 | s:cowbell:3 speed:2 ]",
|
||||
"[ 3/8 → 1/2 | s:brakedrum n:1 speed:2 ]",
|
||||
"[ 3/4 → 1/1 | s:cowbell n:3 speed:2 ]",
|
||||
"[ -3/4 ⇜ (0/1 → 1/4) | note:Bb3 s:clavisynth gain:0.2 delay:0.25 pan:0 ]",
|
||||
"[ (3/4 → 1/1) ⇝ 7/4 | note:Bb3 s:clavisynth gain:0.2 delay:0.25 pan:1 ]",
|
||||
"[ -1/4 ⇜ (0/1 → 3/4) | note:F3 s:clavisynth gain:0.2 delay:0.25 pan:1 ]",
|
||||
|
||||
@@ -19,6 +19,13 @@ npm run build # <- builds repl + tutorial to ../docs
|
||||
npm run preview # <- test static build
|
||||
```
|
||||
|
||||
## Generate PWA icons
|
||||
|
||||
```sh
|
||||
cd website/public
|
||||
npx pwa-asset-generator icon.png icons
|
||||
```
|
||||
|
||||
# Standard Readme of Astro Starter Kit: Docs Site
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,23 +1,47 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import preact from '@astrojs/preact';
|
||||
import react from '@astrojs/react';
|
||||
|
||||
import mdx from '@astrojs/mdx';
|
||||
|
||||
import remarkToc from 'remark-toc';
|
||||
import rehypeSlug from 'rehype-slug';
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||
import rehypeUrls from 'rehype-urls';
|
||||
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import AstroPWA from '@vite-pwa/astro';
|
||||
// import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
const site = `https://strudel.tidalcycles.org`; // root url without a path
|
||||
const base = '/'; // base path of the strudel site
|
||||
|
||||
// this rehype plugin converts relative anchor links to absolute ones
|
||||
// it wokrs by prepending the absolute page path to anchor links
|
||||
// example: #gain -> /learn/effects/#gain
|
||||
// this is necessary when using a base href like <base href={base} />
|
||||
// in this setup, relative anchor links will always link to base, instead of the current page
|
||||
function absoluteAnchors() {
|
||||
return (tree, file) => {
|
||||
const chunks = file.history[0].split('/src/pages/'); // file.history[0] is the file path
|
||||
const path = chunks[chunks.length - 1].slice(0, -4); // only path inside src/pages, without .mdx
|
||||
return rehypeUrls((url) => {
|
||||
if (!url.href.startsWith('#')) {
|
||||
return;
|
||||
}
|
||||
const baseWithSlash = base.endsWith('/') ? base : base + '/';
|
||||
const absoluteUrl = baseWithSlash + path + url.href;
|
||||
// console.log(url.href + ' -> ', absoluteUrl);
|
||||
return absoluteUrl;
|
||||
})(tree);
|
||||
};
|
||||
}
|
||||
const options = {
|
||||
// See https://mdxjs.com/advanced/plugins
|
||||
remarkPlugins: [
|
||||
remarkToc,
|
||||
// E.g. `remark-frontmatter`
|
||||
],
|
||||
rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings],
|
||||
rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'append' }], absoluteAnchors],
|
||||
};
|
||||
|
||||
// https://astro.build/config
|
||||
@@ -29,9 +53,75 @@ export default defineConfig({
|
||||
react(),
|
||||
mdx(options),
|
||||
tailwind(),
|
||||
AstroPWA({
|
||||
registerType: 'autoUpdate',
|
||||
injectRegister: 'auto',
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,json,wav,mp3,ogg}'],
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: ({ url }) =>
|
||||
[
|
||||
/^https:\/\/raw\.githubusercontent\.com\/.*/i,
|
||||
/^https:\/\/freesound\.org\/.*/i,
|
||||
/^https:\/\/cdn\.freesound\.org\/.*/i,
|
||||
/^https:\/\/shabda\.ndre\.gr\/.*/i,
|
||||
].some((regex) => regex.test(url)),
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'external-samples',
|
||||
expiration: {
|
||||
maxEntries: 5000,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 30, // <== 14 days
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
devOptions: {
|
||||
enabled: false,
|
||||
},
|
||||
manifest: {
|
||||
includeAssets: ['favicon.ico', 'icons/apple-icon-180.png', 'favicon.svg'],
|
||||
name: 'Strudel REPL',
|
||||
short_name: 'Strudel',
|
||||
description:
|
||||
'Strudel is a music live coding environment for the browser, porting the TidalCycles pattern language to JavaScript.',
|
||||
theme_color: '#222222',
|
||||
icons: [
|
||||
{
|
||||
src: 'icons/manifest-icon-192.maskable.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
purpose: 'any',
|
||||
},
|
||||
{
|
||||
src: 'icons/manifest-icon-192.maskable.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
purpose: 'maskable',
|
||||
},
|
||||
{
|
||||
src: 'icons/manifest-icon-512.maskable.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any',
|
||||
},
|
||||
{
|
||||
src: 'icons/manifest-icon-512.maskable.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'maskable',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
site: `https://strudel.tidalcycles.org`,
|
||||
base: '/',
|
||||
site,
|
||||
base,
|
||||
vite: {
|
||||
ssr: {
|
||||
// Example: Force a broken package to skip SSR processing, if needed
|
||||
@@ -39,13 +129,3 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
build: {
|
||||
outDir: '../out',
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
plugins: [visualizer({ template: 'treemap' })],
|
||||
},
|
||||
},
|
||||
*/
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
"@docsearch/react": "^3.1.0",
|
||||
"@headlessui/react": "^1.7.7",
|
||||
"@heroicons/react": "^2.0.13",
|
||||
"@nanostores/persistent": "^0.7.0",
|
||||
"@nanostores/react": "^0.4.1",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/csound": "workspace:*",
|
||||
"@strudel.cycles/midi": "workspace:*",
|
||||
@@ -34,23 +36,30 @@
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"@strudel.cycles/xen": "workspace:*",
|
||||
"@supabase/supabase-js": "^1.35.3",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@uiw/codemirror-themes-all": "^4.19.8",
|
||||
"astro": "^1.7.2",
|
||||
"canvas": "^2.11.0",
|
||||
"fraction.js": "^4.2.0",
|
||||
"nanoid": "^4.0.0",
|
||||
"nanostores": "^0.7.4",
|
||||
"preact": "^10.7.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"rehype-autolink-headings": "^6.1.1",
|
||||
"rehype-slug": "^5.0.1",
|
||||
"rehype-urls": "^1.1.1",
|
||||
"remark-toc": "^8.0.1",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"canvas": "^2.11.0"
|
||||
"tailwindcss": "^3.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"html-escaper": "^3.0.3"
|
||||
"@vite-pwa/astro": "^0.0.1",
|
||||
"html-escaper": "^3.0.3",
|
||||
"vite-plugin-pwa": "^0.14.1",
|
||||
"workbox-window": "^6.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
<svg height="100%" viewBox="0.00 0.00 538.13 507.20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet">
|
||||
<defs><pattern id="warning" width="12" height="12" patternUnits="userSpaceOnUse" patternTransform="rotate(45 50 50)">
|
||||
<line class="line0" stroke-width="6px" x1="3" x2="3" y2="12"></line>
|
||||
<line class="line1" stroke-width="6px" x1="9" x2="9" y2="12"></line>
|
||||
</pattern></defs><g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 503.2)">
|
||||
|
||||
<!-- @strudel.cycles/core@0.6.8 -->
|
||||
<g id="node1" class="node module-_strudel_cycles_core maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/core@0.6.8">
|
||||
<title>@strudel.cycles/core@0.6.8</title>
|
||||
<path fill="none" stroke="black" d="M174.8,-37.3C174.8,-37.3 56.9,-37.3 56.9,-37.3 53.66,-37.3 50.43,-34.07 50.43,-30.83 50.43,-30.83 50.43,-24.37 50.43,-24.37 50.43,-21.13 53.66,-17.9 56.9,-17.9 56.9,-17.9 174.8,-17.9 174.8,-17.9 178.03,-17.9 181.26,-21.13 181.26,-24.37 181.26,-24.37 181.26,-30.83 181.26,-30.83 181.26,-34.07 178.03,-37.3 174.8,-37.3"></path>
|
||||
<text text-anchor="middle" x="115.85" y="-24.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/core@0.6.8</text>
|
||||
</g>
|
||||
<!-- fraction.js@4.2.0 -->
|
||||
<g id="node21" class="node module-fraction_js maintainer-infusion license-mit" data-module="fraction.js@4.2.0">
|
||||
<title>fraction.js@4.2.0</title>
|
||||
<path fill="none" stroke="black" d="M343.81,-19.3C343.81,-19.3 273.9,-19.3 273.9,-19.3 270.67,-19.3 267.44,-16.07 267.44,-12.83 267.44,-12.83 267.44,-6.37 267.44,-6.37 267.44,-3.13 270.67,0.1 273.9,0.1 273.9,0.1 343.81,0.1 343.81,0.1 347.05,0.1 350.28,-3.13 350.28,-6.37 350.28,-6.37 350.28,-12.83 350.28,-12.83 350.28,-16.07 347.05,-19.3 343.81,-19.3"></path>
|
||||
<text text-anchor="middle" x="308.86" y="-6.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">fraction.js@4.2.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/core@0.6.8->fraction.js@4.2.0 -->
|
||||
|
||||
<!-- @strudel.cycles/csound@0.6.2 -->
|
||||
<g id="node2" class="node module-_strudel_cycles_csound maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/csound@0.6.2">
|
||||
<title>@strudel.cycles/csound@0.6.2</title>
|
||||
<path fill="none" stroke="black" d="M180.53,-296.3C180.53,-296.3 51.16,-296.3 51.16,-296.3 47.93,-296.3 44.7,-293.07 44.7,-289.83 44.7,-289.83 44.7,-283.37 44.7,-283.37 44.7,-280.13 47.93,-276.9 51.16,-276.9 51.16,-276.9 180.53,-276.9 180.53,-276.9 183.77,-276.9 187,-280.13 187,-283.37 187,-283.37 187,-289.83 187,-289.83 187,-293.07 183.77,-296.3 180.53,-296.3"></path>
|
||||
<text text-anchor="middle" x="115.85" y="-283.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/csound@0.6.2</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/csound@0.6.2->@strudel.cycles/core@0.6.8 -->
|
||||
|
||||
<!-- @strudel.cycles/webaudio@0.6.1 -->
|
||||
<g id="node12" class="node module-_strudel_cycles_webaudio maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/webaudio@0.6.1">
|
||||
<title>@strudel.cycles/webaudio@0.6.1</title>
|
||||
<path fill="none" stroke="black" d="M186.63,-259.3C186.63,-259.3 45.06,-259.3 45.06,-259.3 41.83,-259.3 38.59,-256.07 38.59,-252.83 38.59,-252.83 38.59,-246.37 38.59,-246.37 38.59,-243.13 41.83,-239.9 45.06,-239.9 45.06,-239.9 186.63,-239.9 186.63,-239.9 189.87,-239.9 193.1,-243.13 193.1,-246.37 193.1,-246.37 193.1,-252.83 193.1,-252.83 193.1,-256.07 189.87,-259.3 186.63,-259.3"></path>
|
||||
<text text-anchor="middle" x="115.85" y="-246.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/webaudio@0.6.1</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/csound@0.6.2->@strudel.cycles/webaudio@0.6.1 -->
|
||||
|
||||
<!-- @csound/browser@6.18.5 -->
|
||||
<g id="node14" class="node module-_csound_browser collapsed maintainer-skyne maintainer-kunstmusik maintainer-eddyc maintainer-hlolli license-lgpl_2_1" data-module="@csound/browser@6.18.5">
|
||||
<title>@csound/browser@6.18.5</title>
|
||||
<path fill="none" stroke="black" d="M364.3,-257.3C364.3,-257.3 253.41,-257.3 253.41,-257.3 250.18,-257.3 246.95,-254.07 246.95,-250.83 246.95,-250.83 246.95,-244.37 246.95,-244.37 246.95,-241.13 250.18,-237.9 253.41,-237.9 253.41,-237.9 364.3,-237.9 364.3,-237.9 367.54,-237.9 370.77,-241.13 370.77,-244.37 370.77,-244.37 370.77,-250.83 370.77,-250.83 370.77,-254.07 367.54,-257.3 364.3,-257.3"></path>
|
||||
<text text-anchor="middle" x="308.86" y="-244.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@csound/browser@6.18.5</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/csound@0.6.2->@csound/browser@6.18.5 -->
|
||||
|
||||
<!-- @strudel.cycles/embed@0.2.0 -->
|
||||
<g id="node3" class="node module-_strudel_cycles_embed maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/embed@0.2.0">
|
||||
<title>@strudel.cycles/embed@0.2.0</title>
|
||||
<path fill="none" stroke="black" d="M179.69,-462.3C179.69,-462.3 52.01,-462.3 52.01,-462.3 48.77,-462.3 45.54,-459.07 45.54,-455.83 45.54,-455.83 45.54,-449.37 45.54,-449.37 45.54,-446.13 48.77,-442.9 52.01,-442.9 52.01,-442.9 179.69,-442.9 179.69,-442.9 182.92,-442.9 186.16,-446.13 186.16,-449.37 186.16,-449.37 186.16,-455.83 186.16,-455.83 186.16,-459.07 182.92,-462.3 179.69,-462.3"></path>
|
||||
<text text-anchor="middle" x="115.85" y="-449.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/embed@0.2.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/midi@0.6.0 -->
|
||||
<g id="node4" class="node module-_strudel_cycles_midi maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/midi@0.6.0">
|
||||
<title>@strudel.cycles/midi@0.6.0</title>
|
||||
<path fill="none" stroke="black" d="M175.04,-370.3C175.04,-370.3 56.66,-370.3 56.66,-370.3 53.42,-370.3 50.19,-367.07 50.19,-363.83 50.19,-363.83 50.19,-357.37 50.19,-357.37 50.19,-354.13 53.42,-350.9 56.66,-350.9 56.66,-350.9 175.04,-350.9 175.04,-350.9 178.27,-350.9 181.5,-354.13 181.5,-357.37 181.5,-357.37 181.5,-363.83 181.5,-363.83 181.5,-367.07 178.27,-370.3 175.04,-370.3"></path>
|
||||
<text text-anchor="middle" x="115.85" y="-357.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/midi@0.6.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/midi@0.6.0->@strudel.cycles/core@0.6.8 -->
|
||||
|
||||
<!-- @strudel.cycles/webaudio@0.6.0 -->
|
||||
<g id="node15" class="node module-_strudel_cycles_webaudio maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/webaudio@0.6.0">
|
||||
<title>@strudel.cycles/webaudio@0.6.0</title>
|
||||
<path fill="none" stroke="black" d="M379.64,-370.3C379.64,-370.3 238.07,-370.3 238.07,-370.3 234.84,-370.3 231.6,-367.07 231.6,-363.83 231.6,-363.83 231.6,-357.37 231.6,-357.37 231.6,-354.13 234.84,-350.9 238.07,-350.9 238.07,-350.9 379.64,-350.9 379.64,-350.9 382.88,-350.9 386.11,-354.13 386.11,-357.37 386.11,-357.37 386.11,-363.83 386.11,-363.83 386.11,-367.07 382.88,-370.3 379.64,-370.3"></path>
|
||||
<text text-anchor="middle" x="308.86" y="-357.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/webaudio@0.6.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/midi@0.6.0->@strudel.cycles/webaudio@0.6.0 -->
|
||||
|
||||
<!-- webmidi@3.1.4 -->
|
||||
<g id="node25" class="node module-webmidi collapsed maintainer-djipco license-apache_2_0" data-module="webmidi@3.1.4">
|
||||
<title>webmidi@3.1.4</title>
|
||||
<path fill="none" stroke="black" d="M340.52,-407.3C340.52,-407.3 277.2,-407.3 277.2,-407.3 273.97,-407.3 270.73,-404.07 270.73,-400.83 270.73,-400.83 270.73,-394.37 270.73,-394.37 270.73,-391.13 273.97,-387.9 277.2,-387.9 277.2,-387.9 340.52,-387.9 340.52,-387.9 343.75,-387.9 346.98,-391.13 346.98,-394.37 346.98,-394.37 346.98,-400.83 346.98,-400.83 346.98,-404.07 343.75,-407.3 340.52,-407.3"></path>
|
||||
<text text-anchor="middle" x="308.86" y="-394.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">webmidi@3.1.4</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/midi@0.6.0->webmidi@3.1.4 -->
|
||||
|
||||
<!-- @strudel.cycles/mini@0.6.0 -->
|
||||
<g id="node5" class="node module-_strudel_cycles_mini maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/mini@0.6.0">
|
||||
<title>@strudel.cycles/mini@0.6.0</title>
|
||||
<path fill="none" stroke="black" d="M175.04,-111.3C175.04,-111.3 56.66,-111.3 56.66,-111.3 53.42,-111.3 50.19,-108.07 50.19,-104.83 50.19,-104.83 50.19,-98.37 50.19,-98.37 50.19,-95.13 53.42,-91.9 56.66,-91.9 56.66,-91.9 175.04,-91.9 175.04,-91.9 178.27,-91.9 181.5,-95.13 181.5,-98.37 181.5,-98.37 181.5,-104.83 181.5,-104.83 181.5,-108.07 178.27,-111.3 175.04,-111.3"></path>
|
||||
<text text-anchor="middle" x="115.85" y="-98.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/mini@0.6.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/mini@0.6.0->@strudel.cycles/core@0.6.8 -->
|
||||
|
||||
<!-- @strudel.cycles/osc@0.6.0 -->
|
||||
<g id="node6" class="node module-_strudel_cycles_osc maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/osc@0.6.0">
|
||||
<title>@strudel.cycles/osc@0.6.0</title>
|
||||
<path fill="none" stroke="black" d="M172.53,-222.3C172.53,-222.3 59.16,-222.3 59.16,-222.3 55.93,-222.3 52.7,-219.07 52.7,-215.83 52.7,-215.83 52.7,-209.37 52.7,-209.37 52.7,-206.13 55.93,-202.9 59.16,-202.9 59.16,-202.9 172.53,-202.9 172.53,-202.9 175.76,-202.9 179,-206.13 179,-209.37 179,-209.37 179,-215.83 179,-215.83 179,-219.07 175.76,-222.3 172.53,-222.3"></path>
|
||||
<text text-anchor="middle" x="115.85" y="-209.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/osc@0.6.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/osc@0.6.0->@strudel.cycles/core@0.6.8 -->
|
||||
|
||||
<!-- osc-js@2.4.0 -->
|
||||
<g id="node22" class="node module-osc_js collapsed maintainer-andreasdz license-mit" data-module="osc-js@2.4.0">
|
||||
<title>osc-js@2.4.0</title>
|
||||
<path fill="none" stroke="black" d="M334.18,-220.3C334.18,-220.3 283.53,-220.3 283.53,-220.3 280.3,-220.3 277.06,-217.07 277.06,-213.83 277.06,-213.83 277.06,-207.37 277.06,-207.37 277.06,-204.13 280.3,-200.9 283.53,-200.9 283.53,-200.9 334.18,-200.9 334.18,-200.9 337.42,-200.9 340.65,-204.13 340.65,-207.37 340.65,-207.37 340.65,-213.83 340.65,-213.83 340.65,-217.07 337.42,-220.3 334.18,-220.3"></path>
|
||||
<text text-anchor="middle" x="308.86" y="-207.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">osc-js@2.4.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/osc@0.6.0->osc-js@2.4.0 -->
|
||||
|
||||
<!-- @strudel.cycles/react@0.6.0 -->
|
||||
<g id="node7" class="node module-_strudel_cycles_react collapsed maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/react@0.6.0">
|
||||
<title>@strudel.cycles/react@0.6.0</title>
|
||||
<path fill="none" stroke="black" d="M175.73,-499.3C175.73,-499.3 55.96,-499.3 55.96,-499.3 52.73,-499.3 49.49,-496.07 49.49,-492.83 49.49,-492.83 49.49,-486.37 49.49,-486.37 49.49,-483.13 52.73,-479.9 55.96,-479.9 55.96,-479.9 175.73,-479.9 175.73,-479.9 178.97,-479.9 182.2,-483.13 182.2,-486.37 182.2,-486.37 182.2,-492.83 182.2,-492.83 182.2,-496.07 178.97,-499.3 175.73,-499.3"></path>
|
||||
<text text-anchor="middle" x="115.85" y="-486.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/react@0.6.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/serial@0.6.0 -->
|
||||
<g id="node8" class="node module-_strudel_cycles_serial maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/serial@0.6.0">
|
||||
<title>@strudel.cycles/serial@0.6.0</title>
|
||||
<path fill="none" stroke="black" d="M177.19,-148.3C177.19,-148.3 54.5,-148.3 54.5,-148.3 51.27,-148.3 48.04,-145.07 48.04,-141.83 48.04,-141.83 48.04,-135.37 48.04,-135.37 48.04,-132.13 51.27,-128.9 54.5,-128.9 54.5,-128.9 177.19,-128.9 177.19,-128.9 180.42,-128.9 183.66,-132.13 183.66,-135.37 183.66,-135.37 183.66,-141.83 183.66,-141.83 183.66,-145.07 180.42,-148.3 177.19,-148.3"></path>
|
||||
<text text-anchor="middle" x="115.85" y="-135.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/serial@0.6.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/serial@0.6.0->@strudel.cycles/core@0.6.8 -->
|
||||
|
||||
<!-- @strudel.cycles/soundfonts@0.6.0 -->
|
||||
<g id="node9" class="node module-_strudel_cycles_soundfonts maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/soundfonts@0.6.0">
|
||||
<title>@strudel.cycles/soundfonts@0.6.0</title>
|
||||
<path fill="none" stroke="black" d="M189.15,-333.3C189.15,-333.3 42.55,-333.3 42.55,-333.3 39.31,-333.3 36.08,-330.07 36.08,-326.83 36.08,-326.83 36.08,-320.37 36.08,-320.37 36.08,-317.13 39.31,-313.9 42.55,-313.9 42.55,-313.9 189.15,-313.9 189.15,-313.9 192.38,-313.9 195.62,-317.13 195.62,-320.37 195.62,-320.37 195.62,-326.83 195.62,-326.83 195.62,-330.07 192.38,-333.3 189.15,-333.3"></path>
|
||||
<text text-anchor="middle" x="115.85" y="-320.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/soundfonts@0.6.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/soundfonts@0.6.0->@strudel.cycles/core@0.6.8 -->
|
||||
|
||||
<!-- @strudel.cycles/soundfonts@0.6.0->@strudel.cycles/webaudio@0.6.0 -->
|
||||
|
||||
<!-- sfumato@0.1.2 -->
|
||||
<g id="node23" class="node module-sfumato maintainer-felixroos license-isc" data-module="sfumato@0.1.2">
|
||||
<title>sfumato@0.1.2</title>
|
||||
<path fill="none" stroke="black" d="M338.96,-333.3C338.96,-333.3 278.76,-333.3 278.76,-333.3 275.52,-333.3 272.29,-330.07 272.29,-326.83 272.29,-326.83 272.29,-320.37 272.29,-320.37 272.29,-317.13 275.52,-313.9 278.76,-313.9 278.76,-313.9 338.96,-313.9 338.96,-313.9 342.19,-313.9 345.43,-317.13 345.43,-320.37 345.43,-320.37 345.43,-326.83 345.43,-326.83 345.43,-330.07 342.19,-333.3 338.96,-333.3"></path>
|
||||
<text text-anchor="middle" x="308.86" y="-320.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">sfumato@0.1.2</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/soundfonts@0.6.0->sfumato@0.1.2 -->
|
||||
|
||||
<!-- soundfont2@0.4.0 -->
|
||||
<g id="node24" class="node module-soundfont2 maintainer-mrten license-mit" data-module="soundfont2@0.4.0">
|
||||
<title>soundfont2@0.4.0</title>
|
||||
<path fill="none" stroke="black" d="M513.22,-314.3C513.22,-314.3 438.89,-314.3 438.89,-314.3 435.66,-314.3 432.42,-311.07 432.42,-307.83 432.42,-307.83 432.42,-301.37 432.42,-301.37 432.42,-298.13 435.66,-294.9 438.89,-294.9 438.89,-294.9 513.22,-294.9 513.22,-294.9 516.45,-294.9 519.69,-298.13 519.69,-301.37 519.69,-301.37 519.69,-307.83 519.69,-307.83 519.69,-311.07 516.45,-314.3 513.22,-314.3"></path>
|
||||
<text text-anchor="middle" x="476.06" y="-301.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">soundfont2@0.4.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/soundfonts@0.6.0->soundfont2@0.4.0 -->
|
||||
|
||||
<!-- @strudel.cycles/tonal@0.6.0 -->
|
||||
<g id="node10" class="node module-_strudel_cycles_tonal maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/tonal@0.6.0">
|
||||
<title>@strudel.cycles/tonal@0.6.0</title>
|
||||
<path fill="none" stroke="black" d="M176.36,-425.3C176.36,-425.3 55.33,-425.3 55.33,-425.3 52.1,-425.3 48.86,-422.07 48.86,-418.83 48.86,-418.83 48.86,-412.37 48.86,-412.37 48.86,-409.13 52.1,-405.9 55.33,-405.9 55.33,-405.9 176.36,-405.9 176.36,-405.9 179.6,-405.9 182.83,-409.13 182.83,-412.37 182.83,-412.37 182.83,-418.83 182.83,-418.83 182.83,-422.07 179.6,-425.3 176.36,-425.3"></path>
|
||||
<text text-anchor="middle" x="115.85" y="-412.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/tonal@0.6.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/tonal@0.6.0->@strudel.cycles/core@0.6.8 -->
|
||||
|
||||
<!-- @tonaljs/tonal@4.10.0 -->
|
||||
<g id="node16" class="node module-_tonaljs_tonal collapsed maintainer-danigb license-mit" data-module="@tonaljs/tonal@4.10.0">
|
||||
<title>@tonaljs/tonal@4.10.0</title>
|
||||
<path fill="none" stroke="black" d="M523.73,-463.3C523.73,-463.3 428.38,-463.3 428.38,-463.3 425.15,-463.3 421.91,-460.07 421.91,-456.83 421.91,-456.83 421.91,-450.37 421.91,-450.37 421.91,-447.13 425.15,-443.9 428.38,-443.9 428.38,-443.9 523.73,-443.9 523.73,-443.9 526.96,-443.9 530.2,-447.13 530.2,-450.37 530.2,-450.37 530.2,-456.83 530.2,-456.83 530.2,-460.07 526.96,-463.3 523.73,-463.3"></path>
|
||||
<text text-anchor="middle" x="476.06" y="-450.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@tonaljs/tonal@4.10.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/tonal@0.6.0->@tonaljs/tonal@4.10.0 -->
|
||||
|
||||
<!-- chord-voicings@0.0.1 -->
|
||||
<g id="node18" class="node module-chord_voicings maintainer-felixroos license-isc" data-module="chord-voicings@0.0.1">
|
||||
<title>chord-voicings@0.0.1</title>
|
||||
<path fill="none" stroke="black" d="M354.5,-444.3C354.5,-444.3 263.21,-444.3 263.21,-444.3 259.98,-444.3 256.74,-441.07 256.74,-437.83 256.74,-437.83 256.74,-431.37 256.74,-431.37 256.74,-428.13 259.98,-424.9 263.21,-424.9 263.21,-424.9 354.5,-424.9 354.5,-424.9 357.74,-424.9 360.97,-428.13 360.97,-431.37 360.97,-431.37 360.97,-437.83 360.97,-437.83 360.97,-441.07 357.74,-444.3 354.5,-444.3"></path>
|
||||
<text text-anchor="middle" x="308.86" y="-431.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">chord-voicings@0.0.1</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/tonal@0.6.0->chord-voicings@0.0.1 -->
|
||||
|
||||
<!-- @strudel.cycles/tonal@0.6.0->webmidi@3.1.4 -->
|
||||
|
||||
<!-- @strudel.cycles/transpiler@0.6.0 -->
|
||||
<g id="node11" class="node module-_strudel_cycles_transpiler maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/transpiler@0.6.0">
|
||||
<title>@strudel.cycles/transpiler@0.6.0</title>
|
||||
<path fill="none" stroke="black" d="M185.91,-74.3C185.91,-74.3 45.79,-74.3 45.79,-74.3 42.55,-74.3 39.32,-71.07 39.32,-67.83 39.32,-67.83 39.32,-61.37 39.32,-61.37 39.32,-58.13 42.55,-54.9 45.79,-54.9 45.79,-54.9 185.91,-54.9 185.91,-54.9 189.14,-54.9 192.38,-58.13 192.38,-61.37 192.38,-61.37 192.38,-67.83 192.38,-67.83 192.38,-71.07 189.14,-74.3 185.91,-74.3"></path>
|
||||
<text text-anchor="middle" x="115.85" y="-61.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/transpiler@0.6.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/transpiler@0.6.0->@strudel.cycles/core@0.6.8 -->
|
||||
|
||||
<!-- acorn@8.8.2 -->
|
||||
<g id="node17" class="node module-acorn collapsed maintainer-marijn maintainer-adrianheine maintainer-rreverser license-mit" data-module="acorn@8.8.2">
|
||||
<title>acorn@8.8.2</title>
|
||||
<path fill="none" stroke="black" d="M333.45,-130.3C333.45,-130.3 284.27,-130.3 284.27,-130.3 281.03,-130.3 277.8,-127.07 277.8,-123.83 277.8,-123.83 277.8,-117.37 277.8,-117.37 277.8,-114.13 281.03,-110.9 284.27,-110.9 284.27,-110.9 333.45,-110.9 333.45,-110.9 336.68,-110.9 339.92,-114.13 339.92,-117.37 339.92,-117.37 339.92,-123.83 339.92,-123.83 339.92,-127.07 336.68,-130.3 333.45,-130.3"></path>
|
||||
<text text-anchor="middle" x="308.86" y="-117.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">acorn@8.8.2</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/transpiler@0.6.0->acorn@8.8.2 -->
|
||||
|
||||
<!-- escodegen@2.0.0 -->
|
||||
<g id="node19" class="node module-escodegen collapsed maintainer-constellation maintainer-michaelficarra license-bsd_2_clause" data-module="escodegen@2.0.0">
|
||||
<title>escodegen@2.0.0</title>
|
||||
<path fill="none" stroke="black" d="M344.33,-93.3C344.33,-93.3 273.39,-93.3 273.39,-93.3 270.15,-93.3 266.92,-90.07 266.92,-86.83 266.92,-86.83 266.92,-80.37 266.92,-80.37 266.92,-77.13 270.15,-73.9 273.39,-73.9 273.39,-73.9 344.33,-73.9 344.33,-73.9 347.56,-73.9 350.8,-77.13 350.8,-80.37 350.8,-80.37 350.8,-86.83 350.8,-86.83 350.8,-90.07 347.56,-93.3 344.33,-93.3"></path>
|
||||
<text text-anchor="middle" x="308.86" y="-80.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">escodegen@2.0.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/transpiler@0.6.0->escodegen@2.0.0 -->
|
||||
|
||||
<!-- estree-walker@3.0.3 -->
|
||||
<g id="node20" class="node module-estree_walker collapsed maintainer-rich_harris license-mit" data-module="estree-walker@3.0.3">
|
||||
<title>estree-walker@3.0.3</title>
|
||||
<path fill="none" stroke="black" d="M351.26,-56.3C351.26,-56.3 266.46,-56.3 266.46,-56.3 263.22,-56.3 259.99,-53.07 259.99,-49.83 259.99,-49.83 259.99,-43.37 259.99,-43.37 259.99,-40.13 263.22,-36.9 266.46,-36.9 266.46,-36.9 351.26,-36.9 351.26,-36.9 354.49,-36.9 357.73,-40.13 357.73,-43.37 357.73,-43.37 357.73,-49.83 357.73,-49.83 357.73,-53.07 354.49,-56.3 351.26,-56.3"></path>
|
||||
<text text-anchor="middle" x="308.86" y="-43.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">estree-walker@3.0.3</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/transpiler@0.6.0->estree-walker@3.0.3 -->
|
||||
|
||||
<!-- @strudel.cycles/webaudio@0.6.1->@strudel.cycles/core@0.6.8 -->
|
||||
|
||||
<!-- @strudel.cycles/xen@0.6.0 -->
|
||||
<g id="node13" class="node module-_strudel_cycles_xen maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/xen@0.6.0">
|
||||
<title>@strudel.cycles/xen@0.6.0</title>
|
||||
<path fill="none" stroke="black" d="M173.25,-185.3C173.25,-185.3 58.44,-185.3 58.44,-185.3 55.21,-185.3 51.98,-182.07 51.98,-178.83 51.98,-178.83 51.98,-172.37 51.98,-172.37 51.98,-169.13 55.21,-165.9 58.44,-165.9 58.44,-165.9 173.25,-165.9 173.25,-165.9 176.48,-165.9 179.72,-169.13 179.72,-172.37 179.72,-172.37 179.72,-178.83 179.72,-178.83 179.72,-182.07 176.48,-185.3 173.25,-185.3"></path>
|
||||
<text text-anchor="middle" x="115.85" y="-172.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/xen@0.6.0</text>
|
||||
</g>
|
||||
<!-- @strudel.cycles/xen@0.6.0->@strudel.cycles/core@0.6.8 -->
|
||||
|
||||
<!-- @strudel.cycles/webaudio@0.6.0->@strudel.cycles/core@0.6.8 -->
|
||||
|
||||
<!-- chord-voicings@0.0.1->@tonaljs/tonal@4.10.0 -->
|
||||
<g id="edge27" class="edge">
|
||||
<title>chord-voicings@0.0.1->@tonaljs/tonal@4.10.0</title>
|
||||
<path fill="none" stroke="black" d="M361.22,-440.51C377.14,-442.34 394.88,-444.38 411.53,-446.29"></path>
|
||||
<polygon fill="black" stroke="black" points="411.39,-449.8 421.73,-447.47 412.19,-442.85 411.39,-449.8"></polygon>
|
||||
</g>
|
||||
<!-- sfumato@0.1.2->soundfont2@0.4.0 -->
|
||||
<g id="edge28" class="edge">
|
||||
<title>sfumato@0.1.2->soundfont2@0.4.0</title>
|
||||
<path fill="none" stroke="black" d="M345.7,-319.48C368.06,-316.91 397.22,-313.55 422.19,-310.68"></path>
|
||||
<polygon fill="black" stroke="black" points="422.72,-314.14 432.25,-309.52 421.92,-307.19 422.72,-314.14"></polygon>
|
||||
</g>
|
||||
<g id="edge1" class="edge">
|
||||
<title>@strudel.cycles/core@0.6.8->fraction.js@4.2.0</title>
|
||||
<path fill="none" stroke="black" d="M181.25,-21.53C206.16,-19.18 234.23,-16.54 257.6,-14.34"></path>
|
||||
<polygon fill="black" stroke="black" points="258.02,-17.81 267.64,-13.39 257.36,-10.84 258.02,-17.81"></polygon>
|
||||
</g><g id="edge3" class="edge">
|
||||
<title>@strudel.cycles/csound@0.6.2->@strudel.cycles/core@0.6.8</title>
|
||||
<path fill="none" stroke="black" d="M44.23,-280.41C33.55,-276.09 24.08,-269.5 17.96,-259.6 -5.99,-220.85 -5.99,-93.35 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
|
||||
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
|
||||
</g><g id="edge4" class="edge">
|
||||
<title>@strudel.cycles/csound@0.6.2->@strudel.cycles/webaudio@0.6.1</title>
|
||||
<path fill="none" stroke="black" d="M115.85,-276.84C115.85,-274.39 115.85,-271.94 115.85,-269.49"></path>
|
||||
<polygon fill="black" stroke="black" points="119.35,-269.43 115.85,-259.43 112.35,-269.43 119.35,-269.43"></polygon>
|
||||
</g><g id="edge6" class="edge">
|
||||
<title>@strudel.cycles/midi@0.6.0->@strudel.cycles/core@0.6.8</title>
|
||||
<path fill="none" stroke="black" d="M50.17,-356.56C37.1,-352.37 25.19,-345.3 17.96,-333.6 1.66,-307.23 1.66,-80.97 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
|
||||
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
|
||||
</g><g id="edge7" class="edge">
|
||||
<title>@strudel.cycles/midi@0.6.0->@strudel.cycles/webaudio@0.6.0</title>
|
||||
<path fill="none" stroke="black" d="M181.51,-360.6C194.37,-360.6 208.05,-360.6 221.49,-360.6"></path>
|
||||
<polygon fill="black" stroke="black" points="221.71,-364.1 231.7,-360.6 221.7,-357.1 221.71,-364.1"></polygon>
|
||||
</g><g id="edge5" class="edge">
|
||||
<title>@strudel.cycles/midi@0.6.0->webmidi@3.1.4</title>
|
||||
<path fill="none" stroke="black" d="M166.53,-370.22C195.48,-375.83 231.78,-382.86 260.34,-388.4"></path>
|
||||
<polygon fill="black" stroke="black" points="260.01,-391.9 270.49,-390.36 261.34,-385.02 260.01,-391.9"></polygon>
|
||||
</g><g id="edge8" class="edge">
|
||||
<title>@strudel.cycles/mini@0.6.0->@strudel.cycles/core@0.6.8</title>
|
||||
<path fill="none" stroke="black" d="M50.17,-97.56C37.1,-93.37 25.19,-86.3 17.96,-74.6 7.78,-58.12 21.54,-47.3 41.67,-40.25"></path>
|
||||
<polygon fill="black" stroke="black" points="42.78,-43.57 51.3,-37.27 40.71,-36.88 42.78,-43.57"></polygon>
|
||||
</g><g id="edge10" class="edge">
|
||||
<title>@strudel.cycles/osc@0.6.0->@strudel.cycles/core@0.6.8</title>
|
||||
<path fill="none" stroke="black" d="M52.7,-209.33C38.65,-205.26 25.65,-198.05 17.96,-185.6 2.66,-160.84 2.66,-79.36 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
|
||||
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
|
||||
</g><g id="edge9" class="edge">
|
||||
<title>@strudel.cycles/osc@0.6.0->osc-js@2.4.0</title>
|
||||
<path fill="none" stroke="black" d="M179.13,-211.95C207.93,-211.65 241.33,-211.3 266.9,-211.03"></path>
|
||||
<polygon fill="black" stroke="black" points="267.02,-214.53 276.99,-210.92 266.95,-207.53 267.02,-214.53"></polygon>
|
||||
</g><g id="edge11" class="edge">
|
||||
<title>@strudel.cycles/serial@0.6.0->@strudel.cycles/core@0.6.8</title>
|
||||
<path fill="none" stroke="black" d="M48.17,-133.89C35.89,-129.63 24.82,-122.7 17.96,-111.6 4.64,-90.05 4.64,-76.15 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
|
||||
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
|
||||
</g><g id="edge14" class="edge">
|
||||
<title>@strudel.cycles/soundfonts@0.6.0->@strudel.cycles/core@0.6.8</title>
|
||||
<path fill="none" stroke="black" d="M36.71,-313.85C29.15,-309.7 22.6,-304.1 17.96,-296.6 3.83,-273.73 3.83,-77.47 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
|
||||
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
|
||||
</g><g id="edge15" class="edge">
|
||||
<title>@strudel.cycles/soundfonts@0.6.0->@strudel.cycles/webaudio@0.6.0</title>
|
||||
<path fill="none" stroke="black" d="M166.53,-333.22C191.53,-338.07 222.01,-343.97 248.26,-349.05"></path>
|
||||
<polygon fill="black" stroke="black" points="247.6,-352.49 258.09,-350.96 248.94,-345.62 247.6,-352.49"></polygon>
|
||||
</g><g id="edge12" class="edge">
|
||||
<title>@strudel.cycles/soundfonts@0.6.0->sfumato@0.1.2</title>
|
||||
<path fill="none" stroke="black" d="M195.64,-323.6C218.22,-323.6 242.08,-323.6 261.97,-323.6"></path>
|
||||
<polygon fill="black" stroke="black" points="261.98,-327.1 271.98,-323.6 261.98,-320.1 261.98,-327.1"></polygon>
|
||||
</g><g id="edge13" class="edge">
|
||||
<title>@strudel.cycles/soundfonts@0.6.0->soundfont2@0.4.0</title>
|
||||
<path fill="none" stroke="black" d="M164.14,-313.99C184.8,-310.25 209.37,-306.44 231.73,-304.6 297.18,-299.22 372.86,-300.38 422.21,-302.11"></path>
|
||||
<polygon fill="black" stroke="black" points="422.21,-305.61 432.33,-302.49 422.47,-298.62 422.21,-305.61"></polygon>
|
||||
</g><g id="edge19" class="edge">
|
||||
<title>@strudel.cycles/tonal@0.6.0->@strudel.cycles/core@0.6.8</title>
|
||||
<path fill="none" stroke="black" d="M68.74,-405.99C49.45,-399.45 29.07,-388.58 17.96,-370.6 -0.5,-340.73 -0.5,-84.47 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
|
||||
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
|
||||
</g><g id="edge16" class="edge">
|
||||
<title>@strudel.cycles/tonal@0.6.0->@tonaljs/tonal@4.10.0</title>
|
||||
<path fill="none" stroke="black" d="M168.82,-425.25C177.92,-427.57 187.2,-430.33 195.73,-433.6 212.82,-440.15 214.01,-449.01 231.73,-453.6 291.41,-469.07 362.13,-466.9 411.9,-462.17"></path>
|
||||
<polygon fill="black" stroke="black" points="412.28,-465.65 421.88,-461.17 411.58,-458.69 412.28,-465.65"></polygon>
|
||||
</g><g id="edge17" class="edge">
|
||||
<title>@strudel.cycles/tonal@0.6.0->chord-voicings@0.0.1</title>
|
||||
<path fill="none" stroke="black" d="M182.85,-422.17C203.51,-424.22 226.22,-426.48 246.55,-428.5"></path>
|
||||
<polygon fill="black" stroke="black" points="246.33,-432 256.62,-429.5 247.02,-425.03 246.33,-432"></polygon>
|
||||
</g><g id="edge18" class="edge">
|
||||
<title>@strudel.cycles/tonal@0.6.0->webmidi@3.1.4</title>
|
||||
<path fill="none" stroke="black" d="M182.85,-409.38C208.4,-406.97 237.07,-404.27 260.51,-402.06"></path>
|
||||
<polygon fill="black" stroke="black" points="260.91,-405.54 270.54,-401.12 260.26,-398.57 260.91,-405.54"></polygon>
|
||||
</g><g id="edge23" class="edge">
|
||||
<title>@strudel.cycles/transpiler@0.6.0->@strudel.cycles/core@0.6.8</title>
|
||||
<path fill="none" stroke="black" d="M115.85,-54.84C115.85,-52.39 115.85,-49.94 115.85,-47.49"></path>
|
||||
<polygon fill="black" stroke="black" points="119.35,-47.43 115.85,-37.43 112.35,-47.43 119.35,-47.43"></polygon>
|
||||
</g><g id="edge20" class="edge">
|
||||
<title>@strudel.cycles/transpiler@0.6.0->acorn@8.8.2</title>
|
||||
<path fill="none" stroke="black" d="M168.82,-74.25C177.92,-76.57 187.2,-79.33 195.73,-82.6 212.82,-89.15 214.66,-95.99 231.73,-102.6 243.09,-107 255.84,-110.53 267.61,-113.25"></path>
|
||||
<polygon fill="black" stroke="black" points="267.12,-116.73 277.64,-115.44 268.61,-109.89 267.12,-116.73"></polygon>
|
||||
</g><g id="edge21" class="edge">
|
||||
<title>@strudel.cycles/transpiler@0.6.0->escodegen@2.0.0</title>
|
||||
<path fill="none" stroke="black" d="M192.61,-72.14C214.17,-74.28 237.12,-76.56 256.83,-78.52"></path>
|
||||
<polygon fill="black" stroke="black" points="256.51,-82.01 266.81,-79.52 257.21,-75.04 256.51,-82.01"></polygon>
|
||||
</g><g id="edge22" class="edge">
|
||||
<title>@strudel.cycles/transpiler@0.6.0->estree-walker@3.0.3</title>
|
||||
<path fill="none" stroke="black" d="M192.61,-57.46C211.68,-55.66 231.85,-53.76 249.89,-52.06"></path>
|
||||
<polygon fill="black" stroke="black" points="250.33,-55.54 259.96,-51.11 249.68,-48.57 250.33,-55.54"></polygon>
|
||||
</g><g id="edge24" class="edge">
|
||||
<title>@strudel.cycles/webaudio@0.6.1->@strudel.cycles/core@0.6.8</title>
|
||||
<path fill="none" stroke="black" d="M38.54,-240.82C30.22,-236.58 22.97,-230.7 17.96,-222.6 -1.66,-190.84 -1.66,-86.36 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
|
||||
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
|
||||
</g><g id="edge25" class="edge">
|
||||
<title>@strudel.cycles/xen@0.6.0->@strudel.cycles/core@0.6.8</title>
|
||||
<path fill="none" stroke="black" d="M51.94,-172.1C38.18,-168 25.52,-160.82 17.96,-148.6 -4,-113.06 -4,-90.14 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
|
||||
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
|
||||
</g><g id="edge26" class="edge">
|
||||
<title>@strudel.cycles/webaudio@0.6.0->@strudel.cycles/core@0.6.8</title>
|
||||
<path fill="none" stroke="black" d="M241.66,-350.81C237.99,-348.55 234.63,-345.84 231.73,-342.6 143.1,-243.48 284.75,-144.37 195.73,-45.6 193.96,-43.64 192.03,-41.87 189.96,-40.27"></path>
|
||||
<polygon fill="black" stroke="black" points="191.74,-37.25 181.4,-34.93 188.03,-43.19 191.74,-37.25"></polygon>
|
||||
</g><g id="edge2" class="edge">
|
||||
<title>@strudel.cycles/csound@0.6.2->@csound/browser@6.18.5</title>
|
||||
<path fill="none" stroke="black" d="M164.06,-276.96C190.24,-271.62 222.97,-264.93 250.6,-259.29"></path>
|
||||
<polygon fill="black" stroke="black" points="251.43,-262.69 260.52,-257.26 250.02,-255.84 251.43,-262.69"></polygon>
|
||||
</g></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,51 @@
|
||||
Copyright 2022 The 3270font Authors (https://github.com/rbanffy/3270font)
|
||||
|
||||
Copyright (c) 2011-2022, Ricardo Banffy.
|
||||
Copyright (c) 1993-2011, Paul Mattes.
|
||||
Copyright (c) 2004-2005, Don Russell.
|
||||
Copyright (c) 2004, Dick Altenbern.
|
||||
Copyright (c) 1990, Jeff Sparkes.
|
||||
Copyright (c) 1989, Georgia Tech Research Corporation (GTRC), Atlanta, GA 30332.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of Ricardo Banffy, Paul Mattes, Don Russell,
|
||||
Dick Altenbern, Jeff Sparkes, GTRC nor the names of their contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL RICARDO BANFFY, PAUL MATTES, DON RUSSELL, DICK ALTENBERN, JEFF
|
||||
SPARKES OR GTRC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
||||
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The Debian Logo glyph is based on the Debian Open Use Logo and is
|
||||
Copyright (c) 1999 Software in the Public Interest, Inc., and it is
|
||||
incorporated here under the terms of the Creative Commons
|
||||
Attribution-ShareAlike 3.0 Unported License. The logo is released
|
||||
under the terms of the GNU Lesser General Public License, version 3 or
|
||||
any later version, or, at your option, of the Creative Commons
|
||||
Attribution-ShareAlike 3.0 Unported License.
|
||||
|
||||
Ubuntu, the Ubuntu logo and the Circle of Friends symbol are
|
||||
registered trademarks of Canonical Ltd.
|
||||
|
||||
The Fontforge SFD font description file is optionally licensed under
|
||||
the SIL Open Font License v1.1 with no Reserved Font Name. This
|
||||
license is available with a FAQ at http://scripts.sil.org/OFL.
|
||||
@@ -0,0 +1,428 @@
|
||||
Attribution-ShareAlike 4.0 International
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||
does not provide legal services or legal advice. Distribution of
|
||||
Creative Commons public licenses does not create a lawyer-client or
|
||||
other relationship. Creative Commons makes its licenses and related
|
||||
information available on an "as-is" basis. Creative Commons gives no
|
||||
warranties regarding its licenses, any material licensed under their
|
||||
terms and conditions, or any related information. Creative Commons
|
||||
disclaims all liability for damages resulting from their use to the
|
||||
fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and
|
||||
conditions that creators and other rights holders may use to share
|
||||
original works of authorship and other material subject to copyright
|
||||
and certain other rights specified in the public license below. The
|
||||
following considerations are for informational purposes only, are not
|
||||
exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are
|
||||
intended for use by those authorized to give the public
|
||||
permission to use material in ways otherwise restricted by
|
||||
copyright and certain other rights. Our licenses are
|
||||
irrevocable. Licensors should read and understand the terms
|
||||
and conditions of the license they choose before applying it.
|
||||
Licensors should also secure all rights necessary before
|
||||
applying our licenses so that the public can reuse the
|
||||
material as expected. Licensors should clearly mark any
|
||||
material not subject to the license. This includes other CC-
|
||||
licensed material, or material used under an exception or
|
||||
limitation to copyright. More considerations for licensors:
|
||||
wiki.creativecommons.org/Considerations_for_licensors
|
||||
|
||||
Considerations for the public: By using one of our public
|
||||
licenses, a licensor grants the public permission to use the
|
||||
licensed material under specified terms and conditions. If
|
||||
the licensor's permission is not necessary for any reason--for
|
||||
example, because of any applicable exception or limitation to
|
||||
copyright--then that use is not regulated by the license. Our
|
||||
licenses grant only permissions under copyright and certain
|
||||
other rights that a licensor has authority to grant. Use of
|
||||
the licensed material may still be restricted for other
|
||||
reasons, including because others have copyright or other
|
||||
rights in the material. A licensor may make special requests,
|
||||
such as asking that all changes be marked or described.
|
||||
Although not required by our licenses, you are encouraged to
|
||||
respect those requests where reasonable. More_considerations
|
||||
for the public:
|
||||
wiki.creativecommons.org/Considerations_for_licensees
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||
License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||
License"). To the extent this Public License may be interpreted as a
|
||||
contract, You are granted the Licensed Rights in consideration of Your
|
||||
acceptance of these terms and conditions, and the Licensor grants You
|
||||
such rights in consideration of benefits the Licensor receives from
|
||||
making the Licensed Material available under these terms and
|
||||
conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-SA Compatible License means a license listed at
|
||||
creativecommons.org/compatiblelicenses, approved by Creative
|
||||
Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name
|
||||
of a Creative Commons Public License. The License Elements of this
|
||||
Public License are Attribution and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
k. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
l. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
m. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. Additional offer from the Licensor -- Adapted Material.
|
||||
Every recipient of Adapted Material from You
|
||||
automatically receives an offer from the Licensor to
|
||||
exercise the Licensed Rights in the Adapted Material
|
||||
under the conditions of the Adapter's License You apply.
|
||||
|
||||
c. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share
|
||||
Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons
|
||||
license with the same License Elements, this version or
|
||||
later, or a BY-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the
|
||||
Adapter's License You apply. You may satisfy this condition
|
||||
in any reasonable manner based on the medium, means, and
|
||||
context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms
|
||||
or conditions on, or apply any Effective Technological
|
||||
Measures to, Adapted Material that restrict exercise of the
|
||||
rights granted under the Adapter's License You apply.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material,
|
||||
|
||||
including for purposes of Section 3(b); and
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
Copyright (c) 2012, Cody "CodeMan38" Boisclair (cody@zone38.net), with Reserved Font Name "Press Start 2P"
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 520 KiB |
@@ -1,66 +1,64 @@
|
||||
---
|
||||
// fetch all commits for just this page's path
|
||||
type Props = {
|
||||
path: string;
|
||||
path: string;
|
||||
};
|
||||
const { path } = Astro.props as Props;
|
||||
const resolvedPath = `examples/docs/${path}`;
|
||||
const url = `https://api.github.com/repos/withastro/astro/commits?path=${resolvedPath}`;
|
||||
const commitsURL = `https://github.com/withastro/astro/commits/main/${resolvedPath}`;
|
||||
const resolvedPath = `website/src/pages${path}.mdx`;
|
||||
const url = `https://api.github.com/repos/tidalcycles/strudel/commits?path=${resolvedPath}`;
|
||||
const commitsURL = `https://github.com/tidalcycles/strudel/commits/main/${resolvedPath}`;
|
||||
|
||||
type Commit = {
|
||||
author: {
|
||||
id: string;
|
||||
login: string;
|
||||
};
|
||||
author: {
|
||||
id: string;
|
||||
login: string;
|
||||
};
|
||||
};
|
||||
|
||||
async function getCommits(url: string) {
|
||||
try {
|
||||
const token = import.meta.env.SNOWPACK_PUBLIC_GITHUB_TOKEN ?? 'hello';
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
'Cannot find "SNOWPACK_PUBLIC_GITHUB_TOKEN" used for escaping rate-limiting.'
|
||||
);
|
||||
}
|
||||
try {
|
||||
const token = import.meta.env.SNOWPACK_PUBLIC_GITHUB_TOKEN ?? 'hello';
|
||||
if (!token) {
|
||||
throw new Error('Cannot find "SNOWPACK_PUBLIC_GITHUB_TOKEN" used for escaping rate-limiting.');
|
||||
}
|
||||
|
||||
const auth = `Basic ${Buffer.from(token, 'binary').toString('base64')}`;
|
||||
const auth = `Basic ${Buffer.from(token, 'binary').toString('base64')}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: auth,
|
||||
'User-Agent': 'astro-docs/1.0',
|
||||
},
|
||||
});
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: auth,
|
||||
'User-Agent': 'astro-docs/1.0',
|
||||
},
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Request to fetch commits failed. Reason: ${res.statusText}
|
||||
Message: ${data.message}`
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Request to fetch commits failed. Reason: ${res.statusText}
|
||||
Message: ${data.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return data as Commit[];
|
||||
} catch (e) {
|
||||
console.warn(`[error] /src/components/AvatarList.astro
|
||||
return data as Commit[];
|
||||
} catch (e) {
|
||||
console.warn(`[error] /src/components/AvatarList.astro
|
||||
${(e as any)?.message ?? e}`);
|
||||
return [] as Commit[];
|
||||
}
|
||||
return [] as Commit[];
|
||||
}
|
||||
}
|
||||
|
||||
function removeDups(arr: Commit[]) {
|
||||
const map = new Map<string, Commit['author']>();
|
||||
const map = new Map<string, Commit['author']>();
|
||||
for (let item of arr) {
|
||||
const author = item.author;
|
||||
// Deduplicate based on author.id
|
||||
//map.set(author.id, { login: author.login, id: author.id });
|
||||
author && map.set(author.id, { login: author.login, id: author.id });
|
||||
}
|
||||
|
||||
for (let item of arr) {
|
||||
const author = item.author;
|
||||
// Deduplicate based on author.id
|
||||
map.set(author.id, { login: author.login, id: author.id });
|
||||
}
|
||||
|
||||
return [...map.values()];
|
||||
return [...map.values()];
|
||||
}
|
||||
|
||||
const data = await getCommits(url);
|
||||
@@ -70,102 +68,102 @@ const additionalContributors = unique.length - recentContributors.length; // lis
|
||||
---
|
||||
|
||||
<!-- Thanks to @5t3ph for https://smolcss.dev/#smol-avatar-list! -->
|
||||
<div class="contributors">
|
||||
<ul class="avatar-list" style={`--avatar-count: ${recentContributors.length}`}>
|
||||
{
|
||||
recentContributors.map((item) => (
|
||||
<li>
|
||||
<a href={`https://github.com/${item.login}`}>
|
||||
<img
|
||||
alt={`Contributor ${item.login}`}
|
||||
title={`Contributor ${item.login}`}
|
||||
width="64"
|
||||
height="64"
|
||||
src={`https://avatars.githubusercontent.com/u/${item.id}`}
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
{
|
||||
additionalContributors > 0 && (
|
||||
<span>
|
||||
<a href={commitsURL}>{`and ${additionalContributors} additional contributor${
|
||||
additionalContributors > 1 ? 's' : ''
|
||||
}.`}</a>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{unique.length === 0 && <a href={commitsURL}>Contributors</a>}
|
||||
<div class="contributors px-4 mb-8">
|
||||
<ul class="avatar-list" style={`--avatar-count: ${recentContributors.length}`}>
|
||||
{
|
||||
recentContributors.map((item) => (
|
||||
<li>
|
||||
<a href={`https://github.com/${item.login}`}>
|
||||
<img
|
||||
alt={`Contributor ${item.login}`}
|
||||
title={`Contributor ${item.login}`}
|
||||
width="64"
|
||||
height="64"
|
||||
src={`https://avatars.githubusercontent.com/u/${item.id}`}
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
{
|
||||
additionalContributors > 0 && (
|
||||
<span>
|
||||
<a href={commitsURL}>{`and ${additionalContributors} additional contributor${
|
||||
additionalContributors > 1 ? 's' : ''
|
||||
}.`}</a>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{unique.length === 0 && <a href={commitsURL}>Contributors</a>}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.avatar-list {
|
||||
--avatar-size: 2.5rem;
|
||||
--avatar-count: 3;
|
||||
.avatar-list {
|
||||
--avatar-size: 2.5rem;
|
||||
--avatar-count: 3;
|
||||
|
||||
display: grid;
|
||||
list-style: none;
|
||||
/* Default to displaying most of the avatar to
|
||||
display: grid;
|
||||
list-style: none;
|
||||
/* Default to displaying most of the avatar to
|
||||
enable easier access on touch devices, ensuring
|
||||
the WCAG touch target size is met or exceeded */
|
||||
grid-template-columns: repeat(var(--avatar-count), max(44px, calc(var(--avatar-size) / 1.15)));
|
||||
/* `padding` matches added visual dimensions of
|
||||
grid-template-columns: repeat(var(--avatar-count), max(44px, calc(var(--avatar-size) / 1.15)));
|
||||
/* `padding` matches added visual dimensions of
|
||||
the `box-shadow` to help create a more accurate
|
||||
computed component size */
|
||||
padding: 0.08em;
|
||||
font-size: var(--avatar-size);
|
||||
}
|
||||
padding: 0.08em;
|
||||
font-size: var(--avatar-size);
|
||||
}
|
||||
|
||||
@media (any-hover: hover) and (any-pointer: fine) {
|
||||
.avatar-list {
|
||||
/* We create 1 extra cell to enable the computed
|
||||
@media (any-hover: hover) and (any-pointer: fine) {
|
||||
.avatar-list {
|
||||
/* We create 1 extra cell to enable the computed
|
||||
width to match the final visual width */
|
||||
grid-template-columns: repeat(calc(var(--avatar-count) + 1), calc(var(--avatar-size) / 1.75));
|
||||
}
|
||||
}
|
||||
grid-template-columns: repeat(calc(var(--avatar-count) + 1), calc(var(--avatar-size) / 1.75));
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-list li {
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
}
|
||||
.avatar-list li {
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
}
|
||||
|
||||
.avatar-list li:hover ~ li a,
|
||||
.avatar-list li:focus-within ~ li a {
|
||||
transform: translateX(33%);
|
||||
}
|
||||
.avatar-list li:hover ~ li a,
|
||||
.avatar-list li:focus-within ~ li a {
|
||||
transform: translateX(33%);
|
||||
}
|
||||
|
||||
.avatar-list img,
|
||||
.avatar-list a {
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.avatar-list img,
|
||||
.avatar-list a {
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.avatar-list a {
|
||||
transition: transform 180ms ease-in-out;
|
||||
}
|
||||
.avatar-list a {
|
||||
transition: transform 180ms ease-in-out;
|
||||
}
|
||||
|
||||
.avatar-list img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 0.05em #fff, 0 0 0 0.08em rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.avatar-list img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 0.05em #fff, 0 0 0 0.08em rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.avatar-list a:focus {
|
||||
outline: 2px solid transparent;
|
||||
/* Double-layer trick to work for dark and light backgrounds */
|
||||
box-shadow: 0 0 0 0.08em var(--theme-accent), 0 0 0 0.12em white;
|
||||
}
|
||||
.avatar-list a:focus {
|
||||
outline: 2px solid transparent;
|
||||
/* Double-layer trick to work for dark and light backgrounds */
|
||||
box-shadow: 0 0 0 0.08em var(--theme-accent), 0 0 0 0.12em white;
|
||||
}
|
||||
|
||||
.contributors {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.contributors {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.contributors > * + * {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
.contributors > * + * {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
import AvatarList from './AvatarList.astro';
|
||||
type Props = {
|
||||
path: string;
|
||||
};
|
||||
const { path } = Astro.props as Props;
|
||||
---
|
||||
|
||||
<footer>
|
||||
<AvatarList path={path} />
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
margin-top: auto;
|
||||
padding: 2rem;
|
||||
border-top: 3px solid var(--theme-divider);
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import { pwaInfo } from 'virtual:pwa-info';
|
||||
import '../styles/index.css';
|
||||
|
||||
const { BASE_URL } = import.meta.env;
|
||||
@@ -12,34 +13,75 @@ const base = BASE_URL;
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.ico" />
|
||||
|
||||
<link rel="sitemap" href="./sitemap.xml" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Strudel is a music live coding environment for the browser, porting the TidalCycles pattern language to JavaScript."
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/icons/apple-icon-180.png" sizes="180x180" />
|
||||
<meta name="theme-color" content="#222222" />
|
||||
|
||||
<base href={base} />
|
||||
|
||||
<!-- Preload Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital@0;1&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scrollable a11y code helper -->
|
||||
<script src="./make-scrollable-code-focusable.js" is:inline></script>
|
||||
|
||||
<!-- This is intentionally inlined to avoid FOUC -->
|
||||
<script is:inline>
|
||||
/* const root = document.documentElement;
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
root.classList.add('theme-dark');
|
||||
} else {
|
||||
root.classList.remove('theme-dark');
|
||||
} */
|
||||
</script>
|
||||
<script src="/src/pwa.ts"></script>
|
||||
<!-- this does not work for some reason: -->
|
||||
<!-- <style is:global define:vars={strudelTheme}></style> -->
|
||||
<!-- the following variables are just a fallback to make sure everything is readable without JS -->
|
||||
<style is:global>
|
||||
:root {
|
||||
--background: #222;
|
||||
--lineBackground: #22222250;
|
||||
--foreground: #fff;
|
||||
--caret: #ffcc00;
|
||||
--selection: rgba(128, 203, 196, 0.5);
|
||||
--selectionMatch: #036dd626;
|
||||
--lineHighlight: #00000050;
|
||||
--gutterBackground: transparent;
|
||||
--gutterForeground: #8a919966;
|
||||
}
|
||||
</style>
|
||||
{pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} />}
|
||||
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<!-- <script async src="https://www.googletagmanager.com/gtag/js?id=G-TEL60V1WM9" is:inline></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-TEL60V1WM9');
|
||||
</script> -->
|
||||
import { settings } from '../repl/themes.mjs';
|
||||
import { settingsMap } from '../settings.mjs';
|
||||
import { listenKeys } from 'nanostores';
|
||||
const themeStyle = document.createElement('style');
|
||||
themeStyle.id = 'strudel-theme';
|
||||
document.head.append(themeStyle);
|
||||
function activateTheme(name) {
|
||||
if (!settings[name]) {
|
||||
console.warn('theme', name, 'has no settings.. defaulting to strudelTheme settings');
|
||||
}
|
||||
const themeSettings = settings[name] || settings.strudelTheme;
|
||||
// set css variables
|
||||
themeStyle.innerHTML = `:root {
|
||||
${Object.entries(themeSettings)
|
||||
// important to override fallback
|
||||
.map(([key, value]) => `--${key}: ${value} !important;`)
|
||||
.join('\n')}
|
||||
}`;
|
||||
// tailwind dark mode
|
||||
if (themeSettings.light) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
}
|
||||
|
||||
activateTheme(settingsMap.get().theme);
|
||||
listenKeys(settingsMap, ['theme'], ({ theme }) => activateTheme(theme));
|
||||
|
||||
// https://medium.com/quick-code/100vh-problem-with-ios-safari-92ab23c852a8
|
||||
const appHeight = () => {
|
||||
const doc = document.documentElement;
|
||||
doc.style.setProperty('--app-height', `${window.innerHeight - 1}px`);
|
||||
};
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', appHeight);
|
||||
appHeight();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { SITE, OPEN_GRAPH, Frontmatter } from '../config';
|
||||
|
||||
export interface Props {
|
||||
frontmatter: Frontmatter;
|
||||
canonicalUrl: URL;
|
||||
frontmatter: Frontmatter;
|
||||
canonicalUrl: URL;
|
||||
}
|
||||
|
||||
const { frontmatter, canonicalUrl } = Astro.props as Props;
|
||||
@@ -23,11 +23,7 @@ const imageAlt = frontmatter.image?.alt ?? OPEN_GRAPH.image.alt;
|
||||
<meta property="og:locale" content={frontmatter.ogLocale ?? SITE.defaultLanguage} />
|
||||
<meta property="og:image" content={canonicalImageSrc} />
|
||||
<meta property="og:image:alt" content={imageAlt} />
|
||||
<meta
|
||||
name="description"
|
||||
property="og:description"
|
||||
content={frontmatter.description ?? SITE.description}
|
||||
/>
|
||||
<meta name="description" property="og:description" content={frontmatter.description ?? SITE.description} />
|
||||
<meta property="og:site_name" content={SITE.title} />
|
||||
|
||||
<!-- Twitter Tags -->
|
||||
@@ -37,10 +33,3 @@ const imageAlt = frontmatter.image?.alt ?? OPEN_GRAPH.image.alt;
|
||||
<meta name="twitter:description" content={frontmatter.description ?? SITE.description} />
|
||||
<meta name="twitter:image" content={canonicalImageSrc} />
|
||||
<meta name="twitter:image:alt" content={imageAlt} />
|
||||
|
||||
<!--
|
||||
TODO: Add json+ld data, maybe https://schema.org/APIReference makes sense?
|
||||
Docs: https://developers.google.com/search/docs/advanced/structured-data/intro-structured-data
|
||||
https://www.npmjs.com/package/schema-dts seems like a great resource for implementing this.
|
||||
Even better, there's a React component that integrates with `schema-dts`: https://github.com/google/react-schemaorg
|
||||
-->
|
||||
|
||||
@@ -19,13 +19,16 @@ const langCode = 'en'; // getLanguageFromURL(currentPage);
|
||||
const sidebar = SIDEBAR[langCode];
|
||||
---
|
||||
|
||||
<nav class="flex justify-between py-2 px-4 items-center h-14 max-h-14 bg-[#161616]" title="Top Navigation">
|
||||
<nav
|
||||
class="flex justify-between py-2 px-4 items-center h-14 max-h-14 bg-lineHighlight text-foreground"
|
||||
title="Top Navigation"
|
||||
>
|
||||
<!-- <div class="menu-toggle">
|
||||
<SidebarToggle client:idle />
|
||||
</div> -->
|
||||
<div class="flex overflow-visible items-center grow" style="overflow:visible">
|
||||
<a href="/" class="flex items-center text-2xl space-x-2">
|
||||
<h1 class="text-white font-bold flex space-x-2 items-baseline text-xl">
|
||||
<h1 class="font-bold flex space-x-2 items-baseline text-xl">
|
||||
<span>🌀</span>
|
||||
<div class="flex space-x-1 items-baseline">
|
||||
<span class="">strudel</span>
|
||||
@@ -36,7 +39,7 @@ const sidebar = SIDEBAR[langCode];
|
||||
</div>
|
||||
{/* KNOWN_LANGUAGE_CODES.length > 1 && <LanguageSelect lang={lang} client:idle /> */}
|
||||
<div class="search-item h-10">
|
||||
<!-- <Search client:idle /> -->
|
||||
<!-- <Search client:idle /> -->
|
||||
</div>
|
||||
<a href="./" class="hidden md:flex cursor-pointer items-center space-x-1"
|
||||
><CommandLineIcon className="w-5 h-5" /><span>go to REPL</span>
|
||||
|
||||
@@ -14,7 +14,7 @@ const langCode = 'en'; // getLanguageFromURL(currentPage);
|
||||
const sidebar = SIDEBAR[langCode];
|
||||
---
|
||||
|
||||
<nav aria-labelledby="grid-left" class="max-h-full overflow-auto pb-20">
|
||||
<nav aria-labelledby="grid-left" class="max-h-full overflow-auto pb-20 text-foreground">
|
||||
<ul>
|
||||
{
|
||||
Object.entries(sidebar).map(([header, children]) => (
|
||||
@@ -23,12 +23,12 @@ const sidebar = SIDEBAR[langCode];
|
||||
<h2>{header}</h2>
|
||||
<ul>
|
||||
{children.map((child) => {
|
||||
const url = '.' + Astro.site?.pathname + child.link;
|
||||
const url = Astro.site?.pathname + child.link;
|
||||
return (
|
||||
<li class="">
|
||||
<a
|
||||
class={`pl-4 py-0.5 w-full hover:bg-header block${
|
||||
currentPageMatch === child.link ? ' bg-header' : ''
|
||||
class={`pl-4 py-0.5 w-full hover:bg-lineHighlight block${
|
||||
currentPageMatch === child.link ? ' bg-lineHighlight' : ''
|
||||
}`}
|
||||
href={url}
|
||||
aria-current={currentPageMatch === child.link ? 'page' : false}
|
||||
|
||||
@@ -22,7 +22,7 @@ const currentPage = Astro.url.pathname;
|
||||
<span>On this Page:</span>
|
||||
<TableOfContents client:media="(max-width: 50em)" headings={headings} currentPage={currentPage} />
|
||||
</nav> -->
|
||||
<div class="prose prose-invert max-w-full pb-8">
|
||||
<div class="prose dark:prose-invert max-w-full pb-8">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
|
||||