mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-28 16:16:50 -04:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42d7781d44 | |||
| a746bf5461 | |||
| 700f0be0ba | |||
| cb59d632a8 | |||
| 9fd86458e6 | |||
| 3cd49065c8 | |||
| a7726e2334 | |||
| 26793aec91 | |||
| 7c4ed7e05f | |||
| 93cfe9802c | |||
| 0106a129c9 |
@@ -22,4 +22,3 @@ vite.config.js
|
|||||||
reverbGen.mjs
|
reverbGen.mjs
|
||||||
hydra.mjs
|
hydra.mjs
|
||||||
jsdoc-synonyms.js
|
jsdoc-synonyms.js
|
||||||
packages/node/pattern.mjs
|
|
||||||
@@ -127,6 +127,3 @@ fabric.properties
|
|||||||
.idea/caches/build_file_checksums.ser
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
# END JetBrains -> BEGIN JetBrains
|
# END JetBrains -> BEGIN JetBrains
|
||||||
|
|
||||||
samples/*
|
|
||||||
!samples/README.md
|
|
||||||
|
|||||||
+1
-1
@@ -10,5 +10,5 @@ paper
|
|||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
pnpm-workspace.yaml
|
pnpm-workspace.yaml
|
||||||
**/dev-dist
|
**/dev-dist
|
||||||
|
superdough-wasm
|
||||||
website/.astro
|
website/.astro
|
||||||
packages/node/pattern.mjs
|
|
||||||
@@ -15,15 +15,25 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
|
|||||||
After cloning the project, you can run the REPL locally:
|
After cloning the project, you can run the REPL locally:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm i
|
pnpm run setup
|
||||||
pnpm dev
|
pnpm run repl
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using Strudel In Your Project
|
## Using Strudel In Your Project
|
||||||
|
|
||||||
This project is organized into many [packages](./packages), which are also available on [npm](https://www.npmjs.com/search?q=%40strudel).
|
There are multiple npm packages you can use to use strudel, or only parts of it, in your project:
|
||||||
|
|
||||||
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start).
|
- [`core`](./packages/core/): tidal pattern engine
|
||||||
|
- [`mini`](./packages/mini): mini notation parser + core binding
|
||||||
|
- [`transpiler`](./packages/transpiler): user code transpiler
|
||||||
|
- [`webaudio`](./packages/webaudio): webaudio output
|
||||||
|
- [`osc`](./packages/osc): bindings to communicate via OSC
|
||||||
|
- [`midi`](./packages/midi): webmidi bindings
|
||||||
|
- [`serial`](./packages/serial): webserial bindings
|
||||||
|
- [`tonal`](./packages/tonal): tonal functions
|
||||||
|
- ... [and there are more](./packages/)
|
||||||
|
|
||||||
|
Click on the package names to find out more about each one.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
|
|
||||||
<button id="play">play</button>
|
<button id="play">play</button>
|
||||||
<button id="stop">stop</button>
|
<button id="stop">stop</button>
|
||||||
<script>
|
<script type="module">
|
||||||
strudel.initStrudel();
|
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
|
||||||
|
|
||||||
|
initStrudel();
|
||||||
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
|
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
|
||||||
document.getElementById('play').addEventListener('stop', () => hush());
|
document.getElementById('play').addEventListener('stop', () => hush());
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
|
|
||||||
<button id="a">A</button>
|
<button id="a">A</button>
|
||||||
<button id="b">B</button>
|
<button id="b">B</button>
|
||||||
<button id="c">C</button>
|
<button id="c">C</button>
|
||||||
<button id="stop">stop</button>
|
<button id="stop">stop</button>
|
||||||
<script>
|
<script type="module">
|
||||||
|
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
|
||||||
initStrudel({
|
initStrudel({
|
||||||
prebake: () => samples('github:tidalcycles/dirt-samples'),
|
prebake: () => samples('github:tidalcycles/dirt-samples'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="output"></div>
|
<div id="output"></div>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
// TODO: refactor to use newer version without controls import
|
|
||||||
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel/core@0.11.0';
|
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel/core@0.11.0';
|
||||||
import { mini } from 'https://cdn.skypack.dev/@strudel/mini@0.11.0';
|
import { mini } from 'https://cdn.skypack.dev/@strudel/mini@0.11.0';
|
||||||
import { transpiler } from 'https://cdn.skypack.dev/@strudel/transpiler@0.11.0';
|
import { transpiler } from 'https://cdn.skypack.dev/@strudel/transpiler@0.11.0';
|
||||||
@@ -54,7 +53,7 @@
|
|||||||
|
|
||||||
function getTune() {
|
function getTune() {
|
||||||
return `samples('github:tidalcycles/dirt-samples')
|
return `samples('github:tidalcycles/dirt-samples')
|
||||||
setcps(1);
|
|
||||||
stack(
|
stack(
|
||||||
// amen
|
// amen
|
||||||
n("0 1 2 3 4 5 6 7")
|
n("0 1 2 3 4 5 6 7")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { StrudelMirror } from '@strudel/codemirror';
|
import { StrudelMirror } from '@strudel/codemirror';
|
||||||
import { funk42 } from './tunes';
|
import { funk42 } from './tunes';
|
||||||
import { drawPianoroll, evalScope } from '@strudel/core';
|
import { drawPianoroll, evalScope, controls } from '@strudel/core';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
import { initAudioOnFirstClick } from '@strudel/webaudio';
|
import { initAudioOnFirstClick } from '@strudel/webaudio';
|
||||||
import { transpiler } from '@strudel/transpiler';
|
import { transpiler } from '@strudel/transpiler';
|
||||||
@@ -25,6 +25,7 @@ const editor = new StrudelMirror({
|
|||||||
prebake: async () => {
|
prebake: async () => {
|
||||||
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
|
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
|
||||||
const loadModules = evalScope(
|
const loadModules = evalScope(
|
||||||
|
controls,
|
||||||
import('@strudel/core'),
|
import('@strudel/core'),
|
||||||
import('@strudel/mini'),
|
import('@strudel/mini'),
|
||||||
import('@strudel/tonal'),
|
import('@strudel/tonal'),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { repl, evalScope } from '@strudel/core';
|
import { controls, repl, evalScope } from '@strudel/core';
|
||||||
import { getAudioContext, webaudioOutput, initAudioOnFirstClick, registerSynthSounds } from '@strudel/webaudio';
|
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
|
||||||
import { transpiler } from '@strudel/transpiler';
|
import { transpiler } from '@strudel/transpiler';
|
||||||
import tune from './tune.mjs';
|
import tune from './tune.mjs';
|
||||||
|
|
||||||
@@ -7,9 +7,14 @@ const ctx = getAudioContext();
|
|||||||
const input = document.getElementById('text');
|
const input = document.getElementById('text');
|
||||||
input.innerHTML = tune;
|
input.innerHTML = tune;
|
||||||
initAudioOnFirstClick();
|
initAudioOnFirstClick();
|
||||||
registerSynthSounds();
|
|
||||||
|
|
||||||
evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/webaudio'), import('@strudel/tonal'));
|
evalScope(
|
||||||
|
controls,
|
||||||
|
import('@strudel/core'),
|
||||||
|
import('@strudel/mini'),
|
||||||
|
import('@strudel/webaudio'),
|
||||||
|
import('@strudel/tonal'),
|
||||||
|
);
|
||||||
|
|
||||||
const { evaluate } = repl({
|
const { evaluate } = repl({
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: webaudioOutput,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export default `samples('github:tidalcycles/dirt-samples')
|
export default `samples('github:tidalcycles/dirt-samples')
|
||||||
setcps(1)
|
|
||||||
stack(
|
stack(
|
||||||
// amen
|
// amen
|
||||||
n("0 1 2 3 4 5 6 7")
|
n("0 1 2 3 4 5 6 7")
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
// this barrel export is currently only used to find undocumented exports
|
// this barrel export is currently only used to find undocumented exports
|
||||||
export * from './packages/codemirror/index.mjs';
|
|
||||||
export * from './packages/core/index.mjs';
|
export * from './packages/core/index.mjs';
|
||||||
export * from './packages/csound/index.mjs';
|
export * from './packages/csound/index.mjs';
|
||||||
export * from './packages/desktopbridge/index.mjs';
|
|
||||||
export * from './packages/draw/index.mjs';
|
|
||||||
export * from './packages/embed/index.mjs';
|
export * from './packages/embed/index.mjs';
|
||||||
export * from './packages/hydra/index.mjs';
|
export * from './packages/desktopbridge/index.mjs';
|
||||||
export * from './packages/midi/index.mjs';
|
export * from './packages/midi/index.mjs';
|
||||||
export * from './packages/mini/index.mjs';
|
export * from './packages/mini/index.mjs';
|
||||||
export * from './packages/osc/index.mjs';
|
export * from './packages/osc/index.mjs';
|
||||||
export * from './packages/repl/index.mjs';
|
export * from './packages/react/index.mjs';
|
||||||
export * from './packages/serial/index.mjs';
|
export * from './packages/serial/index.mjs';
|
||||||
export * from './packages/soundfonts/index.mjs';
|
export * from './packages/soundfonts/index.mjs';
|
||||||
export * from './packages/superdough/index.mjs';
|
|
||||||
export * from './packages/tonal/index.mjs';
|
export * from './packages/tonal/index.mjs';
|
||||||
export * from './packages/transpiler/index.mjs';
|
export * from './packages/transpiler/index.mjs';
|
||||||
export * from './packages/web/index.mjs';
|
|
||||||
export * from './packages/webaudio/index.mjs';
|
export * from './packages/webaudio/index.mjs';
|
||||||
export * from './packages/xen/index.mjs';
|
export * from './packages/xen/index.mjs';
|
||||||
|
|||||||
+2
-3
@@ -25,7 +25,6 @@
|
|||||||
"format-check": "prettier --check .",
|
"format-check": "prettier --check .",
|
||||||
"report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json",
|
"report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json",
|
||||||
"check": "npm run format-check && npm run lint && npm run test",
|
"check": "npm run format-check && npm run lint && npm run test",
|
||||||
"sampler": "cd samples && node ../packages/sampler/sample-server.mjs",
|
|
||||||
"iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf"
|
"iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -54,10 +53,10 @@
|
|||||||
"@strudel/xen": "workspace:*"
|
"@strudel/xen": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"dependency-tree": "^10.0.9",
|
||||||
"@tauri-apps/cli": "^1.5.9",
|
"@tauri-apps/cli": "^1.5.9",
|
||||||
"@vitest/ui": "^1.1.0",
|
"@vitest/ui": "^1.1.0",
|
||||||
"acorn": "^8.11.3",
|
"canvas": "^2.11.2",
|
||||||
"dependency-tree": "^10.0.9",
|
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
|
|||||||
@@ -12,22 +12,19 @@ import {
|
|||||||
lineNumbers,
|
lineNumbers,
|
||||||
drawSelection,
|
drawSelection,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { Pattern, repl } from '@strudel/core';
|
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel/core';
|
||||||
import { Drawer, cleanupDraw } from '@strudel/draw';
|
|
||||||
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
||||||
import { isTooltipEnabled } from './tooltip.mjs';
|
import { isTooltipEnabled } from './tooltip.mjs';
|
||||||
import { flash, isFlashEnabled } from './flash.mjs';
|
import { flash, isFlashEnabled } from './flash.mjs';
|
||||||
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
||||||
import { keybindings } from './keybindings.mjs';
|
import { keybindings } from './keybindings.mjs';
|
||||||
import { initTheme, activateTheme, theme } from './themes.mjs';
|
import { initTheme, activateTheme, theme } from './themes.mjs';
|
||||||
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
import { updateWidgets, sliderPlugin } from './slider.mjs';
|
||||||
import { widgetPlugin, updateWidgets } from './widget.mjs';
|
|
||||||
import { persistentAtom } from '@nanostores/persistent';
|
import { persistentAtom } from '@nanostores/persistent';
|
||||||
|
|
||||||
const extensions = {
|
const extensions = {
|
||||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||||
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
|
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
|
||||||
isBracketClosingEnabled: (on) => (on ? closeBrackets() : []),
|
|
||||||
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
|
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
|
||||||
theme,
|
theme,
|
||||||
isAutoCompletionEnabled,
|
isAutoCompletionEnabled,
|
||||||
@@ -42,7 +39,6 @@ const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [ke
|
|||||||
export const defaultSettings = {
|
export const defaultSettings = {
|
||||||
keybindings: 'codemirror',
|
keybindings: 'codemirror',
|
||||||
isBracketMatchingEnabled: false,
|
isBracketMatchingEnabled: false,
|
||||||
isBracketClosingEnabled: true,
|
|
||||||
isLineNumbersDisplayed: true,
|
isLineNumbersDisplayed: true,
|
||||||
isActiveLineHighlighted: false,
|
isActiveLineHighlighted: false,
|
||||||
isAutoCompletionEnabled: false,
|
isAutoCompletionEnabled: false,
|
||||||
@@ -75,9 +71,9 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
...initialSettings,
|
...initialSettings,
|
||||||
javascript(),
|
javascript(),
|
||||||
sliderPlugin,
|
sliderPlugin,
|
||||||
widgetPlugin,
|
|
||||||
// indentOnInput(), // works without. already brought with javascript extension?
|
// indentOnInput(), // works without. already brought with javascript extension?
|
||||||
// bracketMatching(), // does not do anything
|
// bracketMatching(), // does not do anything
|
||||||
|
closeBrackets(),
|
||||||
syntaxHighlighting(defaultHighlightStyle),
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
history(),
|
history(),
|
||||||
EditorView.updateListener.of((v) => onChange(v)),
|
EditorView.updateListener.of((v) => onChange(v)),
|
||||||
@@ -129,7 +125,6 @@ export class StrudelMirror {
|
|||||||
id,
|
id,
|
||||||
initialCode = '',
|
initialCode = '',
|
||||||
onDraw,
|
onDraw,
|
||||||
drawContext,
|
|
||||||
drawTime = [0, 0],
|
drawTime = [0, 0],
|
||||||
autodraw,
|
autodraw,
|
||||||
prebake,
|
prebake,
|
||||||
@@ -142,16 +137,23 @@ export class StrudelMirror {
|
|||||||
this.widgets = [];
|
this.widgets = [];
|
||||||
this.painters = [];
|
this.painters = [];
|
||||||
this.drawTime = drawTime;
|
this.drawTime = drawTime;
|
||||||
this.drawContext = drawContext;
|
this.onDraw = onDraw;
|
||||||
this.onDraw = onDraw || this.draw;
|
const self = this;
|
||||||
this.id = id || s4();
|
this.id = id || s4();
|
||||||
|
|
||||||
this.drawer = new Drawer((haps, time) => {
|
this.drawer = new Drawer((haps, time) => {
|
||||||
const currentFrame = haps.filter((hap) => hap.isActive(time));
|
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
|
||||||
this.highlight(currentFrame, time);
|
this.highlight(currentFrame, time);
|
||||||
this.onDraw(haps, time, this.painters);
|
this.onDraw?.(haps, time, currentFrame, this.painters);
|
||||||
}, drawTime);
|
}, drawTime);
|
||||||
|
|
||||||
|
// this approach does not work with multiple repls on screen
|
||||||
|
// TODO: refactor onPaint usages + find fix, maybe remove painters here?
|
||||||
|
Pattern.prototype.onPaint = function (onPaint) {
|
||||||
|
self.painters.push(onPaint);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
this.prebaked = prebake();
|
this.prebaked = prebake();
|
||||||
autodraw && this.drawFirstFrame();
|
autodraw && this.drawFirstFrame();
|
||||||
|
|
||||||
@@ -177,14 +179,6 @@ export class StrudelMirror {
|
|||||||
beforeEval: async () => {
|
beforeEval: async () => {
|
||||||
cleanupDraw();
|
cleanupDraw();
|
||||||
this.painters = [];
|
this.painters = [];
|
||||||
const self = this;
|
|
||||||
// this is similar to repl.mjs > injectPatternMethods
|
|
||||||
// maybe there is a solution without prototype hacking, but hey, it works
|
|
||||||
// we need to do this befor every eval to make sure it works with multiple StrudelMirror's side by side
|
|
||||||
Pattern.prototype.onPaint = function (onPaint) {
|
|
||||||
self.painters.push(onPaint);
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
await this.prebaked;
|
await this.prebaked;
|
||||||
await replOptions?.beforeEval?.();
|
await replOptions?.beforeEval?.();
|
||||||
},
|
},
|
||||||
@@ -192,10 +186,7 @@ export class StrudelMirror {
|
|||||||
// remember for when highlighting is toggled on
|
// remember for when highlighting is toggled on
|
||||||
this.miniLocations = options.meta?.miniLocations;
|
this.miniLocations = options.meta?.miniLocations;
|
||||||
this.widgets = options.meta?.widgets;
|
this.widgets = options.meta?.widgets;
|
||||||
const sliders = this.widgets.filter((w) => w.type === 'slider');
|
updateWidgets(this.editor, this.widgets);
|
||||||
updateSliderWidgets(this.editor, sliders);
|
|
||||||
const widgets = this.widgets.filter((w) => w.type !== 'slider');
|
|
||||||
updateWidgets(this.editor, widgets);
|
|
||||||
updateMiniLocations(this.editor, this.miniLocations);
|
updateMiniLocations(this.editor, this.miniLocations);
|
||||||
replOptions?.afterEval?.(options);
|
replOptions?.afterEval?.(options);
|
||||||
this.adjustDrawTime();
|
this.adjustDrawTime();
|
||||||
@@ -239,9 +230,6 @@ export class StrudelMirror {
|
|||||||
// when no painters are set, [0,0] is enough (just highlighting)
|
// when no painters are set, [0,0] is enough (just highlighting)
|
||||||
this.drawer.setDrawTime(this.painters.length ? this.drawTime : [0, 0]);
|
this.drawer.setDrawTime(this.painters.length ? this.drawTime : [0, 0]);
|
||||||
}
|
}
|
||||||
draw(haps, time) {
|
|
||||||
this.painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
|
|
||||||
}
|
|
||||||
async drawFirstFrame() {
|
async drawFirstFrame() {
|
||||||
if (!this.onDraw) {
|
if (!this.onDraw) {
|
||||||
return;
|
return;
|
||||||
@@ -252,7 +240,7 @@ export class StrudelMirror {
|
|||||||
await this.repl.evaluate(this.code, false);
|
await this.repl.evaluate(this.code, false);
|
||||||
this.drawer.invalidate(this.repl.scheduler, -0.001);
|
this.drawer.invalidate(this.repl.scheduler, -0.001);
|
||||||
// draw at -0.001 to avoid haps at 0 to be visualized as active
|
// draw at -0.001 to avoid haps at 0 to be visualized as active
|
||||||
this.onDraw?.(this.drawer.visibleHaps, -0.001, this.painters);
|
this.onDraw?.(this.drawer.visibleHaps, -0.001, [], this.painters);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('first frame could not be painted');
|
console.warn('first frame could not be painted');
|
||||||
}
|
}
|
||||||
@@ -310,9 +298,6 @@ export class StrudelMirror {
|
|||||||
setLineNumbersDisplayed(enabled) {
|
setLineNumbersDisplayed(enabled) {
|
||||||
this.reconfigureExtension('isLineNumbersDisplayed', enabled);
|
this.reconfigureExtension('isLineNumbersDisplayed', enabled);
|
||||||
}
|
}
|
||||||
setBracketClosingEnabled(enabled) {
|
|
||||||
this.reconfigureExtension('isBracketClosingEnabled', enabled);
|
|
||||||
}
|
|
||||||
setTheme(theme) {
|
setTheme(theme) {
|
||||||
this.reconfigureExtension('theme', theme);
|
this.reconfigureExtension('theme', theme);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
|
|||||||
|
|
||||||
if (haps.has(id)) {
|
if (haps.has(id)) {
|
||||||
const hap = haps.get(id);
|
const hap = haps.get(id);
|
||||||
const color = hap.value?.color ?? 'var(--foreground)';
|
const color = hap.context.color ?? 'var(--foreground)';
|
||||||
// Get explicit channels for color values
|
// Get explicit channels for color values
|
||||||
/*
|
/*
|
||||||
const swatch = document.createElement('div');
|
const swatch = document.createElement('div');
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ export * from './highlight.mjs';
|
|||||||
export * from './flash.mjs';
|
export * from './flash.mjs';
|
||||||
export * from './slider.mjs';
|
export * from './slider.mjs';
|
||||||
export * from './themes.mjs';
|
export * from './themes.mjs';
|
||||||
export * from './widget.mjs';
|
|
||||||
|
|||||||
@@ -45,8 +45,6 @@
|
|||||||
"@replit/codemirror-vim": "^6.1.0",
|
"@replit/codemirror-vim": "^6.1.0",
|
||||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
|
||||||
"@strudel/transpiler": "workspace:*",
|
|
||||||
"@uiw/codemirror-themes": "^4.21.21",
|
"@uiw/codemirror-themes": "^4.21.21",
|
||||||
"@uiw/codemirror-themes-all": "^4.21.21",
|
"@uiw/codemirror-themes-all": "^4.21.21",
|
||||||
"nanostores": "^0.9.5"
|
"nanostores": "^0.9.5"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ref, pure } from '@strudel/core';
|
import { ref, pure } from '@strudel/core';
|
||||||
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
|
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
|
||||||
import { StateEffect } from '@codemirror/state';
|
import { StateEffect, StateField } from '@codemirror/state';
|
||||||
|
|
||||||
export let sliderValues = {};
|
export let sliderValues = {};
|
||||||
const getSliderID = (from) => `slider_${from}`;
|
const getSliderID = (from) => `slider_${from}`;
|
||||||
@@ -60,16 +60,14 @@ export class SliderWidget extends WidgetType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setSliderWidgets = StateEffect.define();
|
export const setWidgets = StateEffect.define();
|
||||||
|
|
||||||
export const updateSliderWidgets = (view, widgets) => {
|
export const updateWidgets = (view, widgets) => {
|
||||||
view.dispatch({ effects: setSliderWidgets.of(widgets) });
|
view.dispatch({ effects: setWidgets.of(widgets) });
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSliders(widgetConfigs, view) {
|
function getWidgets(widgetConfigs, view) {
|
||||||
return widgetConfigs
|
return widgetConfigs.map(({ from, to, value, min, max, step }) => {
|
||||||
.filter((w) => w.type === 'slider')
|
|
||||||
.map(({ from, to, value, min, max, step }) => {
|
|
||||||
return Decoration.widget({
|
return Decoration.widget({
|
||||||
widget: new SliderWidget(value, min, max, from, to, step, view),
|
widget: new SliderWidget(value, min, max, from, to, step, view),
|
||||||
side: 0,
|
side: 0,
|
||||||
@@ -101,8 +99,8 @@ export const sliderPlugin = ViewPlugin.fromClass(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let e of tr.effects) {
|
for (let e of tr.effects) {
|
||||||
if (e.is(setSliderWidgets)) {
|
if (e.is(setWidgets)) {
|
||||||
this.decorations = Decoration.set(getSliders(e.value, update.view));
|
this.decorations = Decoration.set(getWidgets(e.value, update.view));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
-2
@@ -37,7 +37,6 @@ import whitescreen, { settings as whitescreenSettings } from './themes/whitescre
|
|||||||
import teletext, { settings as teletextSettings } from './themes/teletext';
|
import teletext, { settings as teletextSettings } from './themes/teletext';
|
||||||
import algoboy, { settings as algoboySettings } from './themes/algoboy';
|
import algoboy, { settings as algoboySettings } from './themes/algoboy';
|
||||||
import terminal, { settings as terminalSettings } from './themes/terminal';
|
import terminal, { settings as terminalSettings } from './themes/terminal';
|
||||||
import { setTheme } from '@strudel/draw';
|
|
||||||
|
|
||||||
export const themes = {
|
export const themes = {
|
||||||
strudelTheme,
|
strudelTheme,
|
||||||
@@ -514,7 +513,6 @@ export function activateTheme(name) {
|
|||||||
.map(([key, value]) => `--${key}: ${value} !important;`)
|
.map(([key, value]) => `--${key}: ${value} !important;`)
|
||||||
.join('\n')}
|
.join('\n')}
|
||||||
}`;
|
}`;
|
||||||
setTheme(themeSettings);
|
|
||||||
// tailwind dark mode
|
// tailwind dark mode
|
||||||
if (themeSettings.light) {
|
if (themeSettings.light) {
|
||||||
document.documentElement.classList.remove('dark');
|
document.documentElement.classList.remove('dark');
|
||||||
|
|||||||
-1
@@ -18,7 +18,6 @@ export default createTheme({
|
|||||||
theme: 'light',
|
theme: 'light',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
{ tag: t.labelName, color: '#0f380f' },
|
|
||||||
{ tag: t.keyword, color: '#0f380f' },
|
{ tag: t.keyword, color: '#0f380f' },
|
||||||
{ tag: t.operator, color: '#0f380f' },
|
{ tag: t.operator, color: '#0f380f' },
|
||||||
{ tag: t.special(t.variableName), color: '#0f380f' },
|
{ tag: t.special(t.variableName), color: '#0f380f' },
|
||||||
|
|||||||
-1
@@ -15,7 +15,6 @@ export default createTheme({
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
{ tag: t.labelName, color: 'white' },
|
|
||||||
{ tag: t.keyword, color: 'white' },
|
{ tag: t.keyword, color: 'white' },
|
||||||
{ tag: t.operator, color: 'white' },
|
{ tag: t.operator, color: 'white' },
|
||||||
{ tag: t.special(t.variableName), color: 'white' },
|
{ tag: t.special(t.variableName), color: 'white' },
|
||||||
|
|||||||
-1
@@ -18,7 +18,6 @@ export default createTheme({
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
{ tag: t.labelName, color: 'white' },
|
|
||||||
{ tag: t.keyword, color: 'white' },
|
{ tag: t.keyword, color: 'white' },
|
||||||
{ tag: t.operator, color: 'white' },
|
{ tag: t.operator, color: 'white' },
|
||||||
{ tag: t.special(t.variableName), color: 'white' },
|
{ tag: t.special(t.variableName), color: 'white' },
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export default createTheme({
|
|||||||
gutterForeground: '#8a919966',
|
gutterForeground: '#8a919966',
|
||||||
},
|
},
|
||||||
styles: [
|
styles: [
|
||||||
{ tag: t.labelName, color: '#89ddff' },
|
|
||||||
{ tag: t.keyword, color: '#c792ea' },
|
{ tag: t.keyword, color: '#c792ea' },
|
||||||
{ tag: t.operator, color: '#89ddff' },
|
{ tag: t.operator, color: '#89ddff' },
|
||||||
{ tag: t.special(t.variableName), color: '#eeffff' },
|
{ tag: t.special(t.variableName), color: '#eeffff' },
|
||||||
|
|||||||
-1
@@ -27,7 +27,6 @@ export default createTheme({
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
{ tag: t.labelName, color: colorB },
|
|
||||||
{ tag: t.keyword, color: colorA },
|
{ tag: t.keyword, color: colorA },
|
||||||
{ tag: t.operator, color: mini },
|
{ tag: t.operator, color: mini },
|
||||||
{ tag: t.special(t.variableName), color: colorA },
|
{ tag: t.special(t.variableName), color: colorA },
|
||||||
|
|||||||
-1
@@ -14,7 +14,6 @@ export default createTheme({
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
{ tag: t.labelName, color: '#41FF00' },
|
|
||||||
{ tag: t.keyword, color: '#41FF00' },
|
{ tag: t.keyword, color: '#41FF00' },
|
||||||
{ tag: t.operator, color: '#41FF00' },
|
{ tag: t.operator, color: '#41FF00' },
|
||||||
{ tag: t.special(t.variableName), color: '#41FF00' },
|
{ tag: t.special(t.variableName), color: '#41FF00' },
|
||||||
|
|||||||
-1
@@ -16,7 +16,6 @@ export default createTheme({
|
|||||||
theme: 'light',
|
theme: 'light',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
{ tag: t.labelName, color: 'black' },
|
|
||||||
{ tag: t.keyword, color: 'black' },
|
{ tag: t.keyword, color: 'black' },
|
||||||
{ tag: t.operator, color: 'black' },
|
{ tag: t.operator, color: 'black' },
|
||||||
{ tag: t.special(t.variableName), color: 'black' },
|
{ tag: t.special(t.variableName), color: 'black' },
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
import { StateEffect, StateField } from '@codemirror/state';
|
|
||||||
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
|
||||||
import { getWidgetID, registerWidgetType } from '@strudel/transpiler';
|
|
||||||
import { Pattern } from '@strudel/core';
|
|
||||||
|
|
||||||
export const addWidget = StateEffect.define({
|
|
||||||
map: ({ from, to }, change) => {
|
|
||||||
return { from: change.mapPos(from), to: change.mapPos(to) };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateWidgets = (view, widgets) => {
|
|
||||||
view.dispatch({ effects: addWidget.of(widgets) });
|
|
||||||
};
|
|
||||||
|
|
||||||
function getWidgets(widgetConfigs) {
|
|
||||||
return (
|
|
||||||
widgetConfigs
|
|
||||||
// codemirror throws an error if we don't sort
|
|
||||||
.sort((a, b) => a.to - b.to)
|
|
||||||
.map((widgetConfig) => {
|
|
||||||
return Decoration.widget({
|
|
||||||
widget: new BlockWidget(widgetConfig),
|
|
||||||
side: 0,
|
|
||||||
block: true,
|
|
||||||
}).range(widgetConfig.to);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const widgetField = StateField.define(
|
|
||||||
/* <DecorationSet> */ {
|
|
||||||
create() {
|
|
||||||
return Decoration.none;
|
|
||||||
},
|
|
||||||
update(widgets, tr) {
|
|
||||||
widgets = widgets.map(tr.changes);
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(addWidget)) {
|
|
||||||
try {
|
|
||||||
widgets = widgets.update({
|
|
||||||
filter: () => false,
|
|
||||||
add: getWidgets(e.value),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log('err', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return widgets;
|
|
||||||
},
|
|
||||||
provide: (f) => EditorView.decorations.from(f),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const widgetElements = {};
|
|
||||||
export function setWidget(id, el) {
|
|
||||||
widgetElements[id] = el;
|
|
||||||
el.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BlockWidget extends WidgetType {
|
|
||||||
constructor(widgetConfig) {
|
|
||||||
super();
|
|
||||||
this.widgetConfig = widgetConfig;
|
|
||||||
}
|
|
||||||
eq() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
toDOM() {
|
|
||||||
const id = getWidgetID(this.widgetConfig);
|
|
||||||
const el = widgetElements[id];
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
ignoreEvent(e) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const widgetPlugin = [widgetField];
|
|
||||||
|
|
||||||
// widget implementer API to create a new widget type
|
|
||||||
export function registerWidget(type, fn) {
|
|
||||||
registerWidgetType(type);
|
|
||||||
if (fn) {
|
|
||||||
Pattern.prototype[type] = function (id, options = { fold: 1 }) {
|
|
||||||
// fn is expected to create a dom element and call setWidget(id, el);
|
|
||||||
// fn should also return the pattern
|
|
||||||
return fn(id, options, this);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// wire up @strudel/draw functions
|
|
||||||
|
|
||||||
function getCanvasWidget(id, options = {}) {
|
|
||||||
const { width = 500, height = 60, pixelRatio = window.devicePixelRatio } = options;
|
|
||||||
let canvas = document.getElementById(id) || document.createElement('canvas');
|
|
||||||
canvas.width = width * pixelRatio;
|
|
||||||
canvas.height = height * pixelRatio;
|
|
||||||
canvas.style.width = width + 'px';
|
|
||||||
canvas.style.height = height + 'px';
|
|
||||||
setWidget(id, canvas);
|
|
||||||
return canvas;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerWidget('_pianoroll', (id, options = {}, pat) => {
|
|
||||||
const ctx = getCanvasWidget(id, options).getContext('2d');
|
|
||||||
return pat.tag(id).pianoroll({ fold: 1, ...options, ctx, id });
|
|
||||||
});
|
|
||||||
|
|
||||||
registerWidget('_punchcard', (id, options = {}, pat) => {
|
|
||||||
const ctx = getCanvasWidget(id, options).getContext('2d');
|
|
||||||
return pat.tag(id).punchcard({ fold: 1, ...options, ctx, id });
|
|
||||||
});
|
|
||||||
|
|
||||||
registerWidget('_spiral', (id, options = {}, pat) => {
|
|
||||||
let _size = options.size || 275;
|
|
||||||
options = { width: _size, height: _size, ...options, size: _size / 5 };
|
|
||||||
const ctx = getCanvasWidget(id, options).getContext('2d');
|
|
||||||
return pat.tag(id).spiral({ ...options, ctx, id });
|
|
||||||
});
|
|
||||||
|
|
||||||
registerWidget('_scope', (id, options = {}, pat) => {
|
|
||||||
options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options };
|
|
||||||
const ctx = getCanvasWidget(id, options).getContext('2d');
|
|
||||||
return pat.tag(id).scope({ ...options, ctx, id });
|
|
||||||
});
|
|
||||||
|
|
||||||
registerWidget('_pitchwheel', (id, options = {}, pat) => {
|
|
||||||
let _size = options.size || 200;
|
|
||||||
options = { width: _size, height: _size, ...options, size: _size / 5 };
|
|
||||||
const ctx = getCanvasWidget(id, options).getContext('2d');
|
|
||||||
return pat.pitchwheel({ ...options, ctx, id });
|
|
||||||
});
|
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import { Pattern, silence, register, pure, createParams } from '@strudel/core';
|
import { Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
||||||
import { getDrawContext } from './draw.mjs';
|
import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
|
||||||
|
const { createParams } = controls;
|
||||||
|
|
||||||
let clearColor = '#22222210';
|
let clearColor = '#22222210';
|
||||||
|
|
||||||
Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } = {}) {
|
Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } = {}) {
|
||||||
window.frame && cancelAnimationFrame(window.frame);
|
window.frame && cancelAnimationFrame(window.frame);
|
||||||
const ctx = getDrawContext();
|
const ctx = getDrawContext();
|
||||||
let { clientWidth: ww, clientHeight: wh } = ctx.canvas;
|
const { clientWidth: ww, clientHeight: wh } = ctx.canvas;
|
||||||
ww *= window.devicePixelRatio;
|
|
||||||
wh *= window.devicePixelRatio;
|
|
||||||
let smearPart = smear === 0 ? '99' : Number((1 - smear) * 100).toFixed(0);
|
let smearPart = smear === 0 ? '99' : Number((1 - smear) * 100).toFixed(0);
|
||||||
smearPart = smearPart.length === 1 ? `0${smearPart}` : smearPart;
|
smearPart = smearPart.length === 1 ? `0${smearPart}` : smearPart;
|
||||||
clearColor = `#200010${smearPart}`;
|
clearColor = `#200010${smearPart}`;
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
// eslint-disable-next-line no-undef
|
|
||||||
// TODO: swap below line with above one when firefox supports esm imports in service workers
|
|
||||||
// see https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker?retiredLocale=de#browser_compatibility
|
|
||||||
// import createClock from './zyklus.mjs';
|
|
||||||
|
|
||||||
function getTime() {
|
|
||||||
const precision = 10 ** 4;
|
|
||||||
const seconds = performance.now() / 1000;
|
|
||||||
return Math.round(seconds * precision) / precision;
|
|
||||||
}
|
|
||||||
|
|
||||||
let num_cycles_at_cps_change = 0;
|
|
||||||
let num_ticks_since_cps_change = 0;
|
|
||||||
let num_seconds_at_cps_change = 0;
|
|
||||||
let cps = 0.5;
|
|
||||||
// {id: {started: boolean}}
|
|
||||||
const clients = new Map();
|
|
||||||
const duration = 0.1;
|
|
||||||
const channel = new BroadcastChannel('strudeltick');
|
|
||||||
|
|
||||||
const sendMessage = (type, payload) => {
|
|
||||||
channel.postMessage({ type, payload });
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendTick = (phase, duration, tick, time) => {
|
|
||||||
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
|
|
||||||
|
|
||||||
const tickdeadline = phase - time;
|
|
||||||
const lastTick = time + tickdeadline;
|
|
||||||
const num_cycles_since_cps_change = num_seconds_since_cps_change * cps;
|
|
||||||
|
|
||||||
const begin = num_cycles_at_cps_change + num_cycles_since_cps_change;
|
|
||||||
const secondsSinceLastTick = time - lastTick - duration;
|
|
||||||
|
|
||||||
const eventLength = duration * cps;
|
|
||||||
const end = begin + eventLength;
|
|
||||||
|
|
||||||
const cycle = begin + secondsSinceLastTick * cps;
|
|
||||||
|
|
||||||
sendMessage('tick', {
|
|
||||||
begin,
|
|
||||||
end,
|
|
||||||
cps,
|
|
||||||
tickdeadline,
|
|
||||||
num_cycles_at_cps_change,
|
|
||||||
num_seconds_at_cps_change,
|
|
||||||
num_seconds_since_cps_change,
|
|
||||||
cycle,
|
|
||||||
});
|
|
||||||
num_ticks_since_cps_change++;
|
|
||||||
};
|
|
||||||
|
|
||||||
//create clock method from zyklus
|
|
||||||
const clock = createClock(getTime, sendTick, duration);
|
|
||||||
let started = false;
|
|
||||||
|
|
||||||
const startClock = (id) => {
|
|
||||||
clients.set(id, { started: true });
|
|
||||||
if (started) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clock.start();
|
|
||||||
started = true;
|
|
||||||
};
|
|
||||||
const stopClock = async (id) => {
|
|
||||||
clients.set(id, { started: false });
|
|
||||||
|
|
||||||
const otherClientStarted = Array.from(clients.values()).some((c) => c.started);
|
|
||||||
//dont stop the clock if other instances are running...
|
|
||||||
if (!started || otherClientStarted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clock.stop();
|
|
||||||
setCycle(0);
|
|
||||||
started = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setCycle = (cycle) => {
|
|
||||||
num_ticks_since_cps_change = 0;
|
|
||||||
num_cycles_at_cps_change = cycle;
|
|
||||||
};
|
|
||||||
|
|
||||||
const processMessage = (message) => {
|
|
||||||
const { type, payload } = message;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'cpschange': {
|
|
||||||
if (payload.cps !== cps) {
|
|
||||||
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
|
|
||||||
num_cycles_at_cps_change = num_cycles_at_cps_change + num_seconds_since_cps_change * cps;
|
|
||||||
num_seconds_at_cps_change = num_seconds_at_cps_change + num_seconds_since_cps_change;
|
|
||||||
cps = payload.cps;
|
|
||||||
num_ticks_since_cps_change = 0;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'setcycle': {
|
|
||||||
setCycle(payload.cycle);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'toggle': {
|
|
||||||
if (payload.started) {
|
|
||||||
startClock(message.id);
|
|
||||||
} else {
|
|
||||||
stopClock(message.id);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.onconnect = function (e) {
|
|
||||||
// the incoming port
|
|
||||||
const port = e.ports[0];
|
|
||||||
|
|
||||||
port.addEventListener('message', function (e) {
|
|
||||||
processMessage(e.data);
|
|
||||||
});
|
|
||||||
port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
|
|
||||||
};
|
|
||||||
|
|
||||||
// used to consistently schedule events, for use in a service worker - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/clockworker.mjs>
|
|
||||||
function createClock(
|
|
||||||
getTime,
|
|
||||||
callback, // called slightly before each cycle
|
|
||||||
duration = 0.05, // duration of each cycle
|
|
||||||
interval = 0.1, // interval between callbacks
|
|
||||||
overlap = 0.1, // overlap between callbacks
|
|
||||||
) {
|
|
||||||
let tick = 0; // counts callbacks
|
|
||||||
let phase = 0; // next callback time
|
|
||||||
let precision = 10 ** 4; // used to round phase
|
|
||||||
let minLatency = 0.01;
|
|
||||||
const setDuration = (setter) => (duration = setter(duration));
|
|
||||||
overlap = overlap || interval / 2;
|
|
||||||
const onTick = () => {
|
|
||||||
const t = getTime();
|
|
||||||
const lookahead = t + interval + overlap; // the time window for this tick
|
|
||||||
if (phase === 0) {
|
|
||||||
phase = t + minLatency;
|
|
||||||
}
|
|
||||||
// callback as long as we're inside the lookahead
|
|
||||||
while (phase < lookahead) {
|
|
||||||
phase = Math.round(phase * precision) / precision;
|
|
||||||
phase >= t && callback(phase, duration, tick, t);
|
|
||||||
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
|
|
||||||
phase += duration; // increment phase by duration
|
|
||||||
tick++;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let intervalID;
|
|
||||||
const start = () => {
|
|
||||||
clear(); // just in case start was called more than once
|
|
||||||
onTick();
|
|
||||||
intervalID = setInterval(onTick, interval * 1000);
|
|
||||||
};
|
|
||||||
const clear = () => intervalID !== undefined && clearInterval(intervalID);
|
|
||||||
const pause = () => clear();
|
|
||||||
const stop = () => {
|
|
||||||
tick = 0;
|
|
||||||
phase = 0;
|
|
||||||
clear();
|
|
||||||
};
|
|
||||||
const getPhase = () => phase;
|
|
||||||
// setCallback
|
|
||||||
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
|
|
||||||
}
|
|
||||||
+445
-530
File diff suppressed because it is too large
Load Diff
+20
-33
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs>
|
cyclist.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
@@ -8,7 +8,7 @@ import createClock from './zyklus.mjs';
|
|||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
export class Cyclist {
|
export class Cyclist {
|
||||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) {
|
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||||
this.started = false;
|
this.started = false;
|
||||||
this.cps = 0.5;
|
this.cps = 0.5;
|
||||||
this.num_ticks_since_cps_change = 0;
|
this.num_ticks_since_cps_change = 0;
|
||||||
@@ -16,47 +16,41 @@ export class Cyclist {
|
|||||||
this.lastBegin = 0; // query begin of last tick
|
this.lastBegin = 0; // query begin of last tick
|
||||||
this.lastEnd = 0; // query end of last tick
|
this.lastEnd = 0; // query end of last tick
|
||||||
this.getTime = getTime; // get absolute time
|
this.getTime = getTime; // get absolute time
|
||||||
this.num_cycles_at_cps_change = 0;
|
this.num_cycles_since_last_cps_change = 0;
|
||||||
this.seconds_at_cps_change; // clock phase when cps was changed
|
|
||||||
this.onToggle = onToggle;
|
this.onToggle = onToggle;
|
||||||
this.latency = latency; // fixed trigger time offset
|
this.latency = latency; // fixed trigger time offset
|
||||||
this.clock = createClock(
|
this.clock = createClock(
|
||||||
getTime,
|
getTime,
|
||||||
// called slightly before each cycle
|
// called slightly before each cycle
|
||||||
(phase, duration, _, t) => {
|
(phase, duration, tick) => {
|
||||||
|
if (tick === 0) {
|
||||||
|
this.origin = phase;
|
||||||
|
}
|
||||||
if (this.num_ticks_since_cps_change === 0) {
|
if (this.num_ticks_since_cps_change === 0) {
|
||||||
this.num_cycles_at_cps_change = this.lastEnd;
|
this.num_cycles_since_last_cps_change = this.lastEnd;
|
||||||
this.seconds_at_cps_change = phase;
|
|
||||||
}
|
}
|
||||||
this.num_ticks_since_cps_change++;
|
this.num_ticks_since_cps_change++;
|
||||||
const seconds_since_cps_change = this.num_ticks_since_cps_change * duration;
|
|
||||||
const num_cycles_since_cps_change = seconds_since_cps_change * this.cps;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const time = getTime();
|
||||||
const begin = this.lastEnd;
|
const begin = this.lastEnd;
|
||||||
this.lastBegin = begin;
|
this.lastBegin = begin;
|
||||||
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
|
|
||||||
this.lastEnd = end;
|
|
||||||
this.lastTick = phase;
|
|
||||||
|
|
||||||
if (phase < t) {
|
//convert ticks to cycles, so you can query the pattern for events
|
||||||
// avoid querying haps that are in the past anyway
|
const eventLength = duration * this.cps;
|
||||||
console.log(`skip query: too late`);
|
const end = this.num_cycles_since_last_cps_change + this.num_ticks_since_cps_change * eventLength;
|
||||||
return;
|
this.lastEnd = end;
|
||||||
}
|
|
||||||
|
|
||||||
// query the pattern for events
|
// query the pattern for events
|
||||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||||
|
|
||||||
|
const tickdeadline = phase - time; // time left until the phase is a whole number
|
||||||
|
this.lastTick = time + tickdeadline;
|
||||||
|
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
if (hap.hasOnset()) {
|
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||||
const targetTime =
|
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
|
||||||
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
|
||||||
const duration = hap.duration / this.cps;
|
const duration = hap.duration / this.cps;
|
||||||
// the following line is dumb and only here for backwards compatibility
|
onTrigger?.(hap, deadline, duration, this.cps);
|
||||||
// see https://github.com/tidalcycles/strudel/pull/1004
|
|
||||||
const deadline = targetTime - phase;
|
|
||||||
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -65,16 +59,9 @@ export class Cyclist {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
interval, // duration of each cycle
|
interval, // duration of each cycle
|
||||||
0.1,
|
|
||||||
0.1,
|
|
||||||
setInterval,
|
|
||||||
clearInterval,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
now() {
|
now() {
|
||||||
if (!this.started) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
|
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
|
||||||
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
|
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
|
||||||
}
|
}
|
||||||
@@ -84,7 +71,7 @@ export class Cyclist {
|
|||||||
}
|
}
|
||||||
start() {
|
start() {
|
||||||
this.num_ticks_since_cps_change = 0;
|
this.num_ticks_since_cps_change = 0;
|
||||||
this.num_cycles_at_cps_change = 0;
|
this.num_cycles_since_last_cps_change = 0;
|
||||||
if (!this.pattern) {
|
if (!this.pattern) {
|
||||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +1,80 @@
|
|||||||
/*
|
/*
|
||||||
draw.mjs - <short description TODO>
|
draw.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/canvas/draw.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, getTime, State, TimeSpan } from '@strudel/core';
|
import { Pattern, getTime, State, TimeSpan } from './index.mjs';
|
||||||
|
|
||||||
export const getDrawContext = (id = 'test-canvas', options) => {
|
export const getDrawContext = (id = 'test-canvas') => {
|
||||||
let { contextType = '2d', pixelated = false, pixelRatio = window.devicePixelRatio } = options || {};
|
|
||||||
let canvas = document.querySelector('#' + id);
|
let canvas = document.querySelector('#' + id);
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
|
const scale = 2; // 2 = crisp on retina screens
|
||||||
canvas = document.createElement('canvas');
|
canvas = document.createElement('canvas');
|
||||||
canvas.id = id;
|
canvas.id = id;
|
||||||
canvas.width = window.innerWidth * pixelRatio;
|
canvas.width = window.innerWidth * scale;
|
||||||
canvas.height = window.innerHeight * pixelRatio;
|
canvas.height = window.innerHeight * scale;
|
||||||
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
|
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
|
||||||
pixelated && (canvas.style.imageRendering = 'pixelated');
|
|
||||||
document.body.prepend(canvas);
|
document.body.prepend(canvas);
|
||||||
let timeout;
|
let timeout;
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
timeout && clearTimeout(timeout);
|
timeout && clearTimeout(timeout);
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
canvas.width = window.innerWidth * pixelRatio;
|
canvas.width = window.innerWidth * scale;
|
||||||
canvas.height = window.innerHeight * pixelRatio;
|
canvas.height = window.innerHeight * scale;
|
||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return canvas.getContext(contextType);
|
return canvas.getContext('2d');
|
||||||
};
|
};
|
||||||
|
|
||||||
let animationFrames = {};
|
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
|
||||||
function stopAnimationFrame(id) {
|
|
||||||
if (animationFrames[id] !== undefined) {
|
|
||||||
cancelAnimationFrame(animationFrames[id]);
|
|
||||||
delete animationFrames[id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function stopAllAnimations() {
|
|
||||||
Object.keys(animationFrames).forEach((id) => stopAnimationFrame(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
let memory = {};
|
|
||||||
Pattern.prototype.draw = function (fn, options) {
|
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
let { id = 1, lookbehind = 0, lookahead = 0 } = options;
|
if (window.strudelAnimation) {
|
||||||
let __t = Math.max(getTime(), 0);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
stopAnimationFrame(id);
|
}
|
||||||
lookbehind = Math.abs(lookbehind);
|
const ctx = getDrawContext();
|
||||||
// init memory, clear future haps of old pattern
|
let cycle,
|
||||||
memory[id] = (memory[id] || []).filter((h) => !h.isInFuture(__t));
|
events = [];
|
||||||
let newFuture = this.queryArc(__t, __t + lookahead).filter((h) => h.hasOnset());
|
const animate = (time) => {
|
||||||
memory[id] = memory[id].concat(newFuture);
|
const t = getTime();
|
||||||
|
if (from !== undefined && to !== undefined) {
|
||||||
let last;
|
const currentCycle = Math.floor(t);
|
||||||
const animate = () => {
|
if (cycle !== currentCycle) {
|
||||||
const _t = getTime();
|
cycle = currentCycle;
|
||||||
const t = _t + lookahead;
|
const begin = currentCycle + from;
|
||||||
// filter out haps that are too far in the past
|
const end = currentCycle + to;
|
||||||
memory[id] = memory[id].filter((h) => h.isInNearPast(lookbehind, _t));
|
setTimeout(() => {
|
||||||
// begin where we left off in last frame, but max -0.1s (inactive tab throttles to 1fps)
|
events = this.query(new State(new TimeSpan(begin, end)))
|
||||||
let begin = Math.max(last || t, t - 1 / 10);
|
.filter(Boolean)
|
||||||
const haps = this.queryArc(begin, t).filter((h) => h.hasOnset());
|
.filter((event) => event.part.begin.equals(event.whole.begin));
|
||||||
memory[id] = memory[id].concat(haps);
|
onQuery?.(events);
|
||||||
last = t; // makes sure no haps are missed
|
}, 0);
|
||||||
fn(memory[id], _t, t, this);
|
}
|
||||||
animationFrames[id] = requestAnimationFrame(animate);
|
}
|
||||||
|
callback(ctx, events, t, time);
|
||||||
|
window.strudelAnimation = requestAnimationFrame(animate);
|
||||||
};
|
};
|
||||||
animationFrames[id] = requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanupDraw = (clearScreen = true) => {
|
export const cleanupDraw = (clearScreen = true) => {
|
||||||
const ctx = getDrawContext();
|
const ctx = getDrawContext();
|
||||||
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
|
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
|
||||||
stopAllAnimations();
|
if (window.strudelAnimation) {
|
||||||
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
|
}
|
||||||
if (window.strudelScheduler) {
|
if (window.strudelScheduler) {
|
||||||
clearInterval(window.strudelScheduler);
|
clearInterval(window.strudelScheduler);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.onPaint = function () {
|
Pattern.prototype.onPaint = function (onPaint) {
|
||||||
console.warn('[draw] onPaint was not overloaded. Some drawings might not work');
|
// this is evil! TODO: add pattern.context
|
||||||
|
this.context = { onPaint };
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,7 +134,7 @@ export class Drawer {
|
|||||||
this.lastFrame = phase;
|
this.lastFrame = phase;
|
||||||
this.visibleHaps = (this.visibleHaps || [])
|
this.visibleHaps = (this.visibleHaps || [])
|
||||||
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
|
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
|
||||||
.filter((h) => h.endClipped >= phase - lookbehind - lookahead)
|
.filter((h) => h.whole?.end >= phase - lookbehind - lookahead)
|
||||||
// add new haps with onset (think right edge bars scrolling in)
|
// add new haps with onset (think right edge bars scrolling in)
|
||||||
.concat(haps.filter((h) => h.hasOnset()));
|
.concat(haps.filter((h) => h.hasOnset()));
|
||||||
const time = phase - lookahead;
|
const time = phase - lookahead;
|
||||||
@@ -183,18 +175,3 @@ export class Drawer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getComputedPropertyValue(name) {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return '#fff';
|
|
||||||
}
|
|
||||||
return getComputedStyle(document.documentElement).getPropertyValue(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
let theme = {};
|
|
||||||
export function getTheme() {
|
|
||||||
return theme;
|
|
||||||
}
|
|
||||||
export function setTheme(_theme) {
|
|
||||||
theme = _theme;
|
|
||||||
}
|
|
||||||
@@ -41,17 +41,11 @@ const _bjork = function (n, x) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const bjork = function (ons, steps) {
|
export const bjork = function (ons, steps) {
|
||||||
const inverted = ons < 0;
|
|
||||||
ons = Math.abs(ons);
|
|
||||||
const offs = steps - ons;
|
const offs = steps - ons;
|
||||||
const x = Array(ons).fill([1]);
|
const x = Array(ons).fill([1]);
|
||||||
const y = Array(offs).fill([0]);
|
const y = Array(offs).fill([0]);
|
||||||
const result = _bjork([ons, offs], [x, y]);
|
const result = _bjork([ons, offs], [x, y]);
|
||||||
const p = flatten(result[1][0]).concat(flatten(result[1][1]));
|
return flatten(result[1][0]).concat(flatten(result[1][1]));
|
||||||
if (inverted) {
|
|
||||||
return p.map((x) => (x === 0 ? 1 : 0));
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ export const evalScope = async (...args) => {
|
|||||||
globalThis[name] = value;
|
globalThis[name] = value;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return modules;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function safeEval(str, options = {}) {
|
function safeEval(str, options = {}) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import Fraction from 'fraction.js';
|
import Fraction from 'fraction.js';
|
||||||
import { TimeSpan } from './timespan.mjs';
|
import { TimeSpan } from './timespan.mjs';
|
||||||
import { removeUndefineds } from './util.mjs';
|
|
||||||
|
|
||||||
// Returns the start of the cycle.
|
// Returns the start of the cycle.
|
||||||
Fraction.prototype.sam = function () {
|
Fraction.prototype.sam = function () {
|
||||||
@@ -48,39 +47,14 @@ Fraction.prototype.eq = function (other) {
|
|||||||
return this.compare(other) == 0;
|
return this.compare(other) == 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
Fraction.prototype.ne = function (other) {
|
|
||||||
return this.compare(other) != 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
Fraction.prototype.max = function (other) {
|
Fraction.prototype.max = function (other) {
|
||||||
return this.gt(other) ? this : other;
|
return this.gt(other) ? this : other;
|
||||||
};
|
};
|
||||||
|
|
||||||
Fraction.prototype.maximum = function (...others) {
|
|
||||||
others = others.map((x) => new Fraction(x));
|
|
||||||
return others.reduce((max, other) => other.max(max), this);
|
|
||||||
};
|
|
||||||
|
|
||||||
Fraction.prototype.min = function (other) {
|
Fraction.prototype.min = function (other) {
|
||||||
return this.lt(other) ? this : other;
|
return this.lt(other) ? this : other;
|
||||||
};
|
};
|
||||||
|
|
||||||
Fraction.prototype.mulmaybe = function (other) {
|
|
||||||
return other !== undefined ? this.mul(other) : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
Fraction.prototype.divmaybe = function (other) {
|
|
||||||
return other !== undefined ? this.div(other) : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
Fraction.prototype.addmaybe = function (other) {
|
|
||||||
return other !== undefined ? this.add(other) : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
Fraction.prototype.submaybe = function (other) {
|
|
||||||
return other !== undefined ? this.sub(other) : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
Fraction.prototype.show = function (/* excludeWhole = false */) {
|
Fraction.prototype.show = function (/* excludeWhole = false */) {
|
||||||
// return this.toFraction(excludeWhole);
|
// return this.toFraction(excludeWhole);
|
||||||
return this.s * this.n + '/' + this.d;
|
return this.s * this.n + '/' + this.d;
|
||||||
@@ -106,26 +80,9 @@ const fraction = (n) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const gcd = (...fractions) => {
|
export const gcd = (...fractions) => {
|
||||||
fractions = removeUndefineds(fractions);
|
|
||||||
if (fractions.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
|
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const lcm = (...fractions) => {
|
|
||||||
fractions = removeUndefineds(fractions);
|
|
||||||
if (fractions.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fractions.reduce(
|
|
||||||
(lcm, fraction) => (lcm === undefined || fraction === undefined ? undefined : lcm.lcm(fraction)),
|
|
||||||
fraction(1),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
fraction._original = Fraction;
|
fraction._original = Fraction;
|
||||||
|
|
||||||
export default fraction;
|
export default fraction;
|
||||||
|
|||||||
+1
-36
@@ -3,7 +3,6 @@ hap.mjs - <short description TODO>
|
|||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/hap.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/hap.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import Fraction from './fraction.mjs';
|
|
||||||
|
|
||||||
export class Hap {
|
export class Hap {
|
||||||
/*
|
/*
|
||||||
@@ -33,43 +32,13 @@ export class Hap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get duration() {
|
get duration() {
|
||||||
let duration;
|
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
|
||||||
if (typeof this.value?.duration === 'number') {
|
|
||||||
duration = Fraction(this.value.duration);
|
|
||||||
} else {
|
|
||||||
duration = this.whole.end.sub(this.whole.begin);
|
|
||||||
}
|
|
||||||
if (typeof this.value?.clip === 'number') {
|
|
||||||
return duration.mul(this.value.clip);
|
|
||||||
}
|
|
||||||
return duration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get endClipped() {
|
get endClipped() {
|
||||||
return this.whole.begin.add(this.duration);
|
return this.whole.begin.add(this.duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
isActive(currentTime) {
|
|
||||||
return this.whole.begin <= currentTime && this.endClipped >= currentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
isInPast(currentTime) {
|
|
||||||
return currentTime > this.endClipped;
|
|
||||||
}
|
|
||||||
isInNearPast(margin, currentTime) {
|
|
||||||
return currentTime - margin <= this.endClipped;
|
|
||||||
}
|
|
||||||
|
|
||||||
isInFuture(currentTime) {
|
|
||||||
return currentTime < this.whole.begin;
|
|
||||||
}
|
|
||||||
isInNearFuture(margin, currentTime) {
|
|
||||||
return currentTime < this.whole.begin && currentTime > this.whole.begin - margin;
|
|
||||||
}
|
|
||||||
isWithinTime(min, max) {
|
|
||||||
return this.whole.begin <= max && this.endClipped >= min;
|
|
||||||
}
|
|
||||||
|
|
||||||
wholeOrPart() {
|
wholeOrPart() {
|
||||||
return this.whole ? this.whole : this.part;
|
return this.whole ? this.whole : this.part;
|
||||||
}
|
}
|
||||||
@@ -91,10 +60,6 @@ export class Hap {
|
|||||||
return this.whole != undefined && this.whole.begin.equals(this.part.begin);
|
return this.whole != undefined && this.whole.begin.equals(this.part.begin);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasTag(tag) {
|
|
||||||
return this.context.tags?.includes(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolveState(state) {
|
resolveState(state) {
|
||||||
if (this.stateful && this.hasOnset()) {
|
if (this.stateful && this.hasOnset()) {
|
||||||
console.log('stateful');
|
console.log('stateful');
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as controls from './controls.mjs'; // legacy
|
import controls from './controls.mjs';
|
||||||
export * from './euclid.mjs';
|
export * from './euclid.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
import createClock from './zyklus.mjs';
|
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
export { Fraction, controls, createClock };
|
export { Fraction, controls };
|
||||||
export * from './controls.mjs';
|
|
||||||
export * from './hap.mjs';
|
export * from './hap.mjs';
|
||||||
export * from './pattern.mjs';
|
export * from './pattern.mjs';
|
||||||
export * from './signal.mjs';
|
export * from './signal.mjs';
|
||||||
@@ -23,6 +21,10 @@ export * from './repl.mjs';
|
|||||||
export * from './cyclist.mjs';
|
export * from './cyclist.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './time.mjs';
|
export * from './time.mjs';
|
||||||
|
export * from './draw.mjs';
|
||||||
|
export * from './animate.mjs';
|
||||||
|
export * from './pianoroll.mjs';
|
||||||
|
export * from './spiral.mjs';
|
||||||
export * from './ui.mjs';
|
export * from './ui.mjs';
|
||||||
export { default as drawLine } from './drawLine.mjs';
|
export { default as drawLine } from './drawLine.mjs';
|
||||||
// below won't work with runtime.mjs (json import fails)
|
// below won't work with runtime.mjs (json import fails)
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
export const logKey = 'strudel.log';
|
export const logKey = 'strudel.log';
|
||||||
|
|
||||||
let debounce = 1000,
|
|
||||||
lastMessage,
|
|
||||||
lastTime;
|
|
||||||
|
|
||||||
export function logger(message, type, data = {}) {
|
export function logger(message, type, data = {}) {
|
||||||
let t = performance.now();
|
|
||||||
if (lastMessage === message && t - lastTime < debounce) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastMessage = message;
|
|
||||||
lastTime = t;
|
|
||||||
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
||||||
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
/*
|
|
||||||
neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances.
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { logger } from './logger.mjs';
|
|
||||||
|
|
||||||
export class NeoCyclist {
|
|
||||||
constructor({ onTrigger, onToggle, getTime }) {
|
|
||||||
this.started = false;
|
|
||||||
this.cps = 0.5;
|
|
||||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
|
||||||
this.getTime = getTime; // get absolute time
|
|
||||||
this.time_at_last_tick_message = 0;
|
|
||||||
|
|
||||||
this.num_cycles_at_cps_change = 0;
|
|
||||||
this.onToggle = onToggle;
|
|
||||||
this.latency = 0.1; // fixed trigger time offset
|
|
||||||
this.cycle = 0;
|
|
||||||
this.id = Math.round(Date.now() * Math.random());
|
|
||||||
this.worker_time_dif;
|
|
||||||
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
|
|
||||||
this.worker.port.start();
|
|
||||||
|
|
||||||
this.channel = new BroadcastChannel('strudeltick');
|
|
||||||
let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif
|
|
||||||
const maxWeight = 20;
|
|
||||||
const precision = 10 ** 3; //round off time diff to prevent accumulating outliers
|
|
||||||
|
|
||||||
// the clock of the worker and the audio context clock can drift apart over time
|
|
||||||
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
|
|
||||||
// we need to keep a rolling weighted average of the time difference between the worker clock and audio context clock
|
|
||||||
// in order to schedule events consistently.
|
|
||||||
const setTimeReference = (num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline) => {
|
|
||||||
const time_dif = getTime() - (num_seconds_at_cps_change + num_seconds_since_cps_change) + tickdeadline;
|
|
||||||
if (this.worker_time_dif == null) {
|
|
||||||
this.worker_time_dif = time_dif;
|
|
||||||
} else {
|
|
||||||
const w = 1; //weight of new time diff;
|
|
||||||
const new_dif =
|
|
||||||
Math.round(((this.worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision;
|
|
||||||
|
|
||||||
if (new_dif != this.worker_time_dif) {
|
|
||||||
// reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens
|
|
||||||
weight = 4;
|
|
||||||
}
|
|
||||||
this.worker_time_dif = new_dif;
|
|
||||||
}
|
|
||||||
weight = Math.min(weight + 1, maxWeight);
|
|
||||||
};
|
|
||||||
|
|
||||||
const tickCallback = (payload) => {
|
|
||||||
const {
|
|
||||||
num_cycles_at_cps_change,
|
|
||||||
cps,
|
|
||||||
num_seconds_at_cps_change,
|
|
||||||
num_seconds_since_cps_change,
|
|
||||||
begin,
|
|
||||||
end,
|
|
||||||
tickdeadline,
|
|
||||||
cycle,
|
|
||||||
} = payload;
|
|
||||||
this.cps = cps;
|
|
||||||
this.cycle = cycle;
|
|
||||||
|
|
||||||
setTimeReference(num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline);
|
|
||||||
|
|
||||||
processHaps(begin, end, num_cycles_at_cps_change, num_seconds_at_cps_change);
|
|
||||||
|
|
||||||
this.time_at_last_tick_message = this.getTime();
|
|
||||||
};
|
|
||||||
|
|
||||||
const processHaps = (begin, end, num_cycles_at_cps_change, seconds_at_cps_change) => {
|
|
||||||
if (this.started === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
|
||||||
|
|
||||||
haps.forEach((hap) => {
|
|
||||||
if (hap.hasOnset()) {
|
|
||||||
const targetTime =
|
|
||||||
(hap.whole.begin - num_cycles_at_cps_change) / this.cps +
|
|
||||||
seconds_at_cps_change +
|
|
||||||
this.latency +
|
|
||||||
this.worker_time_dif;
|
|
||||||
const duration = hap.duration / this.cps;
|
|
||||||
onTrigger?.(hap, 0, duration, this.cps, targetTime);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// receive messages from worker clock and process them
|
|
||||||
this.channel.onmessage = (message) => {
|
|
||||||
if (!this.started) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { payload, type } = message.data;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'tick': {
|
|
||||||
tickCallback(payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
sendMessage(type, payload) {
|
|
||||||
this.worker.port.postMessage({ type, payload, id: this.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
now() {
|
|
||||||
const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps;
|
|
||||||
return this.cycle + gap;
|
|
||||||
}
|
|
||||||
setCps(cps = 1) {
|
|
||||||
this.sendMessage('cpschange', { cps });
|
|
||||||
}
|
|
||||||
setCycle(cycle) {
|
|
||||||
this.sendMessage('setcycle', { cycle });
|
|
||||||
}
|
|
||||||
setStarted(started) {
|
|
||||||
this.sendMessage('toggle', { started });
|
|
||||||
this.started = started;
|
|
||||||
this.onToggle?.(started);
|
|
||||||
}
|
|
||||||
start() {
|
|
||||||
logger('[cyclist] start');
|
|
||||||
this.setStarted(true);
|
|
||||||
}
|
|
||||||
stop() {
|
|
||||||
this.worker_time_dif = null;
|
|
||||||
logger('[cyclist] stop');
|
|
||||||
this.setStarted(false);
|
|
||||||
}
|
|
||||||
setPattern(pat, autostart = false) {
|
|
||||||
this.pattern = pat;
|
|
||||||
if (autostart && !this.started) {
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log(begin, end, haps) {
|
|
||||||
const onsets = haps.filter((h) => h.hasOnset());
|
|
||||||
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+201
-723
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
pianoroll.mjs - <short description TODO>
|
pianoroll.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/canvas/pianoroll.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pianoroll.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, noteToMidi, freqToMidi } from '@strudel/core';
|
import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
||||||
import { getTheme, getDrawContext } from './draw.mjs';
|
|
||||||
|
|
||||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||||
const getValue = (e) => {
|
const getValue = (e) => {
|
||||||
@@ -19,13 +18,7 @@ const getValue = (e) => {
|
|||||||
}
|
}
|
||||||
note = note ?? n;
|
note = note ?? n;
|
||||||
if (typeof note === 'string') {
|
if (typeof note === 'string') {
|
||||||
try {
|
|
||||||
// TODO: n(run(32)).scale("D:minor") fails when trying to query negative time..
|
|
||||||
return noteToMidi(note);
|
return noteToMidi(note);
|
||||||
} catch (err) {
|
|
||||||
// console.warn(`error converting note to midi: ${err}`); // this spams to crazy
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (typeof note === 'number') {
|
if (typeof note === 'number') {
|
||||||
return note;
|
return note;
|
||||||
@@ -37,24 +30,25 @@ const getValue = (e) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.pianoroll = function (options = {}) {
|
Pattern.prototype.pianoroll = function (options = {}) {
|
||||||
let { cycles = 4, playhead = 0.5, overscan = 0, hideNegative = false, ctx = getDrawContext(), id = 1 } = options;
|
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
|
||||||
|
|
||||||
let from = -cycles * playhead;
|
let from = -cycles * playhead;
|
||||||
let to = cycles * (1 - playhead);
|
let to = cycles * (1 - playhead);
|
||||||
const inFrame = (hap, t) => (!hideNegative || hap.whole.begin >= 0) && hap.isWithinTime(t + from, t + to);
|
|
||||||
this.draw(
|
this.draw(
|
||||||
(haps, time) => {
|
(ctx, haps, t) => {
|
||||||
|
const inFrame = (event) =>
|
||||||
|
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
|
||||||
pianoroll({
|
pianoroll({
|
||||||
...options,
|
...options,
|
||||||
time,
|
time: t,
|
||||||
ctx,
|
ctx,
|
||||||
haps: haps.filter((hap) => inFrame(hap, time)),
|
haps: haps.filter(inFrame),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
lookbehind: from - overscan,
|
from: from - overscan,
|
||||||
lookahead: to + overscan,
|
to: to + overscan,
|
||||||
id,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return this;
|
return this;
|
||||||
@@ -104,8 +98,11 @@ export function pianoroll({
|
|||||||
flipTime = 0,
|
flipTime = 0,
|
||||||
flipValues = 0,
|
flipValues = 0,
|
||||||
hideNegative = false,
|
hideNegative = false,
|
||||||
inactive = getTheme().foreground,
|
// inactive = '#C9E597',
|
||||||
active = getTheme().foreground,
|
// inactive = '#FFCA28',
|
||||||
|
inactive = '#7491D2',
|
||||||
|
active = '#FFCA28',
|
||||||
|
// background = '#2A3236',
|
||||||
background = 'transparent',
|
background = 'transparent',
|
||||||
smear = 0,
|
smear = 0,
|
||||||
playheadColor = 'white',
|
playheadColor = 'white',
|
||||||
@@ -124,17 +121,12 @@ export function pianoroll({
|
|||||||
colorizeInactive = 1,
|
colorizeInactive = 1,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
ctx,
|
ctx,
|
||||||
id,
|
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const w = ctx.canvas.width;
|
const w = ctx.canvas.width;
|
||||||
const h = ctx.canvas.height;
|
const h = ctx.canvas.height;
|
||||||
let from = -cycles * playhead;
|
let from = -cycles * playhead;
|
||||||
let to = cycles * (1 - playhead);
|
let to = cycles * (1 - playhead);
|
||||||
|
|
||||||
if (id) {
|
|
||||||
haps = haps.filter((hap) => hap.hasTag(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeframeProp) {
|
if (timeframeProp) {
|
||||||
console.warn('timeframe is deprecated! use from/to instead');
|
console.warn('timeframe is deprecated! use from/to instead');
|
||||||
from = 0;
|
from = 0;
|
||||||
@@ -189,14 +181,13 @@ export function pianoroll({
|
|||||||
if (hideInactive && !isActive) {
|
if (hideInactive && !isActive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let color = event.value?.color;
|
let color = event.value?.color || event.context?.color;
|
||||||
active = color || active;
|
active = color || active;
|
||||||
inactive = colorizeInactive ? color || inactive : inactive;
|
inactive = colorizeInactive ? color || inactive : inactive;
|
||||||
color = isActive ? active : inactive;
|
color = isActive ? active : inactive;
|
||||||
ctx.fillStyle = fillCurrent ? color : 'transparent';
|
ctx.fillStyle = fillCurrent ? color : 'transparent';
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = color;
|
||||||
const { velocity = 1, gain = 1 } = event.value || {};
|
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
|
||||||
ctx.globalAlpha = velocity * gain;
|
|
||||||
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
|
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
|
||||||
const timePx = scale(timeProgress, ...timeRange);
|
const timePx = scale(timeProgress, ...timeRange);
|
||||||
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||||
@@ -272,8 +263,8 @@ export function getDrawOptions(drawTime, options = {}) {
|
|||||||
|
|
||||||
export const getPunchcardPainter =
|
export const getPunchcardPainter =
|
||||||
(options = {}) =>
|
(options = {}) =>
|
||||||
(ctx, time, haps, drawTime) =>
|
(ctx, time, haps, drawTime, paintOptions = {}) =>
|
||||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) });
|
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) });
|
||||||
|
|
||||||
Pattern.prototype.punchcard = function (options) {
|
Pattern.prototype.punchcard = function (options) {
|
||||||
return this.onPaint(getPunchcardPainter(options));
|
return this.onPaint(getPunchcardPainter(options));
|
||||||
+11
-30
@@ -1,4 +1,3 @@
|
|||||||
import { NeoCyclist } from './neocyclist.mjs';
|
|
||||||
import { Cyclist } from './cyclist.mjs';
|
import { Cyclist } from './cyclist.mjs';
|
||||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
@@ -7,7 +6,9 @@ import { evalScope } from './evaluate.mjs';
|
|||||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||||
|
|
||||||
export function repl({
|
export function repl({
|
||||||
|
interval,
|
||||||
defaultOutput,
|
defaultOutput,
|
||||||
|
onSchedulerError,
|
||||||
onEvalError,
|
onEvalError,
|
||||||
beforeEval,
|
beforeEval,
|
||||||
afterEval,
|
afterEval,
|
||||||
@@ -16,9 +17,6 @@ export function repl({
|
|||||||
onToggle,
|
onToggle,
|
||||||
editPattern,
|
editPattern,
|
||||||
onUpdateState,
|
onUpdateState,
|
||||||
sync = false,
|
|
||||||
setInterval,
|
|
||||||
clearInterval,
|
|
||||||
}) {
|
}) {
|
||||||
const state = {
|
const state = {
|
||||||
schedulerError: undefined,
|
schedulerError: undefined,
|
||||||
@@ -39,27 +37,21 @@ export function repl({
|
|||||||
onUpdateState?.(state);
|
onUpdateState?.(state);
|
||||||
};
|
};
|
||||||
|
|
||||||
const schedulerOptions = {
|
const scheduler = new Cyclist({
|
||||||
|
interval,
|
||||||
onTrigger: getTrigger({ defaultOutput, getTime }),
|
onTrigger: getTrigger({ defaultOutput, getTime }),
|
||||||
|
onError: onSchedulerError,
|
||||||
getTime,
|
getTime,
|
||||||
onToggle: (started) => {
|
onToggle: (started) => {
|
||||||
updateState({ started });
|
updateState({ started });
|
||||||
onToggle?.(started);
|
onToggle?.(started);
|
||||||
},
|
},
|
||||||
setInterval,
|
});
|
||||||
clearInterval,
|
|
||||||
};
|
|
||||||
|
|
||||||
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
|
||||||
const scheduler =
|
|
||||||
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
|
||||||
let pPatterns = {};
|
let pPatterns = {};
|
||||||
let anonymousIndex = 0;
|
|
||||||
let allTransform;
|
let allTransform;
|
||||||
|
|
||||||
const hush = function () {
|
const hush = function () {
|
||||||
pPatterns = {};
|
pPatterns = {};
|
||||||
anonymousIndex = 0;
|
|
||||||
allTransform = undefined;
|
allTransform = undefined;
|
||||||
return silence;
|
return silence;
|
||||||
};
|
};
|
||||||
@@ -84,15 +76,6 @@ export function repl({
|
|||||||
// set pattern methods that use this repl via closure
|
// set pattern methods that use this repl via closure
|
||||||
const injectPatternMethods = () => {
|
const injectPatternMethods = () => {
|
||||||
Pattern.prototype.p = function (id) {
|
Pattern.prototype.p = function (id) {
|
||||||
if (id.startsWith('_') || id.endsWith('_')) {
|
|
||||||
// allows muting a pattern x with x_ or _x
|
|
||||||
return silence;
|
|
||||||
}
|
|
||||||
if (id === '$') {
|
|
||||||
// allows adding anonymous patterns with $:
|
|
||||||
id = `$${anonymousIndex}`;
|
|
||||||
anonymousIndex++;
|
|
||||||
}
|
|
||||||
pPatterns[id] = this;
|
pPatterns[id] = this;
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
@@ -121,7 +104,7 @@ export function repl({
|
|||||||
const cpm = register('cpm', function (cpm, pat) {
|
const cpm = register('cpm', function (cpm, pat) {
|
||||||
return pat._fast(cpm / 60 / scheduler.cps);
|
return pat._fast(cpm / 60 / scheduler.cps);
|
||||||
});
|
});
|
||||||
return evalScope({
|
evalScope({
|
||||||
all,
|
all,
|
||||||
hush,
|
hush,
|
||||||
cpm,
|
cpm,
|
||||||
@@ -138,7 +121,7 @@ export function repl({
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
updateState({ code, pending: true });
|
updateState({ code, pending: true });
|
||||||
await injectPatternMethods();
|
injectPatternMethods();
|
||||||
await beforeEval?.({ code });
|
await beforeEval?.({ code });
|
||||||
shouldHush && hush();
|
shouldHush && hush();
|
||||||
let { pattern, meta } = await _evaluate(code, transpiler);
|
let { pattern, meta } = await _evaluate(code, transpiler);
|
||||||
@@ -167,7 +150,6 @@ export function repl({
|
|||||||
return pattern;
|
return pattern;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger(`[eval] error: ${err.message}`, 'error');
|
logger(`[eval] error: ${err.message}`, 'error');
|
||||||
console.error(err);
|
|
||||||
updateState({ evalError: err, pending: false });
|
updateState({ evalError: err, pending: false });
|
||||||
onEvalError?.(err);
|
onEvalError?.(err);
|
||||||
}
|
}
|
||||||
@@ -178,15 +160,14 @@ export function repl({
|
|||||||
|
|
||||||
export const getTrigger =
|
export const getTrigger =
|
||||||
({ getTime, defaultOutput }) =>
|
({ getTime, defaultOutput }) =>
|
||||||
async (hap, deadline, duration, cps, t) => {
|
async (hap, deadline, duration, cps) => {
|
||||||
// TODO: get rid of deadline after https://github.com/tidalcycles/strudel/pull/1004
|
|
||||||
try {
|
try {
|
||||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||||
await defaultOutput(hap, deadline, duration, cps, t);
|
await defaultOutput(hap, deadline, duration, cps);
|
||||||
}
|
}
|
||||||
if (hap.context.onTrigger) {
|
if (hap.context.onTrigger) {
|
||||||
// call signature of output / onTrigger is different...
|
// call signature of output / onTrigger is different...
|
||||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t);
|
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger(`[cyclist] error: ${err.message}`, 'error');
|
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||||
|
|||||||
+5
-124
@@ -143,24 +143,7 @@ export const rand = signal(timeToRand);
|
|||||||
export const rand2 = rand.toBipolar();
|
export const rand2 = rand.toBipolar();
|
||||||
|
|
||||||
export const _brandBy = (p) => rand.fmap((x) => x < p);
|
export const _brandBy = (p) => rand.fmap((x) => x < p);
|
||||||
|
|
||||||
/**
|
|
||||||
* A continuous pattern of 0 or 1 (binary random), with a probability for the value being 1
|
|
||||||
*
|
|
||||||
* @name brandBy
|
|
||||||
* @param {number} probability - a number between 0 and 1
|
|
||||||
* @example
|
|
||||||
* s("hh*10").pan(brandBy(0.2))
|
|
||||||
*/
|
|
||||||
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
||||||
|
|
||||||
/**
|
|
||||||
* A continuous pattern of 0 or 1 (binary random)
|
|
||||||
*
|
|
||||||
* @name brand
|
|
||||||
* @example
|
|
||||||
* s("hh*10").pan(brand)
|
|
||||||
*/
|
|
||||||
export const brand = _brandBy(0.5);
|
export const brand = _brandBy(0.5);
|
||||||
|
|
||||||
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
||||||
@@ -261,68 +244,9 @@ export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
|
|||||||
return pat.apply(pickmod(lookup, funcs));
|
return pat.apply(pickmod(lookup, funcs));
|
||||||
});
|
});
|
||||||
|
|
||||||
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
|
|
||||||
* @param {Pattern} pat
|
|
||||||
* @param {*} xs
|
|
||||||
* @returns {Pattern}
|
|
||||||
*/
|
|
||||||
export const pickOut = register('pickOut', function (lookup, pat) {
|
|
||||||
return _pick(lookup, pat, false).outerJoin();
|
|
||||||
});
|
|
||||||
|
|
||||||
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
|
|
||||||
* it wraps around, rather than sticking at the maximum value.
|
|
||||||
* @param {Pattern} pat
|
|
||||||
* @param {*} xs
|
|
||||||
* @returns {Pattern}
|
|
||||||
*/
|
|
||||||
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
|
|
||||||
return _pick(lookup, pat, true).outerJoin();
|
|
||||||
});
|
|
||||||
|
|
||||||
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
|
|
||||||
* @param {Pattern} pat
|
|
||||||
* @param {*} xs
|
|
||||||
* @returns {Pattern}
|
|
||||||
*/
|
|
||||||
export const pickRestart = register('pickRestart', function (lookup, pat) {
|
|
||||||
return _pick(lookup, pat, false).restartJoin();
|
|
||||||
});
|
|
||||||
|
|
||||||
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
|
|
||||||
* it wraps around, rather than sticking at the maximum value.
|
|
||||||
* @param {Pattern} pat
|
|
||||||
* @param {*} xs
|
|
||||||
* @returns {Pattern}
|
|
||||||
*/
|
|
||||||
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
|
|
||||||
return _pick(lookup, pat, true).restartJoin();
|
|
||||||
});
|
|
||||||
|
|
||||||
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
|
|
||||||
* @param {Pattern} pat
|
|
||||||
* @param {*} xs
|
|
||||||
* @returns {Pattern}
|
|
||||||
*/
|
|
||||||
export const pickReset = register('pickReset', function (lookup, pat) {
|
|
||||||
return _pick(lookup, pat, false).resetJoin();
|
|
||||||
});
|
|
||||||
|
|
||||||
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
|
|
||||||
* it wraps around, rather than sticking at the maximum value.
|
|
||||||
* @param {Pattern} pat
|
|
||||||
* @param {*} xs
|
|
||||||
* @returns {Pattern}
|
|
||||||
*/
|
|
||||||
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
|
|
||||||
return _pick(lookup, pat, true).resetJoin();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
/** * 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.
|
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
|
||||||
* @name inhabit
|
|
||||||
* @synonyms pickSqueeze
|
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
@@ -333,23 +257,21 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
|
* 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) {
|
export const inhabit = register('inhabit', function (lookup, pat) {
|
||||||
return _pick(lookup, pat, false).squeezeJoin();
|
return _pick(lookup, pat, true).squeezeJoin();
|
||||||
});
|
});
|
||||||
|
|
||||||
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
|
/** * 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.
|
* 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
|
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||||
* second one.
|
* second one.
|
||||||
* @name inhabitmod
|
|
||||||
* @synonyms pickmodSqueeze
|
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
|
export const inhabitmod = register('inhabit', function (lookup, pat) {
|
||||||
return _pick(lookup, pat, true).squeezeJoin();
|
return _pick(lookup, pat, false).squeezeJoin();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -414,8 +336,6 @@ export const chooseInWith = (pat, xs) => {
|
|||||||
* Chooses randomly from the given list of elements.
|
* Chooses randomly from the given list of elements.
|
||||||
* @param {...any} xs values / patterns to choose from.
|
* @param {...any} xs values / patterns to choose from.
|
||||||
* @returns {Pattern} - a continuous pattern.
|
* @returns {Pattern} - a continuous pattern.
|
||||||
* @example
|
|
||||||
* note("c2 g2!2 d2 f1").s(choose("sine", "triangle", "bd:6"))
|
|
||||||
*/
|
*/
|
||||||
export const choose = (...xs) => chooseWith(rand, xs);
|
export const choose = (...xs) => chooseWith(rand, xs);
|
||||||
|
|
||||||
@@ -442,7 +362,6 @@ Pattern.prototype.choose2 = function (...xs) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Picks one of the elements at random each cycle.
|
* Picks one of the elements at random each cycle.
|
||||||
* @synonyms randcat
|
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* chooseCycles("bd", "hh", "sd").s().fast(8)
|
* chooseCycles("bd", "hh", "sd").s().fast(8)
|
||||||
@@ -471,27 +390,9 @@ const _wchooseWith = function (pat, ...pairs) {
|
|||||||
|
|
||||||
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
|
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
|
||||||
|
|
||||||
/**
|
|
||||||
* Chooses randomly from the given list of elements by giving a probability to each element
|
|
||||||
* @param {...any} pairs arrays of value and weight
|
|
||||||
* @returns {Pattern} - a continuous pattern.
|
|
||||||
* @example
|
|
||||||
* note("c2 g2!2 d2 f1").s(wchoose(["sine",10], ["triangle",1], ["bd:6",1]))
|
|
||||||
*/
|
|
||||||
export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
||||||
|
|
||||||
/**
|
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
|
||||||
* Picks one of the elements at random each cycle by giving a probability to each element
|
|
||||||
* @synonyms wrandcat
|
|
||||||
* @returns {Pattern}
|
|
||||||
* @example
|
|
||||||
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
|
|
||||||
* @example
|
|
||||||
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
|
|
||||||
*/
|
|
||||||
export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pairs).innerJoin();
|
|
||||||
|
|
||||||
export const wrandcat = wchooseCycles;
|
|
||||||
|
|
||||||
// this function expects pat to be a pattern of floats...
|
// this function expects pat to be a pattern of floats...
|
||||||
export const perlinWith = (pat) => {
|
export const perlinWith = (pat) => {
|
||||||
@@ -561,11 +462,6 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
|||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* s("hh*8").undegradeBy(0.2)
|
* s("hh*8").undegradeBy(0.2)
|
||||||
* @example
|
|
||||||
* s("hh*10").layer(
|
|
||||||
* x => x.degradeBy(0.2).pan(0),
|
|
||||||
* x => x.undegradeBy(0.8).pan(1)
|
|
||||||
* )
|
|
||||||
*/
|
*/
|
||||||
export const undegradeBy = register('undegradeBy', function (x, pat) {
|
export const undegradeBy = register('undegradeBy', function (x, pat) {
|
||||||
return pat._degradeByWith(
|
return pat._degradeByWith(
|
||||||
@@ -574,21 +470,6 @@ export const undegradeBy = register('undegradeBy', function (x, pat) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
|
|
||||||
* Events that would be removed by degrade are let through by undegrade and vice versa (see second example).
|
|
||||||
*
|
|
||||||
* @name undegrade
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("hh*8").undegrade()
|
|
||||||
* @example
|
|
||||||
* s("hh*10").layer(
|
|
||||||
* x => x.degrade().pan(0),
|
|
||||||
* x => x.undegrade().pan(1)
|
|
||||||
* )
|
|
||||||
*/
|
|
||||||
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Pattern } from '@strudel/core';
|
import { Pattern } from './index.mjs';
|
||||||
import { getTheme } from './draw.mjs';
|
|
||||||
|
|
||||||
// polar coords -> xy
|
// polar coords -> xy
|
||||||
function fromPolar(angle, radius, cx, cy) {
|
function fromPolar(angle, radius, cx, cy) {
|
||||||
@@ -20,7 +19,7 @@ function spiralSegment(options) {
|
|||||||
cy = 100,
|
cy = 100,
|
||||||
rotate = 0,
|
rotate = 0,
|
||||||
thickness = margin / 2,
|
thickness = margin / 2,
|
||||||
color = getTheme().foreground,
|
color = '#0000ff30',
|
||||||
cap = 'round',
|
cap = 'round',
|
||||||
stretch = 1,
|
stretch = 1,
|
||||||
fromOpacity = 1,
|
fromOpacity = 1,
|
||||||
@@ -50,34 +49,25 @@ function spiralSegment(options) {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawSpiral(options) {
|
Pattern.prototype.spiral = function (options = {}) {
|
||||||
let {
|
const {
|
||||||
stretch = 1,
|
stretch = 1,
|
||||||
size = 80,
|
size = 80,
|
||||||
thickness = size / 2,
|
thickness = size / 2,
|
||||||
cap = 'butt', // round butt squar,
|
cap = 'butt', // round butt squar,
|
||||||
inset = 3, // start angl,
|
inset = 3, // start angl,
|
||||||
playheadColor = '#ffffff',
|
playheadColor = '#ffffff90',
|
||||||
playheadLength = 0.02,
|
playheadLength = 0.02,
|
||||||
playheadThickness = thickness,
|
playheadThickness = thickness,
|
||||||
padding = 0,
|
padding = 0,
|
||||||
steady = 1,
|
steady = 1,
|
||||||
activeColor = getTheme().foreground,
|
inactiveColor = '#ffffff20',
|
||||||
inactiveColor = getTheme().gutterForeground,
|
|
||||||
colorizeInactive = 0,
|
colorizeInactive = 0,
|
||||||
fade = true,
|
fade = true,
|
||||||
// logSpiral = true,
|
// logSpiral = true,
|
||||||
ctx,
|
|
||||||
time,
|
|
||||||
haps,
|
|
||||||
drawTime,
|
|
||||||
id,
|
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
if (id) {
|
function spiral({ ctx, time, haps, drawTime }) {
|
||||||
haps = haps.filter((hap) => hap.hasTag(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
|
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
|
||||||
ctx.clearRect(0, 0, w * 2, h * 2);
|
ctx.clearRect(0, 0, w * 2, h * 2);
|
||||||
const [cx, cy] = [w / 2, h / 2];
|
const [cx, cy] = [w / 2, h / 2];
|
||||||
@@ -104,8 +94,7 @@ function drawSpiral(options) {
|
|||||||
const isActive = hap.whole.begin <= time && hap.endClipped > time;
|
const isActive = hap.whole.begin <= time && hap.endClipped > time;
|
||||||
const from = hap.whole.begin - time + inset;
|
const from = hap.whole.begin - time + inset;
|
||||||
const to = hap.endClipped - time + inset - padding;
|
const to = hap.endClipped - time + inset - padding;
|
||||||
const hapColor = hap.value?.color || activeColor;
|
const { color } = hap.context;
|
||||||
const color = colorizeInactive || isActive ? hapColor : inactiveColor;
|
|
||||||
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
|
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
|
||||||
spiralSegment({
|
spiralSegment({
|
||||||
ctx,
|
ctx,
|
||||||
@@ -113,7 +102,7 @@ function drawSpiral(options) {
|
|||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
rotate,
|
rotate,
|
||||||
color,
|
color: colorizeInactive || isActive ? color : inactiveColor,
|
||||||
fromOpacity: opacity,
|
fromOpacity: opacity,
|
||||||
toOpacity: opacity,
|
toOpacity: opacity,
|
||||||
});
|
});
|
||||||
@@ -123,8 +112,7 @@ function drawSpiral(options) {
|
|||||||
...playhead,
|
...playhead,
|
||||||
rotate,
|
rotate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.spiral = function (options = {}) {
|
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
|
||||||
return this.onPaint((ctx, time, haps, drawTime) => drawSpiral({ ctx, time, haps, drawTime, ...options }));
|
|
||||||
};
|
};
|
||||||
@@ -4,39 +4,25 @@ Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { s, pan } from '../controls.mjs';
|
import controls from '../controls.mjs';
|
||||||
import { mini } from '../../mini/mini.mjs';
|
import { mini } from '../../mini/mini.mjs';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import Fraction from '../fraction.mjs';
|
|
||||||
|
|
||||||
describe('controls', () => {
|
describe('controls', () => {
|
||||||
it('should support controls', () => {
|
it('should support controls', () => {
|
||||||
expect(s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
|
expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
|
||||||
});
|
});
|
||||||
it('should support compound controls', () => {
|
it('should support compound controls', () => {
|
||||||
expect(s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
|
expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
|
||||||
expect(s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
|
expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
|
||||||
{ s: 'bd', n: 3 },
|
{ s: 'bd', n: 3 },
|
||||||
{ s: 'sd', n: 4, gain: 1.4 },
|
{ s: 'sd', n: 4, gain: 1.4 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('should support ignore extra elements in compound controls', () => {
|
it('should support ignore extra elements in compound controls', () => {
|
||||||
expect(s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
|
expect(controls.s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
|
||||||
{ s: 'bd', n: 3, gain: 0.4 },
|
{ s: 'bd', n: 3, gain: 0.4 },
|
||||||
{ s: 'sd', n: 4, gain: 0.5 },
|
{ s: 'sd', n: 4, gain: 0.5 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('should support nested controls', () => {
|
|
||||||
expect(s(mini('bd').pan(1)).firstCycleValues).toEqual([{ s: 'bd', pan: 1 }]);
|
|
||||||
expect(s(mini('bd:1').pan(1)).firstCycleValues).toEqual([{ s: 'bd', n: 1, pan: 1 }]);
|
|
||||||
});
|
|
||||||
it('preserves tactus of the left pattern', () => {
|
|
||||||
expect(s(mini('bd cp mt').pan(mini('1 2 3 4'))).tactus).toEqual(Fraction(3));
|
|
||||||
});
|
|
||||||
it('preserves tactus of the right pattern for .out', () => {
|
|
||||||
expect(s(mini('bd cp mt').set.out(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(4));
|
|
||||||
});
|
|
||||||
it('combines tactus of the pattern for .mix as lcm', () => {
|
|
||||||
expect(s(mini('bd cp mt').set.mix(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(12));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import {
|
|||||||
cat,
|
cat,
|
||||||
sequence,
|
sequence,
|
||||||
palindrome,
|
palindrome,
|
||||||
s_polymeter,
|
polymeter,
|
||||||
s_polymeterSteps,
|
polymeterSteps,
|
||||||
polyrhythm,
|
polyrhythm,
|
||||||
silence,
|
silence,
|
||||||
fast,
|
fast,
|
||||||
@@ -47,17 +47,13 @@ import {
|
|||||||
time,
|
time,
|
||||||
run,
|
run,
|
||||||
pick,
|
pick,
|
||||||
stackLeft,
|
|
||||||
stackRight,
|
|
||||||
stackCentre,
|
|
||||||
s_cat,
|
|
||||||
calculateTactus,
|
|
||||||
} from '../index.mjs';
|
} from '../index.mjs';
|
||||||
|
|
||||||
import { steady } from '../signal.mjs';
|
import { steady } from '../signal.mjs';
|
||||||
|
|
||||||
import { n, s } from '../controls.mjs';
|
import controls from '../controls.mjs';
|
||||||
|
|
||||||
|
const { n, s } = controls;
|
||||||
const st = (begin, end) => new State(ts(begin, end));
|
const st = (begin, end) => new State(ts(begin, end));
|
||||||
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
||||||
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
|
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
|
||||||
@@ -186,18 +182,18 @@ describe('Pattern', () => {
|
|||||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
|
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('can Reset() structure', () => {
|
it('can Trig() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||||
.add.reset(20, 30)
|
.add.trig(20, 30)
|
||||||
.early(2),
|
.early(2),
|
||||||
sequence(26, 27, 36, 37),
|
sequence(26, 27, 36, 37),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Restart() structure', () => {
|
it('can Trigzero() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||||
.add.restart(20, 30)
|
.add.trigzero(20, 30)
|
||||||
.early(2),
|
.early(2),
|
||||||
sequence(21, 22, 31, 32),
|
sequence(21, 22, 31, 32),
|
||||||
);
|
);
|
||||||
@@ -238,18 +234,18 @@ describe('Pattern', () => {
|
|||||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('can Reset() structure', () => {
|
it('can Trig() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||||
.keep.reset(20, 30)
|
.keep.trig(20, 30)
|
||||||
.early(2),
|
.early(2),
|
||||||
sequence(6, 7, 6, 7),
|
sequence(6, 7, 6, 7),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Restart() structure', () => {
|
it('can Trigzero() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||||
.keep.restart(20, 30)
|
.keep.trigzero(20, 30)
|
||||||
.early(2),
|
.early(2),
|
||||||
sequence(1, 2, 1, 2),
|
sequence(1, 2, 1, 2),
|
||||||
);
|
);
|
||||||
@@ -284,18 +280,18 @@ describe('Pattern', () => {
|
|||||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('can Reset() structure', () => {
|
it('can Trig() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||||
.keepif.reset(false, true)
|
.keepif.trig(false, true)
|
||||||
.early(2),
|
.early(2),
|
||||||
sequence(silence, silence, 6, 7),
|
sequence(silence, silence, 6, 7),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Restart() structure', () => {
|
it('can Trigzero() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||||
.keepif.restart(false, true)
|
.keepif.trigzero(false, true)
|
||||||
.early(2),
|
.early(2),
|
||||||
sequence(silence, silence, 1, 2),
|
sequence(silence, silence, 1, 2),
|
||||||
);
|
);
|
||||||
@@ -608,19 +604,16 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('s_polymeter()', () => {
|
describe('polymeter()', () => {
|
||||||
it('Can layer up cycles, stepwise, with lists', () => {
|
it('Can layer up cycles, stepwise', () => {
|
||||||
expect(s_polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
|
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
|
||||||
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
|
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(s_polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
|
expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
|
||||||
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
|
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('Can layer up cycles, stepwise, with weighted patterns', () => {
|
|
||||||
sameFirst(s_polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('firstOf()', () => {
|
describe('firstOf()', () => {
|
||||||
@@ -1124,89 +1117,4 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('tactus', () => {
|
|
||||||
it('Is correctly preserved/calculated through transformations', () => {
|
|
||||||
expect(sequence(0, 1, 2, 3).linger(4).tactus).toStrictEqual(Fraction(4));
|
|
||||||
expect(sequence(0, 1, 2, 3).iter(4).tactus).toStrictEqual(Fraction(4));
|
|
||||||
expect(sequence(0, 1, 2, 3).fast(4).tactus).toStrictEqual(Fraction(4));
|
|
||||||
expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(4));
|
|
||||||
expect(sequence(0, 1, 2, 3).rev().tactus).toStrictEqual(Fraction(4));
|
|
||||||
expect(sequence(1).segment(10).tactus).toStrictEqual(Fraction(10));
|
|
||||||
expect(sequence(1, 0, 1).invert().tactus).toStrictEqual(Fraction(3));
|
|
||||||
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4).tactus).toStrictEqual(Fraction(8));
|
|
||||||
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4).tactus).toStrictEqual(Fraction(8));
|
|
||||||
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
|
|
||||||
Fraction(4),
|
|
||||||
);
|
|
||||||
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
|
|
||||||
Fraction(4),
|
|
||||||
);
|
|
||||||
expect(sequence({ n: 0 }, { n: 1 }, { n: 2 }).chop(4).tactus).toStrictEqual(Fraction(12));
|
|
||||||
expect(
|
|
||||||
pure((x) => x + 1)
|
|
||||||
.setTactus(3)
|
|
||||||
.appBoth(pure(1).setTactus(2)).tactus,
|
|
||||||
).toStrictEqual(Fraction(6));
|
|
||||||
expect(
|
|
||||||
pure((x) => x + 1)
|
|
||||||
.setTactus(undefined)
|
|
||||||
.appBoth(pure(1).setTactus(2)).tactus,
|
|
||||||
).toStrictEqual(Fraction(2));
|
|
||||||
expect(
|
|
||||||
pure((x) => x + 1)
|
|
||||||
.setTactus(3)
|
|
||||||
.appBoth(pure(1).setTactus(undefined)).tactus,
|
|
||||||
).toStrictEqual(Fraction(3));
|
|
||||||
expect(stack(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(6));
|
|
||||||
expect(stack(fastcat(0, 1, 2), fastcat(3, 4).setTactus(undefined)).tactus).toStrictEqual(Fraction(3));
|
|
||||||
expect(stackLeft(fastcat(0, 1, 2, 3), fastcat(3, 4)).tactus).toStrictEqual(Fraction(4));
|
|
||||||
expect(stackRight(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3));
|
|
||||||
// maybe this should double when they are either all even or all odd
|
|
||||||
expect(stackCentre(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3));
|
|
||||||
expect(fastcat(0, 1).ply(3).tactus).toStrictEqual(Fraction(6));
|
|
||||||
expect(fastcat(0, 1).setTactus(undefined).ply(3).tactus).toStrictEqual(undefined);
|
|
||||||
expect(fastcat(0, 1).fast(3).tactus).toStrictEqual(Fraction(2));
|
|
||||||
expect(fastcat(0, 1).setTactus(undefined).fast(3).tactus).toStrictEqual(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('s_cat', () => {
|
|
||||||
it('can cat', () => {
|
|
||||||
expect(sameFirst(s_cat(fastcat(0, 1, 2, 3), fastcat(4, 5)), fastcat(0, 1, 2, 3, 4, 5)));
|
|
||||||
expect(sameFirst(s_cat(pure(1), pure(2), pure(3)), fastcat(1, 2, 3)));
|
|
||||||
});
|
|
||||||
it('calculates undefined tactuses as the average', () => {
|
|
||||||
expect(sameFirst(s_cat(pure(1), pure(2), pure(3).setTactus(undefined)), fastcat(1, 2, 3)));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('s_taper', () => {
|
|
||||||
it('can taper', () => {
|
|
||||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_taper(1, 5), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0)));
|
|
||||||
});
|
|
||||||
it('can taper backwards', () => {
|
|
||||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_taper(-1, 5), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('s_add and s_sub', () => {
|
|
||||||
it('can add from the left', () => {
|
|
||||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(2), sequence(0, 1)));
|
|
||||||
});
|
|
||||||
it('can sub to the left', () => {
|
|
||||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(2), sequence(0, 1, 2)));
|
|
||||||
});
|
|
||||||
it('can add from the right', () => {
|
|
||||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(-2), sequence(3, 4)));
|
|
||||||
});
|
|
||||||
it('can sub to the right', () => {
|
|
||||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(-2), sequence(2, 3, 4)));
|
|
||||||
});
|
|
||||||
it('can subtract nothing', () => {
|
|
||||||
expect(sameFirst(pure('a').s_sub(0), pure('a')));
|
|
||||||
});
|
|
||||||
it('can subtract nothing, repeatedly', () => {
|
|
||||||
expect(sameFirst(pure('a').s_sub(0, 0), fastcat('a', 'a')));
|
|
||||||
for (var i = 0; i < 100; ++i) {
|
|
||||||
expect(sameFirst(pure('a').s_sub(...Array(i).fill(0)), fastcat(...Array(i).fill('a'))));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { map, valued, mul } from '../value.mjs';
|
import { map, valued, mul } from '../value.mjs';
|
||||||
import { n } from '../controls.mjs';
|
import controls from '../controls.mjs';
|
||||||
|
const { n } = controls;
|
||||||
|
|
||||||
describe('Value', () => {
|
describe('Value', () => {
|
||||||
it('unionWith', () => {
|
it('unionWith', () => {
|
||||||
@@ -22,4 +23,8 @@ describe('Value', () => {
|
|||||||
expect(valued(mul).ap(3).ap(3).value).toEqual(9);
|
expect(valued(mul).ap(3).ap(3).value).toEqual(9);
|
||||||
expect(valued(3).mul(3).value).toEqual(9);
|
expect(valued(3).mul(3).value).toEqual(9);
|
||||||
});
|
});
|
||||||
|
it('union bare numbers for numeral props', () => {
|
||||||
|
expect(n(3).cutoff(500).add(10).firstCycleValues).toEqual([{ n: 13, cutoff: 510 }]);
|
||||||
|
expect(n(3).cutoff(500).mul(2).firstCycleValues).toEqual([{ n: 6, cutoff: 1000 }]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,19 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { getTime } from './time.mjs';
|
||||||
|
|
||||||
|
function frame(callback) {
|
||||||
|
if (window.strudelAnimation) {
|
||||||
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
|
}
|
||||||
|
const animate = (animationTime) => {
|
||||||
|
callback(animationTime, getTime());
|
||||||
|
window.strudelAnimation = requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
|
||||||
export const backgroundImage = function (src, animateOptions = {}) {
|
export const backgroundImage = function (src, animateOptions = {}) {
|
||||||
const container = document.getElementById('code');
|
const container = document.getElementById('code');
|
||||||
const bg = 'background-image:url(' + src + ');background-size:contain;';
|
const bg = 'background-image:url(' + src + ');background-size:contain;';
|
||||||
@@ -22,6 +35,11 @@ export const backgroundImage = function (src, animateOptions = {}) {
|
|||||||
if (funcOptions.length === 0) {
|
if (funcOptions.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
frame((_, t) =>
|
||||||
|
funcOptions.forEach(([option, value]) => {
|
||||||
|
handleOption(option, value(t));
|
||||||
|
}),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanupUi = () => {
|
export const cleanupUi = () => {
|
||||||
|
|||||||
@@ -61,11 +61,6 @@ export const valueToMidi = (value, fallbackValue) => {
|
|||||||
return fallbackValue;
|
return fallbackValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// used to schedule external event like midi and osc out
|
|
||||||
export const getEventOffsetMs = (targetTimeSeconds, currentTimeSeconds) => {
|
|
||||||
return (targetTimeSeconds - currentTimeSeconds) * 1000;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
@@ -236,14 +231,6 @@ export const splitAt = function (index, value) {
|
|||||||
|
|
||||||
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||||
|
|
||||||
export const pairs = function (xs) {
|
|
||||||
const result = [];
|
|
||||||
for (let i = 0; i < xs.length - 1; ++i) {
|
|
||||||
result.push([xs[i], xs[i + 1]]);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||||
|
|
||||||
/* solmization, not used yet */
|
/* solmization, not used yet */
|
||||||
@@ -302,30 +289,6 @@ export const sol2note = (n, notation = 'letters') => {
|
|||||||
return note + oct;
|
return note + oct;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove duplicates from list
|
|
||||||
export function uniq(a) {
|
|
||||||
var seen = {};
|
|
||||||
return a.filter(function (item) {
|
|
||||||
return seen.hasOwn(item) ? false : (seen[item] = true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove duplicates from list, sorting in the process. Mutates argument!
|
|
||||||
export function uniqsort(a) {
|
|
||||||
return a.sort().filter(function (item, pos, ary) {
|
|
||||||
return !pos || item != ary[pos - 1];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// rational version
|
|
||||||
export function uniqsortr(a) {
|
|
||||||
return a
|
|
||||||
.sort((x, y) => x.compare(y))
|
|
||||||
.filter(function (item, pos, ary) {
|
|
||||||
return !pos || item.ne(ary[pos - 1]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// code hashing helpers
|
// code hashing helpers
|
||||||
|
|
||||||
export function unicodeToBase64(text) {
|
export function unicodeToBase64(text) {
|
||||||
@@ -360,23 +323,3 @@ export function objectMap(obj, fn) {
|
|||||||
}
|
}
|
||||||
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
|
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Floating point versions, see Fraction for rational versions
|
|
||||||
// // greatest common divisor
|
|
||||||
// export const gcd = function (x, y, ...z) {
|
|
||||||
// if (!y && z.length > 0) {
|
|
||||||
// return gcd(x, ...z);
|
|
||||||
// }
|
|
||||||
// if (!y) {
|
|
||||||
// return x;
|
|
||||||
// }
|
|
||||||
// return gcd(y, x % y, ...z);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // lowest common multiple
|
|
||||||
// export const lcm = function (x, y, ...z) {
|
|
||||||
// if (z.length == 0) {
|
|
||||||
// return (x * y) / gcd(x, y);
|
|
||||||
// }
|
|
||||||
// return lcm((x * y) / gcd(x, y), ...z);
|
|
||||||
// };
|
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { curry } from './util.mjs';
|
import { curry } from './util.mjs';
|
||||||
import { logger } from './logger.mjs';
|
|
||||||
|
|
||||||
export function unionWithObj(a, b, func) {
|
export function unionWithObj(a, b, func) {
|
||||||
if (b?.value !== undefined && Object.keys(b).length === 1) {
|
if (typeof b?.value === 'number') {
|
||||||
// https://github.com/tidalcycles/strudel/issues/1026
|
// https://github.com/tidalcycles/strudel/issues/262
|
||||||
logger(`[warn]: Can't do arithmetic on control pattern.`);
|
const numKeys = Object.keys(a).filter((k) => typeof a[k] === 'number');
|
||||||
return a;
|
const numerals = Object.fromEntries(numKeys.map((k) => [k, b.value]));
|
||||||
|
b = Object.assign(b, numerals);
|
||||||
|
delete b.value;
|
||||||
}
|
}
|
||||||
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
|
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
|
||||||
return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])])));
|
return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])])));
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ function createClock(
|
|||||||
duration = 0.05, // duration of each cycle
|
duration = 0.05, // duration of each cycle
|
||||||
interval = 0.1, // interval between callbacks
|
interval = 0.1, // interval between callbacks
|
||||||
overlap = 0.1, // overlap between callbacks
|
overlap = 0.1, // overlap between callbacks
|
||||||
setInterval = globalThis.setInterval,
|
|
||||||
clearInterval = globalThis.clearInterval,
|
|
||||||
round = true,
|
|
||||||
) {
|
) {
|
||||||
let tick = 0; // counts callbacks
|
let tick = 0; // counts callbacks
|
||||||
let phase = 0; // next callback time
|
let phase = 0; // next callback time
|
||||||
@@ -25,8 +22,9 @@ function createClock(
|
|||||||
}
|
}
|
||||||
// callback as long as we're inside the lookahead
|
// callback as long as we're inside the lookahead
|
||||||
while (phase < lookahead) {
|
while (phase < lookahead) {
|
||||||
phase = round ? Math.round(phase * precision) / precision : phase;
|
phase = Math.round(phase * precision) / precision;
|
||||||
callback(phase, duration, tick, t); // callback has to skip / handle phase < t!
|
phase >= t && callback(phase, duration, tick);
|
||||||
|
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
|
||||||
phase += duration; // increment phase by duration
|
phase += duration; // increment phase by duration
|
||||||
tick++;
|
tick++;
|
||||||
}
|
}
|
||||||
@@ -37,10 +35,7 @@ function createClock(
|
|||||||
onTick();
|
onTick();
|
||||||
intervalID = setInterval(onTick, interval * 1000);
|
intervalID = setInterval(onTick, interval * 1000);
|
||||||
};
|
};
|
||||||
const clear = () => {
|
const clear = () => intervalID !== undefined && clearInterval(intervalID);
|
||||||
intervalID !== undefined && clearInterval(intervalID);
|
|
||||||
intervalID = undefined;
|
|
||||||
};
|
|
||||||
const pause = () => clear();
|
const pause = () => clear();
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
tick = 0;
|
tick = 0;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
instrument = instrument || 'triangle';
|
instrument = instrument || 'triangle';
|
||||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||||
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
||||||
return pat.onTrigger((time_deprecate, hap, currentTime, _cps, targetTime) => {
|
return pat.onTrigger((time, hap) => {
|
||||||
if (!_csound) {
|
if (!_csound) {
|
||||||
logger('[csound] not loaded yet', 'warning');
|
logger('[csound] not loaded yet', 'warning');
|
||||||
return;
|
return;
|
||||||
@@ -38,11 +38,9 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
.join('/');
|
.join('/');
|
||||||
// TODO: find out how to send a precise ctx based time
|
// TODO: find out how to send a precise ctx based time
|
||||||
// http://www.csounds.com/manual/html/i.html
|
// http://www.csounds.com/manual/html/i.html
|
||||||
const timeOffset = targetTime - currentTime; // latency ?
|
|
||||||
//const timeOffset = time_deprecate - getAudioContext().currentTime
|
|
||||||
const params = [
|
const params = [
|
||||||
`"${instrument}"`, // p1: instrument name
|
`"${instrument}"`, // p1: instrument name
|
||||||
timeOffset, // p2: starting time in arbitrary unit called beats
|
time - getAudioContext().currentTime, //.toFixed(precision), // p2: starting time in arbitrary unit called beats
|
||||||
hap.duration + 0, // p3: duration in beats
|
hap.duration + 0, // p3: duration in beats
|
||||||
// instrument specific params:
|
// instrument specific params:
|
||||||
freq, //.toFixed(precision), // p4: frequency
|
freq, //.toFixed(precision), // p4: frequency
|
||||||
@@ -154,14 +152,12 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
|||||||
const p2 = tidal_time - getAudioContext().currentTime;
|
const p2 = tidal_time - getAudioContext().currentTime;
|
||||||
const p3 = hap.duration.valueOf() + 0;
|
const p3 = hap.duration.valueOf() + 0;
|
||||||
const frequency = getFrequency(hap);
|
const frequency = getFrequency(hap);
|
||||||
let { gain = 1, velocity = 0.9 } = hap.value;
|
|
||||||
velocity = gain * velocity;
|
|
||||||
// Translate frequency to MIDI key number _without_ rounding.
|
// Translate frequency to MIDI key number _without_ rounding.
|
||||||
const C4 = 261.62558;
|
const C4 = 261.62558;
|
||||||
let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0;
|
let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0;
|
||||||
const p4 = octave * 12.0 - 36.0;
|
const p4 = octave * 12.0 - 36.0;
|
||||||
// We prefer floating point precision, but over the MIDI range [0, 127].
|
// We prefer floating point precision, but over the MIDI range [0, 127].
|
||||||
const p5 = 127 * velocity;
|
const p5 = 127 * (hap.context?.velocity ?? 0.9);
|
||||||
// The Strudel controls as a string.
|
// The Strudel controls as a string.
|
||||||
const p6 = Object.entries({ ...hap.value, frequency })
|
const p6 = Object.entries({ ...hap.value, frequency })
|
||||||
.flat()
|
.flat()
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { Invoke } from './utils.mjs';
|
import { Invoke } from './utils.mjs';
|
||||||
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core';
|
import { Pattern, noteToMidi } from '@strudel/core';
|
||||||
|
|
||||||
const ON_MESSAGE = 0x90;
|
const ON_MESSAGE = 0x90;
|
||||||
const OFF_MESSAGE = 0x80;
|
const OFF_MESSAGE = 0x80;
|
||||||
const CC_MESSAGE = 0xb0;
|
const CC_MESSAGE = 0xb0;
|
||||||
|
|
||||||
Pattern.prototype.midi = function (output) {
|
Pattern.prototype.midi = function (output) {
|
||||||
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
return this.onTrigger((time, hap, currentTime, cps) => {
|
||||||
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
|
const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
|
||||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
const offset = (time - currentTime) * 1000;
|
||||||
const latencyMs = 34;
|
const velocity = Math.floor((hap.context?.velocity ?? 0.9) * 100); // TODO: refactor velocity
|
||||||
const offset = getEventOffsetMs(targetTime, currentTime) + latencyMs;
|
|
||||||
velocity = Math.floor(gain * velocity * 100);
|
|
||||||
const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10);
|
const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10);
|
||||||
const roundedOffset = Math.round(offset);
|
const roundedOffset = Math.round(offset);
|
||||||
const midichan = (hap.value.midichan ?? 1) - 1;
|
const midichan = (hap.value.midichan ?? 1) - 1;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { parseNumeral, Pattern, getEventOffsetMs } from '@strudel/core';
|
import { parseNumeral, Pattern } from '@strudel/core';
|
||||||
import { Invoke } from './utils.mjs';
|
import { Invoke } from './utils.mjs';
|
||||||
|
|
||||||
Pattern.prototype.osc = function () {
|
Pattern.prototype.osc = function () {
|
||||||
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => {
|
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
|
||||||
hap.ensureObjectValue();
|
hap.ensureObjectValue();
|
||||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||||
const delta = hap.duration.valueOf();
|
const delta = hap.duration.valueOf();
|
||||||
@@ -13,7 +13,7 @@ Pattern.prototype.osc = function () {
|
|||||||
|
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
const timestamp = Math.round(Date.now() + getEventOffsetMs(targetTime, currentTime));
|
const timestamp = Math.round(Date.now() + (time - currentTime) * 1000);
|
||||||
|
|
||||||
Object.keys(controls).forEach((key) => {
|
Object.keys(controls).forEach((key) => {
|
||||||
const val = controls[key];
|
const val = controls[key];
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
# @strudel/canvas
|
|
||||||
|
|
||||||
Helpers for drawing with the Canvas API and Strudel
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm i @strudel/canvas --save
|
|
||||||
```
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export * from './animate.mjs';
|
|
||||||
export * from './color.mjs';
|
|
||||||
export * from './draw.mjs';
|
|
||||||
export * from './pianoroll.mjs';
|
|
||||||
export * from './spiral.mjs';
|
|
||||||
export * from './pitchwheel.mjs';
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@strudel/draw",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"description": "Helpers for drawing with Strudel",
|
|
||||||
"main": "index.mjs",
|
|
||||||
"type": "module",
|
|
||||||
"publishConfig": {
|
|
||||||
"main": "dist/index.mjs"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "vite build",
|
|
||||||
"prepublishOnly": "npm run build"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"titdalcycles",
|
|
||||||
"strudel",
|
|
||||||
"pattern",
|
|
||||||
"livecoding",
|
|
||||||
"algorave"
|
|
||||||
],
|
|
||||||
"author": "Felix Roos <flix91@gmail.com>",
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
|
||||||
"dependencies": {
|
|
||||||
"@strudel/core": "workspace:*"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"vite": "^5.0.10"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import { Pattern, midiToFreq, getFrequency } from '@strudel/core';
|
|
||||||
import { getTheme, getDrawContext } from './draw.mjs';
|
|
||||||
|
|
||||||
const c = midiToFreq(36);
|
|
||||||
|
|
||||||
const circlePos = (cx, cy, radius, angle) => {
|
|
||||||
angle = angle * Math.PI * 2;
|
|
||||||
const x = Math.sin(angle) * radius + cx;
|
|
||||||
const y = Math.cos(angle) * radius + cy;
|
|
||||||
return [x, y];
|
|
||||||
};
|
|
||||||
|
|
||||||
const freq2angle = (freq, root) => {
|
|
||||||
return 0.5 - (Math.log2(freq / root) % 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function pitchwheel({
|
|
||||||
haps,
|
|
||||||
ctx,
|
|
||||||
id,
|
|
||||||
hapcircles = 1,
|
|
||||||
circle = 0,
|
|
||||||
edo = 12,
|
|
||||||
root = c,
|
|
||||||
thickness = 3,
|
|
||||||
hapRadius = 6,
|
|
||||||
mode = 'flake',
|
|
||||||
margin = 10,
|
|
||||||
} = {}) {
|
|
||||||
const connectdots = mode === 'polygon';
|
|
||||||
const centerlines = mode === 'flake';
|
|
||||||
const w = ctx.canvas.width;
|
|
||||||
const h = ctx.canvas.height;
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
|
||||||
const color = getTheme().foreground;
|
|
||||||
|
|
||||||
const size = Math.min(w, h);
|
|
||||||
const radius = size / 2 - thickness / 2 - hapRadius - margin;
|
|
||||||
const centerX = w / 2;
|
|
||||||
const centerY = h / 2;
|
|
||||||
|
|
||||||
if (id) {
|
|
||||||
haps = haps.filter((hap) => hap.hasTag(id));
|
|
||||||
}
|
|
||||||
ctx.strokeStyle = color;
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.globalAlpha = 1;
|
|
||||||
ctx.lineWidth = thickness;
|
|
||||||
|
|
||||||
if (circle) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (edo) {
|
|
||||||
Array.from({ length: edo }, (_, i) => {
|
|
||||||
const angle = freq2angle(root * Math.pow(2, i / edo), root);
|
|
||||||
const [x, y] = circlePos(centerX, centerY, radius, angle);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
|
|
||||||
ctx.fill();
|
|
||||||
});
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
let shape = [];
|
|
||||||
ctx.lineWidth = hapRadius;
|
|
||||||
haps.forEach((hap) => {
|
|
||||||
let freq;
|
|
||||||
try {
|
|
||||||
freq = getFrequency(hap);
|
|
||||||
} catch (err) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const angle = freq2angle(freq, root);
|
|
||||||
const [x, y] = circlePos(centerX, centerY, radius, angle);
|
|
||||||
const hapColor = hap.value.color || color;
|
|
||||||
ctx.strokeStyle = hapColor;
|
|
||||||
ctx.fillStyle = hapColor;
|
|
||||||
const { velocity = 1, gain = 1 } = hap.value || {};
|
|
||||||
const alpha = velocity * gain;
|
|
||||||
ctx.globalAlpha = alpha;
|
|
||||||
shape.push([x, y, angle, hapColor, alpha]);
|
|
||||||
ctx.beginPath();
|
|
||||||
if (hapcircles) {
|
|
||||||
ctx.moveTo(x + hapRadius, y);
|
|
||||||
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
if (centerlines) {
|
|
||||||
ctx.moveTo(centerX, centerY);
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.strokeStyle = color;
|
|
||||||
ctx.globalAlpha = 1;
|
|
||||||
if (connectdots && shape.length) {
|
|
||||||
shape = shape.sort((a, b) => a[2] - b[2]);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(shape[0][0], shape[0][1]);
|
|
||||||
shape.forEach(([x, y, _, color, alpha]) => {
|
|
||||||
ctx.strokeStyle = color;
|
|
||||||
ctx.globalAlpha = alpha;
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
});
|
|
||||||
ctx.lineTo(shape[0][0], shape[0][1]);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Pattern.prototype.pitchwheel = function (options = {}) {
|
|
||||||
let { ctx = getDrawContext(), id = 1 } = options;
|
|
||||||
return this.tag(id).onPaint((_, time, haps) =>
|
|
||||||
pitchwheel({
|
|
||||||
...options,
|
|
||||||
time,
|
|
||||||
ctx,
|
|
||||||
haps: haps.filter((hap) => hap.isActive(time)),
|
|
||||||
id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import { dependencies } from './package.json';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [],
|
|
||||||
build: {
|
|
||||||
lib: {
|
|
||||||
entry: resolve(__dirname, 'index.mjs'),
|
|
||||||
formats: ['es'],
|
|
||||||
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
external: [...Object.keys(dependencies)],
|
|
||||||
},
|
|
||||||
target: 'esnext',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
+20
-51
@@ -2,63 +2,32 @@
|
|||||||
|
|
||||||
This package contains a embeddable web component for the Strudel REPL.
|
This package contains a embeddable web component for the Strudel REPL.
|
||||||
|
|
||||||
## Usage via Script Tag
|
## Usage
|
||||||
|
|
||||||
Use this code in any HTML file:
|
Either install with `npm i @strudel/embed` or just use a cdn to import the script:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="https://unpkg.com/@strudel/embed@latest"></script>
|
<script src="https://unpkg.com/@strudel/embed@latest"></script>
|
||||||
<strudel-repl>
|
<strudel-repl>
|
||||||
<!--
|
<!--
|
||||||
setcps(1)
|
note(`[[e5 [b4 c5] d5 [c5 b4]]
|
||||||
n("<0 1 2 3 4>*8").scale('G4 minor')
|
[a4 [a4 c5] e5 [d5 c5]]
|
||||||
.s("gm_lead_6_voice")
|
[b4 [~ c5] d5 e5]
|
||||||
.clip(sine.range(.2,.8).slow(8))
|
[c5 a4 a4 ~]
|
||||||
.jux(rev)
|
[[~ d5] [~ f5] a5 [g5 f5]]
|
||||||
.room(2)
|
[e5 [~ c5] e5 [d5 c5]]
|
||||||
.sometimes(add(note("12")))
|
[b4 [b4 c5] d5 e5]
|
||||||
.lpf(perlin.range(200,20000).slow(4))
|
[c5 a4 a4 ~]],
|
||||||
-->
|
[[e2 e3]*4]
|
||||||
|
[[a2 a3]*4]
|
||||||
|
[[g#2 g#3]*2 [e2 e3]*2]
|
||||||
|
[a2 a3 a2 a3 a2 a3 b1 c2]
|
||||||
|
[[d2 d3]*4]
|
||||||
|
[[c2 c3]*4]
|
||||||
|
[[b1 b2]*2 [e2 e3]*2]
|
||||||
|
[[a1 a2]*4]`).slow(16)
|
||||||
|
-->
|
||||||
</strudel-repl>
|
</strudel-repl>
|
||||||
```
|
```
|
||||||
|
|
||||||
This will load the strudel website in an iframe, using the code provided within the HTML comments `<!-- -->`.
|
Note that the Code is placed inside HTML comments to prevent the browser from treating it as HTML.
|
||||||
The HTML comments are needed to make sure the browser won't interpret it as HTML.
|
|
||||||
|
|
||||||
Alternatively you can create a REPL from JavaScript like this:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://unpkg.com/@strudel/embed@1.0.2"></script>
|
|
||||||
<div id="strudel"></div>
|
|
||||||
<script>
|
|
||||||
let editor = document.createElement('strudel-repl');
|
|
||||||
editor.setAttribute(
|
|
||||||
'code',
|
|
||||||
`setcps(1)
|
|
||||||
n("<0 1 2 3 4>*8").scale('G4 minor')
|
|
||||||
.s("gm_lead_6_voice")
|
|
||||||
.clip(sine.range(.2,.8).slow(8))
|
|
||||||
.jux(rev)
|
|
||||||
.room(2)
|
|
||||||
.sometimes(add(note("12")))
|
|
||||||
.lpf(perlin.range(200,20000).slow(4))`,
|
|
||||||
);
|
|
||||||
document.getElementById('strudel').append(editor);
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
When you're using JSX, you could also use the `code` attribute in your markup:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://unpkg.com/@strudel/embed@1.0.2"></script>
|
|
||||||
<strudel-repl code={`
|
|
||||||
setcps(1)
|
|
||||||
n("<0 1 2 3 4>*8").scale('G4 minor')
|
|
||||||
.s("gm_lead_6_voice")
|
|
||||||
.clip(sine.range(.2,.8).slow(8))
|
|
||||||
.jux(rev)
|
|
||||||
.room(2)
|
|
||||||
.sometimes(add(note("12")))
|
|
||||||
.lpf(perlin.range(200,20000).slow(4))
|
|
||||||
`}></strudel-repl>
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class Strudel extends HTMLElement {
|
|||||||
}
|
}
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const code = this.getAttribute('code') || (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
|
const code = (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
const src = `https://strudel.cc/#${encodeURIComponent(btoa(code))}`;
|
const src = `https://strudel.cc/#${encodeURIComponent(btoa(code))}`;
|
||||||
// const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`;
|
// const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`;
|
||||||
|
|||||||
+15
-24
@@ -1,49 +1,40 @@
|
|||||||
import { getDrawContext } from '@strudel/draw';
|
import { getDrawContext } from '@strudel/core';
|
||||||
import { controls } from '@strudel/core';
|
|
||||||
|
|
||||||
let latestOptions;
|
let latestOptions;
|
||||||
let hydra;
|
|
||||||
|
function appendCanvas(c) {
|
||||||
|
const { canvas: testCanvas } = getDrawContext();
|
||||||
|
c.canvas.id = 'hydra-canvas';
|
||||||
|
c.canvas.style.position = 'fixed';
|
||||||
|
c.canvas.style.top = '0px';
|
||||||
|
testCanvas.after(c.canvas);
|
||||||
|
return testCanvas;
|
||||||
|
}
|
||||||
|
|
||||||
export async function initHydra(options = {}) {
|
export async function initHydra(options = {}) {
|
||||||
// reset if options have changed since last init
|
// reset if options have changed since last init
|
||||||
if (latestOptions && JSON.stringify(latestOptions) !== JSON.stringify(options)) {
|
if (latestOptions && JSON.stringify(latestOptions) !== JSON.stringify(options)) {
|
||||||
document.getElementById('hydra-canvas')?.remove();
|
document.getElementById('hydra-canvas').remove();
|
||||||
}
|
}
|
||||||
latestOptions = options;
|
latestOptions = options;
|
||||||
//load and init hydra
|
//load and init hydra
|
||||||
if (!document.getElementById('hydra-canvas')) {
|
if (!document.getElementById('hydra-canvas')) {
|
||||||
|
console.log('reinit..');
|
||||||
const {
|
const {
|
||||||
src = 'https://unpkg.com/hydra-synth',
|
src = 'https://unpkg.com/hydra-synth',
|
||||||
feedStrudel = false,
|
feedStrudel = false,
|
||||||
contextType = 'webgl',
|
|
||||||
pixelRatio = 1,
|
|
||||||
pixelated = true,
|
|
||||||
...hydraConfig
|
...hydraConfig
|
||||||
} = {
|
} = { detectAudio: false, ...options };
|
||||||
detectAudio: false,
|
|
||||||
...options,
|
|
||||||
};
|
|
||||||
const { canvas } = getDrawContext('hydra-canvas', { contextType, pixelRatio, pixelated });
|
|
||||||
hydraConfig.canvas = canvas;
|
|
||||||
|
|
||||||
await import(/* @vite-ignore */ src);
|
await import(/* @vite-ignore */ src);
|
||||||
hydra = new Hydra(hydraConfig);
|
const hydra = new Hydra(hydraConfig);
|
||||||
if (feedStrudel) {
|
if (feedStrudel) {
|
||||||
const { canvas } = getDrawContext();
|
const { canvas } = getDrawContext();
|
||||||
canvas.style.display = 'none';
|
canvas.style.display = 'none';
|
||||||
hydra.synth.s0.init({ src: canvas });
|
hydra.synth.s0.init({ src: canvas });
|
||||||
}
|
}
|
||||||
|
appendCanvas(hydra);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearHydra() {
|
|
||||||
if (hydra) {
|
|
||||||
hydra.hush();
|
|
||||||
}
|
|
||||||
globalThis.s0?.clear();
|
|
||||||
document.getElementById('hydra-canvas')?.remove();
|
|
||||||
globalThis.speed = controls.speed;
|
|
||||||
globalThis.shape = controls.shape;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const H = (p) => () => p.queryArc(getTime(), getTime())[0].value;
|
export const H = (p) => () => p.queryArc(getTime(), getTime())[0].value;
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
|
||||||
"hydra-synth": "^1.3.29"
|
"hydra-synth": "^1.3.29"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+11
-11
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _WebMidi from 'webmidi';
|
import * as _WebMidi from 'webmidi';
|
||||||
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
|
import { Pattern, isPattern, logger, ref } from '@strudel/core';
|
||||||
import { noteToMidi } from '@strudel/core';
|
import { noteToMidi } from '@strudel/core';
|
||||||
import { Note } from 'webmidi';
|
import { Note } from 'webmidi';
|
||||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
@@ -112,24 +112,24 @@ Pattern.prototype.midi = function (output) {
|
|||||||
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
return this.onTrigger((time, hap, currentTime, cps) => {
|
||||||
if (!WebMidi.enabled) {
|
if (!WebMidi.enabled) {
|
||||||
console.log('not enabled');
|
console.log('not enabled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const device = getDevice(output, WebMidi.outputs);
|
const device = getDevice(output, WebMidi.outputs);
|
||||||
hap.ensureObjectValue();
|
hap.ensureObjectValue();
|
||||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
|
||||||
const latencyMs = 34;
|
|
||||||
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
|
|
||||||
const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
|
|
||||||
// destructure value
|
|
||||||
let { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd, gain = 1, velocity = 0.9 } = hap.value;
|
|
||||||
|
|
||||||
velocity = gain * velocity;
|
const offset = (time - currentTime) * 1000;
|
||||||
|
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
|
||||||
|
const timeOffsetString = `+${offset}`;
|
||||||
|
|
||||||
|
// destructure value
|
||||||
|
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd } = hap.value;
|
||||||
|
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
|
||||||
|
|
||||||
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
|
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
|
||||||
const duration = (hap.duration.valueOf() / cps) * 1000 - 10;
|
const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10);
|
||||||
if (note != null) {
|
if (note != null) {
|
||||||
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
||||||
const midiNote = new Note(midiNumber, { attack: velocity, duration });
|
const midiNote = new Note(midiNumber, { attack: velocity, duration });
|
||||||
@@ -176,7 +176,7 @@ export async function midin(input) {
|
|||||||
}
|
}
|
||||||
const initial = await enableWebMidi(); // only returns on first init
|
const initial = await enableWebMidi(); // only returns on first init
|
||||||
const device = getDevice(input, WebMidi.inputs);
|
const device = getDevice(input, WebMidi.inputs);
|
||||||
if (initial) {
|
if (initial || WebMidi.enabled) {
|
||||||
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
||||||
logger(
|
logger(
|
||||||
`Midi enabled! Using "${device.name}". ${
|
`Midi enabled! Using "${device.name}". ${
|
||||||
|
|||||||
@@ -295,15 +295,7 @@ function peg$parse(input, options) {
|
|||||||
var peg$f6 = function(a) { return a };
|
var peg$f6 = function(a) { return a };
|
||||||
var peg$f7 = function(s) { s.arguments_.alignment = 'polymeter_slowcat'; return s; };
|
var peg$f7 = function(s) { s.arguments_.alignment = 'polymeter_slowcat'; return s; };
|
||||||
var peg$f8 = function(a) { return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 };
|
var peg$f8 = function(a) { return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 };
|
||||||
var peg$f9 = function(a) { return x => {const reps = (x.options_['reps'] ?? 1) + (a ?? 2) - 1;
|
var peg$f9 = function(a) { return x => x.options_['reps'] = (x.options_['reps'] ?? 1) + (a ?? 2) - 1 };
|
||||||
x.options_['reps'] = reps;
|
|
||||||
console.log("reps: ", reps)
|
|
||||||
x.options_['ops'] = x.options_['ops'].filter(x => x.type_ !== "replicate");
|
|
||||||
x.options_['ops'].push({ type_: "replicate", arguments_ :{ amount:reps }});
|
|
||||||
x.options_['weight'] = reps;
|
|
||||||
console.log("options: ", x.options_);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var peg$f10 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
|
var peg$f10 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
|
||||||
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
|
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
|
||||||
var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
|
var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
|
||||||
@@ -316,11 +308,11 @@ function peg$parse(input, options) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
var peg$f17 = function(tactus, s) { return new PatternStub(s, 'fastcat', undefined, !!tactus); };
|
var peg$f17 = function(s) { return new PatternStub(s, 'fastcat'); };
|
||||||
var peg$f18 = function(tail) { return { alignment: 'stack', list: tail }; };
|
var peg$f18 = function(tail) { return { alignment: 'stack', list: tail }; };
|
||||||
var peg$f19 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
|
var peg$f19 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
|
||||||
var peg$f20 = function(tail) { return { alignment: 'feet', list: tail, seed: seed++ }; };
|
var peg$f20 = function(tail) { return { alignment: 'feet', list: tail, seed: seed++ }; };
|
||||||
var peg$f21 = function(head, tail) {if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
|
var peg$f21 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
|
||||||
var peg$f22 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
var peg$f22 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
||||||
var peg$f23 = function(sc) { return sc; };
|
var peg$f23 = function(sc) { return sc; };
|
||||||
var peg$f24 = function(s) { return { name: "struct", args: { mini:s }}};
|
var peg$f24 = function(s) { return { name: "struct", args: { mini:s }}};
|
||||||
@@ -1485,36 +1477,24 @@ function peg$parse(input, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function peg$parsesequence() {
|
function peg$parsesequence() {
|
||||||
var s0, s1, s2, s3;
|
var s0, s1, s2;
|
||||||
|
|
||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
if (input.charCodeAt(peg$currPos) === 94) {
|
s1 = [];
|
||||||
s1 = peg$c9;
|
s2 = peg$parseslice_with_ops();
|
||||||
peg$currPos++;
|
if (s2 !== peg$FAILED) {
|
||||||
|
while (s2 !== peg$FAILED) {
|
||||||
|
s1.push(s2);
|
||||||
|
s2 = peg$parseslice_with_ops();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
s1 = peg$FAILED;
|
s1 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e17); }
|
|
||||||
}
|
}
|
||||||
if (s1 === peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
s1 = null;
|
|
||||||
}
|
|
||||||
s2 = [];
|
|
||||||
s3 = peg$parseslice_with_ops();
|
|
||||||
if (s3 !== peg$FAILED) {
|
|
||||||
while (s3 !== peg$FAILED) {
|
|
||||||
s2.push(s3);
|
|
||||||
s3 = peg$parseslice_with_ops();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
s2 = peg$FAILED;
|
|
||||||
}
|
|
||||||
if (s2 !== peg$FAILED) {
|
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f17(s1, s2);
|
s1 = peg$f17(s1);
|
||||||
} else {
|
|
||||||
peg$currPos = s0;
|
|
||||||
s0 = peg$FAILED;
|
|
||||||
}
|
}
|
||||||
|
s0 = s1;
|
||||||
|
|
||||||
return s0;
|
return s0;
|
||||||
}
|
}
|
||||||
@@ -2496,10 +2476,10 @@ function peg$parse(input, options) {
|
|||||||
this.location_ = location();
|
this.location_ = location();
|
||||||
}
|
}
|
||||||
|
|
||||||
var PatternStub = function(source, alignment, seed, tactus)
|
var PatternStub = function(source, alignment, seed)
|
||||||
{
|
{
|
||||||
this.type_ = "pattern";
|
this.type_ = "pattern";
|
||||||
this.arguments_ = { alignment: alignment, tactus: tactus };
|
this.arguments_ = { alignment: alignment };
|
||||||
if (seed !== undefined) {
|
if (seed !== undefined) {
|
||||||
this.arguments_.seed = seed;
|
this.arguments_.seed = seed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
this.location_ = location();
|
this.location_ = location();
|
||||||
}
|
}
|
||||||
|
|
||||||
var PatternStub = function(source, alignment, seed, tactus)
|
var PatternStub = function(source, alignment, seed)
|
||||||
{
|
{
|
||||||
this.type_ = "pattern";
|
this.type_ = "pattern";
|
||||||
this.arguments_ = { alignment: alignment, tactus: tactus };
|
this.arguments_ = { alignment: alignment };
|
||||||
if (seed !== undefined) {
|
if (seed !== undefined) {
|
||||||
this.arguments_.seed = seed;
|
this.arguments_.seed = seed;
|
||||||
}
|
}
|
||||||
@@ -135,14 +135,7 @@ op_weight = ws ("@" / "_") a:number?
|
|||||||
{ return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 }
|
{ return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 }
|
||||||
|
|
||||||
op_replicate = ws "!" a:number?
|
op_replicate = ws "!" a:number?
|
||||||
{ return x => {// A bit fiddly, to support both x!4 and x!!! as equivalent..
|
{ return x => x.options_['reps'] = (x.options_['reps'] ?? 1) + (a ?? 2) - 1 }
|
||||||
const reps = (x.options_['reps'] ?? 1) + (a ?? 2) - 1;
|
|
||||||
x.options_['reps'] = reps;
|
|
||||||
x.options_['ops'] = x.options_['ops'].filter(x => x.type_ !== "replicate");
|
|
||||||
x.options_['ops'].push({ type_: "replicate", arguments_ :{ amount:reps }});
|
|
||||||
x.options_['weight'] = reps;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
op_bjorklund = "(" ws p:slice_with_ops ws comma ws s:slice_with_ops ws comma? ws r:slice_with_ops? ws ")"
|
op_bjorklund = "(" ws p:slice_with_ops ws comma ws s:slice_with_ops ws comma? ws r:slice_with_ops? ws ")"
|
||||||
{ return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }
|
{ return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }
|
||||||
@@ -172,8 +165,8 @@ slice_with_ops = s:slice ops:slice_op*
|
|||||||
}
|
}
|
||||||
|
|
||||||
// a sequence is a combination of one or more successive slices (as an array)
|
// a sequence is a combination of one or more successive slices (as an array)
|
||||||
sequence = tactus:'^'? s:(slice_with_ops)+
|
sequence = s:(slice_with_ops)+
|
||||||
{ return new PatternStub(s, 'fastcat', undefined, !!tactus); }
|
{ return new PatternStub(s, 'fastcat'); }
|
||||||
|
|
||||||
// a stack is a series of vertically aligned sequence, separated by a comma
|
// a stack is a series of vertically aligned sequence, separated by a comma
|
||||||
stack_tail = tail:(comma @sequence)+
|
stack_tail = tail:(comma @sequence)+
|
||||||
@@ -191,7 +184,7 @@ dot_tail = tail:(dot @sequence)+
|
|||||||
// if the stack contains only one element, we don't create a stack but return the
|
// if the stack contains only one element, we don't create a stack but return the
|
||||||
// underlying element
|
// underlying element
|
||||||
stack_or_choose = head:sequence tail:(stack_tail / choose_tail / dot_tail)?
|
stack_or_choose = head:sequence tail:(stack_tail / choose_tail / dot_tail)?
|
||||||
{if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
|
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
|
||||||
|
|
||||||
polymeter_stack = head:sequence tail:stack_tail?
|
polymeter_stack = head:sequence tail:stack_tail?
|
||||||
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
|
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
|
||||||
|
|||||||
+34
-63
@@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import * as krill from './krill-parser.js';
|
import * as krill from './krill-parser.js';
|
||||||
import * as strudel from '@strudel/core';
|
import * as strudel from '@strudel/core';
|
||||||
import Fraction, { lcm } from '@strudel/core/fraction.mjs';
|
|
||||||
|
|
||||||
const randOffset = 0.0003;
|
const randOffset = 0.0003;
|
||||||
|
|
||||||
@@ -27,12 +26,6 @@ const applyOptions = (parent, enter) => (pat, i) => {
|
|||||||
pat = strudel.reify(pat)[type](enter(amount));
|
pat = strudel.reify(pat)[type](enter(amount));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'replicate': {
|
|
||||||
const { amount } = op.arguments_;
|
|
||||||
pat = strudel.reify(pat);
|
|
||||||
pat = pat._repeatCycles(amount)._fast(amount);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'bjorklund': {
|
case 'bjorklund': {
|
||||||
if (op.arguments_.rotation) {
|
if (op.arguments_.rotation) {
|
||||||
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
|
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
|
||||||
@@ -73,88 +66,66 @@ const applyOptions = (parent, enter) => (pat, i) => {
|
|||||||
return pat;
|
return pat;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resolveReplications(ast) {
|
||||||
|
ast.source_ = strudel.flatten(
|
||||||
|
ast.source_.map((child) => {
|
||||||
|
const { reps } = child.options_ || {};
|
||||||
|
if (!reps) {
|
||||||
|
return [child];
|
||||||
|
}
|
||||||
|
delete child.options_.reps;
|
||||||
|
return Array(reps).fill(child);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
|
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
|
||||||
export function patternifyAST(ast, code, onEnter, offset = 0) {
|
export function patternifyAST(ast, code, onEnter, offset = 0) {
|
||||||
onEnter?.(ast);
|
onEnter?.(ast);
|
||||||
const enter = (node) => patternifyAST(node, code, onEnter, offset);
|
const enter = (node) => patternifyAST(node, code, onEnter, offset);
|
||||||
switch (ast.type_) {
|
switch (ast.type_) {
|
||||||
case 'pattern': {
|
case 'pattern': {
|
||||||
// resolveReplications(ast);
|
resolveReplications(ast);
|
||||||
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
|
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
|
||||||
const alignment = ast.arguments_.alignment;
|
const alignment = ast.arguments_.alignment;
|
||||||
const with_tactus = children.filter((child) => child.__tactus_source);
|
if (alignment === 'stack') {
|
||||||
let pat;
|
return strudel.stack(...children);
|
||||||
switch (alignment) {
|
|
||||||
case 'stack': {
|
|
||||||
pat = strudel.stack(...children);
|
|
||||||
if (with_tactus.length) {
|
|
||||||
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
|
|
||||||
}
|
}
|
||||||
break;
|
if (alignment === 'polymeter_slowcat') {
|
||||||
|
const aligned = children.map((child) => child._slow(strudel.Fraction(child.__weight ?? 1)));
|
||||||
|
return strudel.stack(...aligned);
|
||||||
}
|
}
|
||||||
case 'polymeter_slowcat': {
|
if (alignment === 'polymeter') {
|
||||||
pat = strudel.stack(...children.map((child) => child._slow(child.__weight)));
|
|
||||||
if (with_tactus.length) {
|
|
||||||
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'polymeter': {
|
|
||||||
// polymeter
|
// polymeter
|
||||||
const stepsPerCycle = ast.arguments_.stepsPerCycle
|
const stepsPerCycle = ast.arguments_.stepsPerCycle
|
||||||
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
|
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
|
||||||
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
|
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
|
||||||
|
|
||||||
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight))));
|
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
|
||||||
pat = strudel.stack(...aligned);
|
return strudel.stack(...aligned);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case 'rand': {
|
if (alignment === 'rand') {
|
||||||
pat = strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
|
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
|
||||||
if (with_tactus.length) {
|
|
||||||
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
|
|
||||||
}
|
}
|
||||||
break;
|
if (alignment === 'feet') {
|
||||||
|
return strudel.fastcat(...children);
|
||||||
}
|
}
|
||||||
case 'feet': {
|
|
||||||
pat = strudel.fastcat(...children);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
||||||
if (weightedChildren) {
|
if (weightedChildren) {
|
||||||
const weightSum = ast.source_.reduce(
|
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
|
||||||
(sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)),
|
const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
|
||||||
strudel.Fraction(0),
|
pat.__weight = weightSum;
|
||||||
);
|
return pat;
|
||||||
pat = strudel.timeCat(
|
|
||||||
...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]),
|
|
||||||
);
|
|
||||||
pat.__weight = weightSum; // for polymeter
|
|
||||||
pat.tactus = weightSum;
|
|
||||||
if (with_tactus.length) {
|
|
||||||
pat.tactus = pat.tactus.mul(lcm(...with_tactus.map((x) => Fraction(x.tactus))));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pat = strudel.sequence(...children);
|
|
||||||
pat.tactus = children.length;
|
|
||||||
}
|
|
||||||
if (ast.arguments_.tactus) {
|
|
||||||
pat.__tactus_source = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (with_tactus.length) {
|
|
||||||
pat.__tactus_source = true;
|
|
||||||
}
|
}
|
||||||
|
const pat = strudel.sequence(...children);
|
||||||
|
pat.__weight = children.length;
|
||||||
return pat;
|
return pat;
|
||||||
}
|
}
|
||||||
case 'element': {
|
case 'element': {
|
||||||
1;
|
|
||||||
return enter(ast.source_);
|
return enter(ast.source_);
|
||||||
}
|
}
|
||||||
case 'atom': {
|
case 'atom': {
|
||||||
if (ast.source_ === '~' || ast.source_ === '-') {
|
if (ast.source_ === '~') {
|
||||||
return strudel.silence;
|
return strudel.silence;
|
||||||
}
|
}
|
||||||
if (!ast.location_) {
|
if (!ast.location_) {
|
||||||
@@ -190,7 +161,7 @@ export const getLeafLocation = (code, leaf, globalOffset = 0) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// takes quoted mini string, returns ast
|
// takes quoted mini string, returns ast
|
||||||
export const mini2ast = (code, start = 0, userCode = code) => {
|
export const mini2ast = (code, start, userCode) => {
|
||||||
try {
|
try {
|
||||||
return krill.parse(code);
|
return krill.parse(code);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs';
|
import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs';
|
||||||
import '@strudel/core/euclid.mjs';
|
import '@strudel/core/euclid.mjs';
|
||||||
import { Fraction } from '@strudel/core/index.mjs';
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('mini', () => {
|
describe('mini', () => {
|
||||||
@@ -118,9 +117,6 @@ describe('mini', () => {
|
|||||||
checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~');
|
checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~');
|
||||||
checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~');
|
checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~');
|
||||||
});
|
});
|
||||||
it('supports the - alias for ~', () => {
|
|
||||||
expect(minS('a - b [- c]')).toEqual(minS('a ~ b [~ c]'));
|
|
||||||
});
|
|
||||||
it('supports the ? operator', () => {
|
it('supports the ? operator', () => {
|
||||||
expect(
|
expect(
|
||||||
mini('a?')
|
mini('a?')
|
||||||
@@ -208,15 +204,6 @@ describe('mini', () => {
|
|||||||
it('_ and @ are almost interchangeable', () => {
|
it('_ and @ are almost interchangeable', () => {
|
||||||
expect(minS('a @ b @ @')).toEqual(minS('a _2 b _3'));
|
expect(minS('a @ b @ @')).toEqual(minS('a _2 b _3'));
|
||||||
});
|
});
|
||||||
it('supports ^ tactus marking', () => {
|
|
||||||
expect(mini('a [^b c]').tactus).toEqual(Fraction(4));
|
|
||||||
expect(mini('[a b c] [d [e f]]').tactus).toEqual(Fraction(2));
|
|
||||||
expect(mini('^[a b c] [d [e f]]').tactus).toEqual(Fraction(2));
|
|
||||||
expect(mini('[a b c] [d [^e f]]').tactus).toEqual(Fraction(8));
|
|
||||||
expect(mini('[a b c] [^d [e f]]').tactus).toEqual(Fraction(4));
|
|
||||||
expect(mini('[^a b c] [^d [e f]]').tactus).toEqual(Fraction(12));
|
|
||||||
expect(mini('[^a b c] [d [^e f]]').tactus).toEqual(Fraction(24));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getLeafLocation', () => {
|
describe('getLeafLocation', () => {
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
# @strudel/node
|
|
||||||
|
|
||||||
This is an experiment to run strudel in node.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Setup
|
|
||||||
# - make sure node >= 20 is installed
|
|
||||||
# - make sure pnpm is installed
|
|
||||||
cd packages/node
|
|
||||||
pnpm i
|
|
||||||
# Usage
|
|
||||||
pnpm start
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run `sclang` with superdirt in another terminal.
|
|
||||||
|
|
||||||
You can now edit and save the file `pattern.mjs` to update your pattern!
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { createClock, evalScope } from '@strudel/core';
|
|
||||||
import { evaluate } from '@strudel/transpiler';
|
|
||||||
import watch from 'node-watch';
|
|
||||||
import fs from 'node:fs/promises';
|
|
||||||
import { AudioContext, OscillatorNode, GainNode } from 'node-web-audio-api';
|
|
||||||
|
|
||||||
const audioContext = new AudioContext();
|
|
||||||
|
|
||||||
let file = 'pattern.mjs';
|
|
||||||
let pattern;
|
|
||||||
async function evaluateFile() {
|
|
||||||
try {
|
|
||||||
console.log('// file evaluated:');
|
|
||||||
const code = await fs.readFile(file, { encoding: 'utf8' });
|
|
||||||
console.log(code);
|
|
||||||
const res = await evaluate(code);
|
|
||||||
pattern = res.pattern;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// const getTime = () => performance.now() / 1000;
|
|
||||||
const getTime = () => audioContext.currentTime;
|
|
||||||
let minLatency = 50;
|
|
||||||
async function main() {
|
|
||||||
await evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/tonal'));
|
|
||||||
await evaluateFile();
|
|
||||||
watch(file, { recursive: true }, () => evaluateFile());
|
|
||||||
let lastEnd;
|
|
||||||
const clock = createClock(getTime, (phase) => {
|
|
||||||
if (!lastEnd) {
|
|
||||||
lastEnd = phase;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const haps = pattern.queryArc(lastEnd, phase);
|
|
||||||
lastEnd = phase;
|
|
||||||
const cps = 1;
|
|
||||||
const cycle = Math.floor(phase);
|
|
||||||
haps
|
|
||||||
.filter((h) => h.hasOnset())
|
|
||||||
.forEach((hap) => {
|
|
||||||
const env = new GainNode(audioContext, { gain: 0 });
|
|
||||||
const { attack = 0.01, gain = 1 } = hap.value;
|
|
||||||
env.connect(audioContext.destination);
|
|
||||||
const now = hap.whole.begin;
|
|
||||||
const duration = hap.duration;
|
|
||||||
env.gain
|
|
||||||
.setValueAtTime(0, now)
|
|
||||||
.linearRampToValueAtTime(gain * 0.2, now + attack)
|
|
||||||
.exponentialRampToValueAtTime(0.0001, now + duration);
|
|
||||||
const frequency = hap.value.freq;
|
|
||||||
|
|
||||||
const osc = new OscillatorNode(audioContext, { frequency });
|
|
||||||
osc.connect(env);
|
|
||||||
osc.start(now);
|
|
||||||
osc.stop(now + duration);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
clock.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { createClock, evalScope } from '@strudel/core';
|
|
||||||
import { evaluate } from '@strudel/transpiler';
|
|
||||||
import OSC from 'osc-js';
|
|
||||||
import watch from 'node-watch';
|
|
||||||
import fs from 'node:fs/promises';
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
receiver: 'udp', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
|
|
||||||
open: {
|
|
||||||
host: 'localhost', // @param {string} Hostname of udp server to bind to
|
|
||||||
port: 57121, // @param {number} Port of udp client for messaging
|
|
||||||
// enabling the following line will receive tidal messages:
|
|
||||||
// port: 57120, // @param {number} Port of udp client for messaging
|
|
||||||
exclusive: false, // @param {boolean} Exclusive flag
|
|
||||||
},
|
|
||||||
send: {
|
|
||||||
host: 'localhost', // @param {string} Hostname of udp client for messaging
|
|
||||||
port: 57120, // @param {number} Port of udp client for messaging
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const osc = new OSC({ plugin: new OSC.DatagramPlugin(config) });
|
|
||||||
|
|
||||||
osc.open(); // start a WebSocket server on port 8080
|
|
||||||
|
|
||||||
console.log('osc client running on port', config.open.port);
|
|
||||||
console.log('osc server running on port', config.send.port);
|
|
||||||
|
|
||||||
let file = 'pattern.mjs';
|
|
||||||
let pattern;
|
|
||||||
async function evaluateFile() {
|
|
||||||
try {
|
|
||||||
console.log('// file evaluated:');
|
|
||||||
const code = await fs.readFile(file, { encoding: 'utf8' });
|
|
||||||
console.log(code);
|
|
||||||
const res = await evaluate(code);
|
|
||||||
pattern = res.pattern;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTime = () => performance.now() / 1000;
|
|
||||||
async function main() {
|
|
||||||
await evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/tonal'));
|
|
||||||
await evaluateFile();
|
|
||||||
watch(file, { recursive: true }, () => evaluateFile());
|
|
||||||
let lastEnd;
|
|
||||||
let minLatency = 50;
|
|
||||||
const clock = createClock(getTime, (phase) => {
|
|
||||||
if (!lastEnd) {
|
|
||||||
lastEnd = phase;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const haps = pattern.queryArc(lastEnd, phase);
|
|
||||||
lastEnd = phase;
|
|
||||||
const cps = 1;
|
|
||||||
const cycle = Math.floor(phase);
|
|
||||||
haps
|
|
||||||
.filter((h) => h.hasOnset())
|
|
||||||
.forEach((hap) => {
|
|
||||||
const delta = hap.duration.valueOf();
|
|
||||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
|
||||||
const keyvals = Object.entries(controls).flat();
|
|
||||||
const ts = Math.floor(performance.timeOrigin + hap.whole.begin * 1000 + minLatency);
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
clock.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@strudel/node",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"description": "Strudel running in node",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
|
||||||
},
|
|
||||||
"main": "index.mjs",
|
|
||||||
"scripts": {
|
|
||||||
"osc": "node osc-superdirt.mjs",
|
|
||||||
"waa": "node node-web-audio-api.mjs"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"tidalcycles",
|
|
||||||
"strudel",
|
|
||||||
"pattern",
|
|
||||||
"livecoding",
|
|
||||||
"algorave"
|
|
||||||
],
|
|
||||||
"author": "Felix Roos <flix91@gmail.com>",
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
|
||||||
"dependencies": {
|
|
||||||
"@strudel/core": "workspace:*",
|
|
||||||
"@strudel/mini": "workspace:*",
|
|
||||||
"@strudel/osc": "workspace:*",
|
|
||||||
"@strudel/tonal": "workspace:*",
|
|
||||||
"@strudel/transpiler": "workspace:*",
|
|
||||||
"node-watch": "^0.7.4",
|
|
||||||
"node-web-audio-api": "^0.20.0",
|
|
||||||
"osc-js": "^2.4.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
freq("110 220 [330 440,550,660]")
|
|
||||||
.attack(.1).gain(.4)
|
|
||||||
@@ -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 OSC from 'osc-js';
|
||||||
|
|
||||||
import { logger, parseNumeral, Pattern, getEventOffsetMs } from '@strudel/core';
|
import { logger, parseNumeral, Pattern } from '@strudel/core';
|
||||||
|
|
||||||
let connection; // Promise<OSC>
|
let connection; // Promise<OSC>
|
||||||
function connect() {
|
function connect() {
|
||||||
@@ -44,7 +44,7 @@ function connect() {
|
|||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.osc = function () {
|
Pattern.prototype.osc = function () {
|
||||||
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => {
|
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
|
||||||
hap.ensureObjectValue();
|
hap.ensureObjectValue();
|
||||||
const osc = await connect();
|
const osc = await connect();
|
||||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||||
@@ -56,8 +56,7 @@ Pattern.prototype.osc = function () {
|
|||||||
const keyvals = Object.entries(controls).flat();
|
const keyvals = Object.entries(controls).flat();
|
||||||
// time should be audio time of onset
|
// time should be audio time of onset
|
||||||
// currentTime should be current time of audio context (slightly before time)
|
// currentTime should be current time of audio context (slightly before time)
|
||||||
const offset = getEventOffsetMs(targetTime, currentTime);
|
const offset = (time - currentTime) * 1000;
|
||||||
|
|
||||||
// timestamp in milliseconds used to trigger the osc bundle at a precise moment
|
// timestamp in milliseconds used to trigger the osc bundle at a precise moment
|
||||||
const ts = Math.floor(Date.now() + offset);
|
const ts = Math.floor(Date.now() + offset);
|
||||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import OSC from 'osc-js';
|
const OSC = require('osc-js');
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
|
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
|
||||||
|
|||||||
+1
-92
@@ -2,95 +2,4 @@
|
|||||||
|
|
||||||
The Strudel REPL as a web component.
|
The Strudel REPL as a web component.
|
||||||
|
|
||||||
## Add Script Tag
|
[Usage example](https://github.com/tidalcycles/strudel/blob/main/examples/buildless/web-component-no-iframe.html)
|
||||||
|
|
||||||
First place this script tag once in your HTML:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://unpkg.com/@strudel/repl@latest"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also pin the version like this:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://unpkg.com/@strudel/repl@1.0.2"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
This has the advantage that your code will always work, regardless of potential breaking changes in the strudel codebase.
|
|
||||||
See [releases](https://github.com/tidalcycles/strudel/releases) for the latest versions.
|
|
||||||
|
|
||||||
## Use Web Component
|
|
||||||
|
|
||||||
When you've added the script tag, you can use the `strudel-editor` web component:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<strudel-editor>
|
|
||||||
<!--
|
|
||||||
setcps(1)
|
|
||||||
n("<0 1 2 3 4>*8").scale('G4 minor')
|
|
||||||
.s("gm_lead_6_voice")
|
|
||||||
.clip(sine.range(.2,.8).slow(8))
|
|
||||||
.jux(rev)
|
|
||||||
.room(2)
|
|
||||||
.sometimes(add(note("12")))
|
|
||||||
.lpf(perlin.range(200,20000).slow(4))
|
|
||||||
-->
|
|
||||||
</strudel-editor>
|
|
||||||
```
|
|
||||||
|
|
||||||
This will load the Strudel REPL using the code provided within the HTML comments `<!-- -->`.
|
|
||||||
The HTML comments are needed to make sure the browser won't interpret it as HTML.
|
|
||||||
|
|
||||||
Alternatively you can create a REPL from JavaScript like this:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://unpkg.com/@strudel/repl@latest"></script>
|
|
||||||
<div id="strudel"></div>
|
|
||||||
<script>
|
|
||||||
const repl = document.createElement('strudel-editor');
|
|
||||||
repl.setAttribute(
|
|
||||||
'code',
|
|
||||||
`setcps(1)
|
|
||||||
n("<0 1 2 3 4>*8").scale('G4 minor')
|
|
||||||
.s("gm_lead_6_voice")
|
|
||||||
.clip(sine.range(.2,.8).slow(8))
|
|
||||||
.jux(rev)
|
|
||||||
.room(2)
|
|
||||||
.sometimes(add(note("12")))
|
|
||||||
.lpf(perlin.range(200,20000).slow(4))`,
|
|
||||||
);
|
|
||||||
document.getElementById('strudel').append(repl);
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Interacting with the REPL
|
|
||||||
|
|
||||||
If you get a hold of the `strudel-editor` element, you can interact with the strudel REPL from Javascript:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://unpkg.com/@strudel/repl@latest"></script>
|
|
||||||
<strudel-editor id="repl">
|
|
||||||
<!-- ... -->
|
|
||||||
</strudel-editor>
|
|
||||||
<script>
|
|
||||||
const repl = document.getElementById('repl');
|
|
||||||
console.log(repl.editor);
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://unpkg.com/@strudel/repl@latest"></script>
|
|
||||||
<div id="strudel"></div>
|
|
||||||
<script>
|
|
||||||
const repl = document.createElement('strudel-editor');
|
|
||||||
repl.setAttribute('code', `...`);
|
|
||||||
document.getElementById('strudel').append(repl);
|
|
||||||
console.log(repl.editor);
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
The `.editor` property on the `strudel-editor` web component gives you the instance of [StrudelMirror](https://github.com/tidalcycles/strudel/blob/a46bd9b36ea7d31c9f1d3fca484297c7da86893f/packages/codemirror/codemirror.mjs#L124) that runs the REPL.
|
|
||||||
|
|
||||||
For example, you could use `setCode` to change the code from the outside, `start` / `stop` to toggle playback or `evaluate` to evaluate the code.
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/codemirror": "workspace:*",
|
"@strudel/codemirror": "workspace:*",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
|
||||||
"@strudel/hydra": "workspace:*",
|
"@strudel/hydra": "workspace:*",
|
||||||
"@strudel/midi": "workspace:*",
|
"@strudel/midi": "workspace:*",
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { noteToMidi, valueToMidi, Pattern, evalScope } from '@strudel/core';
|
import { controls, noteToMidi, valueToMidi, Pattern, evalScope } from '@strudel/core';
|
||||||
import { registerSynthSounds, registerZZFXSounds, samples } from '@strudel/webaudio';
|
import { registerSynthSounds, registerZZFXSounds, samples } from '@strudel/webaudio';
|
||||||
import * as core from '@strudel/core';
|
import * as core from '@strudel/core';
|
||||||
|
|
||||||
@@ -6,7 +6,6 @@ export async function prebake() {
|
|||||||
const modulesLoading = evalScope(
|
const modulesLoading = evalScope(
|
||||||
// import('@strudel/core'),
|
// import('@strudel/core'),
|
||||||
core,
|
core,
|
||||||
import('@strudel/draw'),
|
|
||||||
import('@strudel/mini'),
|
import('@strudel/mini'),
|
||||||
import('@strudel/tonal'),
|
import('@strudel/tonal'),
|
||||||
import('@strudel/webaudio'),
|
import('@strudel/webaudio'),
|
||||||
@@ -18,6 +17,7 @@ export async function prebake() {
|
|||||||
// import('@strudel/serial'),
|
// import('@strudel/serial'),
|
||||||
// import('@strudel/csound'),
|
// import('@strudel/csound'),
|
||||||
// import('@strudel/osc'),
|
// import('@strudel/osc'),
|
||||||
|
controls, // sadly, this cannot be exported from core directly (yet)
|
||||||
);
|
);
|
||||||
// load samples
|
// load samples
|
||||||
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/';
|
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { silence } from '@strudel/core';
|
import { getDrawContext, silence } from '@strudel/core';
|
||||||
import { getDrawContext } from '@strudel/draw';
|
|
||||||
import { transpiler } from '@strudel/transpiler';
|
import { transpiler } from '@strudel/transpiler';
|
||||||
import { getAudioContext, webaudioOutput } from '@strudel/webaudio';
|
import { getAudioContext, webaudioOutput } from '@strudel/webaudio';
|
||||||
import { StrudelMirror, codemirrorSettings } from '@strudel/codemirror';
|
import { StrudelMirror, codemirrorSettings } from '@strudel/codemirror';
|
||||||
@@ -41,8 +40,17 @@ if (typeof HTMLElement !== 'undefined') {
|
|||||||
initialCode: '// LOADING',
|
initialCode: '// LOADING',
|
||||||
pattern: silence,
|
pattern: silence,
|
||||||
drawTime,
|
drawTime,
|
||||||
drawContext,
|
onDraw: (haps, time, frame, painters) => {
|
||||||
|
painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2);
|
||||||
|
painters?.forEach((painter) => {
|
||||||
|
// ctx time haps drawTime paintOptions
|
||||||
|
painter(drawContext, time, haps, drawTime, { clear: false });
|
||||||
|
});
|
||||||
|
},
|
||||||
prebake,
|
prebake,
|
||||||
|
afterEval: ({ code }) => {
|
||||||
|
// window.location.hash = '#' + code2hash(code);
|
||||||
|
},
|
||||||
onUpdateState: (state) => {
|
onUpdateState: (state) => {
|
||||||
const event = new CustomEvent('update', {
|
const event = new CustomEvent('update', {
|
||||||
detail: state,
|
detail: state,
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# @strudel/sampler
|
|
||||||
|
|
||||||
This package allows you to serve your samples on disk to the strudel REPL.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd ~/your/samples/
|
|
||||||
npx @strudel/sampler
|
|
||||||
```
|
|
||||||
|
|
||||||
This will run a server on `http://localhost:5432`.
|
|
||||||
You can now load the samples via:
|
|
||||||
|
|
||||||
```js
|
|
||||||
samples('http://localhost:5432')
|
|
||||||
```
|
|
||||||
|
|
||||||
## Options
|
|
||||||
|
|
||||||
```sh
|
|
||||||
LOG=1 npx @strudel/sampler # adds logging
|
|
||||||
PORT=5555 npx @strudel/sampler # changes port
|
|
||||||
```
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@strudel/sampler",
|
|
||||||
"version": "0.0.9",
|
|
||||||
"description": "",
|
|
||||||
"keywords": [
|
|
||||||
"tidalcycles",
|
|
||||||
"strudel",
|
|
||||||
"pattern",
|
|
||||||
"livecoding",
|
|
||||||
"algorave"
|
|
||||||
],
|
|
||||||
"author": "Felix Roos <flix91@gmail.com>",
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"bin": "./sample-server.mjs",
|
|
||||||
"type": "module",
|
|
||||||
"dependencies": {
|
|
||||||
"cowsay": "^1.6.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import cowsay from 'cowsay';
|
|
||||||
import { createReadStream } from 'fs';
|
|
||||||
import { readdir } from 'fs/promises';
|
|
||||||
import http from 'http';
|
|
||||||
import { join } from 'path';
|
|
||||||
import os from 'os';
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const LOG = !!process.env.LOG || false;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
cowsay.say({
|
|
||||||
text: 'welcome to @strudel/sampler',
|
|
||||||
e: 'oO',
|
|
||||||
T: 'U ',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
async function getFilesInDirectory(directory) {
|
|
||||||
let files = [];
|
|
||||||
const dirents = await readdir(directory, { withFileTypes: true });
|
|
||||||
for (const dirent of dirents) {
|
|
||||||
const fullPath = join(directory, dirent.name);
|
|
||||||
if (dirent.isDirectory()) {
|
|
||||||
if (dirent.name.startsWith('.')) {
|
|
||||||
LOG && console.warn(`ignore hidden folder: ${fullPath}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const subFiles = (await getFilesInDirectory(fullPath)).filter((f) =>
|
|
||||||
['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()),
|
|
||||||
);
|
|
||||||
files = files.concat(subFiles);
|
|
||||||
LOG && console.log(`${dirent.name} (${subFiles.length})`);
|
|
||||||
} catch (err) {
|
|
||||||
LOG && console.warn(`skipped due to error: ${fullPath}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
files.push(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getBanks(directory) {
|
|
||||||
// const directory = resolve(__dirname, '.');
|
|
||||||
let files = await getFilesInDirectory(directory);
|
|
||||||
let banks = {};
|
|
||||||
files = files.map((url) => {
|
|
||||||
const [bank] = url.split('/').slice(-2);
|
|
||||||
banks[bank] = banks[bank] || [];
|
|
||||||
url = url.replace(directory, '');
|
|
||||||
banks[bank].push(url);
|
|
||||||
return url;
|
|
||||||
});
|
|
||||||
banks._base = `http://localhost:5432`;
|
|
||||||
return { banks, files };
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const directory = process.cwd();
|
|
||||||
const server = http.createServer(async (req, res) => {
|
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
||||||
const { banks, files } = await getBanks(directory);
|
|
||||||
if (req.url === '/') {
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
|
||||||
return res.end(JSON.stringify(banks));
|
|
||||||
}
|
|
||||||
let subpath = decodeURIComponent(req.url);
|
|
||||||
if (!files.includes(subpath)) {
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end('File not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const filePath = join(directory, subpath);
|
|
||||||
const readStream = createReadStream(filePath);
|
|
||||||
readStream.on('error', (err) => {
|
|
||||||
res.statusCode = 500;
|
|
||||||
res.end('Internal server error');
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
readStream.pipe(res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const PORT = process.env.PORT || 5432;
|
|
||||||
const IP_ADDRESS = '0.0.0.0';
|
|
||||||
let IP;
|
|
||||||
const networkInterfaces = os.networkInterfaces();
|
|
||||||
|
|
||||||
Object.keys(networkInterfaces).forEach((key) => {
|
|
||||||
networkInterfaces[key].forEach((networkInterface) => {
|
|
||||||
if (networkInterface.family === 'IPv4' && !networkInterface.internal) {
|
|
||||||
IP = networkInterface.address;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!IP) {
|
|
||||||
console.error("Unable to determine server's IP address.");
|
|
||||||
// eslint-disable-next-line
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
server.listen(PORT, IP_ADDRESS, () => {
|
|
||||||
console.log(`@strudel/sampler is now serving audio files from:
|
|
||||||
${directory}
|
|
||||||
|
|
||||||
To use them in the Strudel REPL, run:
|
|
||||||
samples('http://localhost:${PORT}')
|
|
||||||
|
|
||||||
Or on a machine in the same network:
|
|
||||||
samples('http://${IP}:${PORT}')
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
rustsaw/target
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# superdough-wasm
|
||||||
|
|
||||||
|
This is just a very early experiment to find out how to run wasm in an AudioWorklet.
|
||||||
|
WASM can be compiled from several languages, which are tested here..
|
||||||
|
|
||||||
|
## zig
|
||||||
|
|
||||||
|
<https://dev.to/sleibrock/webassembly-with-zig-part-1-4onm>
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# (re)compile dsp.zig
|
||||||
|
brew install zig # prequisite
|
||||||
|
cd zigsaw
|
||||||
|
zig build-lib zigsaw.zig -target wasm32-freestanding -dynamic -rdynamic -O ReleaseSmall # build
|
||||||
|
npx http-server .. -o # run
|
||||||
|
```
|
||||||
|
|
||||||
|
wasm file size: 690B
|
||||||
|
|
||||||
|
## rust
|
||||||
|
|
||||||
|
<https://developer.mozilla.org/en-US/docs/WebAssembly/Rust_to_Wasm>
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# https://www.rust-lang.org/tools/install
|
||||||
|
cargo install wasm-pack # prequisite
|
||||||
|
cd rustsaw
|
||||||
|
wasm-pack build --target bundler # build
|
||||||
|
npx http-server .. -o # run
|
||||||
|
```
|
||||||
|
|
||||||
|
wasm file size: 653B
|
||||||
|
|
||||||
|
## c
|
||||||
|
|
||||||
|
<https://emscripten.org/docs/getting_started/Tutorial.html>
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# brew install emscripten
|
||||||
|
cd csaw
|
||||||
|
emcc -O2 csaw.c -o csaw # build
|
||||||
|
npx http-server .. -o
|
||||||
|
```
|
||||||
|
|
||||||
|
wasm file size: 680B
|
||||||
|
|
||||||
|
## assemblyscript
|
||||||
|
|
||||||
|
<https://www.assemblyscript.org/getting-started.html#setting-up-a-new-project>
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd ascsaw
|
||||||
|
# npm i
|
||||||
|
npm run asbuild # build
|
||||||
|
```
|
||||||
|
|
||||||
|
wasm file size: 122B !
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"targets": {
|
||||||
|
"debug": {
|
||||||
|
"outFile": "build/debug.wasm",
|
||||||
|
"textFile": "build/debug.wat",
|
||||||
|
"sourceMap": true,
|
||||||
|
"debug": true
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"outFile": "build/release.wasm",
|
||||||
|
"textFile": "build/release.wat",
|
||||||
|
"sourceMap": true,
|
||||||
|
"optimizeLevel": 3,
|
||||||
|
"shrinkLevel": 2,
|
||||||
|
"converge": false,
|
||||||
|
"noAssert": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"bindings": "esm"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function saw(t: f64, f: f64): f64 {
|
||||||
|
return (((f * t * 1.0) % 1.0) - 0.5) * 2.0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "assemblyscript/std/assembly.json",
|
||||||
|
"include": [
|
||||||
|
"./**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "ascsaw",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "ascsaw",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"assemblyscript": "^0.27.22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/assemblyscript": {
|
||||||
|
"version": "0.27.22",
|
||||||
|
"resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.27.22.tgz",
|
||||||
|
"integrity": "sha512-6ClobsR4Hxn6K0daYp/+n9qWTqVbpdVeSGSVDqRvUEz66vvFb8atS6nLm+fnQ54JXuXmzLQy0uWYYgB8G59btQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"binaryen": "116.0.0-nightly.20231102",
|
||||||
|
"long": "^5.2.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"asc": "bin/asc.js",
|
||||||
|
"asinit": "bin/asinit.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16",
|
||||||
|
"npm": ">=7"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/assemblyscript"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/binaryen": {
|
||||||
|
"version": "116.0.0-nightly.20231102",
|
||||||
|
"resolved": "https://registry.npmjs.org/binaryen/-/binaryen-116.0.0-nightly.20231102.tgz",
|
||||||
|
"integrity": "sha512-aPU9tlKdw/gcXx6u4PxtDgOtGjg/ZKnYdk23ctYb70GxZgPhWnGWmnBt01aV5dt5yFFo2V4rbB7SzpSFhViFQA==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"wasm-opt": "bin/wasm-opt",
|
||||||
|
"wasm2js": "bin/wasm2js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/long": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "ascsaw",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "node tests",
|
||||||
|
"asbuild:debug": "asc assembly/index.ts --target debug",
|
||||||
|
"asbuild:release": "asc assembly/index.ts --target release",
|
||||||
|
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
|
||||||
|
"start": "npx serve ."
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"assemblyscript": "^0.27.22"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./build/release.js",
|
||||||
|
"types": "./build/release.d.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
csaw
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#include <math.h>
|
||||||
|
#include <emscripten.h>
|
||||||
|
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
double saw(double t, double f) {
|
||||||
|
return fmod((f * t * 1.0), 1.0) - 0.5 * 2.0;
|
||||||
|
}
|
||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,11 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>WASM AudioWorklet Demo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button id="play">play</button>
|
||||||
|
<input type="range" min="55" max="880" id="freq" />
|
||||||
|
</body>
|
||||||
|
<script src="./main.js"></script>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
let ac;
|
||||||
|
document.getElementById('play').addEventListener('click', async () => {
|
||||||
|
ac = ac || new AudioContext();
|
||||||
|
await ac.resume();
|
||||||
|
await ac.audioWorklet.addModule('./worklet.js');
|
||||||
|
const node = new AudioWorkletNode(ac, 'saw-processor');
|
||||||
|
|
||||||
|
//let res = await fetch('./zigsaw/zigsaw.wasm');
|
||||||
|
// let res = await fetch('./csaw/csaw.wasm');
|
||||||
|
let res = await fetch('./ascsaw/build/release.wasm');
|
||||||
|
//let res = await fetch('./rustsaw/pkg/rustsaw_bg.wasm');
|
||||||
|
const buffer = await res.arrayBuffer();
|
||||||
|
node.port.onmessage = (e) => {
|
||||||
|
if (e.data === 'OK') {
|
||||||
|
console.log('worklet ready');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
node.port.postMessage({ webassembly: buffer });
|
||||||
|
node.connect(ac.destination);
|
||||||
|
|
||||||
|
document.getElementById('freq').addEventListener('input', async (e) => {
|
||||||
|
node.port.postMessage({ frequency: e.target.value });
|
||||||
|
});
|
||||||
|
});
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.76"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustsaw"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.48"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-backend"
|
||||||
|
version = "0.2.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-backend",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "rustsaw"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Your Name <you@example.com>"]
|
||||||
|
description = "A sample project with wasm-pack"
|
||||||
|
license = "MIT/Apache-2.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn saw(t: f64, f: f64) -> f64 {
|
||||||
|
return (((f * t * 1.0) % 1.0) - 0.5) * 2.0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
class SawProcessor extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.t = 0; // samples passed
|
||||||
|
this.f = 110;
|
||||||
|
this.port.onmessage = (e) => {
|
||||||
|
const key = Object.keys(e.data)[0];
|
||||||
|
const value = e.data[key];
|
||||||
|
switch (key) {
|
||||||
|
case 'webassembly':
|
||||||
|
WebAssembly.instantiate(value, this.importObject).then((result) => {
|
||||||
|
this.api = result.instance.exports;
|
||||||
|
this.port.postMessage('OK');
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'frequency':
|
||||||
|
this.f = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs, outputs, parameters) {
|
||||||
|
if (this.api) {
|
||||||
|
const output = outputs[0];
|
||||||
|
for (let i = 0; i < output[0].length; i++) {
|
||||||
|
let t = this.t;
|
||||||
|
let out = 0;
|
||||||
|
out = this.api.saw(t / 44100, this.f);
|
||||||
|
output.forEach((channel) => {
|
||||||
|
channel[i] = out;
|
||||||
|
});
|
||||||
|
this.t++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerProcessor('saw-processor', SawProcessor);
|
||||||
Executable
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user