Compare commits

..

5 Commits

Author SHA1 Message Date
Felix Roos 5a42adc366 Merge remote-tracking branch 'origin/main' into objectify-midi 2022-10-29 17:55:53 +02:00
Felix Roos cd3765b8fa Merge remote-tracking branch 'origin/main' into objectify-midi 2022-09-25 21:17:29 +02:00
Felix Roos 787ba9872f Merge remote-tracking branch 'origin/main' into objectify-midi 2022-09-22 19:20:31 +02:00
Felix Roos fd2c7fb4bb allow non notes 2022-08-16 23:12:49 +02:00
Felix Roos 50c84a973c object support for midi #192 2022-08-16 22:31:04 +02:00
177 changed files with 9578 additions and 37811 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [18] node-version: [16, 17]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"source": { "source": {
"includePattern": ".+\\.(js(doc|x)?|mjs)$", "includePattern": ".+\\.(js(doc|x)?|mjs)$",
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist" "excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser"
}, },
"plugins": ["plugins/markdown"], "plugins": ["plugins/markdown"],
"opts": { "opts": {
+3172 -3843
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -4,13 +4,12 @@
"private": true, "private": true,
"description": "Port of tidalcycles to javascript", "description": "Port of tidalcycles to javascript",
"scripts": { "scripts": {
"pretest": "cd tutorial && npm run jsdoc-json",
"test": "vitest run --version", "test": "vitest run --version",
"test-ui": "vitest --ui", "test-ui": "vitest --ui",
"test-coverage": "vitest --coverage", "test-coverage": "vitest --coverage",
"bootstrap": "lerna bootstrap", "bootstrap": "lerna bootstrap",
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i", "setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
"snapshot": "vitest run -u --silent", "snapshot": "cd repl && npm run snapshot",
"repl": "cd repl && npm run dev", "repl": "cd repl && npm run dev",
"osc": "cd packages/osc && npm run server", "osc": "cd packages/osc && npm run server",
"build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build", "build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build",
@@ -44,6 +43,7 @@
"c8": "^7.12.0", "c8": "^7.12.0",
"events": "^3.3.0", "events": "^3.3.0",
"gh-pages": "^4.0.0", "gh-pages": "^4.0.0",
"happy-dom": "^6.0.4",
"jsdoc": "^3.6.10", "jsdoc": "^3.6.10",
"jsdoc-json": "^2.0.2", "jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^7.1.1", "jsdoc-to-markdown": "^7.1.1",
-1
View File
@@ -1 +0,0 @@
examples
+5 -12
View File
@@ -40,6 +40,11 @@ const generic_params = [
* @example * @example
* n("0 1 2 3").s('east').osc() * n("0 1 2 3").s('east').osc()
*/ */
// TODO: nOut does not work
// TODO: notes don't work as expected
// current "workaround" for notes:
// s('superpiano').n("<c0 d0 e0 g0>"._asNumber()).osc()
// -> .n or .osc (or .superdirt) would need to convert note strings to numbers
// also see https://github.com/tidalcycles/strudel/pull/63 // also see https://github.com/tidalcycles/strudel/pull/63
['f', 'n', 'The note or sample number to choose for a synth or sampleset'], ['f', 'n', 'The note or sample number to choose for a synth or sampleset'],
['f', 'note', 'The note or pitch to play a sound or synth with'], ['f', 'note', 'The note or pitch to play a sound or synth with'],
@@ -95,18 +100,6 @@ const generic_params = [
'attack', 'attack',
'a pattern of numbers to specify the attack time (in seconds) of an envelope applied to each sample.', 'a pattern of numbers to specify the attack time (in seconds) of an envelope applied to each sample.',
], ],
/**
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
*
* @name bank
* @param {string | Pattern} bank the name of the bank
* @example
* s("bd sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
*
*/
['f', 'bank', 'selects sound bank to use'],
// TODO: find out how this works? // TODO: find out how this works?
/* /*
* Envelope decay time = the time it takes after the attack time to reach the sustain level. * Envelope decay time = the time it takes after the attack time to reach the sustain level.
-87
View File
@@ -1,87 +0,0 @@
/*
cyclist.mjs - <short description TODO>
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/>.
*/
import createClock from './zyklus.mjs';
import { logger } from './logger.mjs';
export class Cyclist {
worker;
pattern;
started = false;
cps = 1; // TODO
getTime;
phase = 0;
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
this.getTime = getTime;
this.onToggle = onToggle;
this.latency = latency;
const round = (x) => Math.round(x * 1000) / 1000;
this.clock = createClock(
getTime,
(phase, duration, tick) => {
if (tick === 0) {
this.origin = phase;
}
const begin = round(phase - this.origin);
this.phase = begin - latency;
const end = round(begin + duration);
const time = getTime();
try {
const haps = this.pattern.queryArc(begin, end); // get Haps
haps.forEach((hap) => {
if (hap.part.begin.equals(hap.whole.begin)) {
const deadline = hap.whole.begin + this.origin - time + latency;
const duration = hap.duration * 1;
onTrigger?.(hap, deadline, duration);
}
});
} catch (e) {
logger(`[cyclist] error: ${e.message}`);
onError?.(e);
}
}, // called slightly before each cycle
interval, // duration of each cycle
);
}
getPhase() {
return this.getTime() - this.origin - this.latency;
}
setStarted(v) {
this.started = v;
this.onToggle?.(v);
}
start() {
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
logger('[cyclist] start');
this.clock.start();
this.setStarted(true);
}
pause() {
logger('[cyclist] pause');
this.clock.pause();
this.setStarted(false);
}
stop() {
logger('[cyclist] stop');
this.clock.stop();
this.setStarted(false);
}
setPattern(pat, autostart = false) {
this.pattern = pat;
if (autostart && !this.started) {
this.start();
}
}
setCps(cps = 1) {
this.cps = cps;
}
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('')}`);
}
}
-1
View File
@@ -21,7 +21,6 @@ import Fraction, { gcd } from './fraction.mjs';
* @example * @example
* const line = drawLine("0 [1 2 3]", 10); // |0--123|0--123 * const line = drawLine("0 [1 2 3]", 10); // |0--123|0--123
* console.log(line); * console.log(line);
* silence;
*/ */
function drawLine(pat, chars = 60) { function drawLine(pat, chars = 60) {
let cycle = 0; let cycle = 0;
-53
View File
@@ -1,53 +0,0 @@
/*
evaluate.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.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 { isPattern, Pattern } from './index.mjs';
let scoped = false;
export const evalScope = async (...args) => {
if (scoped) {
console.warn('evalScope was called more than once.');
}
scoped = true;
const results = await Promise.allSettled(args);
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
results.forEach((result, i) => {
if (result.status === 'rejected') {
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
}
});
Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap());
};
function safeEval(str, options = {}) {
const { wrapExpression = true, wrapAsync = true } = options;
if (wrapExpression) {
str = `{${str}}`;
}
if (wrapAsync) {
str = `(async ()=>${str})()`;
}
const body = `"use strict";return (${str})`;
return Function(body)();
}
export const evaluate = async (code, transpiler) => {
if (!scoped) {
await evalScope(); // at least scope Pattern.prototype.boostrap
}
if (transpiler) {
code = transpiler(code); // transform syntactically correct js code to semantically usable code
}
// if no transpiler is given, we expect a single instruction (!wrapExpression)
const options = { wrapExpression: !!transpiler };
let evaluated = await safeEval(code, options);
if (!isPattern(evaluated)) {
console.log('evaluated', evaluated);
const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
}
return { mode: 'javascript', pattern: evaluated };
};
-90
View File
@@ -1,90 +0,0 @@
<div style="position: absolute; bottom: 0; right: 0; padding: 4px; width: 100vw; display: flex">
<button id="start" style="font-size: 2em">start</button>
<button id="stop" style="font-size: 2em">stop</button>
<button id="slower" style="font-size: 2em">slower</button>
<button id="faster" style="font-size: 2em">faster</button>
</div>
<textarea
style="font-size: 2em; background: #052b49; color: #fff; height: 100%; width: 100%; outline: none; border: 0"
id="text"
spellcheck="false"
>
Loading...</textarea
>
<script type="module">
document.body.style = 'margin: 0';
import * as strudel from '@strudel.cycles/core';
const { cat, State, TimeSpan, Scheduler, getPlayableNoteValue, getFreq } = strudel;
Object.assign(window, strudel); // add strudel to eval scope
const ctx = new AudioContext();
const scheduler = new Scheduler({
// audioContext: getAudioContext(),
interval: 0.1,
onTrigger: (hap, time, duration) => {
const freq = getFrequency(hap);
const osc = ctx.createOscillator();
const gain = 0.2;
osc.frequency.value = freq;
osc.type = 'triangle';
const onset = ctx.currentTime + time;
const offset = onset + duration;
const attack = 0.05;
const release = 0.05;
osc.start(onset);
osc.stop(offset + release);
const g = ctx.createGain();
g.gain.setValueAtTime(gain, onset);
g.gain.linearRampToValueAtTime(gain, onset + attack);
g.gain.setValueAtTime(gain, offset - release);
g.gain.linearRampToValueAtTime(0, offset);
osc.connect(g);
g.connect(ctx.destination);
},
});
let initialCode = `stack('c4','e4',cat('g4','a4','b4','a4'))
.add(cat(0,1,2,3,4,3,2,1).slow(8))
.fast(2)
.cps(tri.range(1,8).slow(32))`;
try {
const base64 = decodeURIComponent(window.location.href.split('#')[1]);
initialCode = atob(base64);
} catch (err) {
console.warn('failed to decode', err);
}
const input = document.getElementById('text');
input.value = initialCode;
const evaluate = () => {
try {
const pattern = eval(input.value);
scheduler.setPattern(pattern);
window.location.hash = '#' + encodeURIComponent(btoa(input.value)); // update url hash
} catch (err) {
console.warn(err);
}
};
evaluate();
input.addEventListener('input', () => evaluate());
document.getElementById('start').addEventListener('click', async () => {
await ctx.resume();
scheduler.start();
});
document.getElementById('stop').addEventListener('click', () => scheduler.stop());
document.getElementById('slower').addEventListener('click', () => scheduler.setCps(scheduler.cps - 0.1));
document.getElementById('faster').addEventListener('click', () => scheduler.setCps(scheduler.cps + 0.1));
</script>
<!--
sequence(1,2).mul(55/2) // frequencies
.mul(slowcat(1,2))
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
.fast(3)
.freq()
.velocity(.5)
.s('sawtooth')
.cutoff(800)
.out()
-->
-44
View File
@@ -1,44 +0,0 @@
<input
type="text"
id="text"
value="seq('c3','eb3','g3').note().s('sawtooth').out()"
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
spellcheck="false"
/>
<button id="start">play</button>
<div id="output"></div>
<script type="module">
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@latest');
const controls = await import('https://cdn.skypack.dev/@strudel.cycles/core@latest/controls.mjs');
const { getAudioContext, Scheduler } = await import('https://cdn.skypack.dev/@strudel.cycles/webaudio@latest');
let scheduler;
const audioContext = getAudioContext();
const latency = 0.2;
Object.assign(window, strudel);
Object.assign(window, controls.default);
scheduler = new Scheduler({
audioContext,
interval: 0.1,
latency,
onEvent: (hap) => {
if (!hap.context.onTrigger) {
console.warn('no output chosen. use one of .out() .webdirt() .osc()');
}
},
});
let started;
document.getElementById('start').addEventListener('click', async () => {
const code = document.getElementById('text').value;
const pattern = eval(code);
const events = pattern._firstCycleValues;
console.log(code, '->', events);
scheduler.setPattern(pattern);
if (!started) {
scheduler.start();
started = true;
}
});
</script>
-96
View File
@@ -1,96 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Buildless Vanilla Strudel REPL</title>
</head>
<body style="margin: 0; background: #222">
<div style="display: grid; height: 100vh">
<textarea
id="text"
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
spellcheck="false"
></textarea>
</div>
<button
id="start"
style="
position: absolute;
border-radius: 10px;
top: 20px;
right: 20px;
padding: 20px;
border: 2px solid white;
background: transparent;
color: white;
cursor: pointer;
"
>
evaluate
</button>
<div id="output"></div>
<script type="module">
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.3.2';
import { mini } from 'https://cdn.skypack.dev/@strudel.cycles/mini@0.3.2';
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.3.2';
import { getAudioContext, webaudioOutput } from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.3.3';
const ctx = getAudioContext();
const input = document.getElementById('text');
input.innerHTML = getTune();
evalScope(
controls,
import('https://cdn.skypack.dev/@strudel.cycles/core@0.3.2'),
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.3.2'),
import('https://cdn.skypack.dev/@strudel.cycles/tonal@0.3.3'),
import('https://cdn.skypack.dev/@strudel.cycles/webaudio@0.3.3'),
);
const { evaluate } = repl({
defaultOutput: webaudioOutput,
getTime: () => ctx.currentTime,
transpiler,
});
document.getElementById('start').addEventListener('click', () => {
ctx.resume();
evaluate(input.value);
});
function getTune() {
return `await samples('github:tidalcycles/Dirt-Samples/master')
stack(
// amen
n("0 1 2 3 4 5 6 7")
.sometimes(x=>x.ply(2))
.rarely(x=>x.speed("2 | -2"))
.sometimesBy(.4, x=>x.delay(".5"))
.s("amencutup")
.slow(2)
.room(.5)
,
// bass
sine.add(saw.slow(4)).range(0,7).segment(8)
.superimpose(x=>x.add(.1))
.scale('G0 minor').note()
.s("sawtooth").decay(.1).sustain(0)
.gain(.4).cutoff(perlin.range(300,3000).slow(8)).resonance(10)
.degradeBy("0 0.1 .5 .1")
.rarely(add(note("12")))
,
// chord
note("Bb3,D4".superimpose(x=>x.add(.2)))
.s('sawtooth').cutoff(1000).struct("<~@3 [~ x]>")
.decay(.05).sustain(.0).delay(.8).delaytime(.125).room(.8)
,
// alien
s("breath").room(1).shape(.6).chop(16).rev().mask("<x ~@7>")
,
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
).reset("<x@7 x(5,8)>")`;
}
</script>
</body>
</html>
@@ -1 +0,0 @@
!dist
@@ -1,10 +0,0 @@
# vite-vanilla-repl
This folder demonstrates how to set up a strudel repl using vite and vanilla JS. Run it using:
```sh
npm i
npm run dev
```
or view it [live on githack](https://rawcdn.githack.com/tidalcycles/strudel/5fb36acb046ead7cd6ad3cd10f532e7f585f536a/packages/core/examples/vite-vanilla-repl/dist/index.html)
@@ -1 +0,0 @@
import{b as s,h as i,m,a as n,c as p,p as t}from"./index.4cbc0a10.js";export{s as SyntaxError,i as h,m as mini,n as minify,p as parse,t as patternifyAST};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{g as s,f as d,i as t,n as r,l as u,j as o,d as f,k as p,r as g,s as i,w as l,e as m}from"./index.4cbc0a10.js";export{s as getAudioContext,d as getCachedBuffer,t as getLoadedBuffer,r as getLoadedSamples,u as loadBuffer,o as loadGithubSamples,f as panic,p as resetLoadedSamples,g as reverseBuffer,i as samples,l as webaudioOutput,m as webaudioOutputTrigger};
@@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite Vanilla Strudel REPL</title>
<script type="module" crossorigin src="/tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/assets/index.4cbc0a10.js"></script>
</head>
<body style="margin: 0; background: #222">
<div style="display: grid; height: 100vh">
<textarea
id="text"
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
spellcheck="false"
></textarea>
</div>
<button
id="start"
style="
position: absolute;
border-radius: 10px;
top: 20px;
right: 20px;
padding: 20px;
border: 2px solid white;
background: transparent;
color: white;
cursor: pointer;
"
>
evaluate
</button>
<div id="output"></div>
</body>
</html>
@@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite Vanilla Strudel REPL</title>
</head>
<body style="margin: 0; background: #222">
<div style="display: grid; height: 100vh">
<textarea
id="text"
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
spellcheck="false"
></textarea>
</div>
<button
id="start"
style="
position: absolute;
border-radius: 10px;
top: 20px;
right: 20px;
padding: 20px;
border: 2px solid white;
background: transparent;
color: white;
cursor: pointer;
"
>
evaluate
</button>
<div id="output"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
@@ -1,29 +0,0 @@
import { controls, repl, evalScope, setStringParser } from '@strudel.cycles/core';
import { mini } from '@strudel.cycles/mini';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
// import { transpiler } from '@strudel.cycles/transpiler';
import tune from './tune.mjs';
const ctx = getAudioContext();
const input = document.getElementById('text');
input.innerHTML = tune;
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/tonal'),
);
setStringParser(mini)
const { evaluate } = repl({
defaultOutput: webaudioOutput,
getTime: () => ctx.currentTime,
// transpiler,
});
document.getElementById('start').addEventListener('click', () => {
ctx.resume();
evaluate(input.value);
});
-885
View File
@@ -1,885 +0,0 @@
{
"name": "vite-vanilla-repl",
"version": "0.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "vite-vanilla-repl",
"version": "0.0.0",
"devDependencies": {
"vite": "^3.2.0"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.13.tgz",
"integrity": "sha512-RY2fVI8O0iFUNvZirXaQ1vMvK0xhCcl0gqRj74Z6yEiO1zAUa7hbsdwZM1kzqbxHK7LFyMizipfXT3JME+12Hw==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.13.tgz",
"integrity": "sha512-+BoyIm4I8uJmH/QDIH0fu7MG0AEx9OXEDXnqptXCwKOlOqZiS4iraH1Nr7/ObLMokW3sOCeBNyD68ATcV9b9Ag==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.13.tgz",
"integrity": "sha512-Cu3SC84oyzzhrK/YyN4iEVy2jZu5t2fz66HEOShHURcjSkOSAVL8C/gfUT+lDJxkVHpg8GZ10DD0rMHRPqMFaQ==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.15.13",
"@esbuild/linux-loong64": "0.15.13",
"esbuild-android-64": "0.15.13",
"esbuild-android-arm64": "0.15.13",
"esbuild-darwin-64": "0.15.13",
"esbuild-darwin-arm64": "0.15.13",
"esbuild-freebsd-64": "0.15.13",
"esbuild-freebsd-arm64": "0.15.13",
"esbuild-linux-32": "0.15.13",
"esbuild-linux-64": "0.15.13",
"esbuild-linux-arm": "0.15.13",
"esbuild-linux-arm64": "0.15.13",
"esbuild-linux-mips64le": "0.15.13",
"esbuild-linux-ppc64le": "0.15.13",
"esbuild-linux-riscv64": "0.15.13",
"esbuild-linux-s390x": "0.15.13",
"esbuild-netbsd-64": "0.15.13",
"esbuild-openbsd-64": "0.15.13",
"esbuild-sunos-64": "0.15.13",
"esbuild-windows-32": "0.15.13",
"esbuild-windows-64": "0.15.13",
"esbuild-windows-arm64": "0.15.13"
}
},
"node_modules/esbuild-android-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.13.tgz",
"integrity": "sha512-yRorukXBlokwTip+Sy4MYskLhJsO0Kn0/Fj43s1krVblfwP+hMD37a4Wmg139GEsMLl+vh8WXp2mq/cTA9J97g==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-android-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.13.tgz",
"integrity": "sha512-TKzyymLD6PiVeyYa4c5wdPw87BeAiTXNtK6amWUcXZxkV51gOk5u5qzmDaYSwiWeecSNHamFsaFjLoi32QR5/w==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-darwin-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.13.tgz",
"integrity": "sha512-WAx7c2DaOS6CrRcoYCgXgkXDliLnFv3pQLV6GeW1YcGEZq2Gnl8s9Pg7ahValZkpOa0iE/ojRVQ87sbUhF1Cbg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-darwin-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.13.tgz",
"integrity": "sha512-U6jFsPfSSxC3V1CLiQqwvDuj3GGrtQNB3P3nNC3+q99EKf94UGpsG9l4CQ83zBs1NHrk1rtCSYT0+KfK5LsD8A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-freebsd-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.13.tgz",
"integrity": "sha512-whItJgDiOXaDG/idy75qqevIpZjnReZkMGCgQaBWZuKHoElDJC1rh7MpoUgupMcdfOd+PgdEwNQW9DAE6i8wyA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-freebsd-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.13.tgz",
"integrity": "sha512-6pCSWt8mLUbPtygv7cufV0sZLeylaMwS5Fznj6Rsx9G2AJJsAjQ9ifA+0rQEIg7DwJmi9it+WjzNTEAzzdoM3Q==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-32": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.13.tgz",
"integrity": "sha512-VbZdWOEdrJiYApm2kkxoTOgsoCO1krBZ3quHdYk3g3ivWaMwNIVPIfEE0f0XQQ0u5pJtBsnk2/7OPiCFIPOe/w==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.13.tgz",
"integrity": "sha512-rXmnArVNio6yANSqDQlIO4WiP+Cv7+9EuAHNnag7rByAqFVuRusLbGi2697A5dFPNXoO//IiogVwi3AdcfPC6A==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-arm": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.13.tgz",
"integrity": "sha512-Ac6LpfmJO8WhCMQmO253xX2IU2B3wPDbl4IvR0hnqcPrdfCaUa2j/lLMGTjmQ4W5JsJIdHEdW12dG8lFS0MbxQ==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.13.tgz",
"integrity": "sha512-alEMGU4Z+d17U7KQQw2IV8tQycO6T+rOrgW8OS22Ua25x6kHxoG6Ngry6Aq6uranC+pNWNMB6aHFPh7aTQdORQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-mips64le": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.13.tgz",
"integrity": "sha512-47PgmyYEu+yN5rD/MbwS6DxP2FSGPo4Uxg5LwIdxTiyGC2XKwHhHyW7YYEDlSuXLQXEdTO7mYe8zQ74czP7W8A==",
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-ppc64le": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.13.tgz",
"integrity": "sha512-z6n28h2+PC1Ayle9DjKoBRcx/4cxHoOa2e689e2aDJSaKug3jXcQw7mM+GLg+9ydYoNzj8QxNL8ihOv/OnezhA==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-riscv64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.13.tgz",
"integrity": "sha512-+Lu4zuuXuQhgLUGyZloWCqTslcCAjMZH1k3Xc9MSEJEpEFdpsSU0sRDXAnk18FKOfEjhu4YMGaykx9xjtpA6ow==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-s390x": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.13.tgz",
"integrity": "sha512-BMeXRljruf7J0TMxD5CIXS65y7puiZkAh+s4XFV9qy16SxOuMhxhVIXYLnbdfLrsYGFzx7U9mcdpFWkkvy/Uag==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-netbsd-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.13.tgz",
"integrity": "sha512-EHj9QZOTel581JPj7UO3xYbltFTYnHy+SIqJVq6yd3KkCrsHRbapiPb0Lx3EOOtybBEE9EyqbmfW1NlSDsSzvQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-openbsd-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.13.tgz",
"integrity": "sha512-nkuDlIjF/sfUhfx8SKq0+U+Fgx5K9JcPq1mUodnxI0x4kBdCv46rOGWbuJ6eof2n3wdoCLccOoJAbg9ba/bT2w==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-sunos-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.13.tgz",
"integrity": "sha512-jVeu2GfxZQ++6lRdY43CS0Tm/r4WuQQ0Pdsrxbw+aOrHQPHV0+LNOLnvbN28M7BSUGnJnHkHm2HozGgNGyeIRw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-32": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.13.tgz",
"integrity": "sha512-XoF2iBf0wnqo16SDq+aDGi/+QbaLFpkiRarPVssMh9KYbFNCqPLlGAWwDvxEVz+ywX6Si37J2AKm+AXq1kC0JA==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.13.tgz",
"integrity": "sha512-Et6htEfGycjDrtqb2ng6nT+baesZPYQIW+HUEHK4D1ncggNrDNk3yoboYQ5KtiVrw/JaDMNttz8rrPubV/fvPQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.13.tgz",
"integrity": "sha512-3bv7tqntThQC9SWLRouMDmZnlOukBhOCTlkzNqzGCmrkCJI7io5LLjwJBOVY6kOUlIvdxbooNZwjtBvj+7uuVg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/is-core-module": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
"dev": true,
"dependencies": {
"has": "^1.0.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"dev": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
},
"node_modules/postcss": {
"version": "8.4.18",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
"integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
}
],
"dependencies": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
"dev": true,
"dependencies": {
"is-core-module": "^2.9.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rollup": {
"version": "2.79.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=10.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/vite": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz",
"integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==",
"dev": true,
"dependencies": {
"esbuild": "^0.15.9",
"postcss": "^8.4.18",
"resolve": "^1.22.1",
"rollup": "^2.79.1"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"less": "*",
"sass": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"less": {
"optional": true
},
"sass": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
},
"dependencies": {
"@esbuild/android-arm": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.13.tgz",
"integrity": "sha512-RY2fVI8O0iFUNvZirXaQ1vMvK0xhCcl0gqRj74Z6yEiO1zAUa7hbsdwZM1kzqbxHK7LFyMizipfXT3JME+12Hw==",
"dev": true,
"optional": true
},
"@esbuild/linux-loong64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.13.tgz",
"integrity": "sha512-+BoyIm4I8uJmH/QDIH0fu7MG0AEx9OXEDXnqptXCwKOlOqZiS4iraH1Nr7/ObLMokW3sOCeBNyD68ATcV9b9Ag==",
"dev": true,
"optional": true
},
"esbuild": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.13.tgz",
"integrity": "sha512-Cu3SC84oyzzhrK/YyN4iEVy2jZu5t2fz66HEOShHURcjSkOSAVL8C/gfUT+lDJxkVHpg8GZ10DD0rMHRPqMFaQ==",
"dev": true,
"requires": {
"@esbuild/android-arm": "0.15.13",
"@esbuild/linux-loong64": "0.15.13",
"esbuild-android-64": "0.15.13",
"esbuild-android-arm64": "0.15.13",
"esbuild-darwin-64": "0.15.13",
"esbuild-darwin-arm64": "0.15.13",
"esbuild-freebsd-64": "0.15.13",
"esbuild-freebsd-arm64": "0.15.13",
"esbuild-linux-32": "0.15.13",
"esbuild-linux-64": "0.15.13",
"esbuild-linux-arm": "0.15.13",
"esbuild-linux-arm64": "0.15.13",
"esbuild-linux-mips64le": "0.15.13",
"esbuild-linux-ppc64le": "0.15.13",
"esbuild-linux-riscv64": "0.15.13",
"esbuild-linux-s390x": "0.15.13",
"esbuild-netbsd-64": "0.15.13",
"esbuild-openbsd-64": "0.15.13",
"esbuild-sunos-64": "0.15.13",
"esbuild-windows-32": "0.15.13",
"esbuild-windows-64": "0.15.13",
"esbuild-windows-arm64": "0.15.13"
}
},
"esbuild-android-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.13.tgz",
"integrity": "sha512-yRorukXBlokwTip+Sy4MYskLhJsO0Kn0/Fj43s1krVblfwP+hMD37a4Wmg139GEsMLl+vh8WXp2mq/cTA9J97g==",
"dev": true,
"optional": true
},
"esbuild-android-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.13.tgz",
"integrity": "sha512-TKzyymLD6PiVeyYa4c5wdPw87BeAiTXNtK6amWUcXZxkV51gOk5u5qzmDaYSwiWeecSNHamFsaFjLoi32QR5/w==",
"dev": true,
"optional": true
},
"esbuild-darwin-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.13.tgz",
"integrity": "sha512-WAx7c2DaOS6CrRcoYCgXgkXDliLnFv3pQLV6GeW1YcGEZq2Gnl8s9Pg7ahValZkpOa0iE/ojRVQ87sbUhF1Cbg==",
"dev": true,
"optional": true
},
"esbuild-darwin-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.13.tgz",
"integrity": "sha512-U6jFsPfSSxC3V1CLiQqwvDuj3GGrtQNB3P3nNC3+q99EKf94UGpsG9l4CQ83zBs1NHrk1rtCSYT0+KfK5LsD8A==",
"dev": true,
"optional": true
},
"esbuild-freebsd-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.13.tgz",
"integrity": "sha512-whItJgDiOXaDG/idy75qqevIpZjnReZkMGCgQaBWZuKHoElDJC1rh7MpoUgupMcdfOd+PgdEwNQW9DAE6i8wyA==",
"dev": true,
"optional": true
},
"esbuild-freebsd-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.13.tgz",
"integrity": "sha512-6pCSWt8mLUbPtygv7cufV0sZLeylaMwS5Fznj6Rsx9G2AJJsAjQ9ifA+0rQEIg7DwJmi9it+WjzNTEAzzdoM3Q==",
"dev": true,
"optional": true
},
"esbuild-linux-32": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.13.tgz",
"integrity": "sha512-VbZdWOEdrJiYApm2kkxoTOgsoCO1krBZ3quHdYk3g3ivWaMwNIVPIfEE0f0XQQ0u5pJtBsnk2/7OPiCFIPOe/w==",
"dev": true,
"optional": true
},
"esbuild-linux-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.13.tgz",
"integrity": "sha512-rXmnArVNio6yANSqDQlIO4WiP+Cv7+9EuAHNnag7rByAqFVuRusLbGi2697A5dFPNXoO//IiogVwi3AdcfPC6A==",
"dev": true,
"optional": true
},
"esbuild-linux-arm": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.13.tgz",
"integrity": "sha512-Ac6LpfmJO8WhCMQmO253xX2IU2B3wPDbl4IvR0hnqcPrdfCaUa2j/lLMGTjmQ4W5JsJIdHEdW12dG8lFS0MbxQ==",
"dev": true,
"optional": true
},
"esbuild-linux-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.13.tgz",
"integrity": "sha512-alEMGU4Z+d17U7KQQw2IV8tQycO6T+rOrgW8OS22Ua25x6kHxoG6Ngry6Aq6uranC+pNWNMB6aHFPh7aTQdORQ==",
"dev": true,
"optional": true
},
"esbuild-linux-mips64le": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.13.tgz",
"integrity": "sha512-47PgmyYEu+yN5rD/MbwS6DxP2FSGPo4Uxg5LwIdxTiyGC2XKwHhHyW7YYEDlSuXLQXEdTO7mYe8zQ74czP7W8A==",
"dev": true,
"optional": true
},
"esbuild-linux-ppc64le": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.13.tgz",
"integrity": "sha512-z6n28h2+PC1Ayle9DjKoBRcx/4cxHoOa2e689e2aDJSaKug3jXcQw7mM+GLg+9ydYoNzj8QxNL8ihOv/OnezhA==",
"dev": true,
"optional": true
},
"esbuild-linux-riscv64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.13.tgz",
"integrity": "sha512-+Lu4zuuXuQhgLUGyZloWCqTslcCAjMZH1k3Xc9MSEJEpEFdpsSU0sRDXAnk18FKOfEjhu4YMGaykx9xjtpA6ow==",
"dev": true,
"optional": true
},
"esbuild-linux-s390x": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.13.tgz",
"integrity": "sha512-BMeXRljruf7J0TMxD5CIXS65y7puiZkAh+s4XFV9qy16SxOuMhxhVIXYLnbdfLrsYGFzx7U9mcdpFWkkvy/Uag==",
"dev": true,
"optional": true
},
"esbuild-netbsd-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.13.tgz",
"integrity": "sha512-EHj9QZOTel581JPj7UO3xYbltFTYnHy+SIqJVq6yd3KkCrsHRbapiPb0Lx3EOOtybBEE9EyqbmfW1NlSDsSzvQ==",
"dev": true,
"optional": true
},
"esbuild-openbsd-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.13.tgz",
"integrity": "sha512-nkuDlIjF/sfUhfx8SKq0+U+Fgx5K9JcPq1mUodnxI0x4kBdCv46rOGWbuJ6eof2n3wdoCLccOoJAbg9ba/bT2w==",
"dev": true,
"optional": true
},
"esbuild-sunos-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.13.tgz",
"integrity": "sha512-jVeu2GfxZQ++6lRdY43CS0Tm/r4WuQQ0Pdsrxbw+aOrHQPHV0+LNOLnvbN28M7BSUGnJnHkHm2HozGgNGyeIRw==",
"dev": true,
"optional": true
},
"esbuild-windows-32": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.13.tgz",
"integrity": "sha512-XoF2iBf0wnqo16SDq+aDGi/+QbaLFpkiRarPVssMh9KYbFNCqPLlGAWwDvxEVz+ywX6Si37J2AKm+AXq1kC0JA==",
"dev": true,
"optional": true
},
"esbuild-windows-64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.13.tgz",
"integrity": "sha512-Et6htEfGycjDrtqb2ng6nT+baesZPYQIW+HUEHK4D1ncggNrDNk3yoboYQ5KtiVrw/JaDMNttz8rrPubV/fvPQ==",
"dev": true,
"optional": true
},
"esbuild-windows-arm64": {
"version": "0.15.13",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.13.tgz",
"integrity": "sha512-3bv7tqntThQC9SWLRouMDmZnlOukBhOCTlkzNqzGCmrkCJI7io5LLjwJBOVY6kOUlIvdxbooNZwjtBvj+7uuVg==",
"dev": true,
"optional": true
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "^1.1.1"
}
},
"is-core-module": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
"dev": true,
"requires": {
"has": "^1.0.3"
}
},
"nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"dev": true
},
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
},
"postcss": {
"version": "8.4.18",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
"integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==",
"dev": true,
"requires": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
}
},
"resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
"dev": true,
"requires": {
"is-core-module": "^2.9.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
}
},
"rollup": {
"version": "2.79.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
"dev": true,
"requires": {
"fsevents": "~2.3.2"
}
},
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true
},
"supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true
},
"vite": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz",
"integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==",
"dev": true,
"requires": {
"esbuild": "^0.15.9",
"fsevents": "~2.3.2",
"postcss": "^8.4.18",
"resolve": "^1.22.1",
"rollup": "^2.79.1"
}
}
}
}
@@ -1,15 +0,0 @@
{
"name": "vite-vanilla-repl",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build-githack": "vite build --base /tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^3.2.0"
}
}
@@ -1,39 +0,0 @@
/* export default `await samples('github:tidalcycles/Dirt-Samples/master')
stack(
// amen
n("0 1 2 3 4 5 6 7")
.sometimes(x=>x.ply(2))
.rarely(x=>x.speed("2 | -2"))
.sometimesBy(.4, x=>x.delay(".5"))
.s("amencutup")
.slow(2)
.room(.5)
,
// bass
sine.add(saw.slow(4)).range(0,7).segment(8)
.superimpose(x=>x.add(.1))
.scale('G0 minor').note()
.s("sawtooth").decay(.1).sustain(0)
.gain(.4).cutoff(perlin.range(300,3000).slow(8)).resonance(10)
.degradeBy("0 0.1 .5 .1")
.rarely(add(note("12")))
,
// chord
note("Bb3,D4".superimpose(x=>x.add(.2)))
.s('sawtooth').cutoff(1000).struct("<~@3 [~ x]>")
.decay(.05).sustain(.0).delay(.8).delaytime(.125).room(.8)
,
// alien
s("breath").room(1).shape(.6).chop(16).rev().mask("<x ~@7>")
,
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
).reset("<x@7 x(5,8)>")`;
*/
export default `stack(
n("c3 [eb3,g3]")
.delay("<0 .5>")
.delaytime(".16 | .33")
.delayfeedback(".6 | .8")
).sometimes(x=>x.speed("-1"))`;
+2 -3
View File
@@ -55,8 +55,7 @@ Fraction.prototype.min = function (other) {
return this.lt(other) ? this : other; return this.lt(other) ? this : other;
}; };
Fraction.prototype.show = function (/* excludeWhole = false */) { Fraction.prototype.show = function () {
// return this.toFraction(excludeWhole);
return this.s * this.n + '/' + this.d; return this.s * this.n + '/' + this.d;
}; };
@@ -74,7 +73,7 @@ const fraction = (n) => {
-> those farey sequences turn out to make pattern querying ~20 times slower! always use strings! -> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
-> still, some optimizations could be done: .mul .div .add .sub calls still use numbers -> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
*/ */
// n = String(n); // this is actually faster but imprecise... n = String(n);
} }
return Fraction(n); return Fraction(n);
}; };
+2 -6
View File
@@ -85,13 +85,9 @@ export class Hap {
); );
} }
showWhole(compact = false) { showWhole() {
return `${this.whole == undefined ? '~' : this.whole.show()}: ${ return `${this.whole == undefined ? '~' : this.whole.show()}: ${
typeof this.value === 'object' typeof this.value === 'object' ? JSON.stringify(this.value) : this.value
? compact
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
: JSON.stringify(this.value)
: this.value
}`; }`;
} }
+6 -12
View File
@@ -4,11 +4,10 @@ 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 controls from './controls.mjs'; export * from './controls.mjs';
export * from './euclid.mjs'; export * from './euclid.mjs';
import Fraction from './fraction.mjs'; import Fraction from './fraction.mjs';
import { logger } from './logger.mjs'; export { Fraction };
export { Fraction, controls };
export * from './hap.mjs'; export * from './hap.mjs';
export * from './pattern.mjs'; export * from './pattern.mjs';
export * from './signal.mjs'; export * from './signal.mjs';
@@ -16,19 +15,14 @@ export * from './state.mjs';
export * from './timespan.mjs'; export * from './timespan.mjs';
export * from './util.mjs'; export * from './util.mjs';
export * from './speak.mjs'; export * from './speak.mjs';
export * from './evaluate.mjs';
export * from './repl.mjs';
export * from './logger.mjs';
export * from './time.mjs';
export * from './draw.mjs';
export * from './pianoroll.mjs';
export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs';
export { default as gist } from './gist.js'; export { default as gist } from './gist.js';
// below won't work with runtime.mjs (json import fails) // below won't work with runtime.mjs (json import fails)
/* import * as p from './package.json'; /* import * as p from './package.json';
export const version = p.version; */ export const version = p.version; */
logger('🌀 @strudel.cycles/core loaded 🌀'); console.log(
'%c // 🌀 @strudel.cycles/core loaded 🌀', // keep "//" for runnable snapshot source..
'background-color: black;color:white;padding:4px;border-radius:15px',
);
if (globalThis._strudelLoaded) { if (globalThis._strudelLoaded) {
console.warn( console.warn(
`@strudel.cycles/core was loaded more than once... `@strudel.cycles/core was loaded more than once...
-18
View File
@@ -1,18 +0,0 @@
export const logKey = 'strudel.log';
export function logger(message, type, data = {}) {
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
if (typeof CustomEvent !== 'undefined') {
document.dispatchEvent(
new CustomEvent(logKey, {
detail: {
message,
type,
data,
},
}),
);
}
}
logger.key = logKey;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/core", "name": "@strudel.cycles/core",
"version": "0.4.0", "version": "0.2.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/core", "name": "@strudel.cycles/core",
"version": "0.4.0", "version": "0.2.0",
"description": "Port of Tidal Cycles to JavaScript", "description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+614 -688
View File
File diff suppressed because it is too large Load Diff
-59
View File
@@ -1,59 +0,0 @@
import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs';
import { setTime } from './time.mjs';
export function repl({
interval,
defaultOutput,
onSchedulerError,
onEvalError,
beforeEval,
afterEval,
getTime,
transpiler,
onToggle,
}) {
const scheduler = new Cyclist({
interval,
onTrigger: async (hap, deadline, duration) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration);
}
if (hap.context.onTrigger) {
const cps = 1;
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
},
onError: onSchedulerError,
getTime,
onToggle,
});
setTime(() => scheduler.getPhase()); // TODO: refactor?
const evaluate = async (code, autostart = true) => {
if (!code) {
throw new Error('no code to evaluate');
}
try {
beforeEval({ code });
const { pattern } = await _evaluate(code, transpiler);
logger(`[eval] code updated`);
scheduler.setPattern(pattern, autostart);
afterEval({ code, pattern });
return pattern;
} catch (err) {
// console.warn(`[repl] eval error: ${err.message}`);
logger(`[eval] error: ${err.message}`, 'error');
onEvalError?.(err);
}
};
const stop = () => scheduler.stop();
const start = () => scheduler.start();
const pause = () => scheduler.pause();
return { scheduler, evaluate, start, stop, pause };
}
+8 -9
View File
@@ -20,7 +20,7 @@ export const signal = (func) => {
}; };
export const isaw = signal((t) => 1 - (t % 1)); export const isaw = signal((t) => 1 - (t % 1));
export const isaw2 = isaw.toBipolar(); export const isaw2 = isaw._toBipolar();
/** /**
* A sawtooth signal between 0 and 1. * A sawtooth signal between 0 and 1.
@@ -33,7 +33,7 @@ export const isaw2 = isaw.toBipolar();
* *
*/ */
export const saw = signal((t) => t % 1); export const saw = signal((t) => t % 1);
export const saw2 = saw.toBipolar(); export const saw2 = saw._toBipolar();
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t)); export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
@@ -45,7 +45,7 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
* sine.segment(16).range(0,15).slow(2).scale('C minor').note() * sine.segment(16).range(0,15).slow(2).scale('C minor').note()
* *
*/ */
export const sine = sine2.fromBipolar(); export const sine = sine2._fromBipolar();
/** /**
* A cosine signal between 0 and 1. * A cosine signal between 0 and 1.
@@ -67,7 +67,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
* *
*/ */
export const square = signal((t) => Math.floor((t * 2) % 2)); export const square = signal((t) => Math.floor((t * 2) % 2));
export const square2 = square.toBipolar(); export const square2 = square._toBipolar();
/** /**
* A triangle signal between 0 and 1. * A triangle signal between 0 and 1.
@@ -127,7 +127,7 @@ export const rand = signal(timeToRand);
/** /**
* A continuous pattern of random numbers, between -1 and 1 * A continuous pattern of random numbers, between -1 and 1
*/ */
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);
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin(); export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
@@ -201,7 +201,7 @@ Pattern.prototype.choose = function (...xs) {
* @returns {Pattern} * @returns {Pattern}
*/ */
Pattern.prototype.choose2 = function (...xs) { Pattern.prototype.choose2 = function (...xs) {
return chooseWith(this.fromBipolar(), xs); return chooseWith(this._fromBipolar(), xs);
}; };
/** /**
@@ -238,7 +238,6 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin(); export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
// this function expects pat to be a pattern of floats...
export const perlinWith = (pat) => { export const perlinWith = (pat) => {
const pata = pat.fmap(Math.floor); const pata = pat.fmap(Math.floor);
const patb = pat.fmap((t) => Math.floor(t) + 1); const patb = pat.fmap((t) => Math.floor(t) + 1);
@@ -256,10 +255,10 @@ export const perlinWith = (pat) => {
* s("bd sd,hh*4").cutoff(perlin.range(500,2000)) * s("bd sd,hh*4").cutoff(perlin.range(500,2000))
* *
*/ */
export const perlin = perlinWith(time.fmap((v) => Number(v))); export const perlin = perlinWith(time);
Pattern.prototype._degradeByWith = function (withPat, x) { Pattern.prototype._degradeByWith = function (withPat, x) {
return this.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)); return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x));
}; };
/** /**
+5 -2
View File
@@ -32,8 +32,11 @@ function speak(words, lang, voice) {
} }
Pattern.prototype._speak = function (lang, voice) { Pattern.prototype._speak = function (lang, voice) {
return this.onTrigger((_, hap) => { return this._withHap((hap) => {
speak(hap.value, lang, voice); const onTrigger = (time, hap) => {
speak(hap.value, lang, voice);
};
return hap.setContext({ ...hap.context, onTrigger });
}); });
}; };
+30 -56
View File
@@ -47,9 +47,6 @@ import {
import { steady } from '../signal.mjs'; import { steady } from '../signal.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);
@@ -58,7 +55,7 @@ const third = Fraction(1, 3);
const twothirds = Fraction(2, 3); const twothirds = Fraction(2, 3);
const sameFirst = (a, b) => { const sameFirst = (a, b) => {
return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle()); return expect(a._sortHapsByPart().firstCycle()).toStrictEqual(b._sortHapsByPart().firstCycle());
}; };
describe('TimeSpan', () => { describe('TimeSpan', () => {
@@ -140,9 +137,6 @@ describe('Pattern', () => {
it('Can make a pattern', () => { it('Can make a pattern', () => {
expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3); expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3);
}); });
it('Supports zero-width queries', () => {
expect(pure('hello').queryArc(0, 0).length).toBe(1);
});
}); });
describe('fmap()', () => { describe('fmap()', () => {
it('Can add things', () => { it('Can add things', () => {
@@ -197,9 +191,6 @@ describe('Pattern', () => {
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]), sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
); );
}); });
it('can add object patterns', () => {
sameFirst(n(sequence(1, [2, 3])).add(n(10)), n(sequence(11, [12, 13])));
});
}); });
describe('keep()', () => { describe('keep()', () => {
it('can structure In()', () => { it('can structure In()', () => {
@@ -382,10 +373,9 @@ describe('Pattern', () => {
); );
}); });
it('copes with breaking up events across cycles', () => { it('copes with breaking up events across cycles', () => {
expect(pure('a').slow(2)._fastGap(2).setContext({}).query(st(0, 2))).toStrictEqual([ expect(pure('a').slow(2)._fastGap(2)._setContext({}).query(st(0, 2))).toStrictEqual(
hap(ts(0, 1), ts(0, 0.5), 'a'), [hap(ts(0, 1), ts(0, 0.5), 'a'), hap(ts(0.5, 1.5), ts(1, 1.5), 'a')]
hap(ts(0.5, 1.5), ts(1, 1.5), 'a'), );
]);
}); });
}); });
describe('_compressSpan()', () => { describe('_compressSpan()', () => {
@@ -440,16 +430,6 @@ describe('Pattern', () => {
// mini('[c3 g3]/2 eb3') always plays [c3 eb3] // mini('[c3 g3]/2 eb3') always plays [c3 eb3]
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3] // mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
}); });
it('Supports zero-length queries', () => {
expect(steady('a')._slow(1).queryArc(0, 0)).toStrictEqual(steady('a').queryArc(0, 0));
});
});
describe('slow()', () => {
it('Supports zero-length queries', () => {
expect(steady('a').slow(1).setContext({}).queryArc(0, 0)).toStrictEqual(
steady('a').setContext({}).queryArc(0, 0),
);
});
}); });
describe('inside', () => { describe('inside', () => {
it('can rev inside a cycle', () => { it('can rev inside a cycle', () => {
@@ -465,7 +445,7 @@ describe('Pattern', () => {
it('Filters true', () => { it('Filters true', () => {
expect( expect(
pure(true) pure(true)
.filterValues((x) => x) ._filterValues((x) => x)
.firstCycle().length, .firstCycle().length,
).toBe(1); ).toBe(1);
}); });
@@ -490,7 +470,7 @@ describe('Pattern', () => {
pure(10) pure(10)
.when(slowcat(true, false), (x) => x.add(3)) .when(slowcat(true, false), (x) => x.add(3))
.fast(4) .fast(4)
.sortHapsByPart() ._sortHapsByPart()
.firstCycle(), .firstCycle(),
).toStrictEqual(fastcat(13, 10, 13, 10).firstCycle()); ).toStrictEqual(fastcat(13, 10, 13, 10).firstCycle());
}); });
@@ -577,26 +557,26 @@ describe('Pattern', () => {
}); });
}); });
describe('firstOf()', () => { describe('every()', () => {
it('Can apply a function every 3rd time', () => { it('Can apply a function every 3rd time', () => {
expect( expect(
pure('a') pure('a')
.firstOf(3, (x) => x._fast(2)) .every(3, (x) => x._fast(2))
._fast(3) ._fast(3)
.firstCycle(), .firstCycle(),
).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle()); ).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle());
}); });
it('works with currying', () => { it('works with currying', () => {
expect(pure('a').firstOf(3, fast(2))._fast(3).firstCycle()).toStrictEqual( expect(pure('a').every(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(), sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
); );
expect(sequence(3, 4, 5).firstOf(3, add(3)).fast(5).firstCycle()).toStrictEqual( expect(sequence(3, 4, 5).every(3, add(3)).fast(5).firstCycle()).toStrictEqual(
sequence(6, 7, 8, 3, 4, 5, 3, 4, 5, 6, 7, 8, 3, 4, 5).firstCycle(), sequence(6, 7, 8, 3, 4, 5, 3, 4, 5, 6, 7, 8, 3, 4, 5).firstCycle(),
); );
expect(sequence(3, 4, 5).firstOf(2, sub(1)).fast(5).firstCycle()).toStrictEqual( expect(sequence(3, 4, 5).every(2, sub(1)).fast(5).firstCycle()).toStrictEqual(
sequence(2, 3, 4, 3, 4, 5, 2, 3, 4, 3, 4, 5, 2, 3, 4).firstCycle(), sequence(2, 3, 4, 3, 4, 5, 2, 3, 4, 3, 4, 5, 2, 3, 4).firstCycle(),
); );
expect(sequence(3, 4, 5).firstOf(3, add(3)).firstOf(2, sub(1)).fast(2).firstCycle()).toStrictEqual( expect(sequence(3, 4, 5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle()).toStrictEqual(
sequence(5, 6, 7, 3, 4, 5).firstCycle(), sequence(5, 6, 7, 3, 4, 5).firstCycle(),
); );
}); });
@@ -692,7 +672,7 @@ describe('Pattern', () => {
it('Can set the hap context', () => { it('Can set the hap context', () => {
expect( expect(
pure('a') pure('a')
.setContext([ ._setContext([
[ [
[0, 1], [0, 1],
[1, 2], [1, 2],
@@ -713,13 +693,13 @@ describe('Pattern', () => {
it('Can update the hap context', () => { it('Can update the hap context', () => {
expect( expect(
pure('a') pure('a')
.setContext([ ._setContext([
[ [
[0, 1], [0, 1],
[1, 2], [1, 2],
], ],
]) ])
.withContext((c) => [ ._withContext((c) => [
...c, ...c,
[ [
[3, 4], [3, 4],
@@ -743,7 +723,7 @@ describe('Pattern', () => {
}); });
describe('apply', () => { describe('apply', () => {
it('Can apply a function', () => { it('Can apply a function', () => {
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle()); expect(sequence('a', 'b')._apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
}), }),
it('Can apply a pattern of functions', () => { it('Can apply a pattern of functions', () => {
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle()); expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
@@ -784,18 +764,18 @@ describe('Pattern', () => {
}); });
describe('jux', () => { describe('jux', () => {
it('Can juxtapose', () => { it('Can juxtapose', () => {
expect(pure({ a: 1 }).jux(fast(2)).sortHapsByPart().firstCycle()).toStrictEqual( expect(pure({ a: 1 }).jux(fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2)) stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
.sortHapsByPart() ._sortHapsByPart()
.firstCycle(), .firstCycle(),
); );
}); });
}); });
describe('juxBy', () => { describe('juxBy', () => {
it('Can juxtapose by half', () => { it('Can juxtapose by half', () => {
expect(pure({ a: 1 }).juxBy(0.5, fast(2)).sortHapsByPart().firstCycle()).toStrictEqual( expect(pure({ a: 1 }).juxBy(0.5, fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2)) stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
.sortHapsByPart() ._sortHapsByPart()
.firstCycle(), .firstCycle(),
); );
}); });
@@ -805,7 +785,7 @@ describe('Pattern', () => {
expect( expect(
sequence('a', ['a', 'a']) sequence('a', ['a', 'a'])
.fmap((a) => fastcat('b', 'c')) .fmap((a) => fastcat('b', 'c'))
.squeezeJoin() ._squeezeJoin()
.firstCycle(), .firstCycle(),
).toStrictEqual( ).toStrictEqual(
sequence( sequence(
@@ -819,11 +799,10 @@ describe('Pattern', () => {
}); });
it('Squeezes to the correct cycle', () => { it('Squeezes to the correct cycle', () => {
expect( expect(
pure(time.struct(true)) pure(time.struct(true))._squeezeJoin().queryArc(3,4).map(x => x.value)
.squeezeJoin() ).toStrictEqual(
.queryArc(3, 4) [Fraction(3.5)]
.map((x) => x.value), )
).toStrictEqual([Fraction(3.5)]);
}); });
}); });
describe('ply', () => { describe('ply', () => {
@@ -857,7 +836,7 @@ describe('Pattern', () => {
); );
}); });
it('Can chop(2,3)', () => { it('Can chop(2,3)', () => {
expect(pure({ sound: 'a' }).fast(2).chop(2, 3).sortHapsByPart().firstCycle()).toStrictEqual( expect(pure({ sound: 'a' }).fast(2).chop(2, 3)._sortHapsByPart().firstCycle()).toStrictEqual(
sequence( sequence(
[ [
{ sound: 'a', begin: 0, end: 0.5 }, { sound: 'a', begin: 0, end: 0.5 },
@@ -869,14 +848,16 @@ describe('Pattern', () => {
{ sound: 'a', begin: 2 / 3, end: 1 }, { sound: 'a', begin: 2 / 3, end: 1 },
], ],
) )
.sortHapsByPart() ._sortHapsByPart()
.firstCycle(), .firstCycle(),
); );
}); });
}); });
describe('range', () => { describe('range', () => {
it('Can be patterned', () => { it('Can be patterned', () => {
expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(sequence(0, 0.5).firstCycle()); expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(
sequence(0, 0.5).firstCycle(),
);
}); });
}); });
describe('range2', () => { describe('range2', () => {
@@ -893,11 +874,4 @@ describe('Pattern', () => {
); );
}); });
}); });
describe('alignments', () => {
it('Can squeeze arguments', () => {
expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(
sequence(5, 6, 6, 7).firstCycle()
);
});
});
}); });
+15 -78
View File
@@ -5,20 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import { pure } from '../pattern.mjs'; import { pure } from '../pattern.mjs';
import { import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency, getPlayableNoteValue } from '../util.mjs';
isNote,
tokenizeNote,
toMidi,
fromMidi,
mod,
compose,
getFrequency,
getPlayableNoteValue,
parseNumeral,
parseFractional,
numeralArgs,
fractionalArgs,
} from '../util.mjs';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
describe('isNote', () => { describe('isNote', () => {
@@ -109,12 +96,12 @@ describe('getFrequency', () => {
expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432); expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432);
}); });
it('should turn object with a "freq" property into frequency', () => { it('should turn object with a "freq" property into frequency', () => {
expect(getFrequency(happify({ freq: 220 }))).toEqual(220); expect(getFrequency(happify({freq: 220}))).toEqual(220)
expect(getFrequency(happify({ freq: 440 }))).toEqual(440); expect(getFrequency(happify({freq: 440}))).toEqual(440)
}); });
it('should throw an error when given a non-note', () => { it('should throw an error when given a non-note', () => {
expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`); expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`)
expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`); expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`)
}); });
}); });
@@ -153,72 +140,22 @@ describe('compose', () => {
describe('getPlayableNoteValue', () => { describe('getPlayableNoteValue', () => {
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context); const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
it('should return object "note" property', () => { it('should return object "note" property', () => {
expect(getPlayableNoteValue(happify({ note: 'a4' }))).toEqual('a4'); expect(getPlayableNoteValue(happify({note: "a4"}))).toEqual('a4')
}); });
it('should return object "n" property', () => { it('should return object "n" property', () => {
expect(getPlayableNoteValue(happify({ n: 'a4' }))).toEqual('a4'); expect(getPlayableNoteValue(happify({n: "a4"}))).toEqual('a4')
}); });
it('should return object "value" property', () => { it('should return object "value" property', () => {
expect(getPlayableNoteValue(happify({ value: 'a4' }))).toEqual('a4'); expect(getPlayableNoteValue(happify({value: "a4"}))).toEqual('a4')
}); });
it('should turn midi into frequency', () => { it('should turn midi into frequency', () => {
expect(getPlayableNoteValue(happify(57, { type: 'midi' }))).toEqual(220); expect(getPlayableNoteValue(happify(57, {type: 'midi'}))).toEqual(220)
}); })
it('should return frequency value', () => { it('should return frequency value', () => {
expect(getPlayableNoteValue(happify(220, { type: 'frequency' }))).toEqual(220); expect(getPlayableNoteValue(happify(220, {type: 'frequency'}))).toEqual(220)
}); })
it('should throw an error if value is not an object, number, or string', () => { it('should throw an error if value is not an object, number, or string', () => {
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`); expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`)
expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`); expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`)
}); })
});
describe('parseNumeral', () => {
it('should parse numbers as is', () => {
expect(parseNumeral(4)).toBe(4);
expect(parseNumeral(0)).toBe(0);
expect(parseNumeral(20)).toBe(20);
expect(parseNumeral('20')).toBe(20);
expect(parseNumeral(1.5)).toBe(1.5);
});
it('should parse notes', () => {
expect(parseNumeral('c4')).toBe(60);
expect(parseNumeral('c#4')).toBe(61);
expect(parseNumeral('db4')).toBe(61);
});
it('should throw an error for unknown strings', () => {
expect(() => parseNumeral('xyz')).toThrowError('cannot parse as numeral: "xyz"');
});
});
describe('parseFractional', () => {
it('should parse numbers as is', () => {
expect(parseFractional(4)).toBe(4);
expect(parseFractional(0)).toBe(0);
expect(parseFractional(20)).toBe(20);
expect(parseFractional('20')).toBe(20);
expect(parseFractional(1.5)).toBe(1.5);
});
it('should parse fractional shorthands values', () => {
expect(parseFractional('w')).toBe(1);
expect(parseFractional('h')).toBe(0.5);
expect(parseFractional('q')).toBe(0.25);
expect(parseFractional('e')).toBe(0.125);
});
it('should throw an error for unknown strings', () => {
expect(() => parseFractional('xyz')).toThrowError('cannot parse as fractional: "xyz"');
});
});
describe('numeralArgs', () => {
it('should convert function arguments to numbers', () => {
const add = numeralArgs((a, b) => a + b);
expect(add('c4', 2)).toBe(62);
});
});
describe('fractionalArgs', () => {
it('should convert function arguments to numbers', () => {
const add = fractionalArgs((a, b) => a + b);
expect(add('q', 2)).toBe(2.25);
});
}); });
-11
View File
@@ -1,11 +0,0 @@
let time;
export function getTime() {
if (!time) {
throw new Error('no time set! use setTime to define a time source');
}
return time();
}
export function setTime(func) {
time = func;
}
-5
View File
@@ -18,11 +18,6 @@ export class TimeSpan {
const end = this.end; const end = this.end;
const end_sam = end.sam(); const end_sam = end.sam();
// Support zero-width timespans
if (begin.equals(end)) {
return([new TimeSpan(begin, end)]);
}
while (end.gt(begin)) { while (end.gt(begin)) {
// If begin and end are in the same cycle, we're done. // If begin and end are in the same cycle, we're done.
if (begin.sam().equals(end_sam)) { if (begin.sam().equals(end_sam)) {
+8 -51
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
// returns true if the given string is a note // returns true if the given string is a note
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name); export const isNote = (name) => /^[a-gA-G][#b]*[0-9]$/.test(name);
export const tokenizeNote = (note) => { export const tokenizeNote = (note) => {
if (typeof note !== 'string') { if (typeof note !== 'string') {
return []; return [];
@@ -55,13 +55,9 @@ export const midi2note = (n) => {
export const mod = (n, m) => ((n % m) + m) % m; export const mod = (n, m) => ((n % m) + m) % m;
export const getPlayableNoteValue = (hap) => { export const getPlayableNoteValue = (hap) => {
let { value, context } = hap; let { value: note, context } = hap;
let note = value;
if (typeof note === 'object' && !Array.isArray(note)) { if (typeof note === 'object' && !Array.isArray(note)) {
note = note.note || note.n || note.value; note = note.note || note.n || note.value;
if (note === undefined) {
throw new Error(`cannot find a playable note for ${JSON.stringify(value)}`);
}
} }
// if value is number => interpret as midi number as long as its not marked as frequency // if value is number => interpret as midi number as long as its not marked as frequency
if (typeof note === 'number' && context.type !== 'frequency') { if (typeof note === 'number' && context.type !== 'frequency') {
@@ -77,11 +73,8 @@ export const getPlayableNoteValue = (hap) => {
export const getFrequency = (hap) => { export const getFrequency = (hap) => {
let { value, context } = hap; let { value, context } = hap;
// if value is number => interpret as midi number as long as its not marked as frequency // if value is number => interpret as midi number as long as its not marked as frequency
if (typeof value === 'object') { if (typeof value === 'object' && value.freq) {
if (value.freq) { return value.freq;
return value.freq;
}
return getFreq(value.note || value.n || value.value);
} }
if (typeof value === 'number' && context.type !== 'frequency') { if (typeof value === 'number' && context.type !== 'frequency') {
value = fromMidi(hap.value); value = fromMidi(hap.value);
@@ -138,45 +131,9 @@ export function curry(func, overload) {
return fn; return fn;
} }
export function parseNumeral(numOrString) { export function objectify(value) {
const asNumber = Number(numOrString); if (typeof value === 'object' && !Array.isArray(value)) {
if (!isNaN(asNumber)) { return value; // do nothing if its already an object
return asNumber;
} }
if (isNote(numOrString)) { return { value };
return toMidi(numOrString);
}
throw new Error(`cannot parse as numeral: "${numOrString}"`);
} }
export function mapArgs(fn, mapFn) {
return (...args) => fn(...args.map(mapFn));
}
export function numeralArgs(fn) {
return mapArgs(fn, parseNumeral);
}
export function parseFractional(numOrString) {
const asNumber = Number(numOrString);
if (!isNaN(asNumber)) {
return asNumber;
}
const specialValue = {
pi: Math.PI,
w: 1,
h: 0.5,
q: 0.25,
e: 0.125,
s: 0.0625,
t: 1 / 3,
f: 0.2,
x: 1 / 6,
}[numOrString];
if (typeof specialValue !== 'undefined') {
return specialValue;
}
throw new Error(`cannot parse as fractional: "${numOrString}"`);
}
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
-49
View File
@@ -1,49 +0,0 @@
// will move to https://github.com/felixroos/zyklus
// TODO: started flag
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);
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, getPhase };
}
export default createClock;
+9 -16
View File
@@ -10,22 +10,15 @@ Either install with `npm i @strudel.cycles/embed` or just use a cdn to import th
<script src="https://unpkg.com/@strudel.cycles/embed@latest"></script> <script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
<strudel-repl> <strudel-repl>
<!-- <!--
note(`[[e5 [b4 c5] d5 [c5 b4]] "a4 [a3 c3] a3 c3".color('#F9D649')
[a4 [a4 c5] e5 [d5 c5]] .sub("<7 12 5 12>".slow(2))
[b4 [~ c5] d5 e5] .off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928"))
[c5 a4 a4 ~] .off(1/8,x=>x.add(12).color('#215CB6'))
[[~ d5] [~ f5] a5 [g5 f5]] .slow(2)
[e5 [~ c5] e5 [d5 c5]] .legato(sine.range(0.3, 2).slow(28))
[b4 [b4 c5] d5 e5] .wave("sawtooth square".fast(2))
[c5 a4 a4 ~]], .filter('lowpass', cosine.range(500,4000).slow(16))
[[e2 e3]*4] .pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
[[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>
``` ```
+9 -16
View File
@@ -2,21 +2,14 @@
<!-- <script src="./embed.js"></script> --> <!-- <script src="./embed.js"></script> -->
<strudel-repl> <strudel-repl>
<!-- <!--
note(`[[e5 [b4 c5] d5 [c5 b4]] "a4 [a3 c3] a3 c3".color('#F9D649')
[a4 [a4 c5] e5 [d5 c5]] .sub("<7 12 5 12>".slow(2))
[b4 [~ c5] d5 e5] .off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928"))
[c5 a4 a4 ~] .off(1/8,x=>x.add(12).color('#215CB6'))
[[~ d5] [~ f5] a5 [g5 f5]] .slow(2)
[e5 [~ c5] e5 [d5 c5]] .legato(sine.range(0.3, 2).slow(28))
[b4 [b4 c5] d5 e5] .wave("sawtooth square".fast(2))
[c5 a4 a4 ~]], .filter('lowpass', cosine.range(500,4000).slow(16))
[[e2 e3]*4] .pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
[[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>
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/embed", "name": "@strudel.cycles/embed",
"version": "0.2.0", "version": "0.1.1",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe", "description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js", "main": "embed.js",
"type": "module", "type": "module",
+3 -2
View File
@@ -11,9 +11,10 @@ npm i @strudel.cycles/eval --save
## Example ## Example
<!-- TODO: -extend +evalScope -->
```js ```js
import { evalScope } from '@strudel.cycles/core'; import { evaluate, extend } from '@strudel.cycles/eval';
import { evaluate } from '@strudel.cycles/eval';
evalScope( evalScope(
import('@strudel.cycles/core'), import('@strudel.cycles/core'),
+39 -2
View File
@@ -4,9 +4,46 @@ 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 { evaluate as _evaluate } from '@strudel.cycles/core';
import shapeshifter from './shapeshifter.mjs'; import shapeshifter from './shapeshifter.mjs';
import * as strudel from '@strudel.cycles/core';
const { isPattern, Pattern } = strudel;
export const extend = (...args) => {
console.warn('@strudel.cycles/eval extend is deprecated, please use evalScope instead');
Object.assign(globalThis, ...args);
};
let scoped = false;
export const evalScope = async (...args) => {
if (scoped) {
console.warn('@strudel.cycles/eval evalScope was called more than once.');
}
scoped = true;
const results = await Promise.allSettled(args);
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
results.forEach((result, i) => {
if (result.status === 'rejected') {
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
}
});
Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap());
};
function safeEval(str) {
return Function('"use strict";return (' + str + ')')();
}
export const evaluate = async (code) => { export const evaluate = async (code) => {
return _evaluate(code, shapeshifter); if (!scoped) {
await evalScope(); // at least scope Pattern.prototype.boostrap
}
const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code
let evaluated = await safeEval(shapeshifted);
if (!isPattern(evaluated)) {
console.log('evaluated', evaluated);
const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
}
return { mode: 'javascript', pattern: evaluated };
}; };
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/eval", "name": "@strudel.cycles/eval",
"version": "0.4.0", "version": "0.2.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
+6 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/eval", "name": "@strudel.cycles/eval",
"version": "0.4.0", "version": "0.2.0",
"description": "Code evaluator for strudel", "description": "Code evaluator for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -28,12 +28,12 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.4.0", "@strudel.cycles/core": "^0.2.0",
"estraverse": "^5.3.0", "estraverse": "^5.3.0",
"shift-ast": "^7.0.0", "shift-ast": "^6.1.0",
"shift-codegen": "^8.1.0", "shift-codegen": "^7.0.3",
"shift-parser": "^8.0.0", "shift-parser": "^7.0.3",
"shift-spec": "^2019.0.0", "shift-spec": "^2018.0.2",
"shift-traverser": "^1.0.0" "shift-traverser": "^1.0.0"
} }
} }
+3 -4
View File
@@ -30,8 +30,7 @@ const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
const addLocations = true; const addLocations = true;
export const addMiniLocations = true; export const addMiniLocations = true;
export const minifyStrings = true; export const minifyStrings = true;
export const wrappedAsync = false; // this is now handled by core evaluate by default export const wrappedAsync = true;
export const shouldAddReturn = true;
export default (_code) => { export default (_code) => {
const { code, addReturn } = wrapAsync(_code); const { code, addReturn } = wrapAsync(_code);
@@ -126,7 +125,7 @@ export default (_code) => {
}, },
}); });
// add return to last statement (because it's wrapped in an async function artificially) // add return to last statement (because it's wrapped in an async function artificially)
if (shouldAddReturn) { if (wrappedAsync) {
addReturn(shifted); addReturn(shifted);
} }
const generated = undisguiseImports(codegen(shifted)); const generated = undisguiseImports(codegen(shifted));
@@ -154,7 +153,7 @@ ${code}
})()`; })()`;
} }
const addReturn = (ast) => { const addReturn = (ast) => {
const body = wrappedAsync ? ast.statements[0].expression.callee.body : ast; const body = ast.statements[0].expression.callee.body; // actual code ast inside async function body
body.statements = body.statements body.statements = body.statements
.slice(0, -1) .slice(0, -1)
.concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]); .concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]);
+3 -3
View File
@@ -6,14 +6,14 @@ This program is free software: you can redistribute it and/or modify it under th
import { expect, describe, it } from 'vitest'; import { expect, describe, it } from 'vitest';
import { evaluate } from '../evaluate.mjs'; import { evaluate, evalScope } from '../evaluate.mjs';
import { mini } from '@strudel.cycles/mini'; import { mini } from '@strudel.cycles/mini';
import * as strudel from '@strudel.cycles/core'; import * as strudel from '@strudel.cycles/core';
const { fastcat, evalScope } = strudel; const { fastcat } = strudel;
describe('evaluate', async () => { describe('evaluate', async () => {
await evalScope({ mini }, strudel); await evalScope({ mini }, strudel);
const ev = async (code) => (await evaluate(code)).pattern.firstCycleValues; const ev = async (code) => (await evaluate(code)).pattern._firstCycleValues;
it('Should evaluate strudel functions', async () => { it('Should evaluate strudel functions', async () => {
expect(await ev('pure("c3")')).toEqual(['c3']); expect(await ev('pure("c3")')).toEqual(['c3']);
expect(await ev('cat("c3")')).toEqual(['c3']); expect(await ev('cat("c3")')).toEqual(['c3']);
+7 -13
View File
@@ -5,21 +5,15 @@ 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 shapeshifter, { wrappedAsync } from '../shapeshifter.mjs'; import shapeshifter from '../shapeshifter.mjs';
describe('shapeshifter', () => { describe('shapeshifter', () => {
it('Should shift simple double quote string', () => { it('Should shift simple double quote string', () => {
if (wrappedAsync) { expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()'); });
} else { it('Should handle dynamic imports', () => {
expect(shapeshifter('"c3"')).toEqual('return mini("c3").withMiniLocation([1,0,0],[1,4,4])'); expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
} '(async()=>{const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])})()',
);
}); });
if (wrappedAsync) {
it('Should handle dynamic imports', () => {
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
'const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])',
);
});
}
}); });
+29 -48
View File
@@ -4,19 +4,13 @@ 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 _WebMidi from 'webmidi'; import _WebMidi from 'webmidi';
import { Pattern, isPattern, isNote, getPlayableNoteValue, logger } from '@strudel.cycles/core'; import { Pattern, isPattern, isNote, getPlayableNoteValue, objectify } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
// if you use WebMidi from outside of this package, make sure to import that instance: // if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi; export const WebMidi = _WebMidi;
export function enableWebMidi(options = {}) { export function enableWebMidi() {
const { onReady, onConnected, onDisconnected } = options;
if (typeof navigator.requestMIDIAccess !== 'function') {
throw new Error('Your Browser does not support WebMIDI.');
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (WebMidi.enabled) { if (WebMidi.enabled) {
// if already enabled, just resolve WebMidi // if already enabled, just resolve WebMidi
@@ -27,14 +21,6 @@ export function enableWebMidi(options = {}) {
if (err) { if (err) {
reject(err); reject(err);
} }
WebMidi.addListener('connected', (e) => {
onConnected?.(WebMidi);
});
// Reacting when a device becomes unavailable
WebMidi.addListener('disconnected', (e) => {
onDisconnected?.(WebMidi, e);
});
onReady?.(WebMidi);
resolve(WebMidi); resolve(WebMidi);
}); });
}); });
@@ -43,34 +29,22 @@ export function enableWebMidi(options = {}) {
const outputByName = (name) => WebMidi.getOutputByName(name); const outputByName = (name) => WebMidi.getOutputByName(name);
// Pattern.prototype.midi = function (output: string | number, channel = 1) { // Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.midi = async function (output, channel = 1) { Pattern.prototype.midi = function (output, channel = 1) {
await enableWebMidi({ if (output?.constructor?.name && isPattern(output?.constructor?.name)) {
onConnected: ({ outputs }) =>
logger(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
onDisconnected: ({ outputs }) =>
logger(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
onReady: ({ outputs }) => {
const chosenOutput = output ?? outputs[0];
const otherOutputs = outputs
.filter((o) => o.name !== chosenOutput.name)
.map((o) => `'${o.name}'`)
.join(' | ');
logger(`Midi connected! Using "${chosenOutput.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
},
});
if (isPattern(output?.constructor?.name)) {
throw new Error( throw new Error(
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${ `.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1' WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
}')`, }')`,
); );
} }
return this.onTrigger((time, hap) => { return this.onTrigger((time, hap, currentTime) => {
let note = getPlayableNoteValue(hap); let note;
const velocity = hap.context?.velocity ?? 0.9; try {
if (!isNote(note)) { note = getPlayableNoteValue(hap);
throw new Error('not a note: ' + note); } catch (err) {
// dont care if no note is found as we can still send cvs
} }
const { velocity, nrpnn, nrpv, ccn, ccv, legato, duration: durationValue } = objectify(hap.value);
if (!WebMidi.enabled) { if (!WebMidi.enabled) {
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`); throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
} }
@@ -93,14 +67,21 @@ Pattern.prototype.midi = async function (output, channel = 1) {
); );
} }
// console.log('midi', value, output); // console.log('midi', value, output);
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000; const deadline = (time - currentTime) * 1000;
time = time * 1000 + timingOffset; time = WebMidi.time + deadline;
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000; /* const timingOffset = WebMidi.time - currentTime * 1000;
// await enableWebMidi() time = time * 1000 + timingOffset; */
device.playNote(note, channel, { const duration = (durationValue ?? hap.duration.valueOf()) * (legato ?? 1) * 1000 - 5;
time,
duration: hap.duration.valueOf() * 1000 - 5, if (note) {
attack: velocity, device.playNote(note, channel, {
}); time,
duration,
velocity,
});
}
if (ccn && ccv) {
device.sendControlChange(ccn, ccv, channel, { time });
}
}); });
}; };
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/midi", "name": "@strudel.cycles/midi",
"version": "0.4.0", "version": "0.2.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/midi", "name": "@strudel.cycles/midi",
"version": "0.4.0", "version": "0.2.0",
"description": "Midi API for strudel", "description": "Midi API for strudel",
"main": "index.mjs", "main": "index.mjs",
"repository": { "repository": {
@@ -21,8 +21,8 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@strudel.cycles/tone": "^0.4.0", "@strudel.cycles/tone": "^0.2.0",
"tone": "^14.7.77", "tone": "^14.7.77",
"webmidi": "^3.0.21" "webmidi": "^2.5.2"
} }
} }
-9
View File
@@ -33,12 +33,3 @@ yields:
## Mini Notation API ## Mini Notation API
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/) See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
## Building the Parser
The parser [krill-parser.js] is generated from [krill.pegjs](./krill.pegjs) using [peggy](https://peggyjs.org/).
To generate the parser, run
```js
npm run build:parser
```
+1 -1
View File
@@ -32,7 +32,7 @@ function peg$padEnd(str, targetLength, padString) {
} }
peg$SyntaxError.prototype.format = function(sources) { peg$SyntaxError.prototype.format = function(sources) {
var str = "peg error: " + this.message; var str = "Error: " + this.message;
if (this.location) { if (this.location) {
var src = null; var src = null;
var k; var k;
+4 -7
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import * as krill from './krill-parser.js'; import * as krill from './krill-parser.js';
import * as strudel from '@strudel.cycles/core'; import * as strudel from '@strudel.cycles/core';
// import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs'; import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs';
const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel; const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel;
@@ -29,10 +29,7 @@ const applyOptions = (parent) => (pat, i) => {
case 'bjorklund': case 'bjorklund':
return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation); return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation);
case 'degradeBy': case 'degradeBy':
return reify(pat)._degradeByWith( return reify(pat)._degradeByWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), operator.arguments_.amount);
strudel.rand.early(randOffset * _nextSeed()).segment(1),
operator.arguments_.amount,
);
// TODO: case 'fixed-step': "%" // TODO: case 'fixed-step': "%"
} }
console.warn(`operator "${operator.type_}" not implemented`); console.warn(`operator "${operator.type_}" not implemented`);
@@ -115,9 +112,9 @@ export function patternifyAST(ast) {
return silence; return silence;
} }
if (typeof ast.source_ !== 'object') { if (typeof ast.source_ !== 'object') {
/* if (!addMiniLocations) { if (!addMiniLocations) {
return ast.source_; return ast.source_;
} */ }
if (!ast.location_) { if (!ast.location_) {
console.warn('no location for', ast); console.warn('no location for', ast);
return ast.source_; return ast.source_;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/mini", "name": "@strudel.cycles/mini",
"version": "0.4.0", "version": "0.2.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/mini", "name": "@strudel.cycles/mini",
"version": "0.4.0", "version": "0.2.0",
"description": "Mini notation for strudel", "description": "Mini notation for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -26,9 +26,9 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.4.0", "@strudel.cycles/core": "^0.2.0",
"@strudel.cycles/eval": "^0.4.0", "@strudel.cycles/eval": "^0.2.0",
"@strudel.cycles/tone": "^0.4.0" "@strudel.cycles/tone": "^0.2.0"
}, },
"devDependencies": { "devDependencies": {
"peggy": "^2.0.1" "peggy": "^2.0.1"
+2 -2
View File
@@ -9,8 +9,8 @@ import '@strudel.cycles/core/euclid.mjs';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
describe('mini', () => { describe('mini', () => {
const minV = (v) => mini(v).firstCycleValues; const minV = (v) => mini(v)._firstCycleValues;
const minS = (v) => mini(v).showFirstCycle; const minS = (v) => mini(v)._showFirstCycle;
it('supports single elements', () => { it('supports single elements', () => {
expect(minV('a')).toEqual(['a']); expect(minV('a')).toEqual(['a']);
}); });
-2
View File
@@ -35,5 +35,3 @@ s("<bd sd> hh").osc()
``` ```
or just [click here](http://localhost:3000/#cygiPGJkIHNkPiBoaCIpLm9zYygp)... or just [click here](http://localhost:3000/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.tidalcycles.org/tutorial/#superdirt-api)
+22 -47
View File
@@ -5,34 +5,10 @@ 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 } from '@strudel.cycles/core'; import { Pattern } from '@strudel.cycles/core';
let connection; // Promise<OSC>
function connect() {
if (!connection) {
// make sure this runs only once
connection = new Promise((resolve, reject) => {
const osc = new OSC();
osc.open();
osc.on('open', () => {
const url = osc.options?.plugin?.socket?.url;
logger(`[osc] connected${url ? ` to ${url}` : ''}`);
resolve(osc);
});
osc.on('close', () => {
connection = undefined; // allows new connection afterwards
console.log('[osc] disconnected');
reject('OSC connection closed');
});
osc.on('error', (err) => reject(err));
}).catch((err) => {
connection = undefined;
throw new Error('Could not connect to OSC server. Is it running?');
});
}
return connection;
}
const comm = new OSC();
comm.open();
const latency = 0.1; const latency = 0.1;
let startedAt = -1; let startedAt = -1;
@@ -44,25 +20,24 @@ let startedAt = -1;
* @memberof Pattern * @memberof Pattern
* @returns Pattern * @returns Pattern
*/ */
Pattern.prototype.osc = async function () { Pattern.prototype.osc = function () {
const osc = await connect(); return this._withHap((hap) => {
return this.onTrigger((time, hap, currentTime, cps = 1) => { const onTrigger = (time, hap, currentTime, cps) => {
const cycle = hap.wholeOrPart().begin.valueOf(); const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf(); const delta = hap.duration.valueOf();
// 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)
if (startedAt < 0) { if (startedAt < 0) {
startedAt = Date.now() - currentTime * 1000; startedAt = Date.now() - currentTime * 1000;
} }
const controls = Object.assign({}, { cps, cycle, delta }, hap.value); const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
// make sure n and note are numbers const keyvals = Object.entries(controls).flat();
controls.n && (controls.n = parseNumeral(controls.n)); const ts = Math.floor(startedAt + (time + latency) * 1000);
controls.note && (controls.note = parseNumeral(controls.note)); const message = new OSC.Message('/dirt/play', ...keyvals);
const keyvals = Object.entries(controls).flat(); const bundle = new OSC.Bundle([message], ts);
const ts = Math.floor(startedAt + (time + latency) * 1000); bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
const message = new OSC.Message('/dirt/play', ...keyvals); comm.send(bundle);
const bundle = new OSC.Bundle([message], ts); };
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60 return hap.setContext({ ...hap.context, onTrigger });
osc.send(bundle);
}); });
}; };
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/osc", "name": "@strudel.cycles/osc",
"version": "0.3.0", "version": "0.1.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
+3 -7
View File
@@ -1,14 +1,13 @@
{ {
"name": "@strudel.cycles/osc", "name": "@strudel.cycles/osc",
"version": "0.3.0", "version": "0.1.1",
"description": "OSC messaging for strudel", "description": "OSC messaging for strudel",
"main": "osc.mjs", "main": "osc.mjs",
"scripts": { "scripts": {
"test": "echo \"No tests present.\" && exit 0", "test": "echo \"No tests present.\" && exit 0",
"server": "node server.js", "server": "node server.js",
"tidal-sniffer": "node tidal-sniffer.js", "tidal-sniffer": "node tidal-sniffer.js",
"client": "npx serve -p 4321", "client": "npx serve -p 4321"
"build": "npx pkg server.js --targets node16-macos-x64,node16-win-x64,node16-linux-x64 --out-path bin"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -31,9 +30,6 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"osc-js": "^2.4.0" "osc-js": "^2.3.2"
},
"devDependencies": {
"pkg": "^5.7.0"
} }
} }
-1
View File
@@ -1 +0,0 @@
examples
+2 -41
View File
@@ -1,43 +1,4 @@
# @strudel.cycles/react # @strudel.cycles/react
This package contains react hooks and components for strudel. It is used internally by the Strudel REPL. This package contains react hooks and components for strudel.
Example coming soon
## Install
```js
npm i @strudel.cycles/react
```
## Usage
Here is a minimal example of how to set up a MiniRepl:
```jsx
import { evalScope, controls } from '@strudel.cycles/core';
import { MiniRepl } from '@strudel.cycles/react';
import { prebake } from '../repl/src/prebake.mjs';
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
/* probably import other strudel packages */
);
prebake();
export function Repl({ tune }) {
return <MiniRepl tune={tune} hideOutsideView={true} />;
}
```
## Development
If you change something in here and want to see the changes in the repl, make sure to run `npm run build` inside this folder!
```js
npm run dev # dev server
npm run build # build package
```
+3 -1
View File
File diff suppressed because one or more lines are too long
+534 -272
View File
@@ -1,320 +1,582 @@
import n, { useCallback as _, useRef as H, useEffect as L, useMemo as V, useState as w, useLayoutEffect as j } from "react"; import React, { useCallback, useState, useEffect, useMemo, useRef, useLayoutEffect } from 'react';
import X from "@uiw/react-codemirror"; import _CodeMirror from '@uiw/react-codemirror';
import { Decoration as E, EditorView as U } from "@codemirror/view"; import { Decoration, EditorView } from '@codemirror/view';
import { StateEffect as $, StateField as G } from "@codemirror/state"; import { StateEffect, StateField } from '@codemirror/state';
import { javascript as Y } from "@codemirror/lang-javascript"; import { javascript } from '@codemirror/lang-javascript';
import { tags as r } from "@lezer/highlight"; import { tags } from '@lezer/highlight';
import { createTheme as Z } from "@uiw/codemirror-themes"; import { createTheme } from '@uiw/codemirror-themes';
import { useInView as ee } from "react-hook-inview"; import { useInView } from 'react-hook-inview';
import { webaudioOutput as te, getAudioContext as re } from "@strudel.cycles/webaudio"; import { evaluate } from '@strudel.cycles/eval';
import { repl as oe } from "@strudel.cycles/core"; import { Tone } from '@strudel.cycles/tone';
import { transpiler as ne } from "@strudel.cycles/transpiler"; import { TimeSpan, State } from '@strudel.cycles/core';
const ae = Z({ import { webaudioOutputTrigger } from '@strudel.cycles/webaudio';
theme: "dark", import { WebMidi, enableWebMidi } from '@strudel.cycles/midi';
var strudelTheme = createTheme({
theme: 'dark',
settings: { settings: {
background: "#222", background: '#222',
foreground: "#75baff", foreground: '#75baff', // whats that?
caret: "#ffcc00", caret: '#ffcc00',
selection: "rgba(128, 203, 196, 0.5)", selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: "#036dd626", selectionMatch: '#036dd626',
lineHighlight: "#00000050", lineHighlight: '#8a91991a',
gutterBackground: "transparent", gutterBackground: 'transparent',
gutterForeground: "#8a919966" // gutterForeground: '#8a919966',
gutterForeground: '#676e95',
}, },
styles: [ styles: [
{ tag: r.keyword, color: "#c792ea" }, { tag: tags.keyword, color: '#c792ea' },
{ tag: r.operator, color: "#89ddff" }, { tag: tags.operator, color: '#89ddff' },
{ tag: r.special(r.variableName), color: "#eeffff" }, { tag: tags.special(tags.variableName), color: '#eeffff' },
{ tag: r.typeName, color: "#c3e88d" }, { tag: tags.typeName, color: '#f07178' },
{ tag: r.atom, color: "#f78c6c" }, { tag: tags.atom, color: '#f78c6c' },
{ tag: r.number, color: "#c3e88d" }, { tag: tags.number, color: '#ff5370' },
{ tag: r.definition(r.variableName), color: "#82aaff" }, { tag: tags.definition(tags.variableName), color: '#82aaff' },
{ tag: r.string, color: "#c3e88d" }, { tag: tags.string, color: '#c3e88d' },
{ tag: r.special(r.string), color: "#c3e88d" }, { tag: tags.special(tags.string), color: '#f07178' },
{ tag: r.comment, color: "#7d8799" }, { tag: tags.comment, color: '#7d8799' },
{ tag: r.variableName, color: "#c792ea" }, { tag: tags.variableName, color: '#f07178' },
{ tag: r.tagName, color: "#c3e88d" }, { tag: tags.tagName, color: '#ff5370' },
{ tag: r.bracket, color: "#525154" }, { tag: tags.bracket, color: '#a2a1a4' },
{ tag: r.meta, color: "#ffcb6b" }, { tag: tags.meta, color: '#ffcb6b' },
{ tag: r.attributeName, color: "#c792ea" }, { tag: tags.attributeName, color: '#c792ea' },
{ tag: r.propertyName, color: "#c792ea" }, { tag: tags.propertyName, color: '#c792ea' },
{ tag: r.className, color: "#decb6b" }, { tag: tags.className, color: '#decb6b' },
{ tag: r.invalid, color: "#ffffff" } { tag: tags.invalid, color: '#ffffff' },
] ],
}); });
const B = $.define(), se = G.define({
var style = '';
const setFlash = StateEffect.define();
const flashField = StateField.define({
create() { create() {
return E.none; return Decoration.none;
}, },
update(e, t) { update(flash2, tr) {
try { try {
for (let o of t.effects) for (let e of tr.effects) {
if (o.is(B)) if (e.is(setFlash)) {
if (o.value) { if (e.value) {
const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } }); const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
e = E.set([a.range(0, t.newDoc.length)]); flash2 = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else } else {
e = E.set([]); flash2 = Decoration.set([]);
return e; }
} catch (o) { }
return console.warn("flash error", o), e; }
return flash2;
} catch (err) {
console.warn("flash error", err);
return flash2;
} }
}, },
provide: (e) => U.decorations.from(e) provide: (f) => EditorView.decorations.from(f)
}), ce = (e) => { });
e.dispatch({ effects: B.of(!0) }), setTimeout(() => { const flash = (view) => {
e.dispatch({ effects: B.of(!1) }); view.dispatch({ effects: setFlash.of(true) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, 200); }, 200);
}, z = $.define(), ie = G.define({ };
const setHighlights = StateEffect.define();
const highlightField = StateField.define({
create() { create() {
return E.none; return Decoration.none;
}, },
update(e, t) { update(highlights, tr) {
try { try {
for (let o of t.effects) for (let e of tr.effects) {
if (o.is(z)) { if (e.is(setHighlights)) {
const a = o.value.map( const marks = e.value.map(
(s) => (s.context.locations || []).map(({ start: f, end: d }) => { (hap) => (hap.context.locations || []).map(({ start, end }) => {
const u = s.context.color || "#FFCA28"; const color = hap.context.color || "#FFCA28";
let c = t.newDoc.line(f.line).from + f.column, i = t.newDoc.line(d.line).from + d.column; let from = tr.newDoc.line(start.line).from + start.column;
const m = t.newDoc.length; let to = tr.newDoc.line(end.line).from + end.column;
return c > m || i > m ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${u};` } }).range(c, i); const l = tr.newDoc.length;
if (from > l || to > l) {
return;
}
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
return mark.range(from, to);
}) })
).flat().filter(Boolean) || []; ).flat().filter(Boolean) || [];
e = E.set(a, !0); highlights = Decoration.set(marks, true);
} }
return e; }
} catch { return highlights;
return E.set([]); } catch (err) {
return Decoration.set([]);
} }
}, },
provide: (e) => U.decorations.from(e) provide: (f) => EditorView.decorations.from(f)
}), le = [Y(), ae, ie, se]; });
function de({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: f }) { const extensions = [javascript(), strudelTheme, highlightField, flashField];
const d = _( function CodeMirror({ value, onChange, onViewChanged, onSelectionChange, options, editorDidMount }) {
(i) => { const handleOnChange = useCallback(
t?.(i); (value2) => {
onChange?.(value2);
}, },
[t] [onChange]
), u = _(
(i) => {
o?.(i);
},
[o]
), c = _(
(i) => {
i.selectionSet && a && a?.(i.state.selection);
},
[a]
); );
return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(X, { const handleOnCreateEditor = useCallback(
value: e, (view) => {
onChange: d, onViewChanged?.(view);
onCreateEditor: u, },
onUpdate: c, [onViewChanged]
extensions: le );
const handleOnUpdate = useCallback(
(viewUpdate) => {
if (viewUpdate.selectionSet && onSelectionChange) {
onSelectionChange?.(viewUpdate.state.selection);
}
},
[onSelectionChange]
);
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(_CodeMirror, {
value,
onChange: handleOnChange,
onCreateEditor: handleOnCreateEditor,
onUpdate: handleOnUpdate,
extensions
})); }));
} }
function K(...e) {
return e.filter(Boolean).join(" "); /*
useCycle.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useCycle.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/>.
*/
/* export declare interface UseCycleProps {
onEvent: ToneEventCallback<any>;
onQuery?: (state: State) => Hap[];
onSchedule?: (events: Hap[], cycle: number) => void;
onDraw?: ToneEventCallback<any>;
ready?: boolean; // if false, query will not be called on change props
} */
// function useCycle(props: UseCycleProps) {
function useCycle(props) {
// onX must use useCallback!
const { onEvent, onQuery, onSchedule, ready = true, onDraw } = props;
const [started, setStarted] = useState(false);
const cycleDuration = 1;
const activeCycle = () => Math.floor(Tone.getTransport().seconds / cycleDuration);
// pull events with onQuery + count up to next cycle
const query = (cycle = activeCycle()) => {
const timespan = new TimeSpan(cycle, cycle + 1);
const events = onQuery?.(new State(timespan)) || [];
onSchedule?.(events, cycle);
// cancel events after current query. makes sure no old events are player for rescheduled cycles
// console.log('schedule', cycle);
// query next cycle in the middle of the current
const cancelFrom = timespan.begin.valueOf();
Tone.getTransport().cancel(cancelFrom);
// const queryNextTime = (cycle + 1) * cycleDuration - 0.1;
const queryNextTime = (cycle + 1) * cycleDuration - 0.5;
// if queryNextTime would be before current time, execute directly (+0.1 for safety that it won't miss)
const t = Math.max(Tone.getTransport().seconds, queryNextTime) + 0.1;
Tone.getTransport().schedule(() => {
query(cycle + 1);
}, t);
// schedule events for next cycle
events
?.filter((event) => event.part.begin.equals(event.whole?.begin))
.forEach((event) => {
Tone.getTransport().schedule((time) => {
onEvent(time, event, Tone.getContext().currentTime);
Tone.Draw.schedule(() => {
// do drawing or DOM manipulation here
onDraw?.(time, event);
}, time);
}, event.part.begin.valueOf());
});
};
useEffect(() => {
ready && query();
}, [onEvent, onSchedule, onQuery, onDraw, ready]);
const start = async () => {
setStarted(true);
await Tone.start();
Tone.getTransport().start('+0.1');
};
const stop = () => {
Tone.getTransport().pause();
setStarted(false);
};
const toggle = () => (started ? stop() : start());
return {
start,
stop,
onEvent,
started,
setStarted,
toggle,
query,
activeCycle,
};
} }
function ue({ view: e, pattern: t, active: o, getTime: a }) {
const s = H([]), f = H(); /*
L(() => { usePostMessage.mjs - <short description TODO>
if (e) Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/usePostMessage.mjs>
if (t && o) { 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/>.
let u = function() { */
try {
const c = a(), m = [Math.max(f.current || c, c - 1 / 10, 0), c + 1 / 60]; function usePostMessage(listener) {
f.current = m[1], s.current = s.current.filter((g) => g.whole.end > c); useEffect(() => {
const h = t.queryArc(...m).filter((g) => g.hasOnset()); window.addEventListener('message', listener);
s.current = s.current.concat(h), e.dispatch({ effects: z.of(s.current) }); return () => window.removeEventListener('message', listener);
} catch { }, [listener]);
e.dispatch({ effects: z.of([]) }); return useCallback((data) => window.postMessage(data, '*'), []);
}
d = requestAnimationFrame(u);
}, d = requestAnimationFrame(u);
return () => {
cancelAnimationFrame(d);
};
} else
s.current = [], e.dispatch({ effects: z.of([]) });
}, [t, o, e]);
} }
const fe = "_container_3i85k_1", me = "_header_3i85k_5", ge = "_buttons_3i85k_9", pe = "_button_3i85k_9", he = "_buttonDisabled_3i85k_17", be = "_error_3i85k_21", ve = "_body_3i85k_25", v = {
container: fe, /*
header: me, useRepl.mjs - <short description TODO>
buttons: ge, Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useRepl.mjs>
button: pe, 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/>.
buttonDisabled: he, */
error: be,
body: ve let s4 = () => {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}; };
function O({ type: e }) { const generateHash = (code) => encodeURIComponent(btoa(code));
return /* @__PURE__ */ n.createElement("svg", {
function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
const id = useMemo(() => s4(), []);
const [code, setCode] = useState(tune);
const [activeCode, setActiveCode] = useState();
const [log, setLog] = useState('');
const [error, setError] = useState();
const [pending, setPending] = useState(false);
const [hash, setHash] = useState('');
const [pattern, setPattern] = useState();
const dirty = useMemo(() => code !== activeCode || error, [code, activeCode, error]);
const pushLog = useCallback((message) => setLog((log) => log + `${log ? '\n\n' : ''}${message}`), []);
// below block allows disabling the highlighting by including "strudel disable-highlighting" in the code (as comment)
const onDraw = useMemo(() => {
if (activeCode && !activeCode.includes('strudel disable-highlighting')) {
return (time, event) => onDrawProp?.(time, event, activeCode);
}
}, [activeCode, onDrawProp]);
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
// cycle hook to control scheduling
const cycle = useCycle({
onDraw,
onEvent: useCallback(
(time, event, currentTime) => {
try {
onEvent?.(event);
if (event.context.logs?.length) {
event.context.logs.forEach(pushLog);
}
const { onTrigger = webaudioOutputTrigger } = event.context;
onTrigger(time, event, currentTime, 1 /* cps */);
} catch (err) {
console.warn(err);
err.message = 'unplayable event: ' + err?.message;
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
}
},
[onEvent, pushLog],
),
onQuery: useCallback(
(state) => {
try {
return pattern?.query(state) || [];
} catch (err) {
console.warn(err);
err.message = 'query error: ' + err.message;
setError(err);
return [];
}
},
[pattern],
),
onSchedule: useCallback((_events, cycle) => logCycle(_events), []),
ready: !!pattern && !!activeCode,
});
const broadcast = usePostMessage(({ data: { from, type } }) => {
if (type === 'start' && from !== id) {
// console.log('message', from, type);
cycle.setStarted(false);
setActiveCode(undefined);
}
});
const activateCode = useCallback(
async (_code = code) => {
if (activeCode && !dirty) {
setError(undefined);
cycle.start();
return;
}
try {
setPending(true);
const parsed = await evaluate(_code);
cycle.start();
broadcast({ type: 'start', from: id });
setPattern(() => parsed.pattern);
if (autolink) {
window.location.hash = '#' + encodeURIComponent(btoa(code));
}
setHash(generateHash(code));
setError(undefined);
setActiveCode(_code);
setPending(false);
} catch (err) {
err.message = 'evaluation error: ' + err.message;
console.warn(err);
setError(err);
}
},
[activeCode, dirty, code, cycle, autolink, id, broadcast],
);
// logs events of cycle
const logCycle = (_events, cycle) => {
if (_events.length) ;
};
const togglePlay = () => {
if (!cycle.started) {
activateCode();
} else {
cycle.stop();
}
};
return {
hideHeader,
hideConsole,
pending,
code,
setCode,
pattern,
error,
cycle,
setPattern,
dirty,
log,
togglePlay,
setActiveCode,
activateCode,
activeCode,
pushLog,
hash,
};
}
/*
cx.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/cx.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function cx(...classes) {
// : Array<string | undefined>
return classes.filter(Boolean).join(' ');
}
let highlights = []; // actively highlighted events
let lastEnd;
function useHighlighting({ view, pattern, active }) {
useEffect(() => {
if (view) {
if (pattern && active) {
let frame = requestAnimationFrame(updateHighlights);
function updateHighlights() {
try {
const audioTime = Tone.getTransport().seconds;
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
// see https://github.com/tidalcycles/strudel/issues/108
const begin = Math.max(lastEnd || audioTime, audioTime - 1 / 10);
const span = [begin, audioTime + 1 / 60];
lastEnd = audioTime + 1 / 60;
highlights = highlights.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
highlights = highlights.concat(haps); // add potential new onsets
view.dispatch({ effects: setHighlights.of(highlights) }); // highlight all still active + new active haps
} catch (err) {
// console.log('error in updateHighlights', err);
view.dispatch({ effects: setHighlights.of([]) });
}
frame = requestAnimationFrame(updateHighlights);
}
return () => {
cancelAnimationFrame(frame);
};
} else {
highlights = [];
view.dispatch({ effects: setHighlights.of([]) });
}
}
}, [pattern, active, view]);
}
var tailwind = '';
const container = "_container_3i85k_1";
const header = "_header_3i85k_5";
const buttons = "_buttons_3i85k_9";
const button = "_button_3i85k_9";
const buttonDisabled = "_buttonDisabled_3i85k_17";
const error = "_error_3i85k_21";
const body = "_body_3i85k_25";
var styles = {
container: container,
header: header,
buttons: buttons,
button: button,
buttonDisabled: buttonDisabled,
error: error,
body: body
};
function Icon({ type }) {
return /* @__PURE__ */ React.createElement("svg", {
xmlns: "http://www.w3.org/2000/svg", xmlns: "http://www.w3.org/2000/svg",
className: "sc-h-5 sc-w-5", className: "sc-h-5 sc-w-5",
viewBox: "0 0 20 20", viewBox: "0 0 20 20",
fill: "currentColor" fill: "currentColor"
}, { }, {
refresh: /* @__PURE__ */ n.createElement("path", { refresh: /* @__PURE__ */ React.createElement("path", {
fillRule: "evenodd", fillRule: "evenodd",
d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z", d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",
clipRule: "evenodd" clipRule: "evenodd"
}), }),
play: /* @__PURE__ */ n.createElement("path", { play: /* @__PURE__ */ React.createElement("path", {
fillRule: "evenodd", fillRule: "evenodd",
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",
clipRule: "evenodd" clipRule: "evenodd"
}), }),
pause: /* @__PURE__ */ n.createElement("path", { pause: /* @__PURE__ */ React.createElement("path", {
fillRule: "evenodd", fillRule: "evenodd",
d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z", d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",
clipRule: "evenodd" clipRule: "evenodd"
}) })
}[e]); }[type]);
} }
function Ee(e) {
return L(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), _((t) => window.postMessage(t, "*"), []); function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableKeyboard }) {
} const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } = useRepl({
function we({ tune,
defaultOutput: e, autolink: false,
interval: t, onEvent
getTime: o, });
evalOnMount: a = !1, useEffect(() => {
initialCode: s = "", init && evaluateOnly();
autolink: f = !1, }, [tune, init]);
beforeEval: d, const [view, setView] = useState();
afterEval: u, const [ref, isVisible] = useInView({
onEvalError: c,
onToggle: i
}) {
const m = V(() => ye(), []), [h, g] = w(), [C, N] = w(), [p, y] = w(s), [M, S] = w(), [k, D] = w(), [F, x] = w(!1), b = p !== M, { scheduler: A, evaluate: T, start: J, stop: q, pause: Q } = V(
() => oe({
interval: t,
defaultOutput: e,
onSchedulerError: g,
onEvalError: (l) => {
N(l), c?.(l);
},
getTime: o,
transpiler: ne,
beforeEval: ({ code: l }) => {
y(l), d?.();
},
afterEval: ({ pattern: l, code: P }) => {
S(P), D(l), N(), g(), f && (window.location.hash = "#" + encodeURIComponent(btoa(P))), u?.();
},
onToggle: (l) => {
x(l), i?.(l);
}
}),
[e, t, o]
), W = Ee(({ data: { from: l, type: P } }) => {
P === "start" && l !== m && q();
}), R = _(
async (l = !0) => {
await T(p, l), W({ type: "start", from: m });
},
[T, p]
), I = H();
return L(() => {
!I.current && a && p && (I.current = !0, R());
}, [R, a, p]), L(() => () => {
A.stop();
}, [A]), {
code: p,
setCode: y,
error: h || C,
schedulerError: h,
scheduler: A,
evalError: C,
evaluate: T,
activateCode: R,
activeCode: M,
isDirty: b,
pattern: k,
started: F,
start: J,
stop: q,
pause: Q,
togglePlay: async () => {
F ? A.pause() : await R();
}
};
}
function ye() {
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
}
const ke = () => re().currentTime;
function Se({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) {
const {
code: s,
setCode: f,
evaluate: d,
activateCode: u,
error: c,
isDirty: i,
activeCode: m,
pattern: h,
started: g,
scheduler: C,
togglePlay: N,
stop: p
} = we({
initialCode: e,
defaultOutput: te,
getTime: ke
}), [y, M] = w(), [S, k] = ee({
threshold: 0.01 threshold: 0.01
}), D = H(), F = V(() => ((k || !t) && (D.current = !0), k || D.current), [k, t]); });
return ue({ const wasVisible = useRef();
view: y, const show = useMemo(() => {
pattern: h, if (isVisible || !hideOutsideView) {
active: g && !m?.includes("strudel disable-highlighting"), wasVisible.current = true;
getTime: () => C.getPhase()
}), j(() => {
if (a) {
const x = async (b) => {
(b.ctrlKey || b.altKey) && (b.code === "Enter" ? (b.preventDefault(), ce(y), await u()) : b.code === "Period" && (p(), b.preventDefault()));
};
return window.addEventListener("keydown", x, !0), () => window.removeEventListener("keydown", x, !0);
} }
}, [a, h, s, d, p, y]), /* @__PURE__ */ n.createElement("div", { return isVisible || wasVisible.current;
className: v.container, }, [isVisible, hideOutsideView]);
ref: S useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes("strudel disable-highlighting") });
}, /* @__PURE__ */ n.createElement("div", { useLayoutEffect(() => {
className: v.header if (enableKeyboard) {
}, /* @__PURE__ */ n.createElement("div", { const handleKeyPress = async (e) => {
className: v.buttons if (e.ctrlKey || e.altKey) {
}, /* @__PURE__ */ n.createElement("button", { if (e.code === "Enter") {
className: K(v.button, g ? "sc-animate-pulse" : ""), e.preventDefault();
onClick: () => N() flash(view);
}, /* @__PURE__ */ n.createElement(O, { await activateCode();
type: g ? "pause" : "play" } else if (e.code === "Period") {
})), /* @__PURE__ */ n.createElement("button", { cycle.stop();
className: K(i ? v.button : v.buttonDisabled), e.preventDefault();
onClick: () => u() }
}, /* @__PURE__ */ n.createElement(O, { }
};
window.addEventListener("keydown", handleKeyPress, true);
return () => window.removeEventListener("keydown", handleKeyPress, true);
}
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
return /* @__PURE__ */ React.createElement("div", {
className: styles.container,
ref
}, /* @__PURE__ */ React.createElement("div", {
className: styles.header
}, /* @__PURE__ */ React.createElement("div", {
className: styles.buttons
}, /* @__PURE__ */ React.createElement("button", {
className: cx(styles.button, cycle.started ? "sc-animate-pulse" : ""),
onClick: () => togglePlay()
}, /* @__PURE__ */ React.createElement(Icon, {
type: cycle.started ? "pause" : "play"
})), /* @__PURE__ */ React.createElement("button", {
className: cx(dirty ? styles.button : styles.buttonDisabled),
onClick: () => activateCode()
}, /* @__PURE__ */ React.createElement(Icon, {
type: "refresh" type: "refresh"
}))), c && /* @__PURE__ */ n.createElement("div", { }))), error && /* @__PURE__ */ React.createElement("div", {
className: v.error className: styles.error
}, c.message)), /* @__PURE__ */ n.createElement("div", { }, error.message)), /* @__PURE__ */ React.createElement("div", {
className: v.body className: styles.body
}, F && /* @__PURE__ */ n.createElement(de, { }, show && /* @__PURE__ */ React.createElement(CodeMirror, {
value: s, value: code,
onChange: f, onChange: setCode,
onViewChanged: M onViewChanged: setView
}))); })));
} }
const Te = (e) => j(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
export { /*
de as CodeMirror, useWebMidi.js - <short description TODO>
Se as MiniRepl, Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useWebMidi.js>
K as cx, 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/>.
ce as flash, */
ue as useHighlighting,
Te as useKeydown, function useWebMidi(props) {
Ee as usePostMessage, const { ready, connected, disconnected } = props;
we as useStrudel const [loading, setLoading] = useState(true);
}; const [outputs, setOutputs] = useState(WebMidi?.outputs || []);
useEffect(() => {
enableWebMidi()
.then(() => {
// Reacting when a new device becomes available
WebMidi.addListener('connected', (e) => {
setOutputs([...WebMidi.outputs]);
connected?.(WebMidi, e);
});
// Reacting when a device becomes unavailable
WebMidi.addListener('disconnected', (e) => {
setOutputs([...WebMidi.outputs]);
disconnected?.(WebMidi, e);
});
ready?.(WebMidi);
setLoading(false);
})
.catch((err) => {
if (err) {
console.error(err);
//throw new Error("Web Midi could not be enabled...");
console.warn('Web Midi could not be enabled..');
return;
}
});
}, [ready, connected, disconnected, outputs]);
const outputByName = (name) => WebMidi.getOutputByName(name);
return { loading, outputs, outputByName };
}
export { CodeMirror, MiniRepl, cx, flash, useCycle, useHighlighting, usePostMessage, useRepl, useWebMidi };
+1 -1
View File
@@ -1 +1 @@
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:18px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto} .cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:16px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto}
@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
!dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:16px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.fixed{position:fixed}.absolute{position:absolute}.bottom-0{bottom:0px}.z-\[12\]{z-index:12}.flex{display:flex}.w-full{width:100%}.justify-center{justify-content:center}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.rounded-t-md{border-top-left-radius:.375rem;border-top-right-radius:.375rem}.bg-slate-500{--tw-bg-opacity: 1;background-color:rgb(100 116 139 / var(--tw-bg-opacity))}.px-2{padding-left:.5rem;padding-right:.5rem}body{background:#123}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{f as s,i as t,h as o,j as d,q as f,l as p,k as u,p as i,o as l,n as r,w as g}from"./index.ec9f9930.js";export{s as getAudioContext,t as getCachedBuffer,o as getDestination,d as getLoadedBuffer,f as getLoadedSamples,p as loadBuffer,u as loadGithubSamples,i as panic,l as resetLoadedSamples,r as samples,g as webaudioOutput};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{P as w}from"./index.ec9f9930.js";var i,f=!1;async function b(a=38400){if(!f){if(f=!0,i)return i;if("serial"in navigator){const r=await navigator.serial.requestPort();await r.open({baudRate:a});const o=new TextEncoderStream;o.readable.pipeTo(r.writable);const s=o.writable.getWriter();i=function(e){s.write(e)}}else throw"Webserial is not available in this browser."}}const g=.1;w.prototype.serial=function(...a){return this._withHap(r=>{i||b(...a);const o=(s,e,u)=>{var t="";if(typeof e.value=="object")if("action"in e.value){t+=e.value.action+"(";var c=!0;for(const[n,l]of Object.entries(e.value))n!=="action"&&(c?c=!1:t+=",",t+=`${n}:${l}`);t+=")"}else for(const[n,l]of Object.entries(e.value))t+=`${n}:${l};`;else t=e.value;const v=(s-u+g)*1e3;window.setTimeout(i,v,t)};return r.setContext({...r.context,onTrigger:o})})};export{b as getWriter};
-14
View File
@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Strudel Nano REPL</title>
<script type="module" crossorigin src="./assets/index.ec9f9930.js"></script>
<link rel="stylesheet" href="./assets/index.75f8960b.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Strudel Nano REPL</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -1,24 +0,0 @@
{
"name": "nano-repl",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.0.1",
"autoprefixer": "^10.4.8",
"postcss": "^8.4.16",
"tailwindcss": "^3.1.8",
"vite": "^3.0.7"
}
}
@@ -1,139 +0,0 @@
import { evalScope, controls } from '@strudel.cycles/core';
import { getAudioContext, panic, webaudioOutput } from '@strudel.cycles/webaudio';
import { useCallback, useState } from 'react';
import CodeMirror, { flash } from '../../../src/components/CodeMirror6';
import useKeydown from '../../../src/hooks/useKeydown.mjs';
import useStrudel from '../../../src/hooks/useStrudel';
import useHighlighting from '../../../src/hooks/useHighlighting';
import './style.css';
// import { prebake } from '../../../../../repl/src/prebake.mjs';
// TODO: only import stuff when play is pressed?
evalScope(
controls,
import('@strudel.cycles/core'),
// import('@strudel.cycles/tone'),
// import('@strudel.cycles/midi'), // TODO: find out why midi loads tone.js
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
import('@strudel.cycles/webdirt'),
import('@strudel.cycles/serial'),
import('@strudel.cycles/soundfonts'),
);
const defaultTune = `samples({
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
}, 'github:tidalcycles/Dirt-Samples/master/');
stack(
s("bd,[~ <sd!3 sd(3,4,2)>],hh(3,4)") // drums
.speed(perlin.range(.7,.9)) // random sample speed variation
//.hush()
,"<a1 b1*2 a1(3,8) e2>" // bassline
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
.add(perlin.range(0,.5)) // random pitch variation
.superimpose(add(.05)) // add second, slightly detuned voice
.n() // wrap in "n"
.decay(.15).sustain(0) // make each note of equal length
.s('sawtooth') // waveform
.gain(.4) // turn down
.cutoff(sine.slow(7).range(300,5000)) // automate cutoff
//.hush()
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings() // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.s('square') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
.attack(1) // slowly fade in
//.hush()
,"a4 c5 <e6 a6>".struct("x(5,8)")
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.decay(.1).sustain(0) // make notes short
.s('triangle') // waveform
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
.echoWith(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes
//.hush()
)
.fast(2/3)`;
// await prebake();
const ctx = getAudioContext();
const getTime = () => ctx.currentTime;
function App() {
const [code, setCode] = useState(defaultTune);
const [view, setView] = useState();
// const [code, setCode] = useState(`"c3".note().slow(2)`);
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern } = useStrudel({
code,
defaultOutput: webaudioOutput,
getTime,
});
useHighlighting({
view,
pattern,
active: !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.getPhase(),
});
const error = evalError || schedulerError;
useKeydown(
useCallback(
async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await evaluate(code);
if (e.shiftKey) {
panic();
scheduler.stop();
scheduler.start();
}
if (!scheduler.started) {
scheduler.start();
}
} else if (e.code === 'Period') {
scheduler.pause();
panic();
e.preventDefault();
}
}
},
[scheduler, evaluate, view],
),
);
return (
<div>
{/* <textarea value={code} onChange={(e) => setCode(e.target.value)} cols="64" rows="30" /> */}
<nav className="z-[12] w-full flex justify-center absolute bottom-0">
<div className="bg-slate-500 space-x-2 px-2 rounded-t-md">
<button
onClick={async () => {
await evaluate(code);
await getAudioContext().resume();
scheduler.start();
}}
>
start
</button>
<button onClick={() => scheduler.stop()}>stop</button>
{isDirty && <button onClick={() => evaluate(code)}>eval</button>}
</div>
{error && <p>error {error.message}</p>}
</nav>
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
</div>
);
}
export default App;
@@ -1,9 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
@@ -1,7 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
background: #123;
}
@@ -1,14 +0,0 @@
/*
tailwind.config.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/tailwind.config.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
module.exports = {
// TODO: find out if leaving out tutorial path works now
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
@@ -1,7 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()]
})
+10 -12
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/react", "name": "@strudel.cycles/react",
"version": "0.4.1", "version": "0.2.0",
"description": "React components for strudel", "description": "React components for strudel",
"main": "dist/index.cjs.js", "main": "dist/index.cjs.js",
"module": "dist/index.es.js", "module": "dist/index.es.js",
@@ -17,7 +17,6 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"watch": "vite build --watch",
"preview": "vite preview" "preview": "vite preview"
}, },
"repository": { "repository": {
@@ -38,13 +37,12 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@codemirror/lang-javascript": "^6.1.1", "@codemirror/lang-javascript": "^6.0.2",
"@strudel.cycles/core": "^0.4.0", "@strudel.cycles/core": "^0.2.0",
"@strudel.cycles/tone": "^0.4.0", "@strudel.cycles/eval": "^0.2.0",
"@strudel.cycles/transpiler": "^0.4.0", "@strudel.cycles/tone": "^0.2.0",
"@strudel.cycles/webaudio": "^0.4.1", "@uiw/codemirror-themes": "^4.11.4",
"@uiw/codemirror-themes": "^4.12.4", "@uiw/react-codemirror": "^4.11.4",
"@uiw/react-codemirror": "^4.12.4",
"react-hook-inview": "^4.5.0" "react-hook-inview": "^4.5.0"
}, },
"peerDependencies": { "peerDependencies": {
@@ -54,12 +52,12 @@
"devDependencies": { "devDependencies": {
"@types/react": "^17.0.2", "@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2", "@types/react-dom": "^17.0.2",
"@vitejs/plugin-react": "^2.2.0", "@vitejs/plugin-react": "^1.3.0",
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.7",
"postcss": "^8.4.18", "postcss": "^8.4.13",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"tailwindcss": "^3.0.24", "tailwindcss": "^3.0.24",
"vite": "^3.2.2" "vite": "^2.9.9"
} }
} }
+3 -4
View File
@@ -1,12 +1,11 @@
import React from 'react'; import React from 'react';
import { MiniRepl } from './components/MiniRepl'; import { MiniRepl } from './components/MiniRepl';
import 'tailwindcss/tailwind.css'; import 'tailwindcss/tailwind.css';
import { controls, evalScope } from '@strudel.cycles/core'; import { evalScope } from '@strudel.cycles/eval';
evalScope( evalScope(
controls,
import('@strudel.cycles/core'), import('@strudel.cycles/core'),
// import('@strudel.cycles/tone'), import('@strudel.cycles/tone'),
import('@strudel.cycles/tonal'), import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'), import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'), import('@strudel.cycles/midi'),
@@ -17,7 +16,7 @@ evalScope(
function App() { function App() {
return ( return (
<div> <div>
<MiniRepl tune={`note("c3")`} /> <MiniRepl tune={`"c3"`} />
</div> </div>
); );
} }
+17 -37
View File
@@ -1,5 +1,6 @@
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react'; import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
import { useInView } from 'react-hook-inview'; import { useInView } from 'react-hook-inview';
import useRepl from '../hooks/useRepl.mjs';
import cx from '../cx'; import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs'; import useHighlighting from '../hooks/useHighlighting.mjs';
import CodeMirror6, { flash } from './CodeMirror6'; import CodeMirror6, { flash } from './CodeMirror6';
@@ -7,33 +8,17 @@ import 'tailwindcss/tailwind.css';
import './style.css'; import './style.css';
import styles from './MiniRepl.module.css'; import styles from './MiniRepl.module.css';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import useStrudel from '../hooks/useStrudel.mjs';
const getTime = () => getAudioContext().currentTime; export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }) { useRepl({
const { tune,
code, autolink: false,
setCode, onEvent,
evaluate, });
activateCode, useEffect(() => {
error, init && evaluateOnly();
isDirty, }, [tune, init]);
activeCode,
pattern,
started,
scheduler,
togglePlay,
stop,
} = useStrudel({
initialCode: tune,
defaultOutput: webaudioOutput,
getTime,
});
/* useEffect(() => {
init && activateCode();
}, [init, activateCode]); */
const [view, setView] = useState(); const [view, setView] = useState();
const [ref, isVisible] = useInView({ const [ref, isVisible] = useInView({
threshold: 0.01, threshold: 0.01,
@@ -45,12 +30,7 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
} }
return isVisible || wasVisible.current; return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]); }, [isVisible, hideOutsideView]);
useHighlighting({ useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes('strudel disable-highlighting') });
view,
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.getPhase(),
});
// set active pattern on ctrl+enter // set active pattern on ctrl+enter
useLayoutEffect(() => { useLayoutEffect(() => {
@@ -62,7 +42,7 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
flash(view); flash(view);
await activateCode(); await activateCode();
} else if (e.code === 'Period') { } else if (e.code === 'Period') {
stop(); cycle.stop();
e.preventDefault(); e.preventDefault();
} }
} }
@@ -70,16 +50,16 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
window.addEventListener('keydown', handleKeyPress, true); window.addEventListener('keydown', handleKeyPress, true);
return () => window.removeEventListener('keydown', handleKeyPress, true); return () => window.removeEventListener('keydown', handleKeyPress, true);
} }
}, [enableKeyboard, pattern, code, evaluate, stop, view]); }, [enableKeyboard, pattern, code, activateCode, cycle, view]);
return ( return (
<div className={styles.container} ref={ref}> <div className={styles.container} ref={ref}>
<div className={styles.header}> <div className={styles.header}>
<div className={styles.buttons}> <div className={styles.buttons}>
<button className={cx(styles.button, started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}> <button className={cx(styles.button, cycle.started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
<Icon type={started ? 'pause' : 'play'} /> <Icon type={cycle.started ? 'pause' : 'play'} />
</button> </button>
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}> <button className={cx(dirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
<Icon type="refresh" /> <Icon type="refresh" />
</button> </button>
</div> </div>
+1 -1
View File
@@ -2,7 +2,7 @@
background-color: transparent !important; background-color: transparent !important;
height: 100%; height: 100%;
z-index: 11; z-index: 11;
font-size: 18px; font-size: 16px;
} }
.cm-theme-light { .cm-theme-light {
+86
View File
@@ -0,0 +1,86 @@
/*
useCycle.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useCycle.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 { useEffect, useState } from 'react';
import { Tone } from '@strudel.cycles/tone';
import { State, TimeSpan } from '@strudel.cycles/core';
/* export declare interface UseCycleProps {
onEvent: ToneEventCallback<any>;
onQuery?: (state: State) => Hap[];
onSchedule?: (events: Hap[], cycle: number) => void;
onDraw?: ToneEventCallback<any>;
ready?: boolean; // if false, query will not be called on change props
} */
// function useCycle(props: UseCycleProps) {
function useCycle(props) {
// onX must use useCallback!
const { onEvent, onQuery, onSchedule, ready = true, onDraw } = props;
const [started, setStarted] = useState(false);
const cycleDuration = 1;
const activeCycle = () => Math.floor(Tone.getTransport().seconds / cycleDuration);
// pull events with onQuery + count up to next cycle
const query = (cycle = activeCycle()) => {
const timespan = new TimeSpan(cycle, cycle + 1);
const events = onQuery?.(new State(timespan)) || [];
onSchedule?.(events, cycle);
// cancel events after current query. makes sure no old events are player for rescheduled cycles
// console.log('schedule', cycle);
// query next cycle in the middle of the current
const cancelFrom = timespan.begin.valueOf();
Tone.getTransport().cancel(cancelFrom);
// const queryNextTime = (cycle + 1) * cycleDuration - 0.1;
const queryNextTime = (cycle + 1) * cycleDuration - 0.5;
// if queryNextTime would be before current time, execute directly (+0.1 for safety that it won't miss)
const t = Math.max(Tone.getTransport().seconds, queryNextTime) + 0.1;
Tone.getTransport().schedule(() => {
query(cycle + 1);
}, t);
// schedule events for next cycle
events
?.filter((event) => event.part.begin.equals(event.whole?.begin))
.forEach((event) => {
Tone.getTransport().schedule((time) => {
onEvent(time, event, Tone.getContext().currentTime);
Tone.Draw.schedule(() => {
// do drawing or DOM manipulation here
onDraw?.(time, event);
}, time);
}, event.part.begin.valueOf());
});
};
useEffect(() => {
ready && query();
}, [onEvent, onSchedule, onQuery, onDraw, ready]);
const start = async () => {
setStarted(true);
await Tone.start();
Tone.getTransport().start('+0.1');
};
const stop = () => {
Tone.getTransport().pause();
setStarted(false);
};
const toggle = () => (started ? stop() : start());
return {
start,
stop,
onEvent,
started,
setStarted,
toggle,
query,
activeCycle,
};
}
export default useCycle;
+14 -11
View File
@@ -1,9 +1,11 @@
import { useEffect, useRef } from 'react'; import { useEffect } from 'react';
import { setHighlights } from '../components/CodeMirror6'; import { setHighlights } from '../components/CodeMirror6';
import { Tone } from '@strudel.cycles/tone';
function useHighlighting({ view, pattern, active, getTime }) { let highlights = []; // actively highlighted events
const highlights = useRef([]); let lastEnd;
const lastEnd = useRef();
function useHighlighting({ view, pattern, active }) {
useEffect(() => { useEffect(() => {
if (view) { if (view) {
if (pattern && active) { if (pattern && active) {
@@ -11,17 +13,18 @@ function useHighlighting({ view, pattern, active, getTime }) {
function updateHighlights() { function updateHighlights() {
try { try {
const audioTime = getTime(); const audioTime = Tone.getTransport().seconds;
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away // force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
// see https://github.com/tidalcycles/strudel/issues/108 // see https://github.com/tidalcycles/strudel/issues/108
const begin = Math.max(lastEnd.current || audioTime, audioTime - 1 / 10, 0); // negative time seems buggy const begin = Math.max(lastEnd || audioTime, audioTime - 1 / 10);
const span = [begin, audioTime + 1 / 60]; const span = [begin, audioTime + 1 / 60];
lastEnd.current = span[1]; lastEnd = audioTime + 1 / 60;
highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active highlights = highlights.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset()); const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
highlights.current = highlights.current.concat(haps); // add potential new onsets highlights = highlights.concat(haps); // add potential new onsets
view.dispatch({ effects: setHighlights.of(highlights.current) }); // highlight all still active + new active haps view.dispatch({ effects: setHighlights.of(highlights) }); // highlight all still active + new active haps
} catch (err) { } catch (err) {
// console.log('error in updateHighlights', err);
view.dispatch({ effects: setHighlights.of([]) }); view.dispatch({ effects: setHighlights.of([]) });
} }
frame = requestAnimationFrame(updateHighlights); frame = requestAnimationFrame(updateHighlights);
@@ -31,7 +34,7 @@ function useHighlighting({ view, pattern, active, getTime }) {
cancelAnimationFrame(frame); cancelAnimationFrame(frame);
}; };
} else { } else {
highlights.current = []; highlights = [];
view.dispatch({ effects: setHighlights.of([]) }); view.dispatch({ effects: setHighlights.of([]) });
} }
} }
-10
View File
@@ -1,10 +0,0 @@
import { useLayoutEffect } from 'react';
// set active pattern on ctrl+enter
const useKeydown = (callback) =>
useLayoutEffect(() => {
window.addEventListener('keydown', callback, true);
return () => window.removeEventListener('keydown', callback, true);
}, [callback]);
export default useKeydown;
+150
View File
@@ -0,0 +1,150 @@
/*
useRepl.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useRepl.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 { useCallback, useState, useMemo } from 'react';
import { evaluate } from '@strudel.cycles/eval';
import useCycle from './useCycle.mjs';
import usePostMessage from './usePostMessage.mjs';
import { webaudioOutputTrigger } from '@strudel.cycles/webaudio';
let s4 = () => {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
};
const generateHash = (code) => encodeURIComponent(btoa(code));
function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
const id = useMemo(() => s4(), []);
const [code, setCode] = useState(tune);
const [activeCode, setActiveCode] = useState();
const [log, setLog] = useState('');
const [error, setError] = useState();
const [pending, setPending] = useState(false);
const [hash, setHash] = useState('');
const [pattern, setPattern] = useState();
const dirty = useMemo(() => code !== activeCode || error, [code, activeCode, error]);
const pushLog = useCallback((message) => setLog((log) => log + `${log ? '\n\n' : ''}${message}`), []);
// below block allows disabling the highlighting by including "strudel disable-highlighting" in the code (as comment)
const onDraw = useMemo(() => {
if (activeCode && !activeCode.includes('strudel disable-highlighting')) {
return (time, event) => onDrawProp?.(time, event, activeCode);
}
}, [activeCode, onDrawProp]);
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
// cycle hook to control scheduling
const cycle = useCycle({
onDraw,
onEvent: useCallback(
(time, event, currentTime) => {
try {
onEvent?.(event);
if (event.context.logs?.length) {
event.context.logs.forEach(pushLog);
}
const { onTrigger = webaudioOutputTrigger } = event.context;
onTrigger(time, event, currentTime, 1 /* cps */);
} catch (err) {
console.warn(err);
err.message = 'unplayable event: ' + err?.message;
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
}
},
[onEvent, pushLog],
),
onQuery: useCallback(
(state) => {
try {
return pattern?.query(state) || [];
} catch (err) {
console.warn(err);
err.message = 'query error: ' + err.message;
setError(err);
return [];
}
},
[pattern],
),
onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), []),
ready: !!pattern && !!activeCode,
});
const broadcast = usePostMessage(({ data: { from, type } }) => {
if (type === 'start' && from !== id) {
// console.log('message', from, type);
cycle.setStarted(false);
setActiveCode(undefined);
}
});
const activateCode = useCallback(
async (_code = code) => {
if (activeCode && !dirty) {
setError(undefined);
cycle.start();
return;
}
try {
setPending(true);
const parsed = await evaluate(_code);
cycle.start();
broadcast({ type: 'start', from: id });
setPattern(() => parsed.pattern);
if (autolink) {
window.location.hash = '#' + encodeURIComponent(btoa(code));
}
setHash(generateHash(code));
setError(undefined);
setActiveCode(_code);
setPending(false);
} catch (err) {
err.message = 'evaluation error: ' + err.message;
console.warn(err);
setError(err);
}
},
[activeCode, dirty, code, cycle, autolink, id, broadcast],
);
// logs events of cycle
const logCycle = (_events, cycle) => {
if (_events.length) {
// pushLog(`# cycle ${cycle}\n` + _events.map((e: any) => e.show()).join('\n'));
}
};
const togglePlay = () => {
if (!cycle.started) {
activateCode();
} else {
cycle.stop();
}
};
return {
hideHeader,
hideConsole,
pending,
code,
setCode,
pattern,
error,
cycle,
setPattern,
dirty,
log,
togglePlay,
setActiveCode,
activateCode,
activeCode,
pushLog,
hash,
};
}
export default useRepl;
-125
View File
@@ -1,125 +0,0 @@
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
import { repl } from '@strudel.cycles/core';
import { transpiler } from '@strudel.cycles/transpiler';
import usePostMessage from './usePostMessage.mjs';
function useStrudel({
defaultOutput,
interval,
getTime,
evalOnMount = false,
initialCode = '',
autolink = false,
beforeEval,
afterEval,
onEvalError,
onToggle,
}) {
const id = useMemo(() => s4(), []);
// scheduler
const [schedulerError, setSchedulerError] = useState();
const [evalError, setEvalError] = useState();
const [code, setCode] = useState(initialCode);
const [activeCode, setActiveCode] = useState();
const [pattern, setPattern] = useState();
const [started, setStarted] = useState(false);
const isDirty = code !== activeCode;
// TODO: make sure this hook reruns when scheduler.started changes
const { scheduler, evaluate, start, stop, pause } = useMemo(
() =>
repl({
interval,
defaultOutput,
onSchedulerError: setSchedulerError,
onEvalError: (err) => {
setEvalError(err);
onEvalError?.(err);
},
getTime,
transpiler,
beforeEval: ({ code }) => {
setCode(code);
beforeEval?.();
},
afterEval: ({ pattern: _pattern, code }) => {
setActiveCode(code);
setPattern(_pattern);
setEvalError();
setSchedulerError();
if (autolink) {
window.location.hash = '#' + encodeURIComponent(btoa(code));
}
afterEval?.();
},
onToggle: (v) => {
setStarted(v);
onToggle?.(v);
},
}),
[defaultOutput, interval, getTime],
);
const broadcast = usePostMessage(({ data: { from, type } }) => {
if (type === 'start' && from !== id) {
// console.log('message', from, type);
stop();
}
});
const activateCode = useCallback(
async (autostart = true) => {
await evaluate(code, autostart);
broadcast({ type: 'start', from: id });
},
[evaluate, code],
);
const inited = useRef();
useEffect(() => {
if (!inited.current && evalOnMount && code) {
inited.current = true;
activateCode();
}
}, [activateCode, evalOnMount, code]);
// this will stop the scheduler when hot reloading in development
useEffect(() => {
return () => {
scheduler.stop();
};
}, [scheduler]);
const togglePlay = async () => {
if (started) {
scheduler.pause();
} else {
await activateCode();
}
};
const error = schedulerError || evalError;
return {
code,
setCode,
error,
schedulerError,
scheduler,
evalError,
evaluate,
activateCode,
activeCode,
isDirty,
pattern,
started,
start,
stop,
pause,
togglePlay,
};
}
export default useStrudel;
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
+41
View File
@@ -0,0 +1,41 @@
/*
useWebMidi.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useWebMidi.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useEffect, useState } from 'react';
import { enableWebMidi, WebMidi } from '@strudel.cycles/midi'
export function useWebMidi(props) {
const { ready, connected, disconnected } = props;
const [loading, setLoading] = useState(true);
const [outputs, setOutputs] = useState(WebMidi?.outputs || []);
useEffect(() => {
enableWebMidi()
.then(() => {
// Reacting when a new device becomes available
WebMidi.addListener('connected', (e) => {
setOutputs([...WebMidi.outputs]);
connected?.(WebMidi, e);
});
// Reacting when a device becomes unavailable
WebMidi.addListener('disconnected', (e) => {
setOutputs([...WebMidi.outputs]);
disconnected?.(WebMidi, e);
});
ready?.(WebMidi);
setLoading(false);
})
.catch((err) => {
if (err) {
console.error(err);
//throw new Error("Web Midi could not be enabled...");
console.warn('Web Midi could not be enabled..');
return;
}
});
}, [ready, connected, disconnected, outputs]);
const outputByName = (name) => WebMidi.getOutputByName(name);
return { loading, outputs, outputByName };
}
+3 -2
View File
@@ -2,8 +2,9 @@
export { default as CodeMirror, flash } from './components/CodeMirror6'; export { default as CodeMirror, flash } from './components/CodeMirror6';
export * from './components/MiniRepl'; export * from './components/MiniRepl';
export { default as useCycle } from './hooks/useCycle';
export { default as useHighlighting } from './hooks/useHighlighting'; export { default as useHighlighting } from './hooks/useHighlighting';
export { default as usePostMessage } from './hooks/usePostMessage'; export { default as usePostMessage } from './hooks/usePostMessage';
export { default as useStrudel } from './hooks/useStrudel'; export { default as useRepl } from './hooks/useRepl';
export { default as useKeydown } from './hooks/useKeydown';
export { default as cx } from './cx'; export { default as cx } from './cx';
export { useWebMidi } from './hooks/useWebMidi';
+8 -16
View File
@@ -8,36 +8,28 @@ export default createTheme({
caret: '#ffcc00', caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)', selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626', selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original lineHighlight: '#8a91991a',
lineHighlight: '#00000050',
gutterBackground: 'transparent', gutterBackground: 'transparent',
// gutterForeground: '#8a919966', // gutterForeground: '#8a919966',
gutterForeground: '#8a919966', gutterForeground: '#676e95',
}, },
styles: [ styles: [
{ 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' },
// { tag: t.typeName, color: '#f07178' }, // original { tag: t.typeName, color: '#f07178' },
{ tag: t.typeName, color: '#c3e88d' },
{ tag: t.atom, color: '#f78c6c' }, { tag: t.atom, color: '#f78c6c' },
// { tag: t.number, color: '#ff5370' }, // original { tag: t.number, color: '#ff5370' },
{ tag: t.number, color: '#c3e88d' },
{ tag: t.definition(t.variableName), color: '#82aaff' }, { tag: t.definition(t.variableName), color: '#82aaff' },
{ tag: t.string, color: '#c3e88d' }, { tag: t.string, color: '#c3e88d' },
// { tag: t.special(t.string), color: '#f07178' }, // original { tag: t.special(t.string), color: '#f07178' },
{ tag: t.special(t.string), color: '#c3e88d' },
{ tag: t.comment, color: '#7d8799' }, { tag: t.comment, color: '#7d8799' },
// { tag: t.variableName, color: '#f07178' }, // original { tag: t.variableName, color: '#f07178' },
{ tag: t.variableName, color: '#c792ea' }, { tag: t.tagName, color: '#ff5370' },
// { tag: t.tagName, color: '#ff5370' }, // original { tag: t.bracket, color: '#a2a1a4' },
{ tag: t.tagName, color: '#c3e88d' },
{ tag: t.bracket, color: '#525154' },
// { tag: t.bracket, color: '#a2a1a4' }, // original
{ tag: t.meta, color: '#ffcb6b' }, { tag: t.meta, color: '#ffcb6b' },
{ tag: t.attributeName, color: '#c792ea' }, { tag: t.attributeName, color: '#c792ea' },
{ tag: t.propertyName, color: '#c792ea' }, { tag: t.propertyName, color: '#c792ea' },
{ tag: t.className, color: '#decb6b' }, { tag: t.className, color: '#decb6b' },
{ tag: t.invalid, color: '#ffffff' }, { tag: t.invalid, color: '#ffffff' },
], ],

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