mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-04 08:21:59 -04:00
Compare commits
183 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b8d83c548 | |||
| 345ef2276f | |||
| 2c880e2596 | |||
| f61916fffa | |||
| 00b0341839 | |||
| d5ab391a98 | |||
| d2a9b9b750 | |||
| 2348023b22 | |||
| 6e5d8eed19 | |||
| c33c8b5469 | |||
| 7b6074ccef | |||
| 55c226cda6 | |||
| c5de21ef61 | |||
| 36374fb3fb | |||
| 570c7a0d6b | |||
| c3be21e6e5 | |||
| 7410ac5cec | |||
| f5127147de | |||
| 1d144faed7 | |||
| 7b914e7f22 | |||
| 052addd67e | |||
| 3d162536aa | |||
| 2443aad01b | |||
| 08f6c1ad5e | |||
| 974376d404 | |||
| 1da80db048 | |||
| 42519581c1 | |||
| 1e3bbc57cc | |||
| 9bf0f624f5 | |||
| c6418789c4 | |||
| 8b909c54db | |||
| 9d029ac914 | |||
| 8644b3d163 | |||
| 426c5edf25 | |||
| 26cc7e2920 | |||
| 04eac3e7c5 | |||
| 7cf7575e6d | |||
| 111cea2381 | |||
| 896a3828f6 | |||
| eb02997836 | |||
| 8a33c171f5 | |||
| f87f279198 | |||
| 4cc019f038 | |||
| 34f4afad01 | |||
| 947b263b9c | |||
| 0a408e464a | |||
| 0d660277f6 | |||
| 33fef379ad | |||
| 0531369613 | |||
| c66f19108c | |||
| ef53367ba9 | |||
| 726abf76ce | |||
| a7d74ed6d6 | |||
| 648fbf99fa | |||
| b913e1c50d | |||
| ae335ac40c | |||
| 9ecad810b1 | |||
| f18cf76dde | |||
| 08c918f13e | |||
| 2e72c7f4ed | |||
| 614f393a5d | |||
| 75f7f4deac | |||
| 2a67698616 | |||
| 801879bddf | |||
| 7d8984b706 | |||
| 1a2847d953 | |||
| e71db24073 | |||
| f61de0c47c | |||
| 53b8bbb732 | |||
| aeaf447142 | |||
| 1e6e0a61b3 | |||
| dd696ba663 | |||
| be4c8cbdce | |||
| b6b54a8553 | |||
| 1d05051fd6 | |||
| 6bca8e2a2f | |||
| 630ba62cc3 | |||
| 8ac98262bc | |||
| 6057e9ada1 | |||
| 94e8b18ffa | |||
| 2ec20c4ed4 | |||
| c4fe2c7497 | |||
| df2d28f0e5 | |||
| d9bd6f9e7d | |||
| 1e0cc2ab17 | |||
| 835c7b6879 | |||
| 721e7f06b8 | |||
| a46bbeaa10 | |||
| e3a3c0c5f9 | |||
| 67964c7e23 | |||
| 9e5f9d37ef | |||
| 0c1234a35f | |||
| c6cc487fc6 | |||
| dd171f8320 | |||
| c22c786cf2 | |||
| c191471e1e | |||
| 0d0cb3fd93 | |||
| bb4b7dc533 | |||
| 8440fa42ea | |||
| 61f070744a | |||
| 2e6a561184 | |||
| 8703d684c4 | |||
| b0d2115f43 | |||
| 96802a271c | |||
| a91f4576f3 | |||
| ad080d02b3 | |||
| e9e7e02547 | |||
| 2ffc6c9601 | |||
| 3a9349d9f5 | |||
| bd3f2845f8 | |||
| 2d320c459b | |||
| ff9f4a8a0d | |||
| d7b94807cb | |||
| 2c16a37a68 | |||
| 717e4a2c1d | |||
| c225b4719f | |||
| 25f762c393 | |||
| b75561e3c7 | |||
| abaab89061 | |||
| e409086b34 | |||
| 532f1435d9 | |||
| 6cb61ade03 | |||
| a40219ba06 | |||
| 9c99f5ada2 | |||
| 2d72c8faa0 | |||
| b188fc6b6d | |||
| bfb22228cc | |||
| 9714d88d61 | |||
| bdddb91510 | |||
| 07bcaa65a9 | |||
| 5bf1805534 | |||
| 202685e8c4 | |||
| aba633e16e | |||
| da45ce689c | |||
| acea7e435e | |||
| cb4810cbbc | |||
| 5ec205b383 | |||
| ebc0b293a9 | |||
| 4608c321bd | |||
| c4243f41c6 | |||
| d95b8bc667 | |||
| 263c13c36e | |||
| ddceb36613 | |||
| ff8ec628be | |||
| 11a07df120 | |||
| 8befa4382b | |||
| 39006e4d71 | |||
| 812ed6b60d | |||
| 8b6c8eb35d | |||
| dcda9a5753 | |||
| d3733d4209 | |||
| ef6af96947 | |||
| d86ff0a536 | |||
| 2c9ea03fde | |||
| 4eae366bee | |||
| 117f7fa4b5 | |||
| 73ee84a1fb | |||
| 9c899d5308 | |||
| b60beaf508 | |||
| 796f741a92 | |||
| 8da1d9fbf3 | |||
| 5345986b5b | |||
| 3945abd7e9 | |||
| 053e4ee54b | |||
| 25d61a2e31 | |||
| 5e6131af24 | |||
| 2899ee5aef | |||
| 9d1674b13c | |||
| bfead58212 | |||
| 926c893c4f | |||
| 3cd31e4841 | |||
| c2be2ed76a | |||
| ac0dc7a8de | |||
| cba9169b2f | |||
| e9a0a06b77 | |||
| d80c06cd55 | |||
| dffeac41af | |||
| 943d78f268 | |||
| 5ac59e95dd | |||
| 69a5d62027 | |||
| 226cf356d6 | |||
| 73d49f152a | |||
| cdba9efb59 |
@@ -23,3 +23,4 @@ reverbGen.mjs
|
||||
hydra.mjs
|
||||
jsdoc-synonyms.js
|
||||
packages/hs2js/src/hs2js.mjs
|
||||
samples
|
||||
@@ -138,6 +138,18 @@ pnpm --filter "./packages/**" publish --access public
|
||||
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
|
||||
Important: Always publish with `pnpm`, as `npm` does not support overriding main files in `publishConfig`, which is done in all the packages.
|
||||
|
||||
|
||||
## useful commands
|
||||
```sh
|
||||
#regenerate the test snapshots (ex: when updating or creating new pattern functions)
|
||||
pnpm snapshot
|
||||
|
||||
#start the OSC server
|
||||
pnpm run osc
|
||||
|
||||
#build the standalone version
|
||||
pnpm tauri build
|
||||
```
|
||||
## Have Fun
|
||||
|
||||
Remember to have fun, and that this project is driven by the passion of volunteers!
|
||||
|
||||
@@ -14,10 +14,16 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
|
||||
|
||||
After cloning the project, you can run the REPL locally:
|
||||
|
||||
```bash
|
||||
pnpm i
|
||||
pnpm dev
|
||||
```
|
||||
1. Install [Node.js](https://nodejs.org/)
|
||||
2. Install [pnpm](https://pnpm.io/installation)
|
||||
3. Install dependencies by running the following command:
|
||||
```bash
|
||||
pnpm i
|
||||
```
|
||||
4. Run the development server:
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Using Strudel In Your Project
|
||||
|
||||
|
||||
+7
-7
@@ -56,18 +56,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.5.9",
|
||||
"@vitest/ui": "^1.1.0",
|
||||
"acorn": "^8.11.3",
|
||||
"@vitest/ui": "^2.1.3",
|
||||
"acorn": "^8.13.0",
|
||||
"dependency-tree": "^10.0.9",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"events": "^3.3.0",
|
||||
"jsdoc": "^4.0.2",
|
||||
"jsdoc": "^4.0.3",
|
||||
"jsdoc-json": "^2.0.2",
|
||||
"jsdoc-to-markdown": "^8.0.0",
|
||||
"lerna": "^8.0.1",
|
||||
"prettier": "^3.1.1",
|
||||
"lerna": "^8.1.8",
|
||||
"prettier": "^3.3.3",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
lineNumbers,
|
||||
drawSelection,
|
||||
} from '@codemirror/view';
|
||||
import { Pattern, repl } from '@strudel/core';
|
||||
import { repl, registerControl } from '@strudel/core';
|
||||
import { Drawer, cleanupDraw } from '@strudel/draw';
|
||||
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
||||
import { isTooltipEnabled } from './tooltip.mjs';
|
||||
@@ -99,10 +99,8 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
},
|
||||
{
|
||||
key: 'Alt-.',
|
||||
run: (_, e) => {
|
||||
e.preventDefault();
|
||||
onStop?.();
|
||||
},
|
||||
preventDefault: true,
|
||||
run: () => onStop?.(),
|
||||
},
|
||||
/* {
|
||||
key: 'Ctrl-Shift-.',
|
||||
@@ -135,6 +133,7 @@ export class StrudelMirror {
|
||||
autodraw,
|
||||
prebake,
|
||||
bgFill = true,
|
||||
solo = true,
|
||||
...replOptions
|
||||
} = options;
|
||||
this.code = initialCode;
|
||||
@@ -145,6 +144,7 @@ export class StrudelMirror {
|
||||
this.drawContext = drawContext;
|
||||
this.onDraw = onDraw || this.draw;
|
||||
this.id = id || s4();
|
||||
this.solo = solo;
|
||||
|
||||
this.drawer = new Drawer((haps, time, _, painters) => {
|
||||
const currentFrame = haps.filter((hap) => hap.isActive(time));
|
||||
@@ -161,12 +161,14 @@ export class StrudelMirror {
|
||||
replOptions?.onToggle?.(started);
|
||||
if (started) {
|
||||
this.drawer.start(this.repl.scheduler);
|
||||
// stop other repls when this one is started
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('start-repl', {
|
||||
detail: this.id,
|
||||
}),
|
||||
);
|
||||
if (this.solo) {
|
||||
// stop other repls when this one is started
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('start-repl', {
|
||||
detail: this.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.drawer.stop();
|
||||
updateMiniLocations(this.editor, []);
|
||||
@@ -221,7 +223,7 @@ export class StrudelMirror {
|
||||
|
||||
// stop this repl when another repl is started
|
||||
this.onStartRepl = (e) => {
|
||||
if (e.detail !== this.id) {
|
||||
if (this.solo && e.detail !== this.id) {
|
||||
this.stop();
|
||||
}
|
||||
};
|
||||
@@ -356,3 +358,12 @@ function s4() {
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the css of highlighted events. Make sure to use single quotes!
|
||||
* @name markcss
|
||||
* @example
|
||||
* note("c a f e")
|
||||
* .markcss('text-decoration:underline')
|
||||
*/
|
||||
export const markcss = registerControl('markcss');
|
||||
|
||||
@@ -93,6 +93,7 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
|
||||
if (haps.has(id)) {
|
||||
const hap = haps.get(id);
|
||||
const color = hap.value?.color ?? 'var(--foreground)';
|
||||
const style = hap.value?.markcss || `outline: solid 2px ${color}`;
|
||||
// Get explicit channels for color values
|
||||
/*
|
||||
const swatch = document.createElement('div');
|
||||
@@ -114,7 +115,7 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
|
||||
to,
|
||||
Decoration.mark({
|
||||
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
|
||||
attributes: { style: `outline: solid 2px ${color}` },
|
||||
attributes: { style },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
+20
-20
@@ -18,25 +18,25 @@ export default createTheme({
|
||||
theme: 'light',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.labelName, color: '#0f380f' },
|
||||
{ tag: t.keyword, color: '#0f380f' },
|
||||
{ tag: t.operator, color: '#0f380f' },
|
||||
{ tag: t.special(t.variableName), color: '#0f380f' },
|
||||
{ tag: t.typeName, color: '#0f380f' },
|
||||
{ tag: t.atom, color: '#0f380f' },
|
||||
{ tag: t.number, color: '#0f380f' },
|
||||
{ tag: t.definition(t.variableName), color: '#0f380f' },
|
||||
{ tag: t.string, color: '#0f380f' },
|
||||
{ tag: t.special(t.string), color: '#0f380f' },
|
||||
{ tag: t.comment, color: '#0f380f' },
|
||||
{ tag: t.variableName, color: '#0f380f' },
|
||||
{ tag: t.tagName, color: '#0f380f' },
|
||||
{ tag: t.bracket, color: '#0f380f' },
|
||||
{ tag: t.meta, color: '#0f380f' },
|
||||
{ tag: t.attributeName, color: '#0f380f' },
|
||||
{ tag: t.propertyName, color: '#0f380f' },
|
||||
{ tag: t.className, color: '#0f380f' },
|
||||
{ tag: t.invalid, color: '#0f380f' },
|
||||
{ tag: [t.unit, t.punctuation], color: '#0f380f' },
|
||||
{ tag: t.labelName, color: 'inherit' },
|
||||
{ tag: t.keyword, color: 'inherit' },
|
||||
{ tag: t.operator, color: 'inherit' },
|
||||
{ tag: t.special(t.variableName), color: 'inherit' },
|
||||
{ tag: t.typeName, color: 'inherit' },
|
||||
{ tag: t.atom, color: 'inherit' },
|
||||
{ tag: t.number, color: 'inherit' },
|
||||
{ tag: t.definition(t.variableName), color: 'inherit' },
|
||||
{ tag: t.string, color: 'inherit' },
|
||||
{ tag: t.special(t.string), color: 'inherit' },
|
||||
{ tag: t.comment, color: 'inherit' },
|
||||
{ tag: t.variableName, color: 'inherit' },
|
||||
{ tag: t.tagName, color: 'inherit' },
|
||||
{ tag: t.bracket, color: 'inherit' },
|
||||
{ tag: t.meta, color: 'inherit' },
|
||||
{ tag: t.attributeName, color: 'inherit' },
|
||||
{ tag: t.propertyName, color: 'inherit' },
|
||||
{ tag: t.className, color: 'inherit' },
|
||||
{ tag: t.invalid, color: 'inherit' },
|
||||
{ tag: [t.unit, t.punctuation], color: 'inherit' },
|
||||
],
|
||||
});
|
||||
|
||||
+20
-20
@@ -15,25 +15,25 @@ export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.labelName, color: 'white' },
|
||||
{ tag: t.keyword, color: 'white' },
|
||||
{ tag: t.operator, color: 'white' },
|
||||
{ tag: t.special(t.variableName), color: 'white' },
|
||||
{ tag: t.typeName, color: 'white' },
|
||||
{ tag: t.atom, color: 'white' },
|
||||
{ tag: t.number, color: 'white' },
|
||||
{ tag: t.definition(t.variableName), color: 'white' },
|
||||
{ tag: t.string, color: 'white' },
|
||||
{ tag: t.special(t.string), color: 'white' },
|
||||
{ tag: t.comment, color: 'white' },
|
||||
{ tag: t.variableName, color: 'white' },
|
||||
{ tag: t.tagName, color: 'white' },
|
||||
{ tag: t.bracket, color: 'white' },
|
||||
{ tag: t.meta, color: 'white' },
|
||||
{ tag: t.attributeName, color: 'white' },
|
||||
{ tag: t.propertyName, color: 'white' },
|
||||
{ tag: t.className, color: 'white' },
|
||||
{ tag: t.invalid, color: 'white' },
|
||||
{ tag: [t.unit, t.punctuation], color: 'white' },
|
||||
{ tag: t.labelName, color: 'inherit' },
|
||||
{ tag: t.keyword, color: 'inherit' },
|
||||
{ tag: t.operator, color: 'inherit' },
|
||||
{ tag: t.special(t.variableName), color: 'inherit' },
|
||||
{ tag: t.typeName, color: 'inherit' },
|
||||
{ tag: t.atom, color: 'inherit' },
|
||||
{ tag: t.number, color: 'inherit' },
|
||||
{ tag: t.definition(t.variableName), color: 'inherit' },
|
||||
{ tag: t.string, color: 'inherit' },
|
||||
{ tag: t.special(t.string), color: 'inherit' },
|
||||
{ tag: t.comment, color: 'inherit' },
|
||||
{ tag: t.variableName, color: 'inherit' },
|
||||
{ tag: t.tagName, color: 'inherit' },
|
||||
{ tag: t.bracket, color: 'inherit' },
|
||||
{ tag: t.meta, color: 'inherit' },
|
||||
{ tag: t.attributeName, color: 'inherit' },
|
||||
{ tag: t.propertyName, color: 'inherit' },
|
||||
{ tag: t.className, color: 'inherit' },
|
||||
{ tag: t.invalid, color: 'inherit' },
|
||||
{ tag: [t.unit, t.punctuation], color: 'inherit' },
|
||||
],
|
||||
});
|
||||
|
||||
+20
-20
@@ -18,25 +18,25 @@ export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.labelName, color: 'white' },
|
||||
{ tag: t.keyword, color: 'white' },
|
||||
{ tag: t.operator, color: 'white' },
|
||||
{ tag: t.special(t.variableName), color: 'white' },
|
||||
{ tag: t.typeName, color: 'white' },
|
||||
{ tag: t.atom, color: 'white' },
|
||||
{ tag: t.number, color: 'white' },
|
||||
{ tag: t.definition(t.variableName), color: 'white' },
|
||||
{ tag: t.string, color: 'white' },
|
||||
{ tag: t.special(t.string), color: 'white' },
|
||||
{ tag: t.comment, color: 'white' },
|
||||
{ tag: t.variableName, color: 'white' },
|
||||
{ tag: t.tagName, color: 'white' },
|
||||
{ tag: t.bracket, color: 'white' },
|
||||
{ tag: t.meta, color: 'white' },
|
||||
{ tag: t.attributeName, color: 'white' },
|
||||
{ tag: t.propertyName, color: 'white' },
|
||||
{ tag: t.className, color: 'white' },
|
||||
{ tag: t.invalid, color: 'white' },
|
||||
{ tag: [t.unit, t.punctuation], color: 'white' },
|
||||
{ tag: t.labelName, color: 'inherit' },
|
||||
{ tag: t.keyword, color: 'inherit' },
|
||||
{ tag: t.operator, color: 'inherit' },
|
||||
{ tag: t.special(t.variableName), color: 'inherit' },
|
||||
{ tag: t.typeName, color: 'inherit' },
|
||||
{ tag: t.atom, color: 'inherit' },
|
||||
{ tag: t.number, color: 'inherit' },
|
||||
{ tag: t.definition(t.variableName), color: 'inherit' },
|
||||
{ tag: t.string, color: 'inherit' },
|
||||
{ tag: t.special(t.string), color: 'inherit' },
|
||||
{ tag: t.comment, color: 'inherit' },
|
||||
{ tag: t.variableName, color: 'inherit' },
|
||||
{ tag: t.tagName, color: 'inherit' },
|
||||
{ tag: t.bracket, color: 'inherit' },
|
||||
{ tag: t.meta, color: 'inherit' },
|
||||
{ tag: t.attributeName, color: 'inherit' },
|
||||
{ tag: t.propertyName, color: 'inherit' },
|
||||
{ tag: t.className, color: 'inherit' },
|
||||
{ tag: t.invalid, color: 'inherit' },
|
||||
{ tag: [t.unit, t.punctuation], color: 'inherit' },
|
||||
],
|
||||
});
|
||||
|
||||
+19
-19
@@ -14,24 +14,24 @@ export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.labelName, color: '#41FF00' },
|
||||
{ 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' },
|
||||
{ tag: t.labelName, color: 'inherit' },
|
||||
{ tag: t.keyword, color: 'inherit' },
|
||||
{ tag: t.operator, color: 'inherit' },
|
||||
{ tag: t.special(t.variableName), color: 'inherit' },
|
||||
{ tag: t.typeName, color: 'inherit' },
|
||||
{ tag: t.atom, color: 'inherit' },
|
||||
{ tag: t.number, color: 'inherit' },
|
||||
{ tag: t.definition(t.variableName), color: 'inherit' },
|
||||
{ tag: t.string, color: 'inherit' },
|
||||
{ tag: t.special(t.string), color: 'inherit' },
|
||||
{ tag: t.comment, color: 'inherit' },
|
||||
{ tag: t.variableName, color: 'inherit' },
|
||||
{ tag: t.tagName, color: 'inherit' },
|
||||
{ tag: t.bracket, color: 'inherit' },
|
||||
{ tag: t.meta, color: 'inherit' },
|
||||
{ tag: t.attributeName, color: 'inherit' },
|
||||
{ tag: t.propertyName, color: 'inherit' },
|
||||
{ tag: t.className, color: 'inherit' },
|
||||
{ tag: t.invalid, color: 'inherit' },
|
||||
],
|
||||
});
|
||||
|
||||
+19
-19
@@ -16,24 +16,24 @@ export default createTheme({
|
||||
theme: 'light',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.labelName, color: 'black' },
|
||||
{ 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' },
|
||||
{ tag: t.labelName, color: 'inherit' },
|
||||
{ tag: t.keyword, color: 'inherit' },
|
||||
{ tag: t.operator, color: 'inherit' },
|
||||
{ tag: t.special(t.variableName), color: 'inherit' },
|
||||
{ tag: t.typeName, color: 'inherit' },
|
||||
{ tag: t.atom, color: 'inherit' },
|
||||
{ tag: t.number, color: 'inherit' },
|
||||
{ tag: t.definition(t.variableName), color: 'inherit' },
|
||||
{ tag: t.string, color: 'inherit' },
|
||||
{ tag: t.special(t.string), color: 'inherit' },
|
||||
{ tag: t.comment, color: 'inherit' },
|
||||
{ tag: t.variableName, color: 'inherit' },
|
||||
{ tag: t.tagName, color: 'inherit' },
|
||||
{ tag: t.bracket, color: 'inherit' },
|
||||
{ tag: t.meta, color: 'inherit' },
|
||||
{ tag: t.attributeName, color: 'inherit' },
|
||||
{ tag: t.propertyName, color: 'inherit' },
|
||||
{ tag: t.className, color: 'inherit' },
|
||||
{ tag: t.invalid, color: 'inherit' },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -133,3 +133,10 @@ registerWidget('_pitchwheel', (id, options = {}, pat) => {
|
||||
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||
return pat.pitchwheel({ ...options, ctx, id });
|
||||
});
|
||||
|
||||
registerWidget('_spectrum', (id, options = {}, pat) => {
|
||||
let _size = options.size || 200;
|
||||
options = { width: _size, height: _size, ...options, size: _size / 5 };
|
||||
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||
return pat.spectrum({ ...options, ctx, id });
|
||||
});
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
// import createClock from './zyklus.mjs';
|
||||
|
||||
function getTime() {
|
||||
const precision = 10 ** 4;
|
||||
const seconds = performance.now() / 1000;
|
||||
return Math.round(seconds * precision) / precision;
|
||||
const seconds = performance.now() * 0.001;
|
||||
return seconds;
|
||||
// return Math.round(seconds * precision) / precision;
|
||||
}
|
||||
|
||||
let num_cycles_at_cps_change = 0;
|
||||
@@ -24,27 +24,20 @@ const sendMessage = (type, payload) => {
|
||||
|
||||
const sendTick = (phase, duration, tick, time) => {
|
||||
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
|
||||
|
||||
const tickdeadline = phase - time;
|
||||
const lastTick = time + tickdeadline;
|
||||
const num_cycles_since_cps_change = num_seconds_since_cps_change * cps;
|
||||
|
||||
const begin = num_cycles_at_cps_change + num_cycles_since_cps_change;
|
||||
const secondsSinceLastTick = time - lastTick - duration;
|
||||
|
||||
const eventLength = duration * cps;
|
||||
const end = begin + eventLength;
|
||||
|
||||
const cycle = begin + secondsSinceLastTick * cps;
|
||||
|
||||
sendMessage('tick', {
|
||||
begin,
|
||||
end,
|
||||
cps,
|
||||
tickdeadline,
|
||||
num_cycles_at_cps_change,
|
||||
num_seconds_at_cps_change,
|
||||
num_seconds_since_cps_change,
|
||||
time,
|
||||
cycle,
|
||||
});
|
||||
num_ticks_since_cps_change++;
|
||||
|
||||
@@ -454,9 +454,6 @@ export const { drive } = registerControl('drive');
|
||||
*/
|
||||
export const { channels, ch } = registerControl('channels', 'ch');
|
||||
|
||||
// superdirt only
|
||||
export const { phaserrate, phasr } = registerControl('phaserrate', 'phasr');
|
||||
|
||||
/**
|
||||
* Phaser audio effect that approximates popular guitar pedals.
|
||||
*
|
||||
@@ -468,7 +465,11 @@ export const { phaserrate, phasr } = registerControl('phaserrate', 'phasr');
|
||||
* .phaser("<1 2 4 8>")
|
||||
*
|
||||
*/
|
||||
export const { phaser, ph } = registerControl(['phaser', 'phaserdepth', 'phasercenter', 'phasersweep'], 'ph');
|
||||
export const { phaserrate, ph, phaser } = registerControl(
|
||||
['phaserrate', 'phaserdepth', 'phasercenter', 'phasersweep'],
|
||||
'ph',
|
||||
'phaser',
|
||||
);
|
||||
|
||||
/**
|
||||
* The frequency sweep range of the lfo for the phaser effect. Defaults to 2000
|
||||
@@ -545,6 +546,18 @@ export const { cut } = registerControl('cut');
|
||||
*/
|
||||
export const { cutoff, ctf, lpf, lp } = registerControl(['cutoff', 'resonance', 'lpenv'], 'ctf', 'lpf', 'lp');
|
||||
|
||||
/**
|
||||
* Sets the lowpass filter lfo modulation depth.
|
||||
* @name lplfo
|
||||
* @param {number | Pattern} modulation depth of the lowpass filter lfo between 0 and _n_
|
||||
* @example
|
||||
* note("c2 e2 f2 g2")
|
||||
* .sound('sawtooth')
|
||||
* .lpf(300)
|
||||
* .lplfo(2)
|
||||
*/
|
||||
export const { lplfo } = registerControl('lplfo');
|
||||
|
||||
/**
|
||||
* Sets the lowpass filter envelope modulation depth.
|
||||
* @name lpenv
|
||||
|
||||
@@ -68,6 +68,10 @@ export class Cyclist {
|
||||
// see https://github.com/tidalcycles/strudel/pull/1004
|
||||
const deadline = targetTime - phase;
|
||||
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
|
||||
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
||||
this.cps = hap.value.cps;
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -14,6 +14,7 @@ export * from './controls.mjs';
|
||||
export * from './hap.mjs';
|
||||
export * from './pattern.mjs';
|
||||
export * from './signal.mjs';
|
||||
export * from './pick.mjs';
|
||||
export * from './state.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './util.mjs';
|
||||
|
||||
@@ -5,6 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { logger } from './logger.mjs';
|
||||
import { ClockCollator, cycleToSeconds } from './util.mjs';
|
||||
|
||||
export class NeoCyclist {
|
||||
constructor({ onTrigger, onToggle, getTime }) {
|
||||
@@ -13,79 +14,38 @@ export class NeoCyclist {
|
||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||
this.getTime = getTime; // get absolute time
|
||||
this.time_at_last_tick_message = 0;
|
||||
|
||||
this.num_cycles_at_cps_change = 0;
|
||||
// the clock of the worker and the audio context clock can drift apart over time
|
||||
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
|
||||
// we need to keep a rolling average of the time difference between the worker clock and audio context clock
|
||||
// in order to schedule events consistently.
|
||||
this.collator = new ClockCollator({ getTargetClockTime: getTime });
|
||||
this.onToggle = onToggle;
|
||||
this.latency = 0.1; // fixed trigger time offset
|
||||
this.cycle = 0;
|
||||
this.id = Math.round(Date.now() * Math.random());
|
||||
this.worker_time_dif;
|
||||
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
|
||||
this.worker.port.start();
|
||||
|
||||
this.channel = new BroadcastChannel('strudeltick');
|
||||
let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif
|
||||
const maxWeight = 20;
|
||||
const precision = 10 ** 3; //round off time diff to prevent accumulating outliers
|
||||
|
||||
// the clock of the worker and the audio context clock can drift apart over time
|
||||
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
|
||||
// we need to keep a rolling weighted average of the time difference between the worker clock and audio context clock
|
||||
// in order to schedule events consistently.
|
||||
const setTimeReference = (num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline) => {
|
||||
const time_dif = getTime() - (num_seconds_at_cps_change + num_seconds_since_cps_change) + tickdeadline;
|
||||
if (this.worker_time_dif == null) {
|
||||
this.worker_time_dif = time_dif;
|
||||
} else {
|
||||
const w = 1; //weight of new time diff;
|
||||
const new_dif =
|
||||
Math.round(((this.worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision;
|
||||
|
||||
if (new_dif != this.worker_time_dif) {
|
||||
// reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens
|
||||
weight = 4;
|
||||
}
|
||||
this.worker_time_dif = new_dif;
|
||||
}
|
||||
weight = Math.min(weight + 1, maxWeight);
|
||||
};
|
||||
|
||||
const tickCallback = (payload) => {
|
||||
const {
|
||||
num_cycles_at_cps_change,
|
||||
cps,
|
||||
num_seconds_at_cps_change,
|
||||
num_seconds_since_cps_change,
|
||||
begin,
|
||||
end,
|
||||
tickdeadline,
|
||||
cycle,
|
||||
} = payload;
|
||||
const { cps, begin, end, cycle, time } = payload;
|
||||
this.cps = cps;
|
||||
this.cycle = cycle;
|
||||
|
||||
setTimeReference(num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline);
|
||||
|
||||
processHaps(begin, end, num_cycles_at_cps_change, num_seconds_at_cps_change);
|
||||
|
||||
this.time_at_last_tick_message = this.getTime();
|
||||
const currentTime = this.collator.calculateOffset(time) + time;
|
||||
processHaps(begin, end, currentTime);
|
||||
this.time_at_last_tick_message = currentTime;
|
||||
};
|
||||
|
||||
const processHaps = (begin, end, num_cycles_at_cps_change, seconds_at_cps_change) => {
|
||||
const processHaps = (begin, end, currentTime) => {
|
||||
if (this.started === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||
|
||||
haps.forEach((hap) => {
|
||||
if (hap.hasOnset()) {
|
||||
const targetTime =
|
||||
(hap.whole.begin - num_cycles_at_cps_change) / this.cps +
|
||||
seconds_at_cps_change +
|
||||
this.latency +
|
||||
this.worker_time_dif;
|
||||
const duration = hap.duration / this.cps;
|
||||
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
||||
const targetTime = timeUntilTrigger + currentTime + this.latency;
|
||||
const duration = cycleToSeconds(hap.duration, this.cps);
|
||||
onTrigger?.(hap, 0, duration, this.cps, targetTime);
|
||||
}
|
||||
});
|
||||
@@ -129,8 +89,8 @@ export class NeoCyclist {
|
||||
this.setStarted(true);
|
||||
}
|
||||
stop() {
|
||||
this.worker_time_dif = null;
|
||||
logger('[cyclist] stop');
|
||||
this.collator.reset();
|
||||
this.setStarted(false);
|
||||
}
|
||||
setPattern(pat, autostart = false) {
|
||||
|
||||
@@ -36,6 +36,6 @@
|
||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
+173
-39
@@ -386,6 +386,15 @@ export class Pattern {
|
||||
return this.fmap(func).squeezeJoin();
|
||||
}
|
||||
|
||||
polyJoin = function () {
|
||||
const pp = this;
|
||||
return pp.fmap((p) => p.s_extend(pp.tactus.div(p.tactus))).outerJoin();
|
||||
};
|
||||
|
||||
polyBind(func) {
|
||||
return this.fmap(func).polyJoin();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Utility methods mainly for internal use
|
||||
|
||||
@@ -754,6 +763,10 @@ export class Pattern {
|
||||
const otherPat = reify(other);
|
||||
return otherPat.fmap((b) => this.fmap((a) => func(a)(b))).restartJoin();
|
||||
}
|
||||
_opPoly(other, func) {
|
||||
const otherPat = reify(other);
|
||||
return this.fmap((b) => otherPat.fmap((a) => func(a)(b))).polyJoin();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// End-user methods.
|
||||
@@ -796,15 +809,6 @@ export class Pattern {
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Multi-pattern functions
|
||||
|
||||
/**
|
||||
* Stacks the given pattern(s) to the current pattern.
|
||||
* @name stack
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("hh*4").stack(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
stack(...pats) {
|
||||
return stack(this, ...pats);
|
||||
}
|
||||
@@ -813,30 +817,9 @@ export class Pattern {
|
||||
return sequence(this, ...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the given pattern(s) to the current pattern.
|
||||
* @name seq
|
||||
* @memberof Pattern
|
||||
* @synonyms sequence, fastcat
|
||||
* @example
|
||||
* s("hh*4").seq(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
seq(...pats) {
|
||||
return sequence(this, ...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the given pattern(s) to the next cycle.
|
||||
* @name cat
|
||||
* @memberof Pattern
|
||||
* @synonyms slowcat
|
||||
* @example
|
||||
* s("hh*4").cat(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
cat(...pats) {
|
||||
return cat(this, ...pats);
|
||||
}
|
||||
@@ -1040,6 +1023,7 @@ function _composeOp(a, b, func) {
|
||||
div: [numeralArgs((a, b) => a / b)],
|
||||
mod: [numeralArgs(_mod)],
|
||||
pow: [numeralArgs(Math.pow)],
|
||||
log2: [numeralArgs(Math.log2)],
|
||||
band: [numeralArgs((a, b) => a & b)],
|
||||
bor: [numeralArgs((a, b) => a | b)],
|
||||
bxor: [numeralArgs((a, b) => a ^ b)],
|
||||
@@ -1062,7 +1046,7 @@ function _composeOp(a, b, func) {
|
||||
func: [(a, b) => b(a)],
|
||||
};
|
||||
|
||||
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart'];
|
||||
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly'];
|
||||
|
||||
// generate methods to do what and how
|
||||
for (const [what, [op, preprocess]] of Object.entries(composers)) {
|
||||
@@ -1267,6 +1251,12 @@ export function reify(thing) {
|
||||
* @example
|
||||
* stack("g3", "b3", ["e4", "d4"]).note()
|
||||
* // "g3,b3,[e4,d4]".note()
|
||||
*
|
||||
* @example
|
||||
* // As a chained function:
|
||||
* s("hh*4").stack(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
export function stack(...pats) {
|
||||
// Array test here is to avoid infinite recursions..
|
||||
@@ -1395,6 +1385,11 @@ export function slowcatPrime(...pats) {
|
||||
* cat("e5", "b4", ["d5", "c5"]).note()
|
||||
* // "<e5 b4 [d5 c5]>".note()
|
||||
*
|
||||
* @example
|
||||
* // As a chained function:
|
||||
* s("hh*4").cat(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
export function cat(...pats) {
|
||||
return slowcat(...pats);
|
||||
@@ -1417,6 +1412,36 @@ export function arrange(...sections) {
|
||||
return s_cat(...sections).slow(total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Similarly to `arrange`, allows you to arrange multiple patterns together over multiple cycles.
|
||||
* Unlike `arrange`, you specify a start and stop time for each pattern rather than duration, which
|
||||
* means that patterns can overlap.
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
seqPLoop([0, 2, "bd(3,8)"],
|
||||
[1, 3, "cp(3,8)"]
|
||||
)
|
||||
.sound()
|
||||
*/
|
||||
export function seqPLoop(...parts) {
|
||||
let total = Fraction(0);
|
||||
const pats = [];
|
||||
for (let part of parts) {
|
||||
if (part.length == 2) {
|
||||
part.unshift(total);
|
||||
}
|
||||
total = part[1];
|
||||
}
|
||||
|
||||
return stack(
|
||||
...parts.map(([start, stop, pat]) =>
|
||||
pure(reify(pat)).compress(Fraction(start).div(total), Fraction(stop).div(total)),
|
||||
),
|
||||
)
|
||||
.slow(total)
|
||||
.innerJoin(); // or resetJoin or restartJoin ??
|
||||
}
|
||||
|
||||
export function fastcat(...pats) {
|
||||
let result = slowcat(...pats);
|
||||
if (pats.length > 1) {
|
||||
@@ -1435,12 +1460,18 @@ export function sequence(...pats) {
|
||||
}
|
||||
|
||||
/** Like **cat**, but the items are crammed into one cycle.
|
||||
* @synonyms fastcat, sequence
|
||||
* @synonyms sequence, fastcat
|
||||
* @example
|
||||
* seq("e5", "b4", ["d5", "c5"]).note()
|
||||
* // "e5 b4 [d5 c5]".note()
|
||||
*
|
||||
* @example
|
||||
* // As a chained function:
|
||||
* s("hh*4").seq(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
|
||||
export function seq(...pats) {
|
||||
return fastcat(...pats);
|
||||
}
|
||||
@@ -1461,6 +1492,14 @@ function _sequenceCount(x) {
|
||||
export const mask = curry((a, b) => reify(b).mask(a));
|
||||
export const struct = curry((a, b) => reify(b).struct(a));
|
||||
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
|
||||
export const withValue = curry((a, b) => reify(b).withValue(a));
|
||||
|
||||
export const bind = curry((a, b) => reify(b).bind(a));
|
||||
export const innerBind = curry((a, b) => reify(b).innerBind(a));
|
||||
export const outerBind = curry((a, b) => reify(b).outerBind(a));
|
||||
export const squeezeBind = curry((a, b) => reify(b).squeezeBind(a));
|
||||
export const stepBind = curry((a, b) => reify(b).stepBind(a));
|
||||
export const polyBind = curry((a, b) => reify(b).polyBind(a));
|
||||
|
||||
// operators
|
||||
export const set = curry((a, b) => reify(b).set(a));
|
||||
@@ -2003,6 +2042,34 @@ export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a
|
||||
return pat.zoom(a.begin, a.end);
|
||||
});
|
||||
|
||||
/**
|
||||
* Splits a pattern into the given number of slices, and plays them according to a pattern of slice numbers.
|
||||
* Similar to `slice`, but slices up patterns rather than sound samples.
|
||||
* @param {number} number of slices
|
||||
* @param {number} slices to play
|
||||
* @example
|
||||
* note("0 1 2 3 4 5 6 7".scale('c:mixolydian'))
|
||||
*.bite(4, "3 2 1 0")
|
||||
* @example
|
||||
* sound("bd - bd bd*2, - sd:6 - sd:5 sd:1 - [- sd:2] -, hh [- cp:7]")
|
||||
.bank("RolandTR909").speed(1.2)
|
||||
.bite(4, "0 0 [1 2] <3 2> 0 0 [2 1] 3")
|
||||
*/
|
||||
export const bite = register(
|
||||
'bite',
|
||||
(npat, ipat, pat) => {
|
||||
return ipat
|
||||
.fmap((i) => (n) => {
|
||||
const a = Fraction(i).div(n).mod(1);
|
||||
const b = a.add(Fraction(1).div(n));
|
||||
return pat.zoom(a, b);
|
||||
})
|
||||
.appLeft(npat)
|
||||
.squeezeJoin();
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
/**
|
||||
* Selects the given fraction of the pattern and repeats that part to fill the remainder of the cycle.
|
||||
* @param {number} fraction fraction to select
|
||||
@@ -2434,6 +2501,37 @@ Pattern.prototype.tag = function (tag) {
|
||||
return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) }));
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters haps using the given function
|
||||
* @name filter
|
||||
* @param {Function} test function to test Hap
|
||||
* @example
|
||||
* s("hh!7 oh").filter(hap => hap.value.s==='hh')
|
||||
*/
|
||||
export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test)));
|
||||
|
||||
/**
|
||||
* Filters haps by their begin time
|
||||
* @name filterWhen
|
||||
* @noAutocomplete
|
||||
* @param {Function} test function to test Hap.whole.begin
|
||||
*/
|
||||
export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin)));
|
||||
|
||||
/**
|
||||
* Use within to apply a function to only a part of a pattern.
|
||||
* @name within
|
||||
* @param {number} start start within cycle (0 - 1)
|
||||
* @param {number} end end within cycle (0 - 1). Must be > start
|
||||
* @param {Function} func function to be applied to the sub-pattern
|
||||
*/
|
||||
export const within = register('within', (a, b, fn, pat) =>
|
||||
stack(
|
||||
fn(pat.filterWhen((t) => t.cyclePos() >= a && t.cyclePos() <= b)),
|
||||
pat.filterWhen((t) => t.cyclePos() < a || t.cyclePos() > b),
|
||||
),
|
||||
);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Tactus-related functions, i.e. ones that do stepwise
|
||||
// transformations
|
||||
@@ -2450,6 +2548,10 @@ Pattern.prototype.stepJoin = function () {
|
||||
return new Pattern(q, first_t);
|
||||
};
|
||||
|
||||
Pattern.prototype.stepBind = function (func) {
|
||||
return this.fmap(func).stepJoin();
|
||||
};
|
||||
|
||||
export function _retime(timedHaps) {
|
||||
const occupied_perc = timedHaps.filter((t, pat) => pat.hasTactus).reduce((a, b) => a.add(b), Fraction(0));
|
||||
const occupied_tactus = removeUndefineds(timedHaps.map((t, pat) => pat.tactus)).reduce(
|
||||
@@ -2504,7 +2606,7 @@ export const steps = register('steps', function (targetTactus, pat) {
|
||||
// avoid divide by zero..
|
||||
return nothing;
|
||||
}
|
||||
return pat.fast(Fraction(targetTactus).div(pat.tactus));
|
||||
return pat._fast(Fraction(targetTactus).div(pat.tactus)).setTactus(targetTactus);
|
||||
});
|
||||
|
||||
export function _polymeterListSteps(steps, ...args) {
|
||||
@@ -2712,7 +2814,7 @@ export const s_sub = stepRegister('s_sub', function (i, pat) {
|
||||
return pat.s_add(pat.tactus.sub(i));
|
||||
});
|
||||
|
||||
export const s_cycles = stepRegister('s_extend', function (factor, pat) {
|
||||
export const s_extend = stepRegister('s_extend', function (factor, pat) {
|
||||
return pat.fast(factor).s_expand(factor);
|
||||
});
|
||||
|
||||
@@ -2793,6 +2895,13 @@ export const s_tour = function (pat, ...many) {
|
||||
return pat.s_tour(...many);
|
||||
};
|
||||
|
||||
const s_zip = function (...pats) {
|
||||
pats = pats.filter((pat) => pat.hasTactus);
|
||||
const zipped = slowcat(...pats.map((pat) => pat._slow(pat.tactus)));
|
||||
// Should maybe use lcm or gcd for tactus?
|
||||
return zipped._fast(pats[0].tactus).setTactus(pats[0].tactus);
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Control-related functions, i.e. ones that manipulate patterns of
|
||||
// objects
|
||||
@@ -2944,11 +3053,14 @@ export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (facto
|
||||
export const fit = register('fit', (pat) =>
|
||||
pat.withHaps((haps, state) =>
|
||||
haps.map((hap) =>
|
||||
hap.withValue((v) => ({
|
||||
...v,
|
||||
speed: (state.controls._cps || 1) / hap.whole.duration,
|
||||
unit: 'c',
|
||||
})),
|
||||
hap.withValue((v) => {
|
||||
const slicedur = ('end' in v ? v.end : 1) - ('begin' in v ? v.begin : 0);
|
||||
return {
|
||||
...v,
|
||||
speed: ((state.controls._cps || 1) / hap.whole.duration) * slicedur,
|
||||
unit: 'c',
|
||||
};
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -3001,3 +3113,25 @@ export let xfade = (a, pos, b) => {
|
||||
Pattern.prototype.xfade = function (pos, b) {
|
||||
return xfade(this, pos, b);
|
||||
};
|
||||
|
||||
/**
|
||||
* creates a structure pattern from divisions of a cycle
|
||||
* especially useful for creating rhythms
|
||||
* @name beat
|
||||
* @example
|
||||
* s("bd").beat("0:7:10", 16)
|
||||
* @example
|
||||
* s("sd").beat("4:12", 16)
|
||||
*/
|
||||
const __beat = (join) => (t, div, pat) => {
|
||||
t = Fraction(t).mod(div);
|
||||
div = Fraction(div);
|
||||
const b = t.div(div);
|
||||
const e = t.add(1).div(div);
|
||||
return join(pat.fmap((x) => pure(x)._compress(b, e)));
|
||||
};
|
||||
|
||||
export const { beat } = register(
|
||||
['beat'],
|
||||
__beat((x) => x.innerJoin()),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
pick.mjs - methods that use one pattern to pick events from other patterns.
|
||||
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.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 { Pattern, reify, silence, register } from './pattern.mjs';
|
||||
|
||||
import { _mod, clamp, objectMap } from './util.mjs';
|
||||
|
||||
const _pick = function (lookup, pat, modulo = true) {
|
||||
const array = Array.isArray(lookup);
|
||||
const len = Object.keys(lookup).length;
|
||||
|
||||
lookup = objectMap(lookup, reify);
|
||||
|
||||
if (len === 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat.fmap((i) => {
|
||||
let key = i;
|
||||
if (array) {
|
||||
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
|
||||
}
|
||||
return lookup[key];
|
||||
});
|
||||
};
|
||||
|
||||
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
||||
* Similar to `inhabit`, but maintains the structure of the original patterns.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
|
||||
* @example
|
||||
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
|
||||
* @example
|
||||
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
|
||||
* @example
|
||||
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
|
||||
*/
|
||||
|
||||
export const pick = function (lookup, pat) {
|
||||
// backward compatibility - the args used to be flipped
|
||||
if (Array.isArray(pat)) {
|
||||
[pat, lookup] = [lookup, pat];
|
||||
}
|
||||
return __pick(lookup, pat);
|
||||
};
|
||||
|
||||
const __pick = register('pick', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).innerJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pick`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||
* second one.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
|
||||
export const pickmod = register('pickmod', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).innerJoin();
|
||||
});
|
||||
|
||||
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
|
||||
* @param {Pattern} pat
|
||||
* @param {Pattern} lookup a pattern of indices
|
||||
* @param {function[]} funcs the array of functions from which to pull
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
|
||||
* @example
|
||||
* note("<c2 d2>(3,8)").s("square")
|
||||
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
|
||||
*/
|
||||
export const pickF = register('pickF', function (lookup, funcs, pat) {
|
||||
return pat.apply(pick(lookup, funcs));
|
||||
});
|
||||
|
||||
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {Pattern} lookup a pattern of indices
|
||||
* @param {function[]} funcs the array of functions from which to pull
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
|
||||
return pat.apply(pickmod(lookup, funcs));
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickOut = register('pickOut', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).outerJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).outerJoin();
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickRestart = register('pickRestart', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).restartJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* "<a@2 b@2 c@2 d@2>".pickRestart({
|
||||
a: n("0 1 2 0"),
|
||||
b: n("2 3 4 ~"),
|
||||
c: n("[4 5] [4 3] 2 0"),
|
||||
d: n("0 -3 0 ~")
|
||||
}).scale("C:major").s("piano")
|
||||
*/
|
||||
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).restartJoin();
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickReset = register('pickReset', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).resetJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).resetJoin();
|
||||
});
|
||||
|
||||
/**
|
||||
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
||||
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
|
||||
* @name inhabit
|
||||
* @synonyms pickSqueeze
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
|
||||
b: s("cp sd")
|
||||
})
|
||||
* @example
|
||||
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
|
||||
*/
|
||||
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).squeezeJoin();
|
||||
});
|
||||
|
||||
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||
* second one.
|
||||
* @name inhabitmod
|
||||
* @synonyms pickmodSqueeze
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
|
||||
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).squeezeJoin();
|
||||
});
|
||||
|
||||
/**
|
||||
* Pick from the list of values (or patterns of values) via the index using the given
|
||||
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
|
||||
*/
|
||||
|
||||
export const squeeze = (pat, xs) => {
|
||||
xs = xs.map(reify);
|
||||
if (xs.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat
|
||||
.fmap((i) => {
|
||||
const key = _mod(Math.round(i), xs.length);
|
||||
return xs[key];
|
||||
})
|
||||
.squeezeJoin();
|
||||
};
|
||||
+39
-2
@@ -63,11 +63,13 @@ export function repl({
|
||||
let pPatterns = {};
|
||||
let anonymousIndex = 0;
|
||||
let allTransform;
|
||||
let eachTransform;
|
||||
|
||||
const hush = function () {
|
||||
pPatterns = {};
|
||||
anonymousIndex = 0;
|
||||
allTransform = undefined;
|
||||
eachTransform = undefined;
|
||||
return silence;
|
||||
};
|
||||
|
||||
@@ -84,15 +86,42 @@ export function repl({
|
||||
const toggle = () => scheduler.toggle();
|
||||
const setCps = (cps) => scheduler.setCps(cps);
|
||||
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
||||
|
||||
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
|
||||
|
||||
/** Applies a function to all the running patterns. Note that the patterns are groups together into a single `stack` before the function is applied. This is probably what you want, but see `each` for
|
||||
* a version that applies the function to each pattern separately.
|
||||
* ```
|
||||
* $: sound("bd - cp sd")
|
||||
* $: sound("hh*8")
|
||||
* all(fast("<2 3>"))
|
||||
* ```
|
||||
* ```
|
||||
* $: sound("bd - cp sd")
|
||||
* $: sound("hh*8")
|
||||
* all(x => x.pianoroll())
|
||||
* ```
|
||||
*/
|
||||
const all = function (transform) {
|
||||
allTransform = transform;
|
||||
return silence;
|
||||
};
|
||||
/** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern.
|
||||
* ```
|
||||
* $: sound("bd - cp sd")
|
||||
* $: sound("hh*8")
|
||||
* each(fast("<2 3>"))
|
||||
* ```
|
||||
*/
|
||||
const each = function (transform) {
|
||||
eachTransform = transform;
|
||||
return silence;
|
||||
};
|
||||
|
||||
// set pattern methods that use this repl via closure
|
||||
const injectPatternMethods = () => {
|
||||
Pattern.prototype.p = function (id) {
|
||||
if (id.startsWith('_') || id.endsWith('_')) {
|
||||
if (typeof id === 'string' && (id.startsWith('_') || id.endsWith('_'))) {
|
||||
// allows muting a pattern x with x_ or _x
|
||||
return silence;
|
||||
}
|
||||
@@ -131,6 +160,7 @@ export function repl({
|
||||
});
|
||||
return evalScope({
|
||||
all,
|
||||
each,
|
||||
hush,
|
||||
cpm,
|
||||
setCps,
|
||||
@@ -152,7 +182,14 @@ export function repl({
|
||||
shouldHush && hush();
|
||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||
if (Object.keys(pPatterns).length) {
|
||||
pattern = stack(...Object.values(pPatterns));
|
||||
let patterns = Object.values(pPatterns);
|
||||
if (eachTransform) {
|
||||
// Explicit lambda so only element (not index and array) are passed
|
||||
patterns = patterns.map((x) => eachTransform(x));
|
||||
}
|
||||
pattern = stack(...patterns);
|
||||
} else if (eachTransform) {
|
||||
pattern = eachTransform(pattern);
|
||||
}
|
||||
if (allTransform) {
|
||||
pattern = allTransform(pattern);
|
||||
|
||||
+153
-224
@@ -1,13 +1,14 @@
|
||||
/*
|
||||
signal.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
|
||||
signal.mjs - continuous patterns
|
||||
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.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 { Hap } from './hap.mjs';
|
||||
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
|
||||
import { Pattern, fastcat, pure, register, reify, silence, stack } from './pattern.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
import { id, _mod, clamp, objectMap } from './util.mjs';
|
||||
|
||||
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
|
||||
|
||||
export function steady(value) {
|
||||
// A continuous value
|
||||
@@ -138,7 +139,7 @@ const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
|
||||
const timeToRandsPrime = (seed, n) => {
|
||||
const result = [];
|
||||
// eslint-disable-next-line
|
||||
for (let i = 0; i < n; ++n) {
|
||||
for (let i = 0; i < n; ++i) {
|
||||
result.push(intSeedToRand(seed));
|
||||
seed = xorwise(seed);
|
||||
}
|
||||
@@ -159,6 +160,81 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
||||
*/
|
||||
export const run = (n) => saw.range(0, n).floor().segment(n);
|
||||
|
||||
/**
|
||||
* Creates a pattern from a binary number.
|
||||
*
|
||||
* @name binary
|
||||
* @param {number} n - input number to convert to binary
|
||||
* @example
|
||||
* "hh".s().struct(binary(5))
|
||||
* // "hh".s().struct("1 0 1")
|
||||
*/
|
||||
export const binary = (n) => {
|
||||
const nBits = reify(n).log2(0).floor().add(1);
|
||||
return binaryN(n, nBits);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a pattern from a binary number, padded to n bits long.
|
||||
*
|
||||
* @name binaryN
|
||||
* @param {number} n - input number to convert to binary
|
||||
* @param {number} nBits - pattern length, defaults to 16
|
||||
* @example
|
||||
* "hh".s().struct(binaryN(55532, 16))
|
||||
* // "hh".s().struct("1 1 0 1 1 0 0 0 1 1 1 0 1 1 0 0")
|
||||
*/
|
||||
export const binaryN = (n, nBits = 16) => {
|
||||
nBits = reify(nBits);
|
||||
// Shift and mask, putting msb on the right-side
|
||||
const bitPos = run(nBits).mul(-1).add(nBits.sub(1));
|
||||
return reify(n).segment(nBits).brshift(bitPos).band(pure(1));
|
||||
};
|
||||
|
||||
export const randrun = (n) => {
|
||||
return signal((t) => {
|
||||
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
||||
const rands = timeToRands(t.floor().add(0.5), n);
|
||||
const nums = rands
|
||||
.map((n, i) => [n, i])
|
||||
.sort((a, b) => a[0] > b[0] - a[0] < b[0])
|
||||
.map((x) => x[1]);
|
||||
const i = t.cyclePos().mul(n).floor() % n;
|
||||
return nums[i];
|
||||
})._segment(n);
|
||||
};
|
||||
|
||||
const _rearrangeWith = (ipat, n, pat) => {
|
||||
const pats = [...Array(n).keys()].map((i) => pat.zoom(Fraction(i).div(n), Fraction(i + 1).div(n)));
|
||||
return ipat.fmap((i) => pats[i].repeatCycles(n)._fast(n)).innerJoin();
|
||||
};
|
||||
|
||||
/**
|
||||
* @name shuffle
|
||||
* Slices a pattern into the given number of parts, then plays those parts in random order.
|
||||
* Each part will be played exactly once per cycle.
|
||||
* @example
|
||||
* note("c d e f").sound("piano").shuffle(4)
|
||||
* @example
|
||||
* note("c d e f".shuffle(4), "g").sound("piano")
|
||||
*/
|
||||
export const shuffle = register('shuffle', (n, pat) => {
|
||||
return _rearrangeWith(randrun(n), n, pat);
|
||||
});
|
||||
|
||||
/**
|
||||
* @name scramble
|
||||
* Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`,
|
||||
* but parts might be played more than once, or not at all, per cycle.
|
||||
* @example
|
||||
* note("c d e f").sound("piano").scramble(4)
|
||||
* @example
|
||||
* note("c d e f".scramble(4), "g").sound("piano")
|
||||
*/
|
||||
export const scramble = register('scramble', (n, pat) => {
|
||||
return _rearrangeWith(_irand(n)._segment(n), n, pat);
|
||||
});
|
||||
|
||||
/**
|
||||
* A continuous pattern of random numbers, between 0 and 1.
|
||||
*
|
||||
@@ -209,211 +285,6 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
||||
*/
|
||||
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
||||
|
||||
const _pick = function (lookup, pat, modulo = true) {
|
||||
const array = Array.isArray(lookup);
|
||||
const len = Object.keys(lookup).length;
|
||||
|
||||
lookup = objectMap(lookup, reify);
|
||||
|
||||
if (len === 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat.fmap((i) => {
|
||||
let key = i;
|
||||
if (array) {
|
||||
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
|
||||
}
|
||||
return lookup[key];
|
||||
});
|
||||
};
|
||||
|
||||
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
||||
* Similar to `inhabit`, but maintains the structure of the original patterns.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
|
||||
* @example
|
||||
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
|
||||
* @example
|
||||
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
|
||||
* @example
|
||||
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
|
||||
*/
|
||||
|
||||
export const pick = function (lookup, pat) {
|
||||
// backward compatibility - the args used to be flipped
|
||||
if (Array.isArray(pat)) {
|
||||
[pat, lookup] = [lookup, pat];
|
||||
}
|
||||
return __pick(lookup, pat);
|
||||
};
|
||||
|
||||
const __pick = register('pick', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).innerJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pick`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||
* second one.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
|
||||
export const pickmod = register('pickmod', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).innerJoin();
|
||||
});
|
||||
|
||||
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
|
||||
* @param {Pattern} pat
|
||||
* @param {Pattern} lookup a pattern of indices
|
||||
* @param {function[]} funcs the array of functions from which to pull
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
|
||||
* @example
|
||||
* note("<c2 d2>(3,8)").s("square")
|
||||
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
|
||||
*/
|
||||
export const pickF = register('pickF', function (lookup, funcs, pat) {
|
||||
return pat.apply(pick(lookup, funcs));
|
||||
});
|
||||
|
||||
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {Pattern} lookup a pattern of indices
|
||||
* @param {function[]} funcs the array of functions from which to pull
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
|
||||
return pat.apply(pickmod(lookup, funcs));
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickOut = register('pickOut', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).outerJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).outerJoin();
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickRestart = register('pickRestart', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).restartJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* "<a@2 b@2 c@2 d@2>".pickRestart({
|
||||
a: n("0 1 2 0"),
|
||||
b: n("2 3 4 ~"),
|
||||
c: n("[4 5] [4 3] 2 0"),
|
||||
d: n("0 -3 0 ~")
|
||||
}).scale("C:major").s("piano")
|
||||
*/
|
||||
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).restartJoin();
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickReset = register('pickReset', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).resetJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).resetJoin();
|
||||
});
|
||||
|
||||
/**
|
||||
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
||||
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
|
||||
* @name inhabit
|
||||
* @synonyms pickSqueeze
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
|
||||
b: s("cp sd")
|
||||
})
|
||||
* @example
|
||||
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
|
||||
*/
|
||||
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).squeezeJoin();
|
||||
});
|
||||
|
||||
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||
* second one.
|
||||
* @name inhabitmod
|
||||
* @synonyms pickmodSqueeze
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
|
||||
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).squeezeJoin();
|
||||
});
|
||||
|
||||
/**
|
||||
* Pick from the list of values (or patterns of values) via the index using the given
|
||||
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
|
||||
*/
|
||||
|
||||
export const squeeze = (pat, xs) => {
|
||||
xs = xs.map(reify);
|
||||
if (xs.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat
|
||||
.fmap((i) => {
|
||||
const key = _mod(Math.round(i), xs.length);
|
||||
return xs[key];
|
||||
})
|
||||
.squeezeJoin();
|
||||
};
|
||||
|
||||
export const __chooseWith = (pat, xs) => {
|
||||
xs = xs.map(reify);
|
||||
if (xs.length == 0) {
|
||||
@@ -552,8 +423,11 @@ export const perlinWith = (pat) => {
|
||||
*/
|
||||
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
||||
|
||||
export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
|
||||
pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
|
||||
export const degradeByWith = register(
|
||||
'degradeByWith',
|
||||
(withPat, x, pat) => pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -570,9 +444,14 @@ export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
|
||||
* @example
|
||||
* s("[hh?0.2]*8")
|
||||
*/
|
||||
export const degradeBy = register('degradeBy', function (x, pat) {
|
||||
return pat._degradeByWith(rand, x);
|
||||
});
|
||||
export const degradeBy = register(
|
||||
'degradeBy',
|
||||
function (x, pat) {
|
||||
return pat._degradeByWith(rand, x);
|
||||
},
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -586,7 +465,7 @@ export const degradeBy = register('degradeBy', function (x, pat) {
|
||||
* @example
|
||||
* s("[hh?]*8")
|
||||
*/
|
||||
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
||||
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, true);
|
||||
|
||||
/**
|
||||
* Inverse of `degradeBy`: Randomly removes events from the pattern by a given amount.
|
||||
@@ -606,12 +485,17 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
||||
* x => x.undegradeBy(0.8).pan(1)
|
||||
* )
|
||||
*/
|
||||
export const undegradeBy = register('undegradeBy', function (x, pat) {
|
||||
return pat._degradeByWith(
|
||||
rand.fmap((r) => 1 - r),
|
||||
x,
|
||||
);
|
||||
});
|
||||
export const undegradeBy = register(
|
||||
'undegradeBy',
|
||||
function (x, pat) {
|
||||
return pat._degradeByWith(
|
||||
rand.fmap((r) => 1 - r),
|
||||
x,
|
||||
);
|
||||
},
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
/**
|
||||
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
|
||||
@@ -628,7 +512,7 @@ export const undegradeBy = register('undegradeBy', function (x, pat) {
|
||||
* x => x.undegrade().pan(1)
|
||||
* )
|
||||
*/
|
||||
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
||||
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5), true, true);
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -646,7 +530,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
||||
|
||||
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
||||
return reify(patx)
|
||||
.fmap((x) => stack(pat._degradeBy(x), func(pat._undegradeBy(1 - x))))
|
||||
.fmap((x) => stack(pat._degradeBy(x), func(pat)._undegradeBy(1 - x)))
|
||||
.innerJoin();
|
||||
});
|
||||
|
||||
@@ -787,3 +671,48 @@ export const never = register('never', function (_, pat) {
|
||||
export const always = register('always', function (func, pat) {
|
||||
return func(pat);
|
||||
});
|
||||
|
||||
//keyname: string | Array<string>
|
||||
//keyname reference: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
||||
export function _keyDown(keyname) {
|
||||
if (Array.isArray(keyname) === false) {
|
||||
keyname = [keyname];
|
||||
}
|
||||
const keyState = getCurrentKeyboardState();
|
||||
return keyname.every((x) => {
|
||||
const keyName = keyAlias.get(x) ?? x;
|
||||
return keyState[keyName];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Do something on a keypress, or array of keypresses
|
||||
* [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
|
||||
*
|
||||
* @name whenKey
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("bd(5,8)").whenKey("Control:j", x => x.segment(16).color("red")).whenKey("Control:i", x => x.fast(2).color("blue"))
|
||||
*/
|
||||
|
||||
export const whenKey = register('whenKey', function (input, func, pat) {
|
||||
return pat.when(_keyDown(input), func);
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* returns true when a key or array of keys is held
|
||||
* [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
|
||||
*
|
||||
* @name keyDown
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* keyDown("Control:j").pick([s("bd(5,8)"), s("cp(3,8)")])
|
||||
*/
|
||||
|
||||
export const keyDown = register('keyDown', function (pat) {
|
||||
return pat.fmap(_keyDown);
|
||||
});
|
||||
|
||||
@@ -46,12 +46,14 @@ import {
|
||||
rev,
|
||||
time,
|
||||
run,
|
||||
binaryN,
|
||||
pick,
|
||||
stackLeft,
|
||||
stackRight,
|
||||
stackCentre,
|
||||
s_cat,
|
||||
calculateTactus,
|
||||
sometimes,
|
||||
} from '../index.mjs';
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
@@ -958,6 +960,18 @@ describe('Pattern', () => {
|
||||
expect(run(4).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('binaryN', () => {
|
||||
it('Can make a binary pattern from a decimal', () => {
|
||||
expect(binaryN(55532).firstCycle()).toStrictEqual(
|
||||
sequence(1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Can make a binary pattern from patterned inputs', () => {
|
||||
expect(binaryN(pure(0x1337), pure(14)).firstCycle()).toStrictEqual(
|
||||
sequence(0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('ribbon', () => {
|
||||
it('Can ribbon', () => {
|
||||
expect(cat(0, 1, 2, 3, 4, 5, 6, 7).ribbon(2, 4).fast(4).firstCycle()).toStrictEqual(
|
||||
@@ -1253,4 +1267,13 @@ describe('Pattern', () => {
|
||||
expect(s('bev').chop(8).loopAt(2).tactus).toStrictEqual(Fraction(4));
|
||||
});
|
||||
});
|
||||
describe('sometimes', () => {
|
||||
it('works with constant functions', () => {
|
||||
expect(
|
||||
pure('a')
|
||||
.sometimes((x) => pure('b'))
|
||||
.fast(16).firstCycleValues.length,
|
||||
).toStrictEqual(16);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -363,6 +363,108 @@ export function objectMap(obj, fn) {
|
||||
}
|
||||
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
|
||||
}
|
||||
export function cycleToSeconds(cycle, cps) {
|
||||
return cycle / cps;
|
||||
}
|
||||
|
||||
// utility for averaging two clocks together to account for drift
|
||||
export class ClockCollator {
|
||||
constructor({
|
||||
getTargetClockTime = getUnixTimeSeconds,
|
||||
weight = 16,
|
||||
offsetDelta = 0.005,
|
||||
checkAfterTime = 2,
|
||||
resetAfterTime = 8,
|
||||
}) {
|
||||
this.offsetTime;
|
||||
this.timeAtPrevOffsetSample;
|
||||
this.prevOffsetTimes = [];
|
||||
this.getTargetClockTime = getTargetClockTime;
|
||||
this.weight = weight;
|
||||
this.offsetDelta = offsetDelta;
|
||||
this.checkAfterTime = checkAfterTime;
|
||||
this.resetAfterTime = resetAfterTime;
|
||||
this.reset = () => {
|
||||
this.prevOffsetTimes = [];
|
||||
this.offsetTime = null;
|
||||
this.timeAtPrevOffsetSample = null;
|
||||
};
|
||||
}
|
||||
calculateOffset(currentTime) {
|
||||
const targetClockTime = this.getTargetClockTime();
|
||||
const diffBetweenTimeSamples = targetClockTime - this.timeAtPrevOffsetSample;
|
||||
const newOffsetTime = targetClockTime - currentTime;
|
||||
// recalcuate the diff from scratch if the clock has been paused for some time.
|
||||
if (diffBetweenTimeSamples > this.resetAfterTime) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
if (this.offsetTime == null) {
|
||||
this.offsetTime = newOffsetTime;
|
||||
}
|
||||
this.prevOffsetTimes.push(newOffsetTime);
|
||||
if (this.prevOffsetTimes.length > this.weight) {
|
||||
this.prevOffsetTimes.shift();
|
||||
}
|
||||
|
||||
// after X time has passed, the average of the previous weight offset times is calculated and used as a stable reference
|
||||
// for calculating the timestamp
|
||||
if (this.timeAtPrevOffsetSample == null || diffBetweenTimeSamples > this.checkAfterTime) {
|
||||
this.timeAtPrevOffsetSample = targetClockTime;
|
||||
const rollingOffsetTime = averageArray(this.prevOffsetTimes);
|
||||
//when the clock offsets surpass the delta, set the new reference time
|
||||
if (Math.abs(rollingOffsetTime - this.offsetTime) > this.offsetDelta) {
|
||||
this.offsetTime = rollingOffsetTime;
|
||||
}
|
||||
}
|
||||
|
||||
return this.offsetTime;
|
||||
}
|
||||
|
||||
calculateTimestamp(currentTime, targetTime) {
|
||||
return this.calculateOffset(currentTime) + targetTime;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPerformanceTimeSeconds() {
|
||||
return performance.now() * 0.001;
|
||||
}
|
||||
|
||||
function getUnixTimeSeconds() {
|
||||
return Date.now() * 0.001;
|
||||
}
|
||||
|
||||
export const keyAlias = new Map([
|
||||
['control', 'Control'],
|
||||
['ctrl', 'Control'],
|
||||
['alt', 'Alt'],
|
||||
['shift', 'Shift'],
|
||||
['down', 'ArrowDown'],
|
||||
['up', 'ArrowUp'],
|
||||
['left', 'ArrowLeft'],
|
||||
['right', 'ArrowRight'],
|
||||
]);
|
||||
let keyState;
|
||||
|
||||
export function getCurrentKeyboardState() {
|
||||
if (keyState == null) {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
keyState = {};
|
||||
// Listen for the keydown event to mark the key as pressed
|
||||
window.addEventListener('keydown', (event) => {
|
||||
keyState[event.key] = true; // Mark the key as pressed
|
||||
});
|
||||
|
||||
// Listen for the keyup event to mark the key as released
|
||||
window.addEventListener('keyup', (event) => {
|
||||
keyState[event.key] = false; // Mark the key as released
|
||||
});
|
||||
}
|
||||
|
||||
return { ...keyState }; // Return a shallow copy of the key state object
|
||||
}
|
||||
|
||||
// Floating point versions, see Fraction for rational versions
|
||||
// // greatest common divisor
|
||||
|
||||
@@ -1,64 +1,36 @@
|
||||
import { parseNumeral, Pattern, averageArray } from '@strudel/core';
|
||||
import { Pattern, ClockCollator } from '@strudel/core';
|
||||
import { parseControlsFromHap } from 'node_modules/@strudel/osc/osc.mjs';
|
||||
import { Invoke } from './utils.mjs';
|
||||
|
||||
let offsetTime;
|
||||
let timeAtPrevOffsetSample;
|
||||
let prevOffsetTimes = [];
|
||||
const collator = new ClockCollator({});
|
||||
|
||||
Pattern.prototype.osc = function () {
|
||||
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => {
|
||||
hap.ensureObjectValue();
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
// make sure n and note are numbers
|
||||
controls.n && (controls.n = parseNumeral(controls.n));
|
||||
controls.note && (controls.note = parseNumeral(controls.note));
|
||||
export async function oscTriggerTauri(t_deprecate, hap, currentTime, cps = 1, targetTime) {
|
||||
const controls = parseControlsFromHap(hap, cps);
|
||||
const params = [];
|
||||
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
|
||||
|
||||
const params = [];
|
||||
Object.keys(controls).forEach((key) => {
|
||||
const val = controls[key];
|
||||
const value = typeof val === 'number' ? val.toString() : val;
|
||||
|
||||
const unixTimeSecs = Date.now() / 1000;
|
||||
const newOffsetTime = unixTimeSecs - currentTime;
|
||||
if (offsetTime == null) {
|
||||
offsetTime = newOffsetTime;
|
||||
}
|
||||
prevOffsetTimes.push(newOffsetTime);
|
||||
if (prevOffsetTimes.length > 8) {
|
||||
prevOffsetTimes.shift();
|
||||
}
|
||||
// every two seconds, the average of the previous 8 offset times is calculated and used as a stable reference
|
||||
// for calculating the timestamp that will be sent to the backend
|
||||
if (timeAtPrevOffsetSample == null || unixTimeSecs - timeAtPrevOffsetSample > 2) {
|
||||
timeAtPrevOffsetSample = unixTimeSecs;
|
||||
const rollingOffsetTime = averageArray(prevOffsetTimes);
|
||||
//account for the js clock freezing or resets set the new offset
|
||||
if (Math.abs(rollingOffsetTime - offsetTime) > 0.01) {
|
||||
offsetTime = rollingOffsetTime;
|
||||
}
|
||||
}
|
||||
|
||||
const timestamp = offsetTime + targetTime;
|
||||
|
||||
Object.keys(controls).forEach((key) => {
|
||||
const val = controls[key];
|
||||
const value = typeof val === 'number' ? val.toString() : val;
|
||||
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
params.push({
|
||||
name: key,
|
||||
value,
|
||||
valueisnumber: typeof val === 'number',
|
||||
});
|
||||
});
|
||||
|
||||
if (params.length === 0) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
const message = { target: '/dirt/play', timestamp, params };
|
||||
setTimeout(() => {
|
||||
Invoke('sendosc', { messagesfromjs: [message] });
|
||||
params.push({
|
||||
name: key,
|
||||
value,
|
||||
valueisnumber: typeof val === 'number',
|
||||
});
|
||||
});
|
||||
|
||||
if (params.length === 0) {
|
||||
return;
|
||||
}
|
||||
const message = { target: '/dirt/play', timestamp, params };
|
||||
setTimeout(() => {
|
||||
Invoke('sendosc', { messagesfromjs: [message] });
|
||||
});
|
||||
}
|
||||
Pattern.prototype.osc = function () {
|
||||
return this.onTrigger(oscTriggerTauri);
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ export const getDrawContext = (id = 'test-canvas', options) => {
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
return canvas.getContext(contextType);
|
||||
return canvas.getContext(contextType, { willReadFrequently: true });
|
||||
};
|
||||
|
||||
let animationFrames = {};
|
||||
|
||||
@@ -169,13 +169,18 @@ const refs = {};
|
||||
export async function midin(input) {
|
||||
if (isPattern(input)) {
|
||||
throw new Error(
|
||||
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
||||
`midin: does not accept Pattern as input. Make sure to pass device name with single quotes. Example: midin('${
|
||||
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
||||
}')`,
|
||||
);
|
||||
}
|
||||
const initial = await enableWebMidi(); // only returns on first init
|
||||
const device = getDevice(input, WebMidi.inputs);
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`midiin: device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
|
||||
);
|
||||
}
|
||||
if (initial) {
|
||||
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
||||
logger(
|
||||
|
||||
@@ -37,6 +37,6 @@
|
||||
"devDependencies": {
|
||||
"peggy": "^3.0.2",
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# @strudel/serial
|
||||
|
||||
This package adds webserial functionality to strudel Patterns, for e.g. sending messages to arduino microcontrollers.
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
mqtt.mjs - for patterning the internet of things from strudel
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/serial/serial.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 { Pattern, isPattern } from '@strudel/core';
|
||||
import Paho from 'paho-mqtt';
|
||||
|
||||
const connections = {};
|
||||
|
||||
// Handle connection loss
|
||||
function onConnectionLost(responseObject) {
|
||||
if (responseObject.errorCode !== 0) {
|
||||
console.error(' mqtt connection lost: ', responseObject.errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle received messages
|
||||
function onMessageArrived(message) {
|
||||
console.log('incoming mqtt message: ', message.payloadString); // prettier-ignore
|
||||
}
|
||||
|
||||
function onFailure(err) {
|
||||
console.error('Connection failed: ', err);
|
||||
}
|
||||
|
||||
Pattern.prototype.mqtt = function (
|
||||
username = undefined,
|
||||
password = undefined,
|
||||
topic = undefined,
|
||||
host = 'wss://localhost:8883/',
|
||||
client = undefined,
|
||||
latency = 0,
|
||||
) {
|
||||
const key = host + '-' + client;
|
||||
let connected = false;
|
||||
if (!client) {
|
||||
client = 'strudel-' + String(Math.floor(Math.random() * 1000000));
|
||||
}
|
||||
function onConnect() {
|
||||
console.log('Connected to mqtt broker');
|
||||
connected = true;
|
||||
}
|
||||
|
||||
let cx;
|
||||
if (connections[key]) {
|
||||
cx = connections[key];
|
||||
} else {
|
||||
cx = new Paho.Client(host, client);
|
||||
cx.onConnectionLost = onConnectionLost;
|
||||
cx.onMessageArrived = onMessageArrived;
|
||||
const props = {
|
||||
onSuccess: onConnect,
|
||||
onFailure: onFailure,
|
||||
useSSL: true,
|
||||
};
|
||||
|
||||
if (username) {
|
||||
props.userName = username;
|
||||
props.password = password;
|
||||
}
|
||||
cx.connect(props);
|
||||
}
|
||||
return this.withHap((hap) => {
|
||||
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
if (!connected) {
|
||||
return;
|
||||
}
|
||||
let message = '';
|
||||
if (typeof hap.value === 'object') {
|
||||
message = JSON.stringify(hap.value);
|
||||
} else {
|
||||
message = hap.value;
|
||||
}
|
||||
message = new Paho.Message(message);
|
||||
message.destinationName = topic;
|
||||
|
||||
const offset = (targetTime - currentTime + latency) * 1000;
|
||||
|
||||
window.setTimeout(function () {
|
||||
cx.send(message);
|
||||
}, offset);
|
||||
};
|
||||
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@strudel/mqtt",
|
||||
"version": "1.1.0",
|
||||
"description": "MQTT API for strudel",
|
||||
"main": "mqtt.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"titdalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Alex McLean <alex@slab.org>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*",
|
||||
"paho-mqtt": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { dependencies } from './package.json';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'mqtt.mjs'),
|
||||
formats: ['es'],
|
||||
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
+40
-30
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import OSC from 'osc-js';
|
||||
|
||||
import { logger, parseNumeral, Pattern, getEventOffsetMs, isNote, noteToMidi } from '@strudel/core';
|
||||
import { logger, parseNumeral, Pattern, isNote, noteToMidi, ClockCollator } from '@strudel/core';
|
||||
|
||||
let connection; // Promise<OSC>
|
||||
function connect() {
|
||||
@@ -34,6 +34,44 @@ function connect() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
export function parseControlsFromHap(hap, cps) {
|
||||
hap.ensureObjectValue();
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
// make sure n and note are numbers
|
||||
controls.n && (controls.n = parseNumeral(controls.n));
|
||||
if (typeof controls.note !== 'undefined') {
|
||||
if (isNote(controls.note)) {
|
||||
controls.midinote = noteToMidi(controls.note, controls.octave || 3);
|
||||
} else {
|
||||
controls.note = parseNumeral(controls.note);
|
||||
}
|
||||
}
|
||||
controls.bank && (controls.s = controls.bank + controls.s);
|
||||
controls.roomsize && (controls.size = parseNumeral(controls.roomsize));
|
||||
// speed adjustment for CPS is handled on the DSP side in superdirt and pattern side in Strudel,
|
||||
// so we need to undo the adjustment before sending the message to superdirt.
|
||||
controls.unit === 'c' && controls.speed != null && (controls.speed = controls.speed / cps);
|
||||
const channels = controls.channels;
|
||||
channels != undefined && (controls.channels = JSON.stringify(channels));
|
||||
return controls;
|
||||
}
|
||||
|
||||
const collator = new ClockCollator({});
|
||||
|
||||
export async function oscTrigger(t_deprecate, hap, currentTime, cps = 1, targetTime) {
|
||||
const osc = await connect();
|
||||
const controls = parseControlsFromHap(hap, cps);
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
|
||||
const ts = Math.round(collator.calculateTimestamp(currentTime, targetTime) * 1000);
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
const bundle = new OSC.Bundle([message], ts);
|
||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||
osc.send(bundle);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
|
||||
@@ -44,33 +82,5 @@ function connect() {
|
||||
* @returns Pattern
|
||||
*/
|
||||
Pattern.prototype.osc = function () {
|
||||
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => {
|
||||
hap.ensureObjectValue();
|
||||
const osc = await connect();
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
// make sure n and note are numbers
|
||||
controls.n && (controls.n = parseNumeral(controls.n));
|
||||
if (typeof controls.note !== 'undefined') {
|
||||
if (isNote(controls.note)) {
|
||||
controls.midinote = noteToMidi(controls.note, controls.octave || 3);
|
||||
} else {
|
||||
controls.note = parseNumeral(controls.note);
|
||||
}
|
||||
}
|
||||
controls.bank && (controls.s = controls.bank + controls.s);
|
||||
controls.roomsize && (controls.size = parseNumeral(controls.roomsize));
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
// time should be audio time of onset
|
||||
// currentTime should be current time of audio context (slightly before time)
|
||||
const offset = getEventOffsetMs(targetTime, currentTime);
|
||||
|
||||
// timestamp in milliseconds used to trigger the osc bundle at a precise moment
|
||||
const ts = Math.floor(Date.now() + offset);
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
const bundle = new OSC.Bundle([message], ts);
|
||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||
osc.send(bundle);
|
||||
});
|
||||
return this.onTrigger(oscTrigger);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
||||
import { isTauri } from '../desktopbridge/utils.mjs';
|
||||
import { oscTrigger } from './osc.mjs';
|
||||
|
||||
const trigger = isTauri() ? oscTriggerTauri : oscTrigger;
|
||||
|
||||
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
|
||||
const currentTime = performance.now() / 1000;
|
||||
return trigger(null, hap, currentTime, cps, targetTime);
|
||||
};
|
||||
@@ -10,6 +10,8 @@ if (typeof HTMLElement !== 'undefined') {
|
||||
static observedAttributes = ['code'];
|
||||
settings = codemirrorSettings.get();
|
||||
editor = null;
|
||||
sync = false;
|
||||
solo = true;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
@@ -49,6 +51,8 @@ if (typeof HTMLElement !== 'undefined') {
|
||||
});
|
||||
this.dispatchEvent(event);
|
||||
},
|
||||
solo: this.solo,
|
||||
sync: this.sync,
|
||||
});
|
||||
// init settings
|
||||
this.editor.updateSettings(this.settings);
|
||||
|
||||
@@ -68,7 +68,7 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
|
||||
if (!(name in writeMessagers)) {
|
||||
getWriter(name, br);
|
||||
}
|
||||
const onTrigger = (time, hap, currentTime) => {
|
||||
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
var message = '';
|
||||
var chk = 0;
|
||||
if (typeof hap.value === 'object') {
|
||||
@@ -105,7 +105,7 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
|
||||
} else {
|
||||
message = hap.value;
|
||||
}
|
||||
const offset = (time - currentTime + latency) * 1000;
|
||||
const offset = (targetTime - currentTime + latency) * 1000;
|
||||
|
||||
window.setTimeout(function () {
|
||||
writeMessagers[name](message, chk);
|
||||
|
||||
@@ -112,11 +112,27 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
|
||||
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
|
||||
};
|
||||
|
||||
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) {
|
||||
export function createFilter(
|
||||
context,
|
||||
type,
|
||||
frequency,
|
||||
Q,
|
||||
att,
|
||||
dec,
|
||||
sus,
|
||||
rel,
|
||||
fenv,
|
||||
start,
|
||||
end,
|
||||
fanchor,
|
||||
model,
|
||||
drive,
|
||||
flfo,
|
||||
) {
|
||||
const curve = 'exponential';
|
||||
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
|
||||
let filter;
|
||||
let frequencyParam;
|
||||
let frequencyParam, detuneParam;
|
||||
if (model === 'ladder') {
|
||||
filter = getWorklet(context, 'ladder-processor', { frequency, q: Q, drive });
|
||||
frequencyParam = filter.parameters.get('frequency');
|
||||
@@ -126,6 +142,7 @@ export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fe
|
||||
filter.Q.value = Q;
|
||||
filter.frequency.value = frequency;
|
||||
frequencyParam = filter.frequency;
|
||||
detuneParam = filter.detune;
|
||||
}
|
||||
|
||||
// envelope is active when any of these values is set
|
||||
@@ -140,7 +157,10 @@ export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fe
|
||||
let max = clamp(2 ** (fenvAbs - offset) * frequency, 0, 20000);
|
||||
if (fenv < 0) [min, max] = [max, min];
|
||||
getParamADSR(frequencyParam, attack, decay, sustain, release, min, max, start, end, curve);
|
||||
return filter;
|
||||
}
|
||||
const hasLFO = detuneParam && flfo !== undefined;
|
||||
if (hasLFO !== undefined) {
|
||||
let lfo = getVibratoOscillator(detuneParam, { vib: flfo, vibmod: 4 }, start);
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
@@ -22,16 +22,13 @@ function humanFileSize(bytes, si) {
|
||||
return bytes.toFixed(1) + ' ' + units[u];
|
||||
}
|
||||
|
||||
export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resolveUrl) => {
|
||||
let transpose = 0;
|
||||
if (freq !== undefined && note !== undefined) {
|
||||
logger('[sampler] hap has note and freq. ignoring note', 'warning');
|
||||
}
|
||||
let midi = valueToMidi({ freq, note }, 36);
|
||||
transpose = midi - 36; // C3 is middle C
|
||||
|
||||
const ac = getAudioContext();
|
||||
|
||||
// deduces relevant info for sample loading from hap.value and sample definition
|
||||
// it encapsulates the core sampler logic into a pure and synchronous function
|
||||
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
||||
export function getSampleInfo(hapValue, bank) {
|
||||
const { s, n = 0, speed = 1.0 } = hapValue;
|
||||
let midi = valueToMidi(hapValue, 36);
|
||||
let transpose = midi - 36; // C3 is middle C;
|
||||
let sampleUrl;
|
||||
let index = 0;
|
||||
if (Array.isArray(bank)) {
|
||||
@@ -50,19 +47,54 @@ export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resol
|
||||
index = getSoundIndex(n, bank[closest].length);
|
||||
sampleUrl = bank[closest][index];
|
||||
}
|
||||
const label = `${s}:${index}`;
|
||||
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||
return { transpose, sampleUrl, index, midi, label, playbackRate };
|
||||
}
|
||||
|
||||
// takes hapValue and returns buffer + playbackRate.
|
||||
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
|
||||
let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
|
||||
if (resolveUrl) {
|
||||
sampleUrl = await resolveUrl(sampleUrl);
|
||||
}
|
||||
let buffer = await loadBuffer(sampleUrl, ac, s, index);
|
||||
if (speed < 0) {
|
||||
const ac = getAudioContext();
|
||||
const buffer = await loadBuffer(sampleUrl, ac, label);
|
||||
|
||||
if (hapValue.unit === 'c') {
|
||||
playbackRate = playbackRate * buffer.duration;
|
||||
}
|
||||
return { buffer, playbackRate };
|
||||
};
|
||||
|
||||
// creates playback ready AudioBufferSourceNode from hapValue
|
||||
export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
||||
let { buffer, playbackRate } = await getSampleBuffer(hapValue, bank, resolveUrl);
|
||||
if (hapValue.speed < 0) {
|
||||
// should this be cached?
|
||||
buffer = reverseBuffer(buffer);
|
||||
}
|
||||
const ac = getAudioContext();
|
||||
const bufferSource = ac.createBufferSource();
|
||||
bufferSource.buffer = buffer;
|
||||
const playbackRate = 1.0 * Math.pow(2, transpose / 12);
|
||||
bufferSource.playbackRate.value = playbackRate;
|
||||
return bufferSource;
|
||||
|
||||
const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
|
||||
|
||||
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
||||
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
||||
// the midway point through a 10-second audio buffer is still 5."
|
||||
const offset = begin * bufferSource.buffer.duration;
|
||||
|
||||
const loop = s.startsWith('wt_') ? 1 : hapValue.loop;
|
||||
if (loop) {
|
||||
bufferSource.loop = true;
|
||||
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
|
||||
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
|
||||
}
|
||||
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||
const sliceDuration = (end - begin) * bufferDuration;
|
||||
return { bufferSource, offset, bufferDuration, sliceDuration };
|
||||
};
|
||||
|
||||
export const loadBuffer = (url, ac, s, n = 0) => {
|
||||
@@ -232,10 +264,10 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
|
||||
const { prebake, tag } = options;
|
||||
processSampleMap(
|
||||
sampleMap,
|
||||
(key, value) =>
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
|
||||
(key, bank) =>
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
|
||||
type: 'sample',
|
||||
samples: value,
|
||||
samples: bank,
|
||||
baseUrl,
|
||||
prebake,
|
||||
tag,
|
||||
@@ -249,38 +281,26 @@ const cutGroups = [];
|
||||
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
let {
|
||||
s,
|
||||
freq,
|
||||
unit,
|
||||
nudge = 0, // TODO: is this in seconds?
|
||||
cut,
|
||||
loop,
|
||||
clip = undefined, // if set, samples will be cut off when the hap ends
|
||||
n = 0,
|
||||
note,
|
||||
speed = 1, // sample playback speed
|
||||
loopBegin = 0,
|
||||
begin = 0,
|
||||
loopEnd = 1,
|
||||
end = 1,
|
||||
duration,
|
||||
} = value;
|
||||
|
||||
// load sample
|
||||
if (speed === 0) {
|
||||
// no playback
|
||||
return;
|
||||
}
|
||||
loop = s.startsWith('wt_') ? 1 : value.loop;
|
||||
const ac = getAudioContext();
|
||||
|
||||
// destructure adsr here, because the default should be different for synths and samples
|
||||
|
||||
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
||||
//const soundfont = getSoundfontKey(s);
|
||||
const time = t + nudge;
|
||||
|
||||
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl);
|
||||
|
||||
// vibrato
|
||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
||||
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
|
||||
|
||||
// asny stuff above took too long?
|
||||
if (ac.currentTime > t) {
|
||||
@@ -292,26 +312,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
logger(`[sampler] could not load "${s}:${n}"`, 'error');
|
||||
return;
|
||||
}
|
||||
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 * 1; //cps;
|
||||
}
|
||||
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
||||
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
||||
// the midway point through a 10-second audio buffer is still 5."
|
||||
const offset = begin * bufferSource.buffer.duration;
|
||||
if (loop) {
|
||||
bufferSource.loop = true;
|
||||
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
|
||||
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
|
||||
}
|
||||
|
||||
// vibrato
|
||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
||||
|
||||
const time = t + nudge;
|
||||
bufferSource.start(time, offset);
|
||||
|
||||
const envGain = ac.createGain();
|
||||
const node = bufferSource.connect(envGain);
|
||||
|
||||
// if none of these controls is set, the duration of the sound will be set to the duration of the sample slice
|
||||
if (clip == null && loop == null && value.release == null) {
|
||||
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||
duration = (end - begin) * bufferDuration;
|
||||
duration = sliceDuration;
|
||||
}
|
||||
let holdEnd = t + duration;
|
||||
|
||||
|
||||
@@ -87,6 +87,10 @@ export const getAudioContext = () => {
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
export function getAudioContextCurrentTime() {
|
||||
return getAudioContext().currentTime;
|
||||
}
|
||||
|
||||
let workletsLoading;
|
||||
function loadWorklets() {
|
||||
if (!workletsLoading) {
|
||||
@@ -266,11 +270,12 @@ function getReverb(orbit, duration, fade, lp, dim, ir) {
|
||||
export let analysers = {},
|
||||
analysersData = {};
|
||||
|
||||
export function getAnalyserById(id, fftSize = 1024) {
|
||||
export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) {
|
||||
if (!analysers[id]) {
|
||||
// make sure this doesn't happen too often as it piles up garbage
|
||||
const analyserNode = getAudioContext().createAnalyser();
|
||||
analyserNode.fftSize = fftSize;
|
||||
analyserNode.smoothingTimeConstant = smoothingTimeConstant;
|
||||
// getDestination().connect(analyserNode);
|
||||
analysers[id] = analyserNode;
|
||||
analysersData[id] = new Float32Array(analysers[id].frequencyBinCount);
|
||||
@@ -352,6 +357,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
lpdecay,
|
||||
lpsustain,
|
||||
lprelease,
|
||||
lplfo,
|
||||
resonance = getDefaultValue('resonance'),
|
||||
// high pass
|
||||
hpenv,
|
||||
@@ -371,7 +377,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
bandq = getDefaultValue('bandq'),
|
||||
channels = getDefaultValue('channels'),
|
||||
//phaser
|
||||
phaser,
|
||||
phaserrate: phaser,
|
||||
phaserdepth = getDefaultValue('phaserdepth'),
|
||||
phasersweep,
|
||||
phasercenter,
|
||||
@@ -417,6 +423,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
};
|
||||
if (bank && s) {
|
||||
s = `${bank}_${s}`;
|
||||
value.s = s;
|
||||
}
|
||||
|
||||
// get source AudioNode
|
||||
@@ -469,6 +476,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
fanchor,
|
||||
ftype,
|
||||
drive,
|
||||
lplfo,
|
||||
);
|
||||
chain.push(lp());
|
||||
if (ftype === '24db') {
|
||||
|
||||
@@ -37,6 +37,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
export * from './webaudio.mjs';
|
||||
export * from './scope.mjs';
|
||||
export * from './spectrum.mjs';
|
||||
export * from 'superdough';
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Pattern, clamp } from '@strudel/core';
|
||||
import { getDrawContext, getTheme } from '@strudel/draw';
|
||||
import { analysers, getAnalyzerData } from 'superdough';
|
||||
|
||||
/**
|
||||
* Renders a spectrum analyzer for the incoming audio signal.
|
||||
* @name spectrum
|
||||
* @param {object} config optional config with options:
|
||||
* @param {integer} thickness line thickness in px (default 3)
|
||||
* @param {integer} speed scroll speed (default 1)
|
||||
* @param {integer} min min db (default -80)
|
||||
* @param {integer} max max db (default 0)
|
||||
* @example
|
||||
* n("<0 4 <2 3> 1>*3")
|
||||
* .off(1/8, add(n(5)))
|
||||
* .off(1/5, add(n(7)))
|
||||
* .scale("d3:minor:pentatonic")
|
||||
* .s('sine')
|
||||
* .dec(.3).room(.5)
|
||||
* ._spectrum()
|
||||
*/
|
||||
let latestColor = {};
|
||||
Pattern.prototype.spectrum = function (config = {}) {
|
||||
let id = config.id ?? 1;
|
||||
return this.analyze(id).draw(
|
||||
(haps) => {
|
||||
config.color = haps[0]?.value?.color || latestColor[id] || getTheme().foreground;
|
||||
latestColor[id] = config.color;
|
||||
drawSpectrum(analysers[id], config);
|
||||
},
|
||||
{ id },
|
||||
);
|
||||
};
|
||||
|
||||
Pattern.prototype.scope = Pattern.prototype.tscope;
|
||||
|
||||
const lastFrames = new Map();
|
||||
|
||||
function drawSpectrum(
|
||||
analyser,
|
||||
{ thickness = 3, speed = 1, min = -80, max = 0, ctx = getDrawContext(), id = 1, color } = {},
|
||||
) {
|
||||
ctx.lineWidth = thickness;
|
||||
ctx.strokeStyle = color;
|
||||
|
||||
if (!analyser) {
|
||||
// if analyser is undefined, draw straight line
|
||||
// it may be undefined when no sound has been played yet
|
||||
return;
|
||||
}
|
||||
const scrollSize = speed;
|
||||
const dataArray = getAnalyzerData('frequency', id);
|
||||
const canvas = ctx.canvas;
|
||||
ctx.fillStyle = color;
|
||||
const bufferSize = analyser.frequencyBinCount;
|
||||
let imageData = lastFrames.get(id) || ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
lastFrames.set(id, imageData);
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
ctx.putImageData(imageData, -scrollSize, 0);
|
||||
let q = canvas.width - speed;
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
const normalized = clamp((dataArray[i] - min) / (max - min), 0, 1);
|
||||
ctx.globalAlpha = normalized;
|
||||
const next = (Math.log(i + 1) / Math.log(bufferSize)) * canvas.height;
|
||||
const size = 2; //next - pos;
|
||||
ctx.fillRect(q, canvas.height - next, scrollSize, size);
|
||||
}
|
||||
lastFrames.set(id, ctx.getImageData(0, 0, canvas.width, canvas.height));
|
||||
}
|
||||
@@ -34,6 +34,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+11465
-9229
File diff suppressed because it is too large
Load Diff
@@ -954,6 +954,24 @@ exports[`runs examples > example "bank" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "beat" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/16 | s:bd ]",
|
||||
"[ 1/1 → 17/16 | s:bd ]",
|
||||
"[ 2/1 → 33/16 | s:bd ]",
|
||||
"[ 3/1 → 49/16 | s:bd ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "beat" example index 1 1`] = `
|
||||
[
|
||||
"[ 1/48 → 1/12 | s:sd ]",
|
||||
"[ 49/48 → 13/12 | s:sd ]",
|
||||
"[ 97/48 → 25/12 | s:sd ]",
|
||||
"[ 145/48 → 37/12 | s:sd ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "begin" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:rave begin:0 ]",
|
||||
@@ -967,6 +985,210 @@ exports[`runs examples > example "begin" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "binary" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | s:hh ]",
|
||||
"[ 2/3 → 1/1 | s:hh ]",
|
||||
"[ 1/1 → 4/3 | s:hh ]",
|
||||
"[ 5/3 → 2/1 | s:hh ]",
|
||||
"[ 2/1 → 7/3 | s:hh ]",
|
||||
"[ 8/3 → 3/1 | s:hh ]",
|
||||
"[ 3/1 → 10/3 | s:hh ]",
|
||||
"[ 11/3 → 4/1 | s:hh ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "binaryN" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/16 | s:hh ]",
|
||||
"[ 1/16 → 1/8 | s:hh ]",
|
||||
"[ 3/16 → 1/4 | s:hh ]",
|
||||
"[ 1/4 → 5/16 | s:hh ]",
|
||||
"[ 1/2 → 9/16 | s:hh ]",
|
||||
"[ 9/16 → 5/8 | s:hh ]",
|
||||
"[ 5/8 → 11/16 | s:hh ]",
|
||||
"[ 3/4 → 13/16 | s:hh ]",
|
||||
"[ 13/16 → 7/8 | s:hh ]",
|
||||
"[ 1/1 → 17/16 | s:hh ]",
|
||||
"[ 17/16 → 9/8 | s:hh ]",
|
||||
"[ 19/16 → 5/4 | s:hh ]",
|
||||
"[ 5/4 → 21/16 | s:hh ]",
|
||||
"[ 3/2 → 25/16 | s:hh ]",
|
||||
"[ 25/16 → 13/8 | s:hh ]",
|
||||
"[ 13/8 → 27/16 | s:hh ]",
|
||||
"[ 7/4 → 29/16 | s:hh ]",
|
||||
"[ 29/16 → 15/8 | s:hh ]",
|
||||
"[ 2/1 → 33/16 | s:hh ]",
|
||||
"[ 33/16 → 17/8 | s:hh ]",
|
||||
"[ 35/16 → 9/4 | s:hh ]",
|
||||
"[ 9/4 → 37/16 | s:hh ]",
|
||||
"[ 5/2 → 41/16 | s:hh ]",
|
||||
"[ 41/16 → 21/8 | s:hh ]",
|
||||
"[ 21/8 → 43/16 | s:hh ]",
|
||||
"[ 11/4 → 45/16 | s:hh ]",
|
||||
"[ 45/16 → 23/8 | s:hh ]",
|
||||
"[ 3/1 → 49/16 | s:hh ]",
|
||||
"[ 49/16 → 25/8 | s:hh ]",
|
||||
"[ 51/16 → 13/4 | s:hh ]",
|
||||
"[ 13/4 → 53/16 | s:hh ]",
|
||||
"[ 7/2 → 57/16 | s:hh ]",
|
||||
"[ 57/16 → 29/8 | s:hh ]",
|
||||
"[ 29/8 → 59/16 | s:hh ]",
|
||||
"[ 15/4 → 61/16 | s:hh ]",
|
||||
"[ 61/16 → 31/8 | s:hh ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "bite" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:Bb3 ]",
|
||||
"[ 1/8 → 1/4 | note:C4 ]",
|
||||
"[ 1/4 → 3/8 | note:G3 ]",
|
||||
"[ 3/8 → 1/2 | note:A3 ]",
|
||||
"[ 1/2 → 5/8 | note:E3 ]",
|
||||
"[ 5/8 → 3/4 | note:F3 ]",
|
||||
"[ 3/4 → 7/8 | note:C3 ]",
|
||||
"[ 7/8 → 1/1 | note:D3 ]",
|
||||
"[ 1/1 → 9/8 | note:Bb3 ]",
|
||||
"[ 9/8 → 5/4 | note:C4 ]",
|
||||
"[ 5/4 → 11/8 | note:G3 ]",
|
||||
"[ 11/8 → 3/2 | note:A3 ]",
|
||||
"[ 3/2 → 13/8 | note:E3 ]",
|
||||
"[ 13/8 → 7/4 | note:F3 ]",
|
||||
"[ 7/4 → 15/8 | note:C3 ]",
|
||||
"[ 15/8 → 2/1 | note:D3 ]",
|
||||
"[ 2/1 → 17/8 | note:Bb3 ]",
|
||||
"[ 17/8 → 9/4 | note:C4 ]",
|
||||
"[ 9/4 → 19/8 | note:G3 ]",
|
||||
"[ 19/8 → 5/2 | note:A3 ]",
|
||||
"[ 5/2 → 21/8 | note:E3 ]",
|
||||
"[ 21/8 → 11/4 | note:F3 ]",
|
||||
"[ 11/4 → 23/8 | note:C3 ]",
|
||||
"[ 23/8 → 3/1 | note:D3 ]",
|
||||
"[ 3/1 → 25/8 | note:Bb3 ]",
|
||||
"[ 25/8 → 13/4 | note:C4 ]",
|
||||
"[ 13/4 → 27/8 | note:G3 ]",
|
||||
"[ 27/8 → 7/2 | note:A3 ]",
|
||||
"[ 7/2 → 29/8 | note:E3 ]",
|
||||
"[ 29/8 → 15/4 | note:F3 ]",
|
||||
"[ 15/4 → 31/8 | note:C3 ]",
|
||||
"[ 31/8 → 4/1 | note:D3 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "bite" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 0/1 → 1/8 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 1/16 → 1/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 1/8 → 1/4 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 1/8 → 1/4 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 3/16 → 1/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 1/4 → 5/16 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 9/32 → 5/16 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 5/16 → 11/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 5/16 → 3/8 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 3/8 → 7/16 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 3/8 → 1/2 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 13/32 → 7/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 7/16 → 1/2 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 1/2 → 5/8 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 1/2 → 5/8 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 9/16 → 5/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 5/8 → 3/4 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 5/8 → 3/4 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 11/16 → 3/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 3/4 → 25/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 3/4 → 13/16 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 13/16 → 7/8 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 27/32 → 7/8 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 7/8 → 15/16 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 7/8 → 1/1 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 29/32 → 15/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 15/16 → 1/1 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 1/1 → 9/8 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 1/1 → 9/8 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 17/16 → 9/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 9/8 → 5/4 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 9/8 → 5/4 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 19/16 → 5/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 5/4 → 21/16 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 41/32 → 21/16 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 21/16 → 43/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 21/16 → 11/8 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 11/8 → 23/16 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 11/8 → 3/2 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 3/2 → 13/8 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 3/2 → 13/8 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 25/16 → 13/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 13/8 → 7/4 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 13/8 → 7/4 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 27/16 → 7/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 7/4 → 57/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 7/4 → 29/16 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 29/16 → 15/8 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 59/32 → 15/8 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 15/8 → 31/16 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 15/8 → 2/1 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 61/32 → 31/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 31/16 → 2/1 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 2/1 → 17/8 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 2/1 → 17/8 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 33/16 → 17/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 17/8 → 9/4 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 17/8 → 9/4 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 35/16 → 9/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 9/4 → 37/16 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 73/32 → 37/16 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 37/16 → 75/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 37/16 → 19/8 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 19/8 → 39/16 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 19/8 → 5/2 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 77/32 → 39/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 39/16 → 5/2 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 5/2 → 21/8 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 5/2 → 21/8 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 41/16 → 21/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 21/8 → 11/4 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 21/8 → 11/4 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 43/16 → 11/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 11/4 → 89/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 11/4 → 45/16 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 45/16 → 23/8 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 91/32 → 23/8 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 23/8 → 47/16 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 23/8 → 3/1 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 93/32 → 47/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 47/16 → 3/1 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 3/1 → 25/8 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 3/1 → 25/8 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 49/16 → 25/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 25/8 → 13/4 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 25/8 → 13/4 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 51/16 → 13/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 13/4 → 53/16 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 105/32 → 53/16 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 53/16 → 107/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 53/16 → 27/8 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 27/8 → 55/16 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 27/8 → 7/2 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 7/2 → 29/8 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 7/2 → 29/8 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 57/16 → 29/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 29/8 → 15/4 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 29/8 → 15/4 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 59/16 → 15/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 15/4 → 121/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 15/4 → 61/16 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 61/16 → 31/8 | s:hh bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 123/32 → 31/8 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 31/8 → 63/16 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 31/8 → 4/1 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 125/32 → 63/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
|
||||
"[ 63/16 → 4/1 | s:bd bank:RolandTR909 speed:1.2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "bpattack" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
|
||||
@@ -1229,6 +1451,16 @@ exports[`runs examples > example "brandBy" example index 0 1`] = `
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "cat" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:e5 ]",
|
||||
"[ 1/1 → 2/1 | note:b4 ]",
|
||||
"[ 2/1 → 5/2 | note:d5 ]",
|
||||
"[ 5/2 → 3/1 | note:c5 ]",
|
||||
"[ 3/1 → 4/1 | note:e5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "cat" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:hh ]",
|
||||
"[ 1/4 → 1/2 | s:hh ]",
|
||||
@@ -1251,16 +1483,6 @@ exports[`runs examples > example "cat" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "cat" example index 0 2`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:e5 ]",
|
||||
"[ 1/1 → 2/1 | note:b4 ]",
|
||||
"[ 2/1 → 5/2 | note:d5 ]",
|
||||
"[ 5/2 → 3/1 | note:c5 ]",
|
||||
"[ 3/1 → 4/1 | note:e5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "ceil" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:42 ]",
|
||||
@@ -2666,6 +2888,43 @@ exports[`runs examples > example "fastGap" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "filter" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:hh ]",
|
||||
"[ 1/8 → 1/4 | s:hh ]",
|
||||
"[ 1/4 → 3/8 | s:hh ]",
|
||||
"[ 3/8 → 1/2 | s:hh ]",
|
||||
"[ 1/2 → 5/8 | s:hh ]",
|
||||
"[ 5/8 → 3/4 | s:hh ]",
|
||||
"[ 3/4 → 7/8 | s:hh ]",
|
||||
"[ 7/8 → 1/1 | s:oh ]",
|
||||
"[ 1/1 → 9/8 | s:hh ]",
|
||||
"[ 9/8 → 5/4 | s:hh ]",
|
||||
"[ 5/4 → 11/8 | s:hh ]",
|
||||
"[ 11/8 → 3/2 | s:hh ]",
|
||||
"[ 3/2 → 13/8 | s:hh ]",
|
||||
"[ 13/8 → 7/4 | s:hh ]",
|
||||
"[ 7/4 → 15/8 | s:hh ]",
|
||||
"[ 15/8 → 2/1 | s:oh ]",
|
||||
"[ 2/1 → 17/8 | s:hh ]",
|
||||
"[ 17/8 → 9/4 | s:hh ]",
|
||||
"[ 9/4 → 19/8 | s:hh ]",
|
||||
"[ 19/8 → 5/2 | s:hh ]",
|
||||
"[ 5/2 → 21/8 | s:hh ]",
|
||||
"[ 21/8 → 11/4 | s:hh ]",
|
||||
"[ 11/4 → 23/8 | s:hh ]",
|
||||
"[ 23/8 → 3/1 | s:oh ]",
|
||||
"[ 3/1 → 25/8 | s:hh ]",
|
||||
"[ 25/8 → 13/4 | s:hh ]",
|
||||
"[ 13/4 → 27/8 | s:hh ]",
|
||||
"[ 27/8 → 7/2 | s:hh ]",
|
||||
"[ 7/2 → 29/8 | s:hh ]",
|
||||
"[ 29/8 → 15/4 | s:hh ]",
|
||||
"[ 15/4 → 31/8 | s:hh ]",
|
||||
"[ 31/8 → 4/1 | s:oh ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "firstOf" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:g3 ]",
|
||||
@@ -3878,6 +4137,8 @@ exports[`runs examples > example "juxBy" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "keyDown" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "lastOf" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c3 ]",
|
||||
@@ -4235,6 +4496,27 @@ exports[`runs examples > example "lpf" example index 1 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lplfo" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 1/4 → 1/2 | note:e2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 1/2 → 3/4 | note:f2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 3/4 → 1/1 | note:g2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 1/1 → 5/4 | note:c2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 5/4 → 3/2 | note:e2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 3/2 → 7/4 | note:f2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 7/4 → 2/1 | note:g2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 2/1 → 9/4 | note:c2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 9/4 → 5/2 | note:e2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 5/2 → 11/4 | note:f2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 11/4 → 3/1 | note:g2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 3/1 → 13/4 | note:c2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 13/4 → 7/2 | note:e2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 7/2 → 15/4 | note:f2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 15/4 → 4/1 | note:g2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lpq" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:hh cutoff:2000 resonance:0 ]",
|
||||
@@ -4364,6 +4646,27 @@ exports[`runs examples > example "lsize" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "markcss" 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 "mask" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c ]",
|
||||
@@ -4937,149 +5240,149 @@ exports[`runs examples > example "perlin" example index 0 1`] = `
|
||||
|
||||
exports[`runs examples > example "phaser" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaser:1 ]",
|
||||
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaser:1 ]",
|
||||
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaser:1 ]",
|
||||
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaser:1 ]",
|
||||
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaser:1 ]",
|
||||
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaser:1 ]",
|
||||
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaser:1 ]",
|
||||
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaser:1 ]",
|
||||
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaser:2 ]",
|
||||
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaser:2 ]",
|
||||
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaser:2 ]",
|
||||
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaser:2 ]",
|
||||
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaser:2 ]",
|
||||
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaser:2 ]",
|
||||
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaser:2 ]",
|
||||
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaser:2 ]",
|
||||
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaser:4 ]",
|
||||
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaser:4 ]",
|
||||
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaser:4 ]",
|
||||
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaser:4 ]",
|
||||
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaser:4 ]",
|
||||
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaser:4 ]",
|
||||
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaser:4 ]",
|
||||
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaser:4 ]",
|
||||
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaser:8 ]",
|
||||
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaser:8 ]",
|
||||
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaser:8 ]",
|
||||
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaser:8 ]",
|
||||
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaser:8 ]",
|
||||
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaser:8 ]",
|
||||
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaser:8 ]",
|
||||
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaser:8 ]",
|
||||
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaserrate:1 ]",
|
||||
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaserrate:1 ]",
|
||||
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaserrate:1 ]",
|
||||
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaserrate:1 ]",
|
||||
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaserrate:1 ]",
|
||||
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaserrate:1 ]",
|
||||
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaserrate:1 ]",
|
||||
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaserrate:1 ]",
|
||||
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 ]",
|
||||
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 ]",
|
||||
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 ]",
|
||||
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 ]",
|
||||
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 ]",
|
||||
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 ]",
|
||||
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 ]",
|
||||
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 ]",
|
||||
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaserrate:4 ]",
|
||||
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaserrate:4 ]",
|
||||
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaserrate:4 ]",
|
||||
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaserrate:4 ]",
|
||||
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaserrate:4 ]",
|
||||
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaserrate:4 ]",
|
||||
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaserrate:4 ]",
|
||||
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaserrate:4 ]",
|
||||
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaserrate:8 ]",
|
||||
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaserrate:8 ]",
|
||||
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaserrate:8 ]",
|
||||
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaserrate:8 ]",
|
||||
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaserrate:8 ]",
|
||||
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaserrate:8 ]",
|
||||
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaserrate:8 ]",
|
||||
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaserrate:8 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "phasercenter" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
|
||||
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
|
||||
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
|
||||
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
|
||||
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
|
||||
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
|
||||
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
|
||||
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
|
||||
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
|
||||
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
|
||||
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
|
||||
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
|
||||
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
|
||||
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
|
||||
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
|
||||
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
|
||||
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
|
||||
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
|
||||
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
|
||||
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
|
||||
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
|
||||
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
|
||||
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
|
||||
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
|
||||
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
|
||||
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
|
||||
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
|
||||
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
|
||||
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
|
||||
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
|
||||
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
|
||||
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
|
||||
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
|
||||
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "phaserdepth" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
|
||||
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
|
||||
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
|
||||
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
|
||||
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
|
||||
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
|
||||
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
|
||||
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
|
||||
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
|
||||
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
|
||||
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
|
||||
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
|
||||
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
|
||||
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
|
||||
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
|
||||
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
|
||||
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
|
||||
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
|
||||
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
|
||||
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
|
||||
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
|
||||
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
|
||||
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
|
||||
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
|
||||
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
|
||||
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
|
||||
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
|
||||
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
|
||||
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
|
||||
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
|
||||
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
|
||||
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
|
||||
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
|
||||
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
|
||||
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
|
||||
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
|
||||
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
|
||||
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
|
||||
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
|
||||
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
|
||||
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
|
||||
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
|
||||
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
|
||||
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
|
||||
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
|
||||
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
|
||||
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
|
||||
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
|
||||
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
|
||||
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
|
||||
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
|
||||
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
|
||||
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
|
||||
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
|
||||
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
|
||||
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
|
||||
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
|
||||
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
|
||||
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
|
||||
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
|
||||
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
|
||||
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
|
||||
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
|
||||
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "phasersweep" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
|
||||
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
|
||||
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
|
||||
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
|
||||
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
|
||||
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
|
||||
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
|
||||
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
|
||||
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
|
||||
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
|
||||
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
|
||||
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
|
||||
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
|
||||
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
|
||||
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
|
||||
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
|
||||
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
|
||||
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
|
||||
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
|
||||
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
|
||||
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
|
||||
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
|
||||
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
|
||||
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
|
||||
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
|
||||
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
|
||||
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
|
||||
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
|
||||
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
|
||||
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
|
||||
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
|
||||
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
|
||||
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
|
||||
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -6637,6 +6940,56 @@ exports[`runs examples > example "scope" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "scramble
|
||||
Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`,
|
||||
but parts might be played more than once, or not at all, per cycle." example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:e s:piano ]",
|
||||
"[ 1/4 → 1/2 | note:d s:piano ]",
|
||||
"[ 1/2 → 3/4 | note:c s:piano ]",
|
||||
"[ 3/4 → 1/1 | note:d s:piano ]",
|
||||
"[ 1/1 → 5/4 | note:e s:piano ]",
|
||||
"[ 5/4 → 3/2 | note:c s:piano ]",
|
||||
"[ 3/2 → 7/4 | note:d s:piano ]",
|
||||
"[ 7/4 → 2/1 | note:e s:piano ]",
|
||||
"[ 2/1 → 9/4 | note:f s:piano ]",
|
||||
"[ 9/4 → 5/2 | note:f s:piano ]",
|
||||
"[ 5/2 → 11/4 | note:c s:piano ]",
|
||||
"[ 11/4 → 3/1 | note:c s:piano ]",
|
||||
"[ 3/1 → 13/4 | note:d s:piano ]",
|
||||
"[ 13/4 → 7/2 | note:d s:piano ]",
|
||||
"[ 7/2 → 15/4 | note:f s:piano ]",
|
||||
"[ 15/4 → 4/1 | note:e s:piano ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "scramble
|
||||
Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`,
|
||||
but parts might be played more than once, or not at all, per cycle." example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:e s:piano ]",
|
||||
"[ 1/8 → 1/4 | note:d s:piano ]",
|
||||
"[ 1/4 → 3/8 | note:c s:piano ]",
|
||||
"[ 3/8 → 1/2 | note:d s:piano ]",
|
||||
"[ 1/2 → 1/1 | note:g s:piano ]",
|
||||
"[ 1/1 → 9/8 | note:e s:piano ]",
|
||||
"[ 9/8 → 5/4 | note:c s:piano ]",
|
||||
"[ 5/4 → 11/8 | note:d s:piano ]",
|
||||
"[ 11/8 → 3/2 | note:e s:piano ]",
|
||||
"[ 3/2 → 2/1 | note:g s:piano ]",
|
||||
"[ 2/1 → 17/8 | note:f s:piano ]",
|
||||
"[ 17/8 → 9/4 | note:f s:piano ]",
|
||||
"[ 9/4 → 19/8 | note:c s:piano ]",
|
||||
"[ 19/8 → 5/2 | note:c s:piano ]",
|
||||
"[ 5/2 → 3/1 | note:g s:piano ]",
|
||||
"[ 3/1 → 25/8 | note:d s:piano ]",
|
||||
"[ 25/8 → 13/4 | note:d s:piano ]",
|
||||
"[ 13/4 → 27/8 | note:f s:piano ]",
|
||||
"[ 27/8 → 7/2 | note:e s:piano ]",
|
||||
"[ 7/2 → 4/1 | note:g s:piano ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "segment" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/24 | note:40.25 ]",
|
||||
@@ -6739,6 +7092,27 @@ exports[`runs examples > example "segment" example index 0 1`] = `
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "seq" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | note:e5 ]",
|
||||
"[ 1/3 → 2/3 | note:b4 ]",
|
||||
"[ 2/3 → 5/6 | note:d5 ]",
|
||||
"[ 5/6 → 1/1 | note:c5 ]",
|
||||
"[ 1/1 → 4/3 | note:e5 ]",
|
||||
"[ 4/3 → 5/3 | note:b4 ]",
|
||||
"[ 5/3 → 11/6 | note:d5 ]",
|
||||
"[ 11/6 → 2/1 | note:c5 ]",
|
||||
"[ 2/1 → 7/3 | note:e5 ]",
|
||||
"[ 7/3 → 8/3 | note:b4 ]",
|
||||
"[ 8/3 → 17/6 | note:d5 ]",
|
||||
"[ 17/6 → 3/1 | note:c5 ]",
|
||||
"[ 3/1 → 10/3 | note:e5 ]",
|
||||
"[ 10/3 → 11/3 | note:b4 ]",
|
||||
"[ 11/3 → 23/6 | note:d5 ]",
|
||||
"[ 23/6 → 4/1 | note:c5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "seq" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:hh ]",
|
||||
"[ 1/8 → 1/4 | s:hh ]",
|
||||
@@ -6779,24 +7153,23 @@ exports[`runs examples > example "seq" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "seq" example index 0 2`] = `
|
||||
exports[`runs examples > example "seqPLoop" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | note:e5 ]",
|
||||
"[ 1/3 → 2/3 | note:b4 ]",
|
||||
"[ 2/3 → 5/6 | note:d5 ]",
|
||||
"[ 5/6 → 1/1 | note:c5 ]",
|
||||
"[ 1/1 → 4/3 | note:e5 ]",
|
||||
"[ 4/3 → 5/3 | note:b4 ]",
|
||||
"[ 5/3 → 11/6 | note:d5 ]",
|
||||
"[ 11/6 → 2/1 | note:c5 ]",
|
||||
"[ 2/1 → 7/3 | note:e5 ]",
|
||||
"[ 7/3 → 8/3 | note:b4 ]",
|
||||
"[ 8/3 → 17/6 | note:d5 ]",
|
||||
"[ 17/6 → 3/1 | note:c5 ]",
|
||||
"[ 3/1 → 10/3 | note:e5 ]",
|
||||
"[ 10/3 → 11/3 | note:b4 ]",
|
||||
"[ 11/3 → 23/6 | note:d5 ]",
|
||||
"[ 23/6 → 4/1 | note:c5 ]",
|
||||
"[ 0/1 → 1/8 | s:bd ]",
|
||||
"[ 3/8 → 1/2 | s:bd ]",
|
||||
"[ 3/4 → 7/8 | s:bd ]",
|
||||
"[ 1/1 → 9/8 | s:bd ]",
|
||||
"[ 1/1 → 9/8 | s:cp ]",
|
||||
"[ 11/8 → 3/2 | s:bd ]",
|
||||
"[ 11/8 → 3/2 | s:cp ]",
|
||||
"[ 7/4 → 15/8 | s:bd ]",
|
||||
"[ 7/4 → 15/8 | s:cp ]",
|
||||
"[ 2/1 → 17/8 | s:cp ]",
|
||||
"[ 19/8 → 5/2 | s:cp ]",
|
||||
"[ 11/4 → 23/8 | s:cp ]",
|
||||
"[ 3/1 → 25/8 | s:bd ]",
|
||||
"[ 27/8 → 7/2 | s:bd ]",
|
||||
"[ 15/4 → 31/8 | s:bd ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -6853,6 +7226,56 @@ exports[`runs examples > example "shape" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "shuffle
|
||||
Slices a pattern into the given number of parts, then plays those parts in random order.
|
||||
Each part will be played exactly once per cycle." example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c s:piano ]",
|
||||
"[ 1/4 → 1/2 | note:d s:piano ]",
|
||||
"[ 1/2 → 3/4 | note:e s:piano ]",
|
||||
"[ 3/4 → 1/1 | note:f s:piano ]",
|
||||
"[ 1/1 → 5/4 | note:c s:piano ]",
|
||||
"[ 5/4 → 3/2 | note:d s:piano ]",
|
||||
"[ 3/2 → 7/4 | note:e s:piano ]",
|
||||
"[ 7/4 → 2/1 | note:f s:piano ]",
|
||||
"[ 2/1 → 9/4 | note:c s:piano ]",
|
||||
"[ 9/4 → 5/2 | note:d s:piano ]",
|
||||
"[ 5/2 → 11/4 | note:e s:piano ]",
|
||||
"[ 11/4 → 3/1 | note:f s:piano ]",
|
||||
"[ 3/1 → 13/4 | note:c s:piano ]",
|
||||
"[ 13/4 → 7/2 | note:d s:piano ]",
|
||||
"[ 7/2 → 15/4 | note:e s:piano ]",
|
||||
"[ 15/4 → 4/1 | note:f s:piano ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "shuffle
|
||||
Slices a pattern into the given number of parts, then plays those parts in random order.
|
||||
Each part will be played exactly once per cycle." example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:c s:piano ]",
|
||||
"[ 1/8 → 1/4 | note:d s:piano ]",
|
||||
"[ 1/4 → 3/8 | note:e s:piano ]",
|
||||
"[ 3/8 → 1/2 | note:f s:piano ]",
|
||||
"[ 1/2 → 1/1 | note:g s:piano ]",
|
||||
"[ 1/1 → 9/8 | note:c s:piano ]",
|
||||
"[ 9/8 → 5/4 | note:d s:piano ]",
|
||||
"[ 5/4 → 11/8 | note:e s:piano ]",
|
||||
"[ 11/8 → 3/2 | note:f s:piano ]",
|
||||
"[ 3/2 → 2/1 | note:g s:piano ]",
|
||||
"[ 2/1 → 17/8 | note:c s:piano ]",
|
||||
"[ 17/8 → 9/4 | note:d s:piano ]",
|
||||
"[ 9/4 → 19/8 | note:e s:piano ]",
|
||||
"[ 19/8 → 5/2 | note:f s:piano ]",
|
||||
"[ 5/2 → 3/1 | note:g s:piano ]",
|
||||
"[ 3/1 → 25/8 | note:c s:piano ]",
|
||||
"[ 25/8 → 13/4 | note:d s:piano ]",
|
||||
"[ 13/4 → 27/8 | note:e s:piano ]",
|
||||
"[ 27/8 → 7/2 | note:f s:piano ]",
|
||||
"[ 7/2 → 4/1 | note:g s:piano ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "silence" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "sine" example index 0 1`] = `
|
||||
@@ -7180,6 +7603,75 @@ exports[`runs examples > example "sometimesBy" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "spectrum" example index 0 1`] = `
|
||||
[
|
||||
"[ -5/24 ⇜ (0/1 → 1/8) | note:F4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ -2/15 ⇜ (0/1 → 1/5) | note:A4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ -1/120 ⇜ (0/1 → 1/5) ⇝ 13/40 | note:A5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 0/1 → 1/3 | note:D3 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 1/8 → 11/24 | note:D4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ -1/120 ⇜ (1/5 → 13/40) | note:A5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 1/5 → 8/15 | note:G4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 13/40 → 79/120 | note:G5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 1/3 → 2/3 | note:C4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 11/24 → 19/24 | note:C5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 8/15 → 13/15 | note:F5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 79/120 → 119/120 | note:F6 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 2/3 → 1/1 | note:G3 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ (19/24 → 1/1) ⇝ 9/8 | note:G4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ (13/15 → 1/1) ⇝ 6/5 | note:C5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ (119/120 → 1/1) ⇝ 53/40 | note:C6 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 19/24 ⇜ (1/1 → 9/8) | note:G4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 13/15 ⇜ (1/1 → 6/5) | note:C5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 119/120 ⇜ (1/1 → 6/5) ⇝ 53/40 | note:C6 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 1/1 → 4/3 | note:F3 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 9/8 → 35/24 | note:F4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 119/120 ⇜ (6/5 → 53/40) | note:C6 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 6/5 → 23/15 | note:A4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 53/40 → 199/120 | note:A5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 4/3 → 5/3 | note:D3 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 35/24 → 43/24 | note:D4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 23/15 → 28/15 | note:G4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 199/120 → 239/120 | note:G5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 5/3 → 2/1 | note:C4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ (43/24 → 2/1) ⇝ 17/8 | note:C5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ (28/15 → 2/1) ⇝ 11/5 | note:F5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ (239/120 → 2/1) ⇝ 93/40 | note:F6 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 43/24 ⇜ (2/1 → 17/8) | note:C5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 28/15 ⇜ (2/1 → 11/5) | note:F5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 239/120 ⇜ (2/1 → 11/5) ⇝ 93/40 | note:F6 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 2/1 → 7/3 | note:A3 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 17/8 → 59/24 | note:A4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 239/120 ⇜ (11/5 → 93/40) | note:F6 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 11/5 → 38/15 | note:D5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 93/40 → 319/120 | note:D6 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 7/3 → 8/3 | note:F3 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 59/24 → 67/24 | note:F4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 38/15 → 43/15 | note:A4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 319/120 → 359/120 | note:A5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 8/3 → 3/1 | note:D3 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ (67/24 → 3/1) ⇝ 25/8 | note:D4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ (43/15 → 3/1) ⇝ 16/5 | note:G4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ (359/120 → 3/1) ⇝ 133/40 | note:G5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 67/24 ⇜ (3/1 → 25/8) | note:D4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 43/15 ⇜ (3/1 → 16/5) | note:G4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 359/120 ⇜ (3/1 → 16/5) ⇝ 133/40 | note:G5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 3/1 → 10/3 | note:C4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 25/8 → 83/24 | note:C5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 359/120 ⇜ (16/5 → 133/40) | note:G5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 16/5 → 53/15 | note:F5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 133/40 → 439/120 | note:F6 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 10/3 → 11/3 | note:G3 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 83/24 → 91/24 | note:G4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 53/15 → 58/15 | note:C5 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 439/120 → 479/120 | note:C6 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ 11/3 → 4/1 | note:F3 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ (91/24 → 4/1) ⇝ 33/8 | note:F4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ (58/15 → 4/1) ⇝ 21/5 | note:A4 s:sine decay:0.3 room:0.5 ]",
|
||||
"[ (479/120 → 4/1) ⇝ 173/40 | note:A5 s:sine decay:0.3 room:0.5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "speed" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | s:bd speed:1 ]",
|
||||
@@ -7423,6 +7915,27 @@ exports[`runs examples > example "squiz" example index 0 1`] = `
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "stack" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | note:e4 ]",
|
||||
"[ 0/1 → 1/1 | note:g3 ]",
|
||||
"[ 0/1 → 1/1 | note:b3 ]",
|
||||
"[ 1/2 → 1/1 | note:d4 ]",
|
||||
"[ 1/1 → 3/2 | note:e4 ]",
|
||||
"[ 1/1 → 2/1 | note:g3 ]",
|
||||
"[ 1/1 → 2/1 | note:b3 ]",
|
||||
"[ 3/2 → 2/1 | note:d4 ]",
|
||||
"[ 2/1 → 5/2 | note:e4 ]",
|
||||
"[ 2/1 → 3/1 | note:g3 ]",
|
||||
"[ 2/1 → 3/1 | note:b3 ]",
|
||||
"[ 5/2 → 3/1 | note:d4 ]",
|
||||
"[ 3/1 → 7/2 | note:e4 ]",
|
||||
"[ 3/1 → 4/1 | note:g3 ]",
|
||||
"[ 3/1 → 4/1 | note:b3 ]",
|
||||
"[ 7/2 → 4/1 | note:d4 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "stack" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:c4 ]",
|
||||
"[ 0/1 → 1/4 | s:hh ]",
|
||||
@@ -7463,27 +7976,6 @@ exports[`runs examples > example "stack" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "stack" example index 0 2`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | note:e4 ]",
|
||||
"[ 0/1 → 1/1 | note:g3 ]",
|
||||
"[ 0/1 → 1/1 | note:b3 ]",
|
||||
"[ 1/2 → 1/1 | note:d4 ]",
|
||||
"[ 1/1 → 3/2 | note:e4 ]",
|
||||
"[ 1/1 → 2/1 | note:g3 ]",
|
||||
"[ 1/1 → 2/1 | note:b3 ]",
|
||||
"[ 3/2 → 2/1 | note:d4 ]",
|
||||
"[ 2/1 → 5/2 | note:e4 ]",
|
||||
"[ 2/1 → 3/1 | note:g3 ]",
|
||||
"[ 2/1 → 3/1 | note:b3 ]",
|
||||
"[ 5/2 → 3/1 | note:d4 ]",
|
||||
"[ 3/1 → 7/2 | note:e4 ]",
|
||||
"[ 3/1 → 4/1 | note:g3 ]",
|
||||
"[ 3/1 → 4/1 | note:b3 ]",
|
||||
"[ 7/2 → 4/1 | note:d4 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "steps" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:bd ]",
|
||||
@@ -8435,6 +8927,8 @@ exports[`runs examples > example "when" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "whenKey" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "withValue" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | 10 ]",
|
||||
|
||||
@@ -8,6 +8,7 @@ exports[`renders tunes > tune: amensister 1`] = `
|
||||
"[ 0/1 → 1/4 | n:0 s:amencutup room:0.5 ]",
|
||||
"[ 1/16 → 1/8 | s:breath room:1 shape:0.6 begin:0.875 end:0.9375 ]",
|
||||
"[ 1/8 → 3/16 | s:breath room:1 shape:0.6 begin:0.8125 end:0.875 ]",
|
||||
"[ 1/8 → 1/4 | n:0 s:amencutup room:0.5 ]",
|
||||
"[ 1/8 → 1/4 | note:45 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.174310575404 ]",
|
||||
"[ 1/8 → 1/4 | note:45 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.174310575404 ]",
|
||||
"[ 3/16 → 1/4 | s:breath room:1 shape:0.6 begin:0.75 end:0.8125 ]",
|
||||
@@ -17,22 +18,20 @@ exports[`renders tunes > tune: amensister 1`] = `
|
||||
"[ 1/4 → 3/8 | note:A1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.7878869297153 ]",
|
||||
"[ 5/16 → 3/8 | s:breath room:1 shape:0.6 begin:0.625 end:0.6875 ]",
|
||||
"[ 3/8 → 7/16 | s:breath room:1 shape:0.6 begin:0.5625 end:0.625 ]",
|
||||
"[ 3/8 → 1/2 | n:1 s:amencutup room:0.5 ]",
|
||||
"[ 3/8 → 1/2 | note:F1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:302.11020572391345 ]",
|
||||
"[ 3/8 → 1/2 | note:F1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:302.11020572391345 ]",
|
||||
"[ 7/16 → 1/2 | s:breath room:1 shape:0.6 begin:0.5 end:0.5625 ]",
|
||||
"[ 1/2 → 9/16 | s:breath room:1 shape:0.6 begin:0.4375 end:0.5 ]",
|
||||
"[ 1/2 → 5/8 | n:2 s:amencutup room:0.5 ]",
|
||||
"[ 1/2 → 3/4 | n:2 s:amencutup room:0.5 ]",
|
||||
"[ 9/16 → 5/8 | s:breath room:1 shape:0.6 begin:0.375 end:0.4375 ]",
|
||||
"[ 5/8 → 11/16 | s:breath room:1 shape:0.6 begin:0.3125 end:0.375 ]",
|
||||
"[ 11/16 → 3/4 | s:breath room:1 shape:0.6 begin:0.25 end:0.3125 ]",
|
||||
"[ 3/4 → 13/16 | s:breath room:1 shape:0.6 begin:0.1875 end:0.25 ]",
|
||||
"[ 3/4 → 7/8 | n:3 s:amencutup room:0.5 ]",
|
||||
"[ 3/4 → 7/8 | note:Bb0 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:312.54769231985796 ]",
|
||||
"[ 3/4 → 7/8 | note:Bb0 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:312.54769231985796 ]",
|
||||
"[ 13/16 → 7/8 | s:breath room:1 shape:0.6 begin:0.125 end:0.1875 ]",
|
||||
"[ 7/8 → 15/16 | s:breath room:1 shape:0.6 begin:0.0625 end:0.125 ]",
|
||||
"[ 7/8 → 1/1 | n:3 s:amencutup room:0.5 ]",
|
||||
"[ 7/8 → 1/1 | note:D1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:318.7927796831686 ]",
|
||||
"[ 7/8 → 1/1 | note:D1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:318.7927796831686 ]",
|
||||
"[ 15/16 → 1/1 | s:breath room:1 shape:0.6 begin:0 end:0.0625 ]",
|
||||
@@ -6906,10 +6905,10 @@ exports[`renders tunes > tune: flatrave 1`] = `
|
||||
"[ 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 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
|
||||
"[ 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 ]",
|
||||
"[ 1/4 → 3/8 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
|
||||
"[ 3/8 → 1/2 | s:hh n:1 end:0.020023446730265706 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/2 → 5/8 | s:hh n:1 speed:0.5 delay:0.5 end:0.020048626493108724 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/2 → 5/8 | s:hh n:1 speed:0.5 delay:0.5 end:0.020048626493108724 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/2 → 5/8 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
|
||||
"[ 1/2 → 1/1 | s:bd bank:RolandTR909 ]",
|
||||
"[ 1/2 → 1/1 | s:cp bank:RolandTR909 ]",
|
||||
@@ -6917,7 +6916,6 @@ exports[`renders tunes > tune: flatrave 1`] = `
|
||||
"[ 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 ]",
|
||||
"[ 5/8 → 3/4 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
|
||||
"[ 3/4 → 7/8 | s:hh n:1 end:0.02013941880355398 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 7/8 → 1/1 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -134,6 +134,12 @@ strudel.Pattern.prototype._pitchwheel = function () {
|
||||
strudel.Pattern.prototype._pianoroll = function () {
|
||||
return this;
|
||||
};
|
||||
strudel.Pattern.prototype._spectrum = function () {
|
||||
return this;
|
||||
};
|
||||
strudel.Pattern.prototype.markcss = function () {
|
||||
return this;
|
||||
};
|
||||
|
||||
const uiHelpersMocked = {
|
||||
backgroundImage: id,
|
||||
|
||||
@@ -3,11 +3,6 @@ import { configDefaults, defineConfig } from 'vitest/config';
|
||||
/// <reference types="vitest" />
|
||||
export default defineConfig({
|
||||
test: {
|
||||
// ...
|
||||
deps: {
|
||||
registerNodeLoader: true,
|
||||
},
|
||||
threads: false,
|
||||
reporters: 'verbose',
|
||||
isolate: false,
|
||||
silent: true,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"devToolbar": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
Vendored
-290
@@ -1,290 +0,0 @@
|
||||
declare module 'astro:content' {
|
||||
interface Render {
|
||||
'.mdx': Promise<{
|
||||
Content: import('astro').MarkdownInstance<{}>['Content'];
|
||||
headings: import('astro').MarkdownHeading[];
|
||||
remarkPluginFrontmatter: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'astro:content' {
|
||||
interface Render {
|
||||
'.md': Promise<{
|
||||
Content: import('astro').MarkdownInstance<{}>['Content'];
|
||||
headings: import('astro').MarkdownHeading[];
|
||||
remarkPluginFrontmatter: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'astro:content' {
|
||||
export { z } from 'astro/zod';
|
||||
|
||||
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
|
||||
|
||||
export type CollectionKey = keyof AnyEntryMap;
|
||||
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
|
||||
|
||||
export type ContentCollectionKey = keyof ContentEntryMap;
|
||||
export type DataCollectionKey = keyof DataEntryMap;
|
||||
|
||||
// This needs to be in sync with ImageMetadata
|
||||
export type ImageFunction = () => import('astro/zod').ZodObject<{
|
||||
src: import('astro/zod').ZodString;
|
||||
width: import('astro/zod').ZodNumber;
|
||||
height: import('astro/zod').ZodNumber;
|
||||
format: import('astro/zod').ZodUnion<
|
||||
[
|
||||
import('astro/zod').ZodLiteral<'png'>,
|
||||
import('astro/zod').ZodLiteral<'jpg'>,
|
||||
import('astro/zod').ZodLiteral<'jpeg'>,
|
||||
import('astro/zod').ZodLiteral<'tiff'>,
|
||||
import('astro/zod').ZodLiteral<'webp'>,
|
||||
import('astro/zod').ZodLiteral<'gif'>,
|
||||
import('astro/zod').ZodLiteral<'svg'>,
|
||||
import('astro/zod').ZodLiteral<'avif'>,
|
||||
]
|
||||
>;
|
||||
}>;
|
||||
|
||||
type BaseSchemaWithoutEffects =
|
||||
| import('astro/zod').AnyZodObject
|
||||
| import('astro/zod').ZodUnion<[BaseSchemaWithoutEffects, ...BaseSchemaWithoutEffects[]]>
|
||||
| import('astro/zod').ZodDiscriminatedUnion<string, import('astro/zod').AnyZodObject[]>
|
||||
| import('astro/zod').ZodIntersection<BaseSchemaWithoutEffects, BaseSchemaWithoutEffects>;
|
||||
|
||||
type BaseSchema =
|
||||
| BaseSchemaWithoutEffects
|
||||
| import('astro/zod').ZodEffects<BaseSchemaWithoutEffects>;
|
||||
|
||||
export type SchemaContext = { image: ImageFunction };
|
||||
|
||||
type DataCollectionConfig<S extends BaseSchema> = {
|
||||
type: 'data';
|
||||
schema?: S | ((context: SchemaContext) => S);
|
||||
};
|
||||
|
||||
type ContentCollectionConfig<S extends BaseSchema> = {
|
||||
type?: 'content';
|
||||
schema?: S | ((context: SchemaContext) => S);
|
||||
};
|
||||
|
||||
type CollectionConfig<S> = ContentCollectionConfig<S> | DataCollectionConfig<S>;
|
||||
|
||||
export function defineCollection<S extends BaseSchema>(
|
||||
input: CollectionConfig<S>
|
||||
): CollectionConfig<S>;
|
||||
|
||||
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
|
||||
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
|
||||
ContentEntryMap[C]
|
||||
>['slug'];
|
||||
|
||||
export function getEntryBySlug<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
// Note that this has to accept a regular string too, for SSR
|
||||
entrySlug: E
|
||||
): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
|
||||
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
|
||||
collection: C,
|
||||
entryId: E
|
||||
): Promise<CollectionEntry<C>>;
|
||||
|
||||
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
|
||||
collection: C,
|
||||
filter?: (entry: CollectionEntry<C>) => entry is E
|
||||
): Promise<E[]>;
|
||||
export function getCollection<C extends keyof AnyEntryMap>(
|
||||
collection: C,
|
||||
filter?: (entry: CollectionEntry<C>) => unknown
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
|
||||
export function getEntry<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(entry: {
|
||||
collection: C;
|
||||
slug: E;
|
||||
}): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof DataEntryMap,
|
||||
E extends keyof DataEntryMap[C] | (string & {}),
|
||||
>(entry: {
|
||||
collection: C;
|
||||
id: E;
|
||||
}): E extends keyof DataEntryMap[C]
|
||||
? Promise<DataEntryMap[C][E]>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
slug: E
|
||||
): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof DataEntryMap,
|
||||
E extends keyof DataEntryMap[C] | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
id: E
|
||||
): E extends keyof DataEntryMap[C]
|
||||
? Promise<DataEntryMap[C][E]>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
|
||||
/** Resolve an array of entry references from the same collection */
|
||||
export function getEntries<C extends keyof ContentEntryMap>(
|
||||
entries: {
|
||||
collection: C;
|
||||
slug: ValidContentEntrySlug<C>;
|
||||
}[]
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
export function getEntries<C extends keyof DataEntryMap>(
|
||||
entries: {
|
||||
collection: C;
|
||||
id: keyof DataEntryMap[C];
|
||||
}[]
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
|
||||
export function reference<C extends keyof AnyEntryMap>(
|
||||
collection: C
|
||||
): import('astro/zod').ZodEffects<
|
||||
import('astro/zod').ZodString,
|
||||
C extends keyof ContentEntryMap
|
||||
? {
|
||||
collection: C;
|
||||
slug: ValidContentEntrySlug<C>;
|
||||
}
|
||||
: {
|
||||
collection: C;
|
||||
id: keyof DataEntryMap[C];
|
||||
}
|
||||
>;
|
||||
// Allow generic `string` to avoid excessive type errors in the config
|
||||
// if `dev` is not running to update as you edit.
|
||||
// Invalid collection names will be caught at build time.
|
||||
export function reference<C extends string>(
|
||||
collection: C
|
||||
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
|
||||
|
||||
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
|
||||
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
|
||||
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
|
||||
>;
|
||||
|
||||
type ContentEntryMap = {
|
||||
"blog": {
|
||||
"release-0.0.2-schwindlig.mdx": {
|
||||
id: "release-0.0.2-schwindlig.mdx";
|
||||
slug: "release-002-schwindlig";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.0.2.1-stuermisch.mdx": {
|
||||
id: "release-0.0.2.1-stuermisch.mdx";
|
||||
slug: "release-0021-stuermisch";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.0.3-maelstrom.mdx": {
|
||||
id: "release-0.0.3-maelstrom.mdx";
|
||||
slug: "release-003-maelstrom";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.0.4-gischt.mdx": {
|
||||
id: "release-0.0.4-gischt.mdx";
|
||||
slug: "release-004-gischt";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.3.0-donauwelle.mdx": {
|
||||
id: "release-0.3.0-donauwelle.mdx";
|
||||
slug: "release-030-donauwelle";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.4.0-brandung.mdx": {
|
||||
id: "release-0.4.0-brandung.mdx";
|
||||
slug: "release-040-brandung";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.5.0-wirbel.mdx": {
|
||||
id: "release-0.5.0-wirbel.mdx";
|
||||
slug: "release-050-wirbel";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.6.0-zimtschnecke.mdx": {
|
||||
id: "release-0.6.0-zimtschnecke.mdx";
|
||||
slug: "release-060-zimtschnecke";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.7.0-zuckerguss.mdx": {
|
||||
id: "release-0.7.0-zuckerguss.mdx";
|
||||
slug: "release-070-zuckerguss";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.8.0-himbeermuffin.mdx": {
|
||||
id: "release-0.8.0-himbeermuffin.mdx";
|
||||
slug: "release-080-himbeermuffin";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.9.0-bananenbrot.mdx": {
|
||||
id: "release-0.9.0-bananenbrot.mdx";
|
||||
slug: "release-090-bananenbrot";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-1.0.0-geburtstagskuchen.mdx": {
|
||||
id: "release-1.0.0-geburtstagskuchen.mdx";
|
||||
slug: "release-100-geburtstagskuchen";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"year-2.mdx": {
|
||||
id: "year-2.mdx";
|
||||
slug: "year-2";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
type DataEntryMap = {
|
||||
|
||||
};
|
||||
|
||||
type AnyEntryMap = ContentEntryMap & DataEntryMap;
|
||||
|
||||
type ContentConfig = typeof import("../src/content/config");
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
.astro
|
||||
# build output
|
||||
dist/
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@strudel/hydra": "workspace:*",
|
||||
"@strudel/midi": "workspace:*",
|
||||
"@strudel/mini": "workspace:*",
|
||||
"@strudel/mqtt": "workspace:*",
|
||||
"@strudel/osc": "workspace:*",
|
||||
"@strudel/serial": "workspace:*",
|
||||
"@strudel/soundfonts": "workspace:*",
|
||||
|
||||
@@ -26,15 +26,15 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
||||
title="Top Navigation"
|
||||
>
|
||||
<div class="flex overflow-visible items-center grow" style="overflow:visible">
|
||||
<a href={`${baseNoTrailing}/`} class="flex items-center text-2xl space-x-2">
|
||||
<div class="flex items-center text-2xl space-x-2">
|
||||
<h1 class="font-bold flex space-x-2 items-baseline text-xl">
|
||||
<span>🌀</span>
|
||||
<div class="flex space-x-1 items-baseline">
|
||||
<span class="block rotate-90 text-blue-500">꩜</span>
|
||||
<div class="flex space-x-2 items-baseline">
|
||||
<span class="">strudel</span>
|
||||
<span class="text-sm">DOCS</span>
|
||||
<span class="text-sm font-medium">DOCS</span>
|
||||
</div>
|
||||
</h1>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/* KNOWN_LANGUAGE_CODES.length > 1 && <LanguageSelect lang={lang} client:idle /> */}
|
||||
<div class="search-item h-10">
|
||||
|
||||
@@ -1,34 +1,26 @@
|
||||
import { ReplContext } from '@src/repl/util.mjs';
|
||||
|
||||
import Loader from '@src/repl/components/Loader';
|
||||
import { Panel } from '@src/repl/components/panel/Panel';
|
||||
import { HorizontalPanel } from '@src/repl/components/panel/Panel';
|
||||
import { Code } from '@src/repl/components/Code';
|
||||
import BigPlayButton from '@src/repl/components/BigPlayButton';
|
||||
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
|
||||
|
||||
// type Props = {
|
||||
// context: replcontext,
|
||||
// containerRef: React.MutableRefObject<HTMLElement | null>,
|
||||
// editorRef: React.MutableRefObject<HTMLElement | null>,
|
||||
// error: Error
|
||||
// init: () => void
|
||||
// }
|
||||
|
||||
export default function UdelsEditor(Props) {
|
||||
const { context, containerRef, editorRef, error, init } = Props;
|
||||
const { pending, started, handleTogglePlay } = context;
|
||||
const { context } = Props;
|
||||
const { containerRef, editorRef, error, init, pending, started, handleTogglePlay } = context;
|
||||
|
||||
return (
|
||||
<ReplContext.Provider value={context}>
|
||||
<div className={'h-full flex w-full flex-col relative'}>
|
||||
<Loader active={pending} />
|
||||
{/* <Header context={context} /> */}
|
||||
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
</div>
|
||||
<UserFacingErrorMessage error={error} />
|
||||
<Panel context={context} />
|
||||
<div className={'h-full flex w-full flex-col relative'}>
|
||||
<Loader active={pending} />
|
||||
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
</div>
|
||||
</ReplContext.Provider>
|
||||
<UserFacingErrorMessage error={error} />
|
||||
<HorizontalPanel context={context} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ author: froos
|
||||
- fix: 💄 Enhance visualisation of the Tutorial on mobile by @puria in https://github.com/tidalcycles/strudel/pull/19
|
||||
- Stateful queries and events (WIP) by @yaxu in https://github.com/tidalcycles/strudel/pull/14
|
||||
- Fix resolveState by @yaxu in https://github.com/tidalcycles/strudel/pull/22
|
||||
- added \_asNumber + interprete numbers as midi by @felixroos in https://github.com/tidalcycles/strudel/pull/21
|
||||
- added \_asNumber + interpret numbers as midi by @felixroos in https://github.com/tidalcycles/strudel/pull/21
|
||||
- Update package.json by @ChiakiUehira in https://github.com/tidalcycles/strudel/pull/23
|
||||
- packaging by @felixroos in https://github.com/tidalcycles/strudel/pull/24
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ author: froos
|
||||
|
||||
- support freq in pianoroll by @felixroos in https://github.com/tidalcycles/strudel/pull/308
|
||||
- ICLC2023 paper WIP by @yaxu in https://github.com/tidalcycles/strudel/pull/306
|
||||
- fix: copy share link to clipboard was broken for some browers by @felixroos in https://github.com/tidalcycles/strudel/pull/311
|
||||
- fix: copy share link to clipboard was broken for some browsers by @felixroos in https://github.com/tidalcycles/strudel/pull/311
|
||||
- Jsdoc component by @felixroos in https://github.com/tidalcycles/strudel/pull/312
|
||||
- object support for .scale by @felixroos in https://github.com/tidalcycles/strudel/pull/307
|
||||
- Astro build by @felixroos in https://github.com/tidalcycles/strudel/pull/315
|
||||
|
||||
@@ -138,7 +138,7 @@ Plenty of things have been added to the docs, including a [showcase of what peop
|
||||
- Export patterns + ui tweaks by @felixroos in https://github.com/tidalcycles/strudel/pull/855
|
||||
- Pattern organization by @felixroos in https://github.com/tidalcycles/strudel/pull/858
|
||||
- Sound Import from local file system by @daslyfe in https://github.com/tidalcycles/strudel/pull/839
|
||||
- bugfix: suspend and close exisiting audio context when changing interface by @daslyfe in https://github.com/tidalcycles/strudel/pull/882
|
||||
- bugfix: suspend and close existing audio context when changing interface by @daslyfe in https://github.com/tidalcycles/strudel/pull/882
|
||||
- add root mode for voicings by @felixroos in https://github.com/tidalcycles/strudel/pull/887
|
||||
- scales can now be anchored by @felixroos in https://github.com/tidalcycles/strudel/pull/888
|
||||
- add dough function for raw dsp by @felixroos in https://github.com/tidalcycles/strudel/pull/707 (experimental)
|
||||
@@ -195,7 +195,7 @@ Plenty of things have been added to the docs, including a [showcase of what peop
|
||||
- Update tauri.yml workflow file by @vasilymilovidov in https://github.com/tidalcycles/strudel/pull/705
|
||||
- vite-vanilla-repl readme fix by @felixroos in https://github.com/tidalcycles/strudel/pull/737
|
||||
- completely revert config mess by @felixroos in https://github.com/tidalcycles/strudel/pull/745
|
||||
- hopefully fix trainling slashes bug by @felixroos in https://github.com/tidalcycles/strudel/pull/753
|
||||
- hopefully fix trailing slashes bug by @felixroos in https://github.com/tidalcycles/strudel/pull/753
|
||||
- Update vite pwa by @felixroos in https://github.com/tidalcycles/strudel/pull/772
|
||||
- Update to Astro 3 by @felixroos in https://github.com/tidalcycles/strudel/pull/775
|
||||
- support multiple named serial connections, change default baudrate by @yaxu in https://github.com/tidalcycles/strudel/pull/551
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
||||
import { Icon } from './Icon';
|
||||
import { silence, noteToMidi, _mod } from '@strudel/core';
|
||||
import { clearHydra } from '@strudel/hydra';
|
||||
import { getDrawContext, getPunchcardPainter } from '@strudel/draw';
|
||||
import { transpiler } from '@strudel/transpiler';
|
||||
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
|
||||
@@ -37,7 +36,7 @@ export function MiniRepl({
|
||||
const shouldShowCanvas = !!punchcard;
|
||||
const canvasId = shouldShowCanvas ? useMemo(() => `canvas-${id}`, [id]) : null;
|
||||
autodraw = !!punchcard || !!claviature || !!autodraw;
|
||||
drawTime = drawTime ?? punchcard ? [0, 4] : [-2, 2];
|
||||
drawTime = (drawTime ?? punchcard) ? [0, 4] : [-2, 2];
|
||||
if (claviature) {
|
||||
drawTime = [0, 0];
|
||||
}
|
||||
@@ -120,6 +119,7 @@ export function MiniRepl({
|
||||
'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' : '',
|
||||
)}
|
||||
aria-label={started ? 'stop' : 'play'}
|
||||
onClick={() => editorRef.current?.toggle()}
|
||||
>
|
||||
<Icon type={started ? 'stop' : 'play'} />
|
||||
@@ -129,6 +129,7 @@ export function MiniRepl({
|
||||
'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',
|
||||
)}
|
||||
aria-label="update"
|
||||
onClick={() => editorRef.current?.evaluate()}
|
||||
>
|
||||
<Icon type="refresh" />
|
||||
@@ -140,6 +141,7 @@ export function MiniRepl({
|
||||
className={
|
||||
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background'
|
||||
}
|
||||
aria-label="previous example"
|
||||
onClick={() => changeTune(tuneIndex - 1)}
|
||||
>
|
||||
<div className="rotate-180">
|
||||
@@ -150,6 +152,7 @@ export function MiniRepl({
|
||||
className={
|
||||
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background'
|
||||
}
|
||||
aria-label="next example"
|
||||
onClick={() => changeTune(tuneIndex + 1)}
|
||||
>
|
||||
<Icon type="skip" />
|
||||
|
||||
@@ -82,18 +82,7 @@ To modify a parameter value, you can either:
|
||||
|
||||
<MiniRepl client:only="react" tune={`note("50 60 70").room(.1).add(note("<0 1 2>")).log()`} />
|
||||
|
||||
- Modify _all_ numeral params:
|
||||
|
||||
<MiniRepl client:only="react" tune={`note("50 60 70").room(.1).add("<0 1 2>").log()`} />
|
||||
|
||||
Which of these 3 ways to use strongly depends on the context!
|
||||
Note that the order of chaining param functions also matters!
|
||||
In the last example, the `room` value would not have changed if it was applied later:
|
||||
|
||||
<MiniRepl client:only="react" tune={`note("50 60 70").add("<0 1 2>").room(.1).log()`} />
|
||||
|
||||
This shows how the execution of the chained functions goes from left to right.
|
||||
In this case, the `.add` will only modify what's on the left side.
|
||||
Remember the execution of the chained functions goes from left to right.
|
||||
|
||||
# Operators
|
||||
|
||||
|
||||
@@ -25,26 +25,14 @@ These are the equivalents used by the Mini Notation:
|
||||
|
||||
<JsDoc client:idle name="cat" h={0} />
|
||||
|
||||
You can also use cat as a chained function like this:
|
||||
|
||||
<JsDoc client:idle name="Pattern.cat" h={0} hideDescription />
|
||||
|
||||
## seq
|
||||
|
||||
<JsDoc client:idle name="seq" h={0} />
|
||||
|
||||
Or as a chained function:
|
||||
|
||||
<JsDoc client:idle name="Pattern.seq" h={0} hideDescription />
|
||||
|
||||
## stack
|
||||
|
||||
<JsDoc client:idle name="stack" h={0} />
|
||||
|
||||
As a chained function:
|
||||
|
||||
<JsDoc client:idle name="Pattern.stack" h={0} hideDescription />
|
||||
|
||||
## s_cat
|
||||
|
||||
<JsDoc client:idle name="s_cat" h={0} />
|
||||
@@ -67,6 +55,14 @@ As a chained function:
|
||||
|
||||
## run
|
||||
|
||||
<JsDoc client:idle name="run" h={0} punchcard />
|
||||
<JsDoc client:idle name="run" h={0} />
|
||||
|
||||
## binary
|
||||
|
||||
<JsDoc client:idle name="binary" h={0} />
|
||||
|
||||
## binaryN
|
||||
|
||||
<JsDoc client:idle name="binaryN" h={0} />
|
||||
|
||||
After Pattern Constructors, let's see what [Time Modifiers](/learn/time-modifiers) are available.
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
---
|
||||
title: MIDI & OSC
|
||||
title: MIDI, OSC & MQTT
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MiniRepl } from '../../docs/MiniRepl';
|
||||
import { JsDoc } from '../../docs/JsDoc';
|
||||
|
||||
# MIDI and OSC
|
||||
# MIDI, OSC and MQTT
|
||||
|
||||
The default audio output of Strudel uses the [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API).
|
||||
It is also possible to use Strudel with MIDI and OSC / [SuperDirt](https://github.com/musikinformatik/SuperDirt/) instead.
|
||||
Normally, Strudel is used to pattern sound, using its own '[web audio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)'-based synthesiser called [SuperDough](https://github.com/tidalcycles/strudel/tree/main/packages/superdough).
|
||||
|
||||
# MIDI API
|
||||
It is also possible to pattern other things with Strudel, such as software and hardware synthesisers with MIDI, other software using Open Sound Control/OSC (including the [SuperDirt](https://github.com/musikinformatik/SuperDirt/) synthesiser commonly used with Strudel's sibling [TidalCycles](https://tidalcycles.org/)), or the MQTT 'internet of things' protocol.
|
||||
|
||||
Strudel also supports midi via [webmidi](https://npmjs.com/package/webmidi).
|
||||
# MIDI
|
||||
|
||||
Strudel supports MIDI without any additional software (thanks to [webmidi](https://npmjs.com/package/webmidi)), just by adding methods to your pattern:
|
||||
|
||||
## midi(outputName?)
|
||||
|
||||
@@ -45,20 +46,22 @@ But you can also control cc messages separately like this:
|
||||
$: ccv(sine.segment(16).slow(4)).ccn(74).midi()`}
|
||||
/>
|
||||
|
||||
# SuperDirt API
|
||||
# OSC/SuperDirt/StrudelDirt
|
||||
|
||||
In mainline tidal, the actual sound is generated via [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider.
|
||||
Strudel also supports using [SuperDirt](https://github.com/musikinformatik/SuperDirt/) as a backend, although it requires some developer tooling to run.
|
||||
In TidalCycles, sound is usually generated using [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software.
|
||||
|
||||
There is also [StrudelDirt](https://github.com/daslyfe/StrudelDirt) which is SuperDirt with some optimisations for working with Strudel. (A longer term aim is to merge these optimisations back into mainline SuperDirt)
|
||||
|
||||
## Prequisites
|
||||
|
||||
Getting [SuperDirt](https://github.com/musikinformatik/SuperDirt/) to work with Strudel, you need to
|
||||
To get SuperDirt to work with Strudel, you need to
|
||||
|
||||
1. install SuperCollider + sc3 plugins, see [Tidal Docs](https://tidalcycles.org/docs/) (Install Tidal) for more info.
|
||||
2. install [node.js](https://nodejs.org/en/)
|
||||
3. download [Strudel Repo](https://github.com/tidalcycles/strudel/) (or git clone, if you have git installed)
|
||||
4. run `pnpm i` in the strudel directory
|
||||
5. run `pnpm run osc` to start the osc server, which forwards OSC messages from Strudel REPL to SuperCollider
|
||||
2. install SuperDirt, or the [StrudelDirt](https://github.com/daslyfe/StrudelDirt) fork which is optimised for use with Strudel
|
||||
3. install [node.js](https://nodejs.org/en/)
|
||||
4. download [Strudel Repo](https://github.com/tidalcycles/strudel/) (or git clone, if you have git installed)
|
||||
5. run `pnpm i` in the strudel directory
|
||||
6. run `pnpm run osc` to start the osc server, which forwards OSC messages from Strudel REPL to SuperCollider
|
||||
|
||||
Now you're all set!
|
||||
|
||||
@@ -73,18 +76,80 @@ Now you're all set!
|
||||
|
||||
If you now hear sound, congratulations! If not, you can get help on the [#strudel channel in the TidalCycles discord](https://discord.com/invite/HGEdXmRkzT).
|
||||
|
||||
Note: if you have the 'Audio Engine Target' in settings set to 'OSC', you do not need to add .osc() to the end of your pattern.
|
||||
|
||||
### Pattern.osc
|
||||
|
||||
<JsDoc client:idle name="Pattern.osc" h={0} />
|
||||
|
||||
## SuperDirt Params
|
||||
|
||||
The following functions can be used with [SuperDirt](https://github.com/musikinformatik/SuperDirt/):
|
||||
|
||||
`s n note freq channel orbit cutoff resonance hcutoff hresonance bandf bandq djf vowel cut begin end loop fadeTime speed unitA gain amp accelerate crush coarse delay lock leslie lrate lsize pan panspan pansplay room size dry shape squiz waveloss attack decay octave detune tremolodepth`
|
||||
|
||||
Please refer to [Tidal Docs](https://tidalcycles.org/) for more info.
|
||||
|
||||
<br />
|
||||
|
||||
But can we use Strudel [offline](/learn/pwa)?
|
||||
|
||||
# MQTT
|
||||
|
||||
MQTT is a lightweight network protocol, designed for 'internet of things' devices. For use with strudel, you will
|
||||
need access to an MQTT server known as a 'broker' configured to accept secure 'websocket' connections. You could
|
||||
run one yourself (e.g. by running [mosquitto](https://mosquitto.org/)), although getting an SSL certificate that
|
||||
your web browser will trust might be a bit tricky for those without systems administration experience.
|
||||
Alternatively, you can use [a public broker](https://www.hivemq.com/mqtt/public-mqtt-broker/).
|
||||
|
||||
Strudel does not yet support receiving messages over MQTT, only sending them.
|
||||
|
||||
## Usage
|
||||
|
||||
The following example shows how to send a pattern to an MQTT broker:
|
||||
|
||||
<MiniRepl
|
||||
client:only="react"
|
||||
tune={`"hello world"
|
||||
.mqtt(undefined, // username (undefined for open/public servers)
|
||||
undefined, // password
|
||||
'/strudel-pattern', // mqtt 'topic'
|
||||
'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address
|
||||
'mystrudel', // MQTT client id - randomly generated if not supplied
|
||||
0 // latency / delay before sending messages (0 = no delay)
|
||||
)`}
|
||||
|
||||
/>
|
||||
|
||||
Other software can then receive the messages. For example using the [mosquitto](https://mosquitto.org/) commandline client tools:
|
||||
|
||||
```
|
||||
> mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/strudel-pattern"
|
||||
hello
|
||||
world
|
||||
hello
|
||||
world
|
||||
...
|
||||
```
|
||||
|
||||
Control patterns will be encoded as JSON, for example:
|
||||
|
||||
<MiniRepl
|
||||
client:only="react"
|
||||
tune={`sound("sax(3,8)").speed("2 3")
|
||||
.mqtt(undefined, // username (undefined for open/public servers)
|
||||
undefined, // password
|
||||
'/strudel-pattern', // mqtt 'topic'
|
||||
'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address
|
||||
'mystrudel', // MQTT client id - randomly generated if not supplied
|
||||
0 // latency / delay before sending messages (0 = no delay)
|
||||
)`}
|
||||
/>
|
||||
|
||||
Will send messages like the following:
|
||||
|
||||
```
|
||||
{"s":"sax","speed":2}
|
||||
{"s":"sax","speed":2}
|
||||
{"s":"sax","speed":3}
|
||||
{"s":"sax","speed":2}
|
||||
...
|
||||
```
|
||||
|
||||
Libraries for receiving MQTT are available for many programming languages.
|
||||
|
||||
@@ -98,3 +98,11 @@ What follows is the API doc of all the options you can pass:
|
||||
## Pitchwheel
|
||||
|
||||
<JsDoc client:idle name="pitchwheel" h={0} />
|
||||
|
||||
## Spectrum
|
||||
|
||||
<JsDoc client:idle name="spectrum" h={0} />
|
||||
|
||||
## markcss
|
||||
|
||||
<JsDoc client:idle name="markcss" h={0} />
|
||||
|
||||
+7
-244
@@ -4,252 +4,15 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { code2hash, logger, silence } from '@strudel/core';
|
||||
import { getDrawContext } from '@strudel/draw';
|
||||
import { transpiler } from '@strudel/transpiler';
|
||||
import {
|
||||
getAudioContext,
|
||||
webaudioOutput,
|
||||
resetGlobalEffects,
|
||||
resetLoadedSounds,
|
||||
initAudioOnFirstClick,
|
||||
} from '@strudel/webaudio';
|
||||
import { defaultAudioDeviceName } from '../settings.mjs';
|
||||
import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs';
|
||||
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
|
||||
import { clearHydra } from '@strudel/hydra';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { settingsMap, useSettings } from '../settings.mjs';
|
||||
import {
|
||||
setActivePattern,
|
||||
setLatestCode,
|
||||
createPatternID,
|
||||
userPattern,
|
||||
getViewingPatternData,
|
||||
setViewingPatternData,
|
||||
} from '../user_pattern_utils.mjs';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { prebake } from './prebake.mjs';
|
||||
import { getRandomTune, initCode, loadModules, shareCode, ReplContext, isUdels } from './util.mjs';
|
||||
import './Repl.css';
|
||||
import { setInterval, clearInterval } from 'worker-timers';
|
||||
import { getMetadata } from '../metadata_parser';
|
||||
import { isIframe, isUdels } from './util.mjs';
|
||||
import UdelsEditor from '@components/Udels/UdelsEditor';
|
||||
|
||||
import ReplEditor from './components/ReplEditor';
|
||||
|
||||
const { latestCode } = settingsMap.get();
|
||||
|
||||
let modulesLoading, presets, drawContext, clearCanvas, isIframe, audioReady;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
audioReady = initAudioOnFirstClick();
|
||||
modulesLoading = loadModules();
|
||||
presets = prebake();
|
||||
drawContext = getDrawContext();
|
||||
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
|
||||
isIframe = window.location !== window.parent.location;
|
||||
}
|
||||
|
||||
async function getModule(name) {
|
||||
if (!modulesLoading) {
|
||||
return;
|
||||
}
|
||||
const modules = await modulesLoading;
|
||||
return modules.find((m) => m.packageName === name);
|
||||
}
|
||||
import EmbeddedReplEditor from './components/EmbeddedReplEditor';
|
||||
import { useReplContext } from './useReplContext';
|
||||
|
||||
export function Repl({ embedded = false }) {
|
||||
const isEmbedded = embedded || isIframe;
|
||||
const { panelPosition, isZen, isSyncEnabled } = useSettings();
|
||||
const init = useCallback(() => {
|
||||
const drawTime = [-2, 2];
|
||||
const drawContext = getDrawContext();
|
||||
const editor = new StrudelMirror({
|
||||
sync: isSyncEnabled,
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime: () => getAudioContext().currentTime,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
transpiler,
|
||||
autodraw: false,
|
||||
root: containerRef.current,
|
||||
initialCode: '// LOADING',
|
||||
pattern: silence,
|
||||
drawTime,
|
||||
drawContext,
|
||||
prebake: async () => Promise.all([modulesLoading, presets]),
|
||||
onUpdateState: (state) => {
|
||||
setReplState({ ...state });
|
||||
},
|
||||
onToggle: (playing) => {
|
||||
if (!playing) {
|
||||
clearHydra();
|
||||
}
|
||||
},
|
||||
beforeEval: () => audioReady,
|
||||
afterEval: (all) => {
|
||||
const { code } = all;
|
||||
//post to iframe parent (like Udels) if it exists...
|
||||
window.parent?.postMessage(code);
|
||||
|
||||
setLatestCode(code);
|
||||
window.location.hash = '#' + code2hash(code);
|
||||
setDocumentTitle(code);
|
||||
const viewingPatternData = getViewingPatternData();
|
||||
setVersionDefaultsFrom(code);
|
||||
const data = { ...viewingPatternData, code };
|
||||
let id = data.id;
|
||||
const isExamplePattern = viewingPatternData.collection !== userPattern.collection;
|
||||
|
||||
if (isExamplePattern) {
|
||||
const codeHasChanged = code !== viewingPatternData.code;
|
||||
if (codeHasChanged) {
|
||||
// fork example
|
||||
const newPattern = userPattern.duplicate(data);
|
||||
id = newPattern.id;
|
||||
setViewingPatternData(newPattern.data);
|
||||
}
|
||||
} else {
|
||||
id = userPattern.isValidID(id) ? id : createPatternID();
|
||||
setViewingPatternData(userPattern.update(id, data).data);
|
||||
}
|
||||
setActivePattern(id);
|
||||
},
|
||||
bgFill: false,
|
||||
});
|
||||
window.strudelMirror = editor;
|
||||
|
||||
// init settings
|
||||
|
||||
initCode().then(async (decoded) => {
|
||||
let code, msg;
|
||||
if (decoded) {
|
||||
code = decoded;
|
||||
msg = `I have loaded the code from the URL.`;
|
||||
} else if (latestCode) {
|
||||
code = latestCode;
|
||||
msg = `Your last session has been loaded!`;
|
||||
} else {
|
||||
const { code: randomTune, name } = await getRandomTune();
|
||||
code = randomTune;
|
||||
msg = `A random code snippet named "${name}" has been loaded!`;
|
||||
}
|
||||
editor.setCode(code);
|
||||
setDocumentTitle(code);
|
||||
logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
|
||||
});
|
||||
|
||||
editorRef.current = editor;
|
||||
}, []);
|
||||
|
||||
const [replState, setReplState] = useState({});
|
||||
const { started, isDirty, error, activeCode, pending } = replState;
|
||||
const editorRef = useRef();
|
||||
const containerRef = useRef();
|
||||
|
||||
// this can be simplified once SettingsTab has been refactored to change codemirrorSettings directly!
|
||||
// this will be the case when the main repl is being replaced
|
||||
const _settings = useStore(settingsMap, { keys: Object.keys(defaultSettings) });
|
||||
useEffect(() => {
|
||||
let editorSettings = {};
|
||||
Object.keys(defaultSettings).forEach((key) => {
|
||||
if (_settings.hasOwnProperty(key)) {
|
||||
editorSettings[key] = _settings[key];
|
||||
}
|
||||
});
|
||||
editorRef.current?.updateSettings(editorSettings);
|
||||
}, [_settings]);
|
||||
|
||||
// on first load, set stored audio device if possible
|
||||
useEffect(() => {
|
||||
const { audioDeviceName } = _settings;
|
||||
if (audioDeviceName !== defaultAudioDeviceName) {
|
||||
getAudioDevices().then((devices) => {
|
||||
const deviceID = devices.get(audioDeviceName);
|
||||
if (deviceID == null) {
|
||||
return;
|
||||
}
|
||||
setAudioDevice(deviceID);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
//
|
||||
// UI Actions
|
||||
//
|
||||
|
||||
const setDocumentTitle = (code) => {
|
||||
const meta = getMetadata(code);
|
||||
document.title = (meta.title ? `${meta.title} - ` : '') + 'Strudel REPL';
|
||||
};
|
||||
|
||||
const handleTogglePlay = async () => {
|
||||
editorRef.current?.toggle();
|
||||
};
|
||||
|
||||
const resetEditor = async () => {
|
||||
(await getModule('@strudel/tonal'))?.resetVoicings();
|
||||
resetGlobalEffects();
|
||||
clearCanvas();
|
||||
clearHydra();
|
||||
resetLoadedSounds();
|
||||
editorRef.current.repl.setCps(0.5);
|
||||
await prebake(); // declare default samples
|
||||
};
|
||||
|
||||
const handleUpdate = async (patternData, reset = false) => {
|
||||
setViewingPatternData(patternData);
|
||||
editorRef.current.setCode(patternData.code);
|
||||
if (reset) {
|
||||
await resetEditor();
|
||||
handleEvaluate();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEvaluate = () => {
|
||||
editorRef.current.evaluate();
|
||||
};
|
||||
const handleShuffle = async () => {
|
||||
const patternData = await getRandomTune();
|
||||
const code = patternData.code;
|
||||
logger(`[repl] ✨ loading random tune "${patternData.id}"`);
|
||||
setActivePattern(patternData.id);
|
||||
setViewingPatternData(patternData);
|
||||
await resetEditor();
|
||||
editorRef.current.setCode(code);
|
||||
editorRef.current.repl.evaluate(code);
|
||||
};
|
||||
|
||||
const handleShare = async () => shareCode(replState.code);
|
||||
const context = {
|
||||
embedded,
|
||||
started,
|
||||
pending,
|
||||
isDirty,
|
||||
activeCode,
|
||||
handleTogglePlay,
|
||||
handleUpdate,
|
||||
handleShuffle,
|
||||
handleShare,
|
||||
handleEvaluate,
|
||||
};
|
||||
|
||||
if (isUdels()) {
|
||||
return (
|
||||
<UdelsEditor context={context} error={error} init={init} editorRef={editorRef} containerRef={containerRef} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ReplEditor
|
||||
panelPosition={panelPosition}
|
||||
isEmbedded={isEmbedded}
|
||||
context={context}
|
||||
error={error}
|
||||
init={init}
|
||||
editorRef={editorRef}
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
);
|
||||
const isEmbedded = embedded || isIframe();
|
||||
const Editor = isUdels() ? UdelsEditor : isEmbedded ? EmbeddedReplEditor : ReplEditor;
|
||||
const context = useReplContext();
|
||||
return <Editor context={context} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import Loader from '@src/repl/components/Loader';
|
||||
import { Code } from '@src/repl/components/Code';
|
||||
import BigPlayButton from '@src/repl/components/BigPlayButton';
|
||||
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
|
||||
import { Header } from './Header';
|
||||
|
||||
// type Props = {
|
||||
// context: replcontext,
|
||||
// }
|
||||
|
||||
export default function EmbeddedReplEditor(Props) {
|
||||
const { context } = Props;
|
||||
const { pending, started, handleTogglePlay, containerRef, editorRef, error, init } = context;
|
||||
return (
|
||||
<div className="h-full flex flex-col relative">
|
||||
<Loader active={pending} />
|
||||
<Header context={context} embedded={true} />
|
||||
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
</div>
|
||||
<UserFacingErrorMessage error={error} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +1,29 @@
|
||||
import AcademicCapIcon from '@heroicons/react/20/solid/AcademicCapIcon';
|
||||
import ArrowPathIcon from '@heroicons/react/20/solid/ArrowPathIcon';
|
||||
import LinkIcon from '@heroicons/react/20/solid/LinkIcon';
|
||||
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
||||
import SparklesIcon from '@heroicons/react/20/solid/SparklesIcon';
|
||||
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { useSettings, setIsZen } from '../../settings.mjs';
|
||||
// import { ReplContext } from './Repl';
|
||||
import '../Repl.css';
|
||||
|
||||
const { BASE_URL } = import.meta.env;
|
||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||
|
||||
export function Header({ context }) {
|
||||
const {
|
||||
embedded,
|
||||
started,
|
||||
pending,
|
||||
isDirty,
|
||||
activeCode,
|
||||
handleTogglePlay,
|
||||
handleEvaluate,
|
||||
handleShuffle,
|
||||
handleShare,
|
||||
} = context;
|
||||
export function Header({ context, embedded = false }) {
|
||||
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } =
|
||||
context;
|
||||
const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location);
|
||||
const { isZen } = useSettings();
|
||||
const { isZen, isButtonRowHidden, isCSSAnimationDisabled } = useSettings();
|
||||
|
||||
return (
|
||||
<header
|
||||
id="header"
|
||||
className={cx(
|
||||
'flex-none text-black z-[100] text-lg select-none',
|
||||
'flex-none text-black z-[100] text-lg select-none h-20 md:h-14',
|
||||
!isZen && !isEmbedded && 'bg-lineHighlight',
|
||||
isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full py-1 justify-between',
|
||||
isEmbedded ? 'flex' : 'md:flex',
|
||||
)}
|
||||
>
|
||||
<div className="px-4 flex space-x-2 md:pt-0 select-none">
|
||||
{/* <img
|
||||
src={logo}
|
||||
className={cx('Tidal-logo', isEmbedded ? 'w-8 h-8' : 'w-10 h-10', started && 'animate-pulse')} // 'bg-[#ffffff80] rounded-full'
|
||||
alt="logo"
|
||||
/> */}
|
||||
<h1
|
||||
onClick={() => {
|
||||
if (isEmbedded) window.open(window.location.href.replace('embed', ''));
|
||||
@@ -52,31 +34,46 @@ export function Header({ context }) {
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cx('mt-[1px]', started && 'animate-spin', 'cursor-pointer')}
|
||||
className={cx(
|
||||
'mt-[1px]',
|
||||
started && !isCSSAnimationDisabled && 'animate-spin',
|
||||
'cursor-pointer text-blue-500',
|
||||
isZen && 'fixed top-2 right-4',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isEmbedded) {
|
||||
setIsZen(!isZen);
|
||||
}
|
||||
}}
|
||||
>
|
||||
🌀
|
||||
<span className="block rotate-90">꩜</span>
|
||||
</div>
|
||||
{!isZen && (
|
||||
<div className={cx(started && 'animate-pulse')}>
|
||||
<span className="">strudel</span> <span className="text-sm">REPL</span>
|
||||
<div className="space-x-2">
|
||||
<span className="">strudel</span>
|
||||
<span className="text-sm font-medium">REPL</span>
|
||||
{!isEmbedded && isButtonRowHidden && (
|
||||
<a href={`${baseNoTrailing}/learn`} className="text-sm opacity-25 font-medium">
|
||||
DOCS
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</h1>
|
||||
</div>
|
||||
{!isZen && (
|
||||
<div className="flex max-w-full overflow-auto text-foreground">
|
||||
{!isZen && !isButtonRowHidden && (
|
||||
<div className="flex max-w-full overflow-auto text-foreground px-1 md:px-2">
|
||||
<button
|
||||
onClick={handleTogglePlay}
|
||||
title={started ? 'stop' : 'play'}
|
||||
className={cx(!isEmbedded ? 'p-2' : 'px-2', 'hover:opacity-50', !started && 'animate-pulse')}
|
||||
className={cx(
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
'hover:opacity-50',
|
||||
!started && !isCSSAnimationDisabled && 'animate-pulse',
|
||||
)}
|
||||
>
|
||||
{!pending ? (
|
||||
<span className={cx('flex items-center space-x-1', isEmbedded ? '' : 'w-16')}>
|
||||
<span className={cx('flex items-center space-x-2')}>
|
||||
{started ? <StopCircleIcon className="w-6 h-6" /> : <PlayCircleIcon className="w-6 h-6" />}
|
||||
{!isEmbedded && <span>{started ? 'stop' : 'play'}</span>}
|
||||
</span>
|
||||
@@ -93,8 +90,6 @@ export function Header({ context }) {
|
||||
!isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50',
|
||||
)}
|
||||
>
|
||||
{/* <CommandLineIcon className="w-6 h-6" /> */}
|
||||
<ArrowPathIcon className="w-6 h-6" />
|
||||
{!isEmbedded && <span>update</span>}
|
||||
</button>
|
||||
{!isEmbedded && (
|
||||
@@ -103,7 +98,6 @@ export function Header({ context }) {
|
||||
className="hover:opacity-50 p-2 flex items-center space-x-1"
|
||||
onClick={handleShuffle}
|
||||
>
|
||||
<SparklesIcon className="w-6 h-6" />
|
||||
<span> shuffle</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -116,7 +110,6 @@ export function Header({ context }) {
|
||||
)}
|
||||
onClick={handleShare}
|
||||
>
|
||||
<LinkIcon className="w-6 h-6" />
|
||||
<span>share</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -126,7 +119,6 @@ export function Header({ context }) {
|
||||
href={`${baseNoTrailing}/workshop/getting-started/`}
|
||||
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
>
|
||||
<AcademicCapIcon className="w-6 h-6" />
|
||||
<span>learn</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -1,38 +1,30 @@
|
||||
import { ReplContext } from '@src/repl/util.mjs';
|
||||
|
||||
import Loader from '@src/repl/components/Loader';
|
||||
import { Panel } from '@src/repl/components/panel/Panel';
|
||||
import { HorizontalPanel, VerticalPanel } from '@src/repl/components/panel/Panel';
|
||||
import { Code } from '@src/repl/components/Code';
|
||||
import BigPlayButton from '@src/repl/components/BigPlayButton';
|
||||
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
|
||||
import { Header } from './Header';
|
||||
import { useSettings } from '@src/settings.mjs';
|
||||
|
||||
// type Props = {
|
||||
// context: replcontext,
|
||||
// containerRef: React.MutableRefObject<HTMLElement | null>,
|
||||
// editorRef: React.MutableRefObject<HTMLElement | null>,
|
||||
// error: Error
|
||||
// init: () => void
|
||||
// isEmbedded: boolean
|
||||
// }
|
||||
|
||||
export default function ReplEditor(Props) {
|
||||
const { context, containerRef, editorRef, error, init, panelPosition } = Props;
|
||||
const { pending, started, handleTogglePlay, isEmbedded } = context;
|
||||
const showPanel = !isEmbedded;
|
||||
const { context } = Props;
|
||||
const { containerRef, editorRef, error, init, pending } = context;
|
||||
const settings = useSettings();
|
||||
const { panelPosition, isZen } = settings;
|
||||
|
||||
return (
|
||||
<ReplContext.Provider value={context}>
|
||||
<div className="h-full flex flex-col relative">
|
||||
<Loader active={pending} />
|
||||
<Header context={context} />
|
||||
{isEmbedded && <BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />}
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
{panelPosition === 'right' && showPanel && <Panel context={context} />}
|
||||
</div>
|
||||
<UserFacingErrorMessage error={error} />
|
||||
{panelPosition === 'bottom' && showPanel && <Panel context={context} />}
|
||||
<div className="h-full flex flex-col relative">
|
||||
<Loader active={pending} />
|
||||
<Header context={context} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
{!isZen && panelPosition === 'right' && <VerticalPanel context={context} />}
|
||||
</div>
|
||||
</ReplContext.Provider>
|
||||
<UserFacingErrorMessage error={error} />
|
||||
{!isZen && panelPosition === 'bottom' && <HorizontalPanel context={context} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { audioEngineTargets } from '../../../settings.mjs';
|
||||
import { SelectInput } from './SelectInput';
|
||||
|
||||
// Allows the user to select an audio interface for Strudel to play through
|
||||
export function AudioEngineTargetSelector({ target, onChange, isDisabled }) {
|
||||
const onTargetChange = (target) => {
|
||||
onChange(target);
|
||||
};
|
||||
const options = new Map([
|
||||
[audioEngineTargets.webaudio, audioEngineTargets.webaudio],
|
||||
[audioEngineTargets.osc, audioEngineTargets.osc],
|
||||
]);
|
||||
return (
|
||||
<div className=" flex flex-col gap-1">
|
||||
<SelectInput isDisabled={isDisabled} options={options} value={target} onChange={onTargetChange} />
|
||||
{target === audioEngineTargets.osc && (
|
||||
<div>
|
||||
<p className="text-sm italic">
|
||||
⚠ All events routed to OSC, audio is silenced! See{' '}
|
||||
<a className="text-blue-500" href="https://strudel.cc/learn/input-output/">
|
||||
Docs
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,48 @@
|
||||
import { logger } from '@strudel/core';
|
||||
import useEvent from '@src/useEvent.mjs';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useSettings } from '../../../settings.mjs';
|
||||
|
||||
export function ConsoleTab({ log }) {
|
||||
export function ConsoleTab() {
|
||||
const [log, setLog] = useState([]);
|
||||
const { fontFamily, fontSize } = useSettings();
|
||||
useLogger(
|
||||
useCallback((e) => {
|
||||
const { message, type, data } = e.detail;
|
||||
setLog((l) => {
|
||||
const lastLog = l.length ? l[l.length - 1] : undefined;
|
||||
const id = nanoid(12);
|
||||
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
|
||||
if (type === 'loaded-sample') {
|
||||
// const loadIndex = l.length - 1;
|
||||
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
|
||||
l[loadIndex] = { message, type, id, data };
|
||||
} else if (lastLog && lastLog.message === message) {
|
||||
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
|
||||
} else {
|
||||
l = l.concat([{ message, type, id, data }]);
|
||||
}
|
||||
return l.slice(-20);
|
||||
});
|
||||
}, []),
|
||||
);
|
||||
return (
|
||||
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm">
|
||||
<pre>{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
|
||||
██╔════╝╚══██╔══╝██╔══██╗██║ ██║██╔══██╗██╔════╝██║
|
||||
███████╗ ██║ ██████╔╝██║ ██║██║ ██║█████╗ ██║
|
||||
╚════██║ ██║ ██╔══██╗██║ ██║██║ ██║██╔══╝ ██║
|
||||
███████║ ██║ ██║ ██║╚██████╔╝██████╔╝███████╗███████╗
|
||||
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝`}</pre>
|
||||
<div
|
||||
id="console-tab"
|
||||
className="break-all px-4 dark:text-white text-stone-900 text-sm py-2 space-y-1"
|
||||
style={{ fontFamily, fontSize }}
|
||||
>
|
||||
{log.map((l, i) => {
|
||||
const message = linkify(l.message);
|
||||
const color = l.data?.hap?.value?.color;
|
||||
return (
|
||||
<div key={l.id} className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}>
|
||||
<div
|
||||
key={l.id}
|
||||
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}
|
||||
style={color ? { color } : {}}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />
|
||||
{l.count ? ` (${l.count})` : ''}
|
||||
</div>
|
||||
@@ -42,3 +72,7 @@ function linkify(inputText) {
|
||||
|
||||
return replacedText;
|
||||
}
|
||||
|
||||
function useLogger(onTrigger) {
|
||||
useEvent(logger.key, onTrigger);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export function ButtonGroup({ value, onChange, items }) {
|
||||
{Object.entries(items).map(([key, label], i, arr) => (
|
||||
<button
|
||||
key={key}
|
||||
id={key}
|
||||
onClick={() => onChange(key)}
|
||||
className={cx(
|
||||
'px-2 border-b h-8 whitespace-nowrap',
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import XMarkIcon from '@heroicons/react/20/solid/XMarkIcon';
|
||||
import { logger } from '@strudel/core';
|
||||
import useEvent from '@src/useEvent.mjs';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react';
|
||||
import { setActiveFooter, useSettings } from '../../../settings.mjs';
|
||||
import { setPanelPinned, setActiveFooter as setTab, setIsPanelOpened, useSettings } from '../../../settings.mjs';
|
||||
import { ConsoleTab } from './ConsoleTab';
|
||||
import { FilesTab } from './FilesTab';
|
||||
import { Reference } from './Reference';
|
||||
@@ -12,117 +7,206 @@ import { SettingsTab } from './SettingsTab';
|
||||
import { SoundsTab } from './SoundsTab';
|
||||
import { WelcomeTab } from './WelcomeTab';
|
||||
import { PatternsTab } from './PatternsTab';
|
||||
import useClient from '@src/useClient.mjs';
|
||||
|
||||
// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
|
||||
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
|
||||
|
||||
const TAURI = typeof window !== 'undefined' && window.__TAURI__;
|
||||
|
||||
export function Panel({ context }) {
|
||||
const footerContent = useRef();
|
||||
const [log, setLog] = useState([]);
|
||||
const { activeFooter, isZen, panelPosition } = useSettings();
|
||||
export function HorizontalPanel({ context }) {
|
||||
const settings = useSettings();
|
||||
const { isPanelOpen, activeFooter: tab } = settings;
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (footerContent.current && activeFooter === 'console') {
|
||||
// scroll log box to bottom when log changes
|
||||
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
|
||||
}
|
||||
}, [log, activeFooter]);
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (!footerContent.current) {
|
||||
} else if (activeFooter === 'console') {
|
||||
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
|
||||
} else {
|
||||
footerContent.current.scrollTop = 0;
|
||||
}
|
||||
}, [activeFooter]);
|
||||
|
||||
useLogger(
|
||||
useCallback((e) => {
|
||||
const { message, type, data } = e.detail;
|
||||
setLog((l) => {
|
||||
const lastLog = l.length ? l[l.length - 1] : undefined;
|
||||
const id = nanoid(12);
|
||||
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
|
||||
if (type === 'loaded-sample') {
|
||||
// const loadIndex = l.length - 1;
|
||||
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
|
||||
l[loadIndex] = { message, type, id, data };
|
||||
} else if (lastLog && lastLog.message === message) {
|
||||
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
|
||||
} else {
|
||||
l = l.concat([{ message, type, id, data }]);
|
||||
}
|
||||
return l.slice(-20);
|
||||
});
|
||||
}, []),
|
||||
);
|
||||
|
||||
const PanelTab = ({ children, name, label }) => (
|
||||
<>
|
||||
<div
|
||||
onClick={() => setActiveFooter(name)}
|
||||
className={cx(
|
||||
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
|
||||
activeFooter === name ? 'border-foreground' : 'border-transparent',
|
||||
)}
|
||||
>
|
||||
{label || name}
|
||||
</div>
|
||||
{activeFooter === name && <>{children}</>}
|
||||
</>
|
||||
);
|
||||
const client = useClient();
|
||||
if (isZen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isActive = activeFooter !== '';
|
||||
|
||||
let positions = {
|
||||
right: cx('max-w-full flex-grow-0 flex-none overflow-hidden', isActive ? 'w-[600px] h-full' : 'absolute right-0'),
|
||||
bottom: cx('relative', isActive ? 'h-[360px] min-h-[360px]' : ''),
|
||||
};
|
||||
if (!client) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<nav className={cx('bg-lineHighlight z-[10] flex flex-col', positions[panelPosition])}>
|
||||
<div className="flex justify-between px-2">
|
||||
<div className={cx('flex select-none max-w-full overflow-auto', activeFooter && 'pb-2')}>
|
||||
<PanelTab name="intro" label="welcome" />
|
||||
<PanelTab name="patterns" />
|
||||
<PanelTab name="sounds" />
|
||||
<PanelTab name="console" />
|
||||
<PanelTab name="reference" />
|
||||
<PanelTab name="settings" />
|
||||
{TAURI && <PanelTab name="files" />}
|
||||
</div>
|
||||
{activeFooter !== '' && (
|
||||
<button onClick={() => setActiveFooter('')} className="text-foreground px-2" aria-label="Close Panel">
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{activeFooter !== '' && (
|
||||
<div className="relative overflow-hidden">
|
||||
<div className="text-white overflow-auto h-full max-w-full" ref={footerContent}>
|
||||
{activeFooter === 'intro' && <WelcomeTab context={context} />}
|
||||
{activeFooter === 'patterns' && <PatternsTab context={context} />}
|
||||
{activeFooter === 'console' && <ConsoleTab log={log} />}
|
||||
{activeFooter === 'sounds' && <SoundsTab />}
|
||||
{activeFooter === 'reference' && <Reference />}
|
||||
{activeFooter === 'settings' && <SettingsTab started={context.started} />}
|
||||
{activeFooter === 'files' && <FilesTab />}
|
||||
</div>
|
||||
<PanelNav
|
||||
settings={settings}
|
||||
className={cx(isPanelOpen ? `min-h-[360px] max-h-[360px]` : 'min-h-12 max-h-12', 'overflow-hidden flex flex-col')}
|
||||
>
|
||||
{isPanelOpen && (
|
||||
<div className="flex h-full overflow-auto pr-10 ">
|
||||
<PanelContent context={context} tab={tab} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute right-4 pt-4">
|
||||
<PanelActionButton settings={settings} />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between min-h-12 max-h-12 grid-cols-2 items-center">
|
||||
<Tabs setTab={setTab} tab={tab} />
|
||||
</div>
|
||||
</PanelNav>
|
||||
);
|
||||
}
|
||||
|
||||
export function VerticalPanel({ context }) {
|
||||
const settings = useSettings();
|
||||
const { activeFooter: tab, isPanelOpen } = settings;
|
||||
|
||||
return (
|
||||
<PanelNav
|
||||
settings={settings}
|
||||
className={cx(isPanelOpen ? `min-w-[min(600px,80vw)] max-w-[min(600px,80vw)]` : 'min-w-12 max-w-12')}
|
||||
>
|
||||
{isPanelOpen ? (
|
||||
<div className={cx('flex flex-col h-full')}>
|
||||
<div className="flex justify-between w-full ">
|
||||
<Tabs setTab={setTab} tab={tab} />
|
||||
<PanelActionButton settings={settings} />
|
||||
</div>
|
||||
|
||||
<div className="overflow-auto h-full">
|
||||
<PanelContent context={context} tab={tab} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
setIsPanelOpened(true);
|
||||
}}
|
||||
aria-label="open menu panel"
|
||||
className={cx(
|
||||
'flex flex-col hover:bg-lineBackground items-center cursor-pointer justify-center w-full h-full',
|
||||
)}
|
||||
>
|
||||
<ChevronLeftIcon className="text-foreground opacity-50 w-6 h-6" />
|
||||
</button>
|
||||
)}
|
||||
</PanelNav>
|
||||
);
|
||||
}
|
||||
|
||||
const tabNames = {
|
||||
welcome: 'intro',
|
||||
patterns: 'patterns',
|
||||
sounds: 'sounds',
|
||||
reference: 'reference',
|
||||
console: 'console',
|
||||
settings: 'settings',
|
||||
};
|
||||
if (TAURI) {
|
||||
tabNames.files = 'files';
|
||||
}
|
||||
|
||||
function PanelNav({ children, className, settings, ...props }) {
|
||||
const isHoverBehavior = settings.togglePanelTrigger === 'hover';
|
||||
return (
|
||||
<nav
|
||||
onClick={() => {
|
||||
if (!settings.isPanelOpen) {
|
||||
setIsPanelOpened(true);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (isHoverBehavior && !settings.isPanelOpen) {
|
||||
setIsPanelOpened(true);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (isHoverBehavior && !settings.isPanelPinned) {
|
||||
setIsPanelOpened(false);
|
||||
}
|
||||
}}
|
||||
aria-label="Menu Panel"
|
||||
className={cx('bg-lineHighlight group overflow-x-auto', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function useLogger(onTrigger) {
|
||||
useEvent(logger.key, onTrigger);
|
||||
function PanelContent({ context, tab }) {
|
||||
switch (tab) {
|
||||
case tabNames.patterns:
|
||||
return <PatternsTab context={context} />;
|
||||
case tabNames.console:
|
||||
return <ConsoleTab />;
|
||||
case tabNames.sounds:
|
||||
return <SoundsTab />;
|
||||
case tabNames.reference:
|
||||
return <Reference />;
|
||||
case tabNames.settings:
|
||||
return <SettingsTab started={context.started} />;
|
||||
case tabNames.files:
|
||||
return <FilesTab />;
|
||||
default:
|
||||
return <WelcomeTab context={context} />;
|
||||
}
|
||||
}
|
||||
|
||||
function PanelTab({ label, isSelected, onClick }) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cx(
|
||||
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
|
||||
isSelected ? 'border-foreground' : 'border-transparent',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
function Tabs({ setTab, tab, className }) {
|
||||
return (
|
||||
<div className={cx('flex select-none max-w-full overflow-auto pb-2', className)}>
|
||||
{Object.keys(tabNames).map((key) => {
|
||||
const val = tabNames[key];
|
||||
return <PanelTab key={key} isSelected={tab === val} label={key} onClick={() => setTab(val)} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PanelActionButton({ settings }) {
|
||||
const { togglePanelTrigger, isPanelPinned, isPanelOpen } = settings;
|
||||
const isHoverBehavior = togglePanelTrigger === 'hover';
|
||||
if (!isPanelOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHoverBehavior) {
|
||||
return <PinButton pinned={isPanelPinned} />;
|
||||
}
|
||||
return <CloseButton onClick={() => setIsPanelOpened(false)} />;
|
||||
}
|
||||
|
||||
function PinButton({ pinned }) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setPanelPinned(!pinned)}
|
||||
className={cx(
|
||||
'text-foreground max-h-8 min-h-8 max-w-8 min-w-8 items-center justify-center p-1.5 group-hover:flex',
|
||||
pinned ? 'flex' : 'hidden',
|
||||
)}
|
||||
aria-label="Pin Menu Panel"
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill={'currentColor'}
|
||||
strokeWidth="0"
|
||||
className="w-full h-full"
|
||||
opacity={pinned ? 1 : '.3'}
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a6 6 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707s.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a6 6 0 0 1 1.013.16l3.134-3.133a3 3 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146"></path>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function CloseButton({ onClick }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cx(
|
||||
'text-foreground max-h-8 min-h-8 max-w-8 min-w-8 items-center justify-center p-1.5 group-hover:flex',
|
||||
)}
|
||||
aria-label="Close Menu"
|
||||
>
|
||||
<XMarkIcon />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
|
||||
|
||||
function ActionButton({ children, onClick, label, labelIsHidden }) {
|
||||
return (
|
||||
<button className="hover:opacity-50" onClick={onClick} title={label}>
|
||||
<button className="hover:opacity-50 text-nowrap" onClick={onClick} title={label}>
|
||||
{labelIsHidden !== true && label}
|
||||
{children}
|
||||
</button>
|
||||
@@ -102,7 +102,7 @@ export function PatternsTab({ context }) {
|
||||
const autoResetPatternOnChange = !isUdels();
|
||||
|
||||
return (
|
||||
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 pb-4 flex flex-col overflow-hidden max-h-full">
|
||||
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 flex flex-col overflow-hidden max-h-full h-full">
|
||||
<ButtonGroup
|
||||
value={patternFilter}
|
||||
onChange={(value) => settingsMap.setKey('patternFilter', value)}
|
||||
@@ -155,7 +155,7 @@ export function PatternsTab({ context }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className="flex overflow-y-scroll max-h-full flex-col">
|
||||
<section className="flex overflow-y-auto max-h-full flex-grow flex-col">
|
||||
{patternFilter === patternFilterName.user && (
|
||||
<PatternButtons
|
||||
onClick={(id) =>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import jsdocJson from '../../../../../doc.json';
|
||||
const visibleFunctions = jsdocJson.docs
|
||||
const availableFunctions = jsdocJson.docs
|
||||
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
|
||||
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||
|
||||
@@ -10,29 +12,54 @@ const getInnerText = (html) => {
|
||||
};
|
||||
|
||||
export function Reference() {
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const visibleFunctions = useMemo(() => {
|
||||
return availableFunctions.filter((entry) => {
|
||||
if (!search) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return entry.name.includes(search) || (entry.synonyms?.some((s) => s.includes(search)) ?? false);
|
||||
});
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full pt-2 text-foreground overflow-hidden">
|
||||
<div className="w-42 flex-none h-full overflow-y-auto overflow-x-hidden pr-4">
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<a
|
||||
key={i}
|
||||
className="cursor-pointer block hover:bg-lineHighlight py-1 px-4"
|
||||
onClick={() => {
|
||||
const el = document.getElementById(`doc-${i}`);
|
||||
const container = document.getElementById('reference-container');
|
||||
container.scrollTo(0, el.offsetTop);
|
||||
}}
|
||||
>
|
||||
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
|
||||
</a>
|
||||
))}
|
||||
<div className="flex h-full w-full p-2 text-foreground overflow-hidden">
|
||||
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
|
||||
<div class="w-full flex">
|
||||
<input
|
||||
className="w-full p-1 bg-background rounded-md border-none"
|
||||
placeholder="Search"
|
||||
value={search}
|
||||
onInput={(event) => setSearch(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<a
|
||||
key={i}
|
||||
className="cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
|
||||
onClick={() => {
|
||||
const el = document.getElementById(`doc-${i}`);
|
||||
const container = document.getElementById('reference-container');
|
||||
container.scrollTo(0, el.offsetTop);
|
||||
}}
|
||||
>
|
||||
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="break-normal w-full h-full overflow-auto pl-4 flex relative" id="reference-container">
|
||||
<div className="prose dark:prose-invert max-w-full pr-4">
|
||||
<div
|
||||
className="break-normal flex-grow flex-col overflow-y-auto overflow-x-hidden px-2 flex relative"
|
||||
id="reference-container"
|
||||
>
|
||||
<div className="prose dark:prose-invert min-w-full px-1 ">
|
||||
<h2>API Reference</h2>
|
||||
<p>
|
||||
This is the long list functions you can use! Remember that you don't need to remember all of those and that
|
||||
you can already make music with a small set of functions!
|
||||
This is the long list of functions you can use. Remember that you don't need to remember all of those and
|
||||
that you can already make music with a small set of functions!
|
||||
</p>
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<section key={i}>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { themes } from '@strudel/codemirror';
|
||||
import { isUdels } from '../../util.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
||||
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
||||
import { confirmDialog } from '../../util.mjs';
|
||||
|
||||
function Checkbox({ label, value, onChange, disabled = false }) {
|
||||
return (
|
||||
@@ -78,6 +80,8 @@ const fontFamilyOptions = {
|
||||
galactico: 'galactico',
|
||||
};
|
||||
|
||||
const RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
|
||||
|
||||
export function SettingsTab({ started }) {
|
||||
const {
|
||||
theme,
|
||||
@@ -90,25 +94,50 @@ export function SettingsTab({ started }) {
|
||||
isAutoCompletionEnabled,
|
||||
isTooltipEnabled,
|
||||
isFlashEnabled,
|
||||
isButtonRowHidden,
|
||||
isCSSAnimationDisabled,
|
||||
isSyncEnabled,
|
||||
isLineWrappingEnabled,
|
||||
fontSize,
|
||||
fontFamily,
|
||||
panelPosition,
|
||||
audioDeviceName,
|
||||
audioEngineTarget,
|
||||
togglePanelTrigger,
|
||||
} = useSettings();
|
||||
const shouldAlwaysSync = isUdels();
|
||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||
return (
|
||||
<div className="text-foreground p-4 space-y-4">
|
||||
{AudioContext.prototype.setSinkId != null && (
|
||||
<div className="text-foreground p-4 space-y-4 w-full">
|
||||
{canChangeAudioDevice && (
|
||||
<FormItem label="Audio Output Device">
|
||||
<AudioDeviceSelector
|
||||
isDisabled={started}
|
||||
audioDeviceName={audioDeviceName}
|
||||
onChange={(audioDeviceName) => settingsMap.setKey('audioDeviceName', audioDeviceName)}
|
||||
onChange={(audioDeviceName) => {
|
||||
confirmDialog(RELOAD_MSG).then((r) => {
|
||||
if (r == true) {
|
||||
settingsMap.setKey('audioDeviceName', audioDeviceName);
|
||||
return window.location.reload();
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
<FormItem label="Audio Engine Target">
|
||||
<AudioEngineTargetSelector
|
||||
target={audioEngineTarget}
|
||||
onChange={(target) => {
|
||||
confirmDialog(RELOAD_MSG).then((r) => {
|
||||
if (r == true) {
|
||||
settingsMap.setKey('audioEngineTarget', target);
|
||||
return window.location.reload();
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="Theme">
|
||||
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
|
||||
</FormItem>
|
||||
@@ -144,7 +173,32 @@ export function SettingsTab({ started }) {
|
||||
items={{ bottom: 'Bottom', right: 'Right' }}
|
||||
></ButtonGroup>
|
||||
</FormItem>
|
||||
<FormItem label="Code Settings">
|
||||
<FormItem label="Open Panel on: ">
|
||||
<ButtonGroup
|
||||
value={togglePanelTrigger}
|
||||
onChange={(value) => settingsMap.setKey('togglePanelTrigger', value)}
|
||||
items={{ click: 'Click', hover: 'Hover' }}
|
||||
></ButtonGroup>
|
||||
{/* <Checkbox
|
||||
label="Click"
|
||||
onChange={(cbEvent) => {
|
||||
if (cbEvent.target.checked) {
|
||||
settingsMap.setKey('togglePanelTrigger', 'click');
|
||||
}
|
||||
}}
|
||||
value={togglePanelTrigger != 'hover'}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Hover"
|
||||
onChange={(cbEvent) => {
|
||||
if (cbEvent.target.checked) {
|
||||
settingsMap.setKey('togglePanelTrigger', 'hover');
|
||||
}
|
||||
}}
|
||||
value={togglePanelTrigger == 'hover'}
|
||||
/> */}
|
||||
</FormItem>
|
||||
<FormItem label="More Settings">
|
||||
<Checkbox
|
||||
label="Enable bracket matching"
|
||||
onChange={(cbEvent) => settingsMap.setKey('isBracketMatchingEnabled', cbEvent.target.checked)}
|
||||
@@ -193,23 +247,38 @@ export function SettingsTab({ started }) {
|
||||
<Checkbox
|
||||
label="Sync across Browser Tabs / Windows"
|
||||
onChange={(cbEvent) => {
|
||||
if (confirm('Changing this setting requires the window to reload itself. OK?')) {
|
||||
settingsMap.setKey('isSyncEnabled', cbEvent.target.checked);
|
||||
window.location.reload();
|
||||
}
|
||||
const newVal = cbEvent.target.checked;
|
||||
confirmDialog(RELOAD_MSG).then((r) => {
|
||||
if (r) {
|
||||
settingsMap.setKey('isSyncEnabled', newVal);
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={shouldAlwaysSync}
|
||||
value={isSyncEnabled}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Hide top buttons"
|
||||
onChange={(cbEvent) => settingsMap.setKey('isButtonRowHidden', cbEvent.target.checked)}
|
||||
value={isButtonRowHidden}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Disable CSS Animations"
|
||||
onChange={(cbEvent) => settingsMap.setKey('isCSSAnimationDisabled', cbEvent.target.checked)}
|
||||
value={isCSSAnimationDisabled}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
|
||||
<FormItem label="Reset Settings">
|
||||
<button
|
||||
className="bg-background p-2 max-w-[300px] rounded-md hover:opacity-50"
|
||||
onClick={() => {
|
||||
if (confirm('Sure?')) {
|
||||
settingsMap.set(defaultSettings);
|
||||
}
|
||||
confirmDialog('Sure?').then((r) => {
|
||||
if (r) {
|
||||
settingsMap.set(defaultSettings);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
restore default settings
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import useEvent from '@src/useEvent.mjs';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
||||
@@ -12,15 +12,20 @@ const getSamples = (samples) =>
|
||||
export function SoundsTab() {
|
||||
const sounds = useStore(soundMap);
|
||||
const { soundsFilter } = useSettings();
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const soundEntries = useMemo(() => {
|
||||
let filtered = Object.entries(sounds)
|
||||
.filter(([key]) => !key.startsWith('_'))
|
||||
.sort((a, b) => a[0].localeCompare(b[0]));
|
||||
if (!sounds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let filtered = Object.entries(sounds)
|
||||
.filter(([key]) => !key.startsWith('_'))
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.filter(([name]) => name.toLowerCase().includes(search.toLowerCase()));
|
||||
|
||||
if (soundsFilter === 'user') {
|
||||
return filtered.filter(([key, { data }]) => !data.prebake);
|
||||
return filtered.filter(([_, { data }]) => !data.prebake);
|
||||
}
|
||||
if (soundsFilter === 'drums') {
|
||||
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
|
||||
@@ -32,9 +37,11 @@ export function SoundsTab() {
|
||||
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
|
||||
}
|
||||
return filtered;
|
||||
}, [sounds, soundsFilter]);
|
||||
}, [sounds, soundsFilter, search]);
|
||||
|
||||
// holds mutable ref to current triggered sound
|
||||
const trigRef = useRef();
|
||||
|
||||
// stop current sound on mouseup
|
||||
useEvent('mouseup', () => {
|
||||
const t = trigRef.current;
|
||||
@@ -43,8 +50,16 @@ export function SoundsTab() {
|
||||
ref?.stop(getAudioContext().currentTime + 0.01);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
|
||||
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
|
||||
<input
|
||||
className="w-full p-1 bg-background rounded-md my-2"
|
||||
placeholder="Search"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="pb-2 flex shrink-0 flex-wrap">
|
||||
<ButtonGroup
|
||||
value={soundsFilter}
|
||||
@@ -58,7 +73,8 @@ export function SoundsTab() {
|
||||
></ButtonGroup>
|
||||
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
|
||||
</div>
|
||||
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal">
|
||||
|
||||
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal pb-2">
|
||||
{soundEntries.map(([name, { data, onTrigger }]) => {
|
||||
return (
|
||||
<span
|
||||
|
||||
@@ -5,10 +5,8 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
||||
|
||||
export function WelcomeTab({ context }) {
|
||||
return (
|
||||
<div className="prose dark:prose-invert max-w-[600px] pt-2 font-sans pb-8 px-4">
|
||||
<h3>
|
||||
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
|
||||
</h3>
|
||||
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
|
||||
<h3>꩜ welcome</h3>
|
||||
<p>
|
||||
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music
|
||||
pieces in the browser! It is free and open-source and made for beginners and experts alike. To get started:
|
||||
@@ -30,7 +28,7 @@ export function WelcomeTab({ context }) {
|
||||
</a>{' '}
|
||||
to ask any questions, give feedback or just say hello.
|
||||
</p>
|
||||
<h3>about</h3>
|
||||
<h3>꩜ about</h3>
|
||||
<p>
|
||||
strudel is a JavaScript version of{' '}
|
||||
<a href="https://tidalcycles.org/" target="_blank">
|
||||
|
||||
@@ -23,10 +23,10 @@ async function hasStrudelJson(subpath) {
|
||||
async function loadStrudelJson(subpath) {
|
||||
const contents = await readTextFile(subpath + '/strudel.json', { dir });
|
||||
const sampleMap = JSON.parse(contents);
|
||||
processSampleMap(sampleMap, (key, value) => {
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value, fileResolver(subpath)), {
|
||||
processSampleMap(sampleMap, (key, bank) => {
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank, fileResolver(subpath)), {
|
||||
type: 'sample',
|
||||
samples: value,
|
||||
samples: bank,
|
||||
fileSystem: true,
|
||||
tag: 'local',
|
||||
});
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
Repl.jsx - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/App.js>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
|
||||
import { getDrawContext } from '@strudel/draw';
|
||||
import { transpiler } from '@strudel/transpiler';
|
||||
import {
|
||||
getAudioContextCurrentTime,
|
||||
webaudioOutput,
|
||||
resetGlobalEffects,
|
||||
resetLoadedSounds,
|
||||
initAudioOnFirstClick,
|
||||
} from '@strudel/webaudio';
|
||||
import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs';
|
||||
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
|
||||
import { clearHydra } from '@strudel/hydra';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { settingsMap, useSettings } from '../settings.mjs';
|
||||
import {
|
||||
setActivePattern,
|
||||
setLatestCode,
|
||||
createPatternID,
|
||||
userPattern,
|
||||
getViewingPatternData,
|
||||
setViewingPatternData,
|
||||
} from '../user_pattern_utils.mjs';
|
||||
import { superdirtOutput } from '@strudel/osc/superdirtoutput';
|
||||
import { audioEngineTargets, defaultAudioDeviceName } from '../settings.mjs';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { prebake } from './prebake.mjs';
|
||||
import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs';
|
||||
import './Repl.css';
|
||||
import { setInterval, clearInterval } from 'worker-timers';
|
||||
import { getMetadata } from '../metadata_parser';
|
||||
|
||||
const { latestCode } = settingsMap.get();
|
||||
let modulesLoading, presets, drawContext, clearCanvas, audioReady;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
audioReady = initAudioOnFirstClick();
|
||||
modulesLoading = loadModules();
|
||||
presets = prebake();
|
||||
drawContext = getDrawContext();
|
||||
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
|
||||
}
|
||||
|
||||
async function getModule(name) {
|
||||
if (!modulesLoading) {
|
||||
return;
|
||||
}
|
||||
const modules = await modulesLoading;
|
||||
return modules.find((m) => m.packageName === name);
|
||||
}
|
||||
|
||||
export function useReplContext() {
|
||||
const { isSyncEnabled, audioEngineTarget } = useSettings();
|
||||
const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc;
|
||||
const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput;
|
||||
const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds;
|
||||
|
||||
const init = useCallback(() => {
|
||||
const drawTime = [-2, 2];
|
||||
const drawContext = getDrawContext();
|
||||
const editor = new StrudelMirror({
|
||||
sync: isSyncEnabled,
|
||||
defaultOutput,
|
||||
getTime,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
transpiler,
|
||||
autodraw: false,
|
||||
root: containerRef.current,
|
||||
initialCode: '// LOADING',
|
||||
pattern: silence,
|
||||
drawTime,
|
||||
drawContext,
|
||||
prebake: async () => Promise.all([modulesLoading, presets]),
|
||||
onUpdateState: (state) => {
|
||||
setReplState({ ...state });
|
||||
},
|
||||
onToggle: (playing) => {
|
||||
if (!playing) {
|
||||
clearHydra();
|
||||
}
|
||||
},
|
||||
beforeEval: () => audioReady,
|
||||
afterEval: (all) => {
|
||||
const { code } = all;
|
||||
//post to iframe parent (like Udels) if it exists...
|
||||
window.parent?.postMessage(code);
|
||||
|
||||
setLatestCode(code);
|
||||
window.location.hash = '#' + code2hash(code);
|
||||
setDocumentTitle(code);
|
||||
const viewingPatternData = getViewingPatternData();
|
||||
setVersionDefaultsFrom(code);
|
||||
const data = { ...viewingPatternData, code };
|
||||
let id = data.id;
|
||||
const isExamplePattern = viewingPatternData.collection !== userPattern.collection;
|
||||
|
||||
if (isExamplePattern) {
|
||||
const codeHasChanged = code !== viewingPatternData.code;
|
||||
if (codeHasChanged) {
|
||||
// fork example
|
||||
const newPattern = userPattern.duplicate(data);
|
||||
id = newPattern.id;
|
||||
setViewingPatternData(newPattern.data);
|
||||
}
|
||||
} else {
|
||||
id = userPattern.isValidID(id) ? id : createPatternID();
|
||||
setViewingPatternData(userPattern.update(id, data).data);
|
||||
}
|
||||
setActivePattern(id);
|
||||
},
|
||||
bgFill: false,
|
||||
});
|
||||
window.strudelMirror = editor;
|
||||
|
||||
// init settings
|
||||
initCode().then(async (decoded) => {
|
||||
let code, msg;
|
||||
if (decoded) {
|
||||
code = decoded;
|
||||
msg = `I have loaded the code from the URL.`;
|
||||
} else if (latestCode) {
|
||||
code = latestCode;
|
||||
msg = `Your last session has been loaded!`;
|
||||
} else {
|
||||
const { code: randomTune, name } = await getRandomTune();
|
||||
code = randomTune;
|
||||
msg = `A random code snippet named "${name}" has been loaded!`;
|
||||
}
|
||||
editor.setCode(code);
|
||||
setDocumentTitle(code);
|
||||
logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
|
||||
});
|
||||
|
||||
editorRef.current = editor;
|
||||
}, []);
|
||||
|
||||
const [replState, setReplState] = useState({});
|
||||
const { started, isDirty, error, activeCode, pending } = replState;
|
||||
const editorRef = useRef();
|
||||
const containerRef = useRef();
|
||||
|
||||
// this can be simplified once SettingsTab has been refactored to change codemirrorSettings directly!
|
||||
// this will be the case when the main repl is being replaced
|
||||
const _settings = useStore(settingsMap, { keys: Object.keys(defaultSettings) });
|
||||
useEffect(() => {
|
||||
let editorSettings = {};
|
||||
Object.keys(defaultSettings).forEach((key) => {
|
||||
if (Object.prototype.hasOwnProperty.call(_settings, key)) {
|
||||
editorSettings[key] = _settings[key];
|
||||
}
|
||||
});
|
||||
editorRef.current?.updateSettings(editorSettings);
|
||||
}, [_settings]);
|
||||
|
||||
// on first load, set stored audio device if possible
|
||||
useEffect(() => {
|
||||
const { audioDeviceName } = _settings;
|
||||
if (audioDeviceName !== defaultAudioDeviceName) {
|
||||
getAudioDevices().then((devices) => {
|
||||
const deviceID = devices.get(audioDeviceName);
|
||||
if (deviceID == null) {
|
||||
return;
|
||||
}
|
||||
setAudioDevice(deviceID);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
//
|
||||
// UI Actions
|
||||
//
|
||||
|
||||
const setDocumentTitle = (code) => {
|
||||
const meta = getMetadata(code);
|
||||
document.title = (meta.title ? `${meta.title} - ` : '') + 'Strudel REPL';
|
||||
};
|
||||
|
||||
const handleTogglePlay = async () => {
|
||||
editorRef.current?.toggle();
|
||||
};
|
||||
|
||||
const resetEditor = async () => {
|
||||
(await getModule('@strudel/tonal'))?.resetVoicings();
|
||||
resetGlobalEffects();
|
||||
clearCanvas();
|
||||
clearHydra();
|
||||
resetLoadedSounds();
|
||||
editorRef.current.repl.setCps(0.5);
|
||||
await prebake(); // declare default samples
|
||||
};
|
||||
|
||||
const handleUpdate = async (patternData, reset = false) => {
|
||||
setViewingPatternData(patternData);
|
||||
editorRef.current.setCode(patternData.code);
|
||||
if (reset) {
|
||||
await resetEditor();
|
||||
handleEvaluate();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEvaluate = () => {
|
||||
editorRef.current.evaluate();
|
||||
};
|
||||
const handleShuffle = async () => {
|
||||
const patternData = await getRandomTune();
|
||||
const code = patternData.code;
|
||||
logger(`[repl] ✨ loading random tune "${patternData.id}"`);
|
||||
setActivePattern(patternData.id);
|
||||
setViewingPatternData(patternData);
|
||||
await resetEditor();
|
||||
editorRef.current.setCode(code);
|
||||
editorRef.current.repl.evaluate(code);
|
||||
};
|
||||
|
||||
const handleShare = async () => shareCode(replState.code);
|
||||
const context = {
|
||||
started,
|
||||
pending,
|
||||
isDirty,
|
||||
activeCode,
|
||||
handleTogglePlay,
|
||||
handleUpdate,
|
||||
handleShuffle,
|
||||
handleShare,
|
||||
handleEvaluate,
|
||||
init,
|
||||
error,
|
||||
editorRef,
|
||||
containerRef,
|
||||
};
|
||||
return context;
|
||||
}
|
||||
+46
-27
@@ -2,14 +2,11 @@ import { evalScope, hash2code, logger } from '@strudel/core';
|
||||
import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs';
|
||||
import { getAudioContext, initializeAudioOutput, setDefaultAudioContext, setVersionDefaults } from '@strudel/webaudio';
|
||||
import { getMetadata } from '../metadata_parser';
|
||||
|
||||
import { isTauri } from '../tauri.mjs';
|
||||
import './Repl.css';
|
||||
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { writeText } from '@tauri-apps/api/clipboard';
|
||||
import { createContext } from 'react';
|
||||
import { $featuredPatterns, loadDBPatterns } from '@src/user_pattern_utils.mjs';
|
||||
|
||||
// Create a single supabase client for interacting with your database
|
||||
@@ -84,6 +81,7 @@ export function loadModules() {
|
||||
import('@strudel/soundfonts'),
|
||||
import('@strudel/csound'),
|
||||
import('@strudel/tidal'),
|
||||
import('@strudel/mqtt'),
|
||||
];
|
||||
if (isTauri()) {
|
||||
modules = modules.concat([
|
||||
@@ -97,6 +95,16 @@ export function loadModules() {
|
||||
|
||||
return evalScope(settingPatterns, ...modules);
|
||||
}
|
||||
// confirm dialog is a promise in webkit and a boolean in other browsers... normalize it to be a promise everywhere
|
||||
export function confirmDialog(msg) {
|
||||
const confirmed = confirm(msg);
|
||||
if (confirmed instanceof Promise) {
|
||||
return confirmed;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
resolve(confirmed);
|
||||
});
|
||||
}
|
||||
|
||||
let lastShared;
|
||||
export async function shareCode(codeToShare) {
|
||||
@@ -105,37 +113,48 @@ export async function shareCode(codeToShare) {
|
||||
logger(`Link already generated!`, 'error');
|
||||
return;
|
||||
}
|
||||
const isPublic = confirm(
|
||||
|
||||
confirmDialog(
|
||||
'Do you want your pattern to be public? If no, press cancel and you will get just a private link.',
|
||||
);
|
||||
// generate uuid in the browser
|
||||
const hash = nanoid(12);
|
||||
const shareUrl = window.location.origin + window.location.pathname + '?' + hash;
|
||||
const { error } = await supabase.from('code_v1').insert([{ code: codeToShare, hash, ['public']: isPublic }]);
|
||||
if (!error) {
|
||||
lastShared = codeToShare;
|
||||
// copy shareUrl to clipboard
|
||||
if (isTauri()) {
|
||||
await writeText(shareUrl);
|
||||
).then(async (isPublic) => {
|
||||
const hash = nanoid(12);
|
||||
const shareUrl = window.location.origin + window.location.pathname + '?' + hash;
|
||||
const { error } = await supabase.from('code_v1').insert([{ code: codeToShare, hash, ['public']: isPublic }]);
|
||||
if (!error) {
|
||||
lastShared = codeToShare;
|
||||
// copy shareUrl to clipboard
|
||||
if (isTauri()) {
|
||||
await writeText(shareUrl);
|
||||
} else {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
}
|
||||
const message = `Link copied to clipboard: ${shareUrl}`;
|
||||
alert(message);
|
||||
// alert(message);
|
||||
logger(message, 'highlight');
|
||||
} else {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
console.log('error', error);
|
||||
const message = `Error: ${error.message}`;
|
||||
// alert(message);
|
||||
logger(message);
|
||||
}
|
||||
const message = `Link copied to clipboard: ${shareUrl}`;
|
||||
alert(message);
|
||||
// alert(message);
|
||||
logger(message, 'highlight');
|
||||
} else {
|
||||
console.log('error', error);
|
||||
const message = `Error: ${error.message}`;
|
||||
// alert(message);
|
||||
logger(message);
|
||||
});
|
||||
}
|
||||
|
||||
export const isIframe = () => window.location !== window.parent.location;
|
||||
function isCrossOriginFrame() {
|
||||
try {
|
||||
return !window.top.location.hostname;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const ReplContext = createContext(null);
|
||||
|
||||
export const isUdels = () => {
|
||||
return window.parent?.location.pathname.includes('udels');
|
||||
if (isCrossOriginFrame()) {
|
||||
return false;
|
||||
}
|
||||
return window.top?.location?.pathname.includes('udels');
|
||||
};
|
||||
|
||||
export const getAudioDevices = async () => {
|
||||
|
||||
@@ -5,6 +5,11 @@ import { isUdels } from './repl/util.mjs';
|
||||
|
||||
export const defaultAudioDeviceName = 'System Standard';
|
||||
|
||||
export const audioEngineTargets = {
|
||||
webaudio: 'webaudio',
|
||||
osc: 'osc',
|
||||
};
|
||||
|
||||
export const defaultSettings = {
|
||||
activeFooter: 'intro',
|
||||
keybindings: 'codemirror',
|
||||
@@ -25,9 +30,16 @@ export const defaultSettings = {
|
||||
isZen: false,
|
||||
soundsFilter: 'all',
|
||||
patternFilter: 'community',
|
||||
// panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro
|
||||
panelPosition: 'right',
|
||||
isPanelPinned: false,
|
||||
isPanelOpen: true,
|
||||
togglePanelTrigger: 'click', //click | hover
|
||||
userPatterns: '{}',
|
||||
audioDeviceName: defaultAudioDeviceName,
|
||||
audioEngineTarget: audioEngineTargets.webaudio,
|
||||
isButtonRowHidden: false,
|
||||
isCSSAnimationDisabled: false,
|
||||
};
|
||||
|
||||
let search = null;
|
||||
@@ -60,17 +72,23 @@ export function useSettings() {
|
||||
isActiveLineHighlighted: parseBoolean(state.isActiveLineHighlighted),
|
||||
isAutoCompletionEnabled: parseBoolean(state.isAutoCompletionEnabled),
|
||||
isPatternHighlightingEnabled: parseBoolean(state.isPatternHighlightingEnabled),
|
||||
isButtonRowHidden: parseBoolean(state.isButtonRowHidden),
|
||||
isCSSAnimationDisabled: parseBoolean(state.isCSSAnimationDisabled),
|
||||
isTooltipEnabled: parseBoolean(state.isTooltipEnabled),
|
||||
isLineWrappingEnabled: parseBoolean(state.isLineWrappingEnabled),
|
||||
isFlashEnabled: parseBoolean(state.isFlashEnabled),
|
||||
isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled),
|
||||
fontSize: Number(state.fontSize),
|
||||
panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is!
|
||||
isPanelPinned: parseBoolean(state.isPanelPinned),
|
||||
isPanelOpen: parseBoolean(state.isPanelOpen),
|
||||
userPatterns: userPatterns,
|
||||
};
|
||||
}
|
||||
|
||||
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
||||
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
|
||||
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
|
||||
|
||||
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
}
|
||||
:root {
|
||||
--app-height: 100vh;
|
||||
--app-width: 100vw;
|
||||
}
|
||||
|
||||
#console-tab {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { atom } from 'nanostores';
|
||||
import { persistentAtom } from '@nanostores/persistent';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { logger } from '@strudel/core';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { settingsMap } from './settings.mjs';
|
||||
import { parseJSON, supabase } from './repl/util.mjs';
|
||||
import { confirmDialog, parseJSON, supabase } from './repl/util.mjs';
|
||||
|
||||
export let $publicPatterns = atom([]);
|
||||
export let $featuredPatterns = atom([]);
|
||||
@@ -131,17 +130,19 @@ export const userPattern = {
|
||||
return this.update(newPattern.id, { ...newPattern.data, code: data.code });
|
||||
},
|
||||
clearAll() {
|
||||
if (!confirm(`This will delete all your patterns. Are you really sure?`)) {
|
||||
return;
|
||||
}
|
||||
const viewingPatternData = getViewingPatternData();
|
||||
setUserPatterns({});
|
||||
confirmDialog(`This will delete all your patterns. Are you really sure?`).then((r) => {
|
||||
if (r == false) {
|
||||
return;
|
||||
}
|
||||
const viewingPatternData = getViewingPatternData();
|
||||
setUserPatterns({});
|
||||
|
||||
if (viewingPatternData.collection !== this.collection) {
|
||||
return { id: viewingPatternData.id, data: viewingPatternData };
|
||||
}
|
||||
setActivePattern(null);
|
||||
return this.create();
|
||||
if (viewingPatternData.collection !== this.collection) {
|
||||
return { id: viewingPatternData.id, data: viewingPatternData };
|
||||
}
|
||||
setActivePattern(null);
|
||||
return this.create();
|
||||
});
|
||||
},
|
||||
delete(id) {
|
||||
const userPatterns = this.getAll();
|
||||
|
||||
@@ -34,6 +34,7 @@ module.exports = {
|
||||
},
|
||||
spacing: {
|
||||
'app-height': 'var(--app-height)',
|
||||
'app-width': 'var(--app-width)',
|
||||
},
|
||||
typography(theme) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user