mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 21:23:21 -04:00
Compare commits
154 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f57060708 | |||
| 1507724ecd | |||
| 4bc2f64b24 | |||
| 839ba31dfb | |||
| 2341452d91 | |||
| 7ab263b514 | |||
| 45b5b5960f | |||
| 1b8ec1f01c | |||
| c92cb1c096 | |||
| 8f31fbe275 | |||
| aa76419eda | |||
| 21503c0c38 | |||
| d957b19f55 | |||
| 122c4242ce | |||
| e769ed2fd8 | |||
| c722904831 | |||
| c93faa9564 | |||
| ce6495a46d | |||
| 04f2b905af | |||
| b668a2c0d2 | |||
| 5fc8f10602 | |||
| a27f399a9e | |||
| ff5d14fb63 | |||
| 27fb2d2d5b | |||
| 0b9d59bf0e | |||
| 23e059a065 | |||
| 45c7b29a96 | |||
| 3e6c3fda60 | |||
| 1a1363ef77 | |||
| b5c51e231b | |||
| 22711dd7be | |||
| 1ba4d3d5d1 | |||
| 823ab73585 | |||
| 66c444efac | |||
| b688b8921a | |||
| 00550ddab5 | |||
| 42fedec088 | |||
| ff3bc0e8c2 | |||
| dde7dff870 | |||
| 25ffc9cf96 | |||
| eca92cf5b5 | |||
| 372bdf8be7 | |||
| c7c90b0647 | |||
| 14c2da4fa2 | |||
| c99d957bc8 | |||
| 51b14d55ec | |||
| f92d9994df | |||
| 431e1b87bd | |||
| 3ec2fc6030 | |||
| 99f416bf6b | |||
| 89acc4870b | |||
| 314c739c3a | |||
| 30b18fc997 | |||
| 168ac60853 | |||
| dff0c0c3b0 | |||
| ea3e9de7ab | |||
| c93fd900bc | |||
| a761aa9a37 | |||
| f37eb92d93 | |||
| 2c7cc5d325 | |||
| 16659c82d2 | |||
| 4ed3ca4128 | |||
| 9b2899c8fb | |||
| fada7fc9cc | |||
| 2be5bca42b | |||
| b1ebf6367c | |||
| 57e373f578 | |||
| 5c01874a32 | |||
| dd89e11633 | |||
| 4e51f19780 | |||
| 33d37d8dee | |||
| 60ce34af43 | |||
| 2602ff6e34 | |||
| ef5cd36a37 | |||
| 7d47f5aba3 | |||
| 33cb506c71 | |||
| 5fb36acb04 | |||
| 5f985f359b | |||
| 09045d5387 | |||
| 371af755df | |||
| 0485632e22 | |||
| c3d6de2412 | |||
| 36d49b11d9 | |||
| 866277510e | |||
| da8e00eb4e | |||
| cd30b2af3f | |||
| 38b3337d35 | |||
| 84d34aa12b | |||
| 96b7b68900 | |||
| 2a6ffbf5a4 | |||
| 1ac99e260b | |||
| 519062c8d0 | |||
| 78d7cdf662 | |||
| a191b1e811 | |||
| b372161874 | |||
| 5bbec7b17c | |||
| 54b847a8ef | |||
| a40a0f730f | |||
| a677b94297 | |||
| e5f38dbbc6 | |||
| 5534db95bc | |||
| 37c45424fd | |||
| ed44f4ed0c | |||
| 2168dbc6fc | |||
| 97db895354 | |||
| 48fc68bfdb | |||
| 566c358e8d | |||
| 0fd851999f | |||
| 50f88798d8 | |||
| 8033cfc49d | |||
| d45c380389 | |||
| 7e7e299bce | |||
| e715e2ba2f | |||
| 63c6a9735c | |||
| 0c1529237c | |||
| 4f8a2a0900 | |||
| 9a2a26e904 | |||
| 067c3433b6 | |||
| d0b07c4890 | |||
| 24f8f48a2f | |||
| acafcc9888 | |||
| ec4cfe6f86 | |||
| 7b0c80c8be | |||
| 29029714ee | |||
| cd1203d1e2 | |||
| 4dfc34515c | |||
| 11a9f3ced3 | |||
| 5be1a7000c | |||
| 9059c1b850 | |||
| ec60fe42be | |||
| 275931969e | |||
| e37e0f189a | |||
| 8103943396 | |||
| 9497c99e9e | |||
| 2172d1a767 | |||
| e72c47869a | |||
| 5ea2124e91 | |||
| 0c08919bfa | |||
| a7562c3803 | |||
| 5f4d8f6862 | |||
| 9c7d0951b7 | |||
| 75e57c75cf | |||
| 04502b05d4 | |||
| e3e3a8404a | |||
| 48bf17614f | |||
| 1941b1ff65 | |||
| 46aa3d275a | |||
| d667e790b9 | |||
| bc345b569b | |||
| ee84b4e013 | |||
| e77129fa51 | |||
| 591855919e | |||
| 7af6188a8a | |||
| 1cd31cfce9 |
+1
-1
@@ -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"
|
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
|
||||||
},
|
},
|
||||||
"plugins": ["plugins/markdown"],
|
"plugins": ["plugins/markdown"],
|
||||||
"opts": {
|
"opts": {
|
||||||
|
|||||||
Generated
+3210
-3169
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
examples
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
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('')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Tone } from './tone.mjs';
|
import { Pattern, getTime } from './index.mjs';
|
||||||
import { Pattern } from '@strudel.cycles/core';
|
|
||||||
|
|
||||||
export const getDrawContext = (id = 'test-canvas') => {
|
export const getDrawContext = (id = 'test-canvas') => {
|
||||||
let canvas = document.querySelector('#' + id);
|
let canvas = document.querySelector('#' + id);
|
||||||
@@ -28,7 +27,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
|||||||
let cycle,
|
let cycle,
|
||||||
events = [];
|
events = [];
|
||||||
const animate = (time) => {
|
const animate = (time) => {
|
||||||
const t = Tone.getTransport().seconds;
|
const t = getTime();
|
||||||
if (from !== undefined && to !== undefined) {
|
if (from !== undefined && to !== undefined) {
|
||||||
const currentCycle = Math.floor(t);
|
const currentCycle = Math.floor(t);
|
||||||
if (cycle !== currentCycle) {
|
if (cycle !== currentCycle) {
|
||||||
@@ -50,9 +49,9 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
|||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanupDraw = () => {
|
export const cleanupDraw = (clearScreen = true) => {
|
||||||
const ctx = getDrawContext();
|
const ctx = getDrawContext();
|
||||||
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||||
if (window.strudelAnimation) {
|
if (window.strudelAnimation) {
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
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 };
|
||||||
|
};
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<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()
|
||||||
|
-->
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
!dist
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# 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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
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
@@ -0,0 +1 @@
|
|||||||
|
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};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
@@ -0,0 +1,885 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/* 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"))`;
|
||||||
@@ -55,7 +55,8 @@ Fraction.prototype.min = function (other) {
|
|||||||
return this.lt(other) ? this : other;
|
return this.lt(other) ? this : other;
|
||||||
};
|
};
|
||||||
|
|
||||||
Fraction.prototype.show = function () {
|
Fraction.prototype.show = function (/* excludeWhole = false */) {
|
||||||
|
// return this.toFraction(excludeWhole);
|
||||||
return this.s * this.n + '/' + this.d;
|
return this.s * this.n + '/' + this.d;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -85,9 +85,13 @@ export class Hap {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
showWhole() {
|
showWhole(compact = false) {
|
||||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
|
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
|
||||||
typeof this.value === 'object' ? JSON.stringify(this.value) : this.value
|
typeof this.value === 'object'
|
||||||
|
? compact
|
||||||
|
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||||
|
: JSON.stringify(this.value)
|
||||||
|
: this.value
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import controls from './controls.mjs';
|
import controls 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, controls };
|
export { Fraction, controls };
|
||||||
export * from './hap.mjs';
|
export * from './hap.mjs';
|
||||||
export * from './pattern.mjs';
|
export * from './pattern.mjs';
|
||||||
@@ -15,15 +16,19 @@ 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 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; */
|
||||||
console.log(
|
logger('🌀 @strudel.cycles/core loaded 🌀');
|
||||||
'%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...
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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;
|
||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/core",
|
"name": "@strudel.cycles/core",
|
||||||
"version": "0.2.0",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/core",
|
"name": "@strudel.cycles/core",
|
||||||
"version": "0.2.0",
|
"version": "0.4.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",
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ import { unionWithObj } from './value.mjs';
|
|||||||
|
|
||||||
import { compose, removeUndefineds, flatten, id, listRange, curry, mod, numeralArgs, parseNumeral } from './util.mjs';
|
import { compose, removeUndefineds, flatten, id, listRange, curry, mod, numeralArgs, parseNumeral } from './util.mjs';
|
||||||
import drawLine from './drawLine.mjs';
|
import drawLine from './drawLine.mjs';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
|
let stringParser;
|
||||||
|
// parser is expected to turn a string into a pattern
|
||||||
|
// if set, the reify function will parse all strings with it
|
||||||
|
// intended to use with mini to automatically interpret all strings as mini notation
|
||||||
|
export const setStringParser = (parser) => (stringParser = parser);
|
||||||
|
|
||||||
/** @class Class representing a pattern. */
|
/** @class Class representing a pattern. */
|
||||||
export class Pattern {
|
export class Pattern {
|
||||||
@@ -1322,22 +1329,25 @@ export class Pattern {
|
|||||||
.unit('c')
|
.unit('c')
|
||||||
.slow(factor);
|
.slow(factor);
|
||||||
}
|
}
|
||||||
onTrigger(onTrigger) {
|
onTrigger(onTrigger, dominant = true) {
|
||||||
return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger }));
|
|
||||||
}
|
|
||||||
log(func = id) {
|
|
||||||
return this._withHap((hap) =>
|
return this._withHap((hap) =>
|
||||||
hap.setContext({
|
hap.setContext({
|
||||||
...hap.context,
|
...hap.context,
|
||||||
onTrigger: (...args) => {
|
onTrigger: (...args) => {
|
||||||
if (hap.context.onTrigger) {
|
if (!dominant && hap.context.onTrigger) {
|
||||||
hap.context.onTrigger(...args);
|
hap.context.onTrigger(...args);
|
||||||
}
|
}
|
||||||
console.log(func(...args));
|
onTrigger(...args);
|
||||||
},
|
},
|
||||||
|
// we need this to know later if the default trigger should still fire
|
||||||
|
dominantTrigger: dominant,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`) {
|
||||||
|
return this.onTrigger((...args) => logger(func(...args)), false);
|
||||||
|
}
|
||||||
logValues(func = id) {
|
logValues(func = id) {
|
||||||
return this.log((_, hap) => func(hap.value));
|
return this.log((_, hap) => func(hap.value));
|
||||||
}
|
}
|
||||||
@@ -1561,6 +1571,9 @@ export function reify(thing) {
|
|||||||
if (isPattern(thing)) {
|
if (isPattern(thing)) {
|
||||||
return thing;
|
return thing;
|
||||||
}
|
}
|
||||||
|
if (stringParser && typeof thing === 'string') {
|
||||||
|
return stringParser(thing);
|
||||||
|
}
|
||||||
return pure(thing);
|
return pure(thing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern } from '@strudel.cycles/core';
|
import { Pattern } from './index.mjs';
|
||||||
|
|
||||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||||
const getValue = (e) => {
|
const getValue = (e) => {
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
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 };
|
||||||
|
}
|
||||||
@@ -32,11 +32,8 @@ function speak(words, lang, voice) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype._speak = function (lang, voice) {
|
Pattern.prototype._speak = function (lang, voice) {
|
||||||
return this._withHap((hap) => {
|
return this.onTrigger((_, hap) => {
|
||||||
const onTrigger = (time, hap) => {
|
speak(hap.value, lang, voice);
|
||||||
speak(hap.value, lang, voice);
|
|
||||||
};
|
|
||||||
return hap.setContext({ ...hap.context, onTrigger });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -4,19 +4,14 @@ 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 { Tone } from './tone.mjs';
|
import { getTime } from './time.mjs';
|
||||||
|
|
||||||
export const hideHeader = () => {
|
|
||||||
document.getElementById('header').style = 'display:none';
|
|
||||||
};
|
|
||||||
|
|
||||||
function frame(callback) {
|
function frame(callback) {
|
||||||
if (window.strudelAnimation) {
|
if (window.strudelAnimation) {
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
}
|
}
|
||||||
const animate = (animationTime) => {
|
const animate = (animationTime) => {
|
||||||
const toneTime = Tone.getTransport().seconds;
|
callback(animationTime, getTime());
|
||||||
callback(animationTime, toneTime);
|
|
||||||
window.strudelAnimation = requestAnimationFrame(animate);
|
window.strudelAnimation = requestAnimationFrame(animate);
|
||||||
};
|
};
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
@@ -51,6 +46,7 @@ export const cleanupUi = () => {
|
|||||||
const container = document.getElementById('code');
|
const container = document.getElementById('code');
|
||||||
if (container) {
|
if (container) {
|
||||||
container.style = '';
|
container.style = '';
|
||||||
container.className = 'grow relative'; // has to match App.tsx
|
// TODO: find a way to remove that duplication..
|
||||||
|
container.className = 'grow flex text-gray-100 relative overflow-auto cursor-text pb-0'; // has to match App.tsx
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
+10
-3
@@ -55,9 +55,13 @@ 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: note, context } = hap;
|
let { value, 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') {
|
||||||
@@ -73,8 +77,11 @@ 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' && value.freq) {
|
if (typeof value === 'object') {
|
||||||
return value.freq;
|
if (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);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// 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;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/embed",
|
"name": "@strudel.cycles/embed",
|
||||||
"version": "0.1.1",
|
"version": "0.2.0",
|
||||||
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
||||||
"main": "embed.js",
|
"main": "embed.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ npm i @strudel.cycles/eval --save
|
|||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
<!-- TODO: -extend +evalScope -->
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { evaluate, extend } from '@strudel.cycles/eval';
|
import { evalScope } from '@strudel.cycles/core';
|
||||||
|
import { evaluate } from '@strudel.cycles/eval';
|
||||||
|
|
||||||
evalScope(
|
evalScope(
|
||||||
import('@strudel.cycles/core'),
|
import('@strudel.cycles/core'),
|
||||||
|
|||||||
@@ -4,46 +4,9 @@ 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) => {
|
||||||
if (!scoped) {
|
return _evaluate(code, shapeshifter);
|
||||||
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 };
|
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/eval",
|
"name": "@strudel.cycles/eval",
|
||||||
"version": "0.2.0",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/eval",
|
"name": "@strudel.cycles/eval",
|
||||||
"version": "0.2.0",
|
"version": "0.4.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.2.0",
|
"@strudel.cycles/core": "^0.4.0",
|
||||||
"estraverse": "^5.3.0",
|
"estraverse": "^5.3.0",
|
||||||
"shift-ast": "^6.1.0",
|
"shift-ast": "^7.0.0",
|
||||||
"shift-codegen": "^7.0.3",
|
"shift-codegen": "^8.1.0",
|
||||||
"shift-parser": "^7.0.3",
|
"shift-parser": "^8.0.0",
|
||||||
"shift-spec": "^2018.0.2",
|
"shift-spec": "^2019.0.0",
|
||||||
"shift-traverser": "^1.0.0"
|
"shift-traverser": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ 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 = true;
|
export const wrappedAsync = false; // this is now handled by core evaluate by default
|
||||||
|
export const shouldAddReturn = true;
|
||||||
|
|
||||||
export default (_code) => {
|
export default (_code) => {
|
||||||
const { code, addReturn } = wrapAsync(_code);
|
const { code, addReturn } = wrapAsync(_code);
|
||||||
@@ -125,7 +126,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 (wrappedAsync) {
|
if (shouldAddReturn) {
|
||||||
addReturn(shifted);
|
addReturn(shifted);
|
||||||
}
|
}
|
||||||
const generated = undisguiseImports(codegen(shifted));
|
const generated = undisguiseImports(codegen(shifted));
|
||||||
@@ -153,7 +154,7 @@ ${code}
|
|||||||
})()`;
|
})()`;
|
||||||
}
|
}
|
||||||
const addReturn = (ast) => {
|
const addReturn = (ast) => {
|
||||||
const body = ast.statements[0].expression.callee.body; // actual code ast inside async function body
|
const body = wrappedAsync ? ast.statements[0].expression.callee.body : ast;
|
||||||
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] })]);
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ 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, evalScope } from '../evaluate.mjs';
|
import { evaluate } 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 } = strudel;
|
const { fastcat, evalScope } = strudel;
|
||||||
|
|
||||||
describe('evaluate', async () => {
|
describe('evaluate', async () => {
|
||||||
await evalScope({ mini }, strudel);
|
await evalScope({ mini }, strudel);
|
||||||
|
|||||||
@@ -5,15 +5,21 @@ 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 from '../shapeshifter.mjs';
|
import shapeshifter, { wrappedAsync } from '../shapeshifter.mjs';
|
||||||
|
|
||||||
describe('shapeshifter', () => {
|
describe('shapeshifter', () => {
|
||||||
it('Should shift simple double quote string', () => {
|
it('Should shift simple double quote string', () => {
|
||||||
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
|
if (wrappedAsync) {
|
||||||
});
|
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
|
||||||
it('Should handle dynamic imports', () => {
|
} else {
|
||||||
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
|
expect(shapeshifter('"c3"')).toEqual('return mini("c3").withMiniLocation([1,0,0],[1,4,4])');
|
||||||
'(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])',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+70
-48
@@ -4,15 +4,19 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isNote } from 'tone';
|
import * as _WebMidi from 'webmidi';
|
||||||
import _WebMidi from 'webmidi';
|
import { Pattern, isPattern, isNote, getPlayableNoteValue, logger } from '@strudel.cycles/core';
|
||||||
import { Pattern, isPattern } from '@strudel.cycles/core';
|
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||||
import { Tone } from '@strudel.cycles/tone';
|
|
||||||
|
|
||||||
// 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() {
|
export function enableWebMidi(options = {}) {
|
||||||
|
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
|
||||||
@@ -23,6 +27,14 @@ export function enableWebMidi() {
|
|||||||
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -31,7 +43,21 @@ export function enableWebMidi() {
|
|||||||
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 = function (output, channel = 1) {
|
Pattern.prototype.midi = async function (output, channel = 1) {
|
||||||
|
await enableWebMidi({
|
||||||
|
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)) {
|
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('${
|
||||||
@@ -39,46 +65,42 @@ Pattern.prototype.midi = function (output, channel = 1) {
|
|||||||
}')`,
|
}')`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this._withHap((hap) => {
|
return this.onTrigger((time, hap) => {
|
||||||
// const onTrigger = (time: number, hap: any) => {
|
let note = getPlayableNoteValue(hap);
|
||||||
const onTrigger = (time, hap) => {
|
const velocity = hap.context?.velocity ?? 0.9;
|
||||||
let note = hap.value;
|
if (!isNote(note)) {
|
||||||
const velocity = hap.context?.velocity ?? 0.9;
|
throw new Error('not a note: ' + note);
|
||||||
if (!isNote(note)) {
|
}
|
||||||
throw new Error('not a note: ' + note);
|
if (!WebMidi.enabled) {
|
||||||
}
|
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||||
if (!WebMidi.enabled) {
|
}
|
||||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
if (!WebMidi.outputs.length) {
|
||||||
}
|
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||||
if (!WebMidi.outputs.length) {
|
}
|
||||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
let device;
|
||||||
}
|
if (typeof output === 'number') {
|
||||||
let device;
|
device = WebMidi.outputs[output];
|
||||||
if (typeof output === 'number') {
|
} else if (typeof output === 'string') {
|
||||||
device = WebMidi.outputs[output];
|
device = outputByName(output);
|
||||||
} else if (typeof output === 'string') {
|
} else {
|
||||||
device = outputByName(output);
|
device = WebMidi.outputs[0];
|
||||||
} else {
|
}
|
||||||
device = WebMidi.outputs[0];
|
if (!device) {
|
||||||
}
|
throw new Error(
|
||||||
if (!device) {
|
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||||
throw new Error(
|
.map((o) => `'${o.name}'`)
|
||||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
.join(' | ')}`,
|
||||||
.map((o) => `'${o.name}'`)
|
);
|
||||||
.join(' | ')}`,
|
}
|
||||||
);
|
// console.log('midi', value, output);
|
||||||
}
|
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
|
||||||
// console.log('midi', value, output);
|
time = time * 1000 + timingOffset;
|
||||||
const timingOffset = WebMidi.time - Tone.getContext().currentTime * 1000;
|
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
||||||
time = time * 1000 + timingOffset;
|
// await enableWebMidi()
|
||||||
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
device.playNote(note, channel, {
|
||||||
// await enableWebMidi()
|
time,
|
||||||
device.playNote(note, channel, {
|
duration: hap.duration.valueOf() * 1000 - 5,
|
||||||
time,
|
attack: velocity,
|
||||||
duration: hap.duration.valueOf() * 1000 - 5,
|
});
|
||||||
velocity,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return hap.setContext({ ...hap.context, onTrigger });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/midi",
|
"name": "@strudel.cycles/midi",
|
||||||
"version": "0.2.0",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/midi",
|
"name": "@strudel.cycles/midi",
|
||||||
"version": "0.2.0",
|
"version": "0.4.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.2.0",
|
"@strudel.cycles/tone": "^0.4.0",
|
||||||
"tone": "^14.7.77",
|
"tone": "^14.7.77",
|
||||||
"webmidi": "^2.5.2"
|
"webmidi": "^3.0.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function peg$padEnd(str, targetLength, padString) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
peg$SyntaxError.prototype.format = function(sources) {
|
peg$SyntaxError.prototype.format = function(sources) {
|
||||||
var str = "Error: " + this.message;
|
var str = "peg error: " + this.message;
|
||||||
if (this.location) {
|
if (this.location) {
|
||||||
var src = null;
|
var src = null;
|
||||||
var k;
|
var k;
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/mini",
|
"name": "@strudel.cycles/mini",
|
||||||
"version": "0.2.0",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/mini",
|
"name": "@strudel.cycles/mini",
|
||||||
"version": "0.2.0",
|
"version": "0.4.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.2.0",
|
"@strudel.cycles/core": "^0.4.0",
|
||||||
"@strudel.cycles/eval": "^0.2.0",
|
"@strudel.cycles/eval": "^0.4.0",
|
||||||
"@strudel.cycles/tone": "^0.2.0"
|
"@strudel.cycles/tone": "^0.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"peggy": "^2.0.1"
|
"peggy": "^2.0.1"
|
||||||
|
|||||||
+47
-26
@@ -5,10 +5,34 @@ 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 { parseNumeral, Pattern } from '@strudel.cycles/core';
|
import { logger, parseNumeral, 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;
|
||||||
|
|
||||||
@@ -20,28 +44,25 @@ let startedAt = -1;
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.osc = function () {
|
Pattern.prototype.osc = async function () {
|
||||||
return this._withHap((hap) => {
|
const osc = await connect();
|
||||||
const onTrigger = (time, hap, currentTime, cps) => {
|
return this.onTrigger((time, hap, currentTime, cps = 1) => {
|
||||||
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
|
// make sure n and note are numbers
|
||||||
controls.n && (controls.n = parseNumeral(controls.n));
|
controls.n && (controls.n = parseNumeral(controls.n));
|
||||||
controls.note && (controls.note = parseNumeral(controls.note));
|
controls.note && (controls.note = parseNumeral(controls.note));
|
||||||
|
const keyvals = Object.entries(controls).flat();
|
||||||
const keyvals = Object.entries(controls).flat();
|
const ts = Math.floor(startedAt + (time + latency) * 1000);
|
||||||
const ts = Math.floor(startedAt + (time + latency) * 1000);
|
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
const bundle = new OSC.Bundle([message], ts);
|
||||||
const bundle = new OSC.Bundle([message], ts);
|
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
osc.send(bundle);
|
||||||
comm.send(bundle);
|
|
||||||
};
|
|
||||||
return hap.setContext({ ...hap.context, onTrigger });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/osc",
|
"name": "@strudel.cycles/osc",
|
||||||
"version": "0.1.1",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/osc",
|
"name": "@strudel.cycles/osc",
|
||||||
"version": "0.1.1",
|
"version": "0.3.0",
|
||||||
"description": "OSC messaging for strudel",
|
"description": "OSC messaging for strudel",
|
||||||
"main": "osc.mjs",
|
"main": "osc.mjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -30,6 +30,6 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"osc-js": "^2.3.2"
|
"osc-js": "^2.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
examples
|
||||||
@@ -13,9 +13,8 @@ npm i @strudel.cycles/react
|
|||||||
Here is a minimal example of how to set up a MiniRepl:
|
Here is a minimal example of how to set up a MiniRepl:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
import { evalScope } from '@strudel.cycles/eval';
|
import { evalScope, controls } from '@strudel.cycles/core';
|
||||||
import { MiniRepl } from '@strudel.cycles/react';
|
import { MiniRepl } from '@strudel.cycles/react';
|
||||||
import controls from '@strudel.cycles/core/controls.mjs';
|
|
||||||
import { prebake } from '../repl/src/prebake.mjs';
|
import { prebake } from '../repl/src/prebake.mjs';
|
||||||
|
|
||||||
evalScope(
|
evalScope(
|
||||||
|
|||||||
Vendored
+1
-3
File diff suppressed because one or more lines are too long
Vendored
+254
-526
@@ -1,582 +1,310 @@
|
|||||||
import React, { useCallback, useState, useEffect, useMemo, useRef, useLayoutEffect } from 'react';
|
import n, { useCallback as N, useRef as x, useEffect as R, useState as w, useMemo as I, useLayoutEffect as K } from "react";
|
||||||
import _CodeMirror from '@uiw/react-codemirror';
|
import Q from "@uiw/react-codemirror";
|
||||||
import { Decoration, EditorView } from '@codemirror/view';
|
import { Decoration as E, EditorView as O } from "@codemirror/view";
|
||||||
import { StateEffect, StateField } from '@codemirror/state';
|
import { StateEffect as j, StateField as U } from "@codemirror/state";
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript as W } from "@codemirror/lang-javascript";
|
||||||
import { tags } from '@lezer/highlight';
|
import { tags as r } from "@lezer/highlight";
|
||||||
import { createTheme } from '@uiw/codemirror-themes';
|
import { createTheme as X } from "@uiw/codemirror-themes";
|
||||||
import { useInView } from 'react-hook-inview';
|
import { useInView as Y } from "react-hook-inview";
|
||||||
import { evaluate } from '@strudel.cycles/eval';
|
import { webaudioOutput as Z, getAudioContext as ee } from "@strudel.cycles/webaudio";
|
||||||
import { Tone } from '@strudel.cycles/tone';
|
import { repl as te } from "@strudel.cycles/core";
|
||||||
import { TimeSpan, State } from '@strudel.cycles/core';
|
import { transpiler as re } from "@strudel.cycles/transpiler";
|
||||||
import { webaudioOutputTrigger } from '@strudel.cycles/webaudio';
|
const oe = X({
|
||||||
import { WebMidi, enableWebMidi } from '@strudel.cycles/midi';
|
theme: "dark",
|
||||||
|
|
||||||
var strudelTheme = createTheme({
|
|
||||||
theme: 'dark',
|
|
||||||
settings: {
|
settings: {
|
||||||
background: '#222',
|
background: "#222",
|
||||||
foreground: '#75baff', // whats that?
|
foreground: "#75baff",
|
||||||
caret: '#ffcc00',
|
caret: "#ffcc00",
|
||||||
selection: 'rgba(128, 203, 196, 0.5)',
|
selection: "rgba(128, 203, 196, 0.5)",
|
||||||
selectionMatch: '#036dd626',
|
selectionMatch: "#036dd626",
|
||||||
lineHighlight: '#8a91991a',
|
lineHighlight: "#00000050",
|
||||||
gutterBackground: 'transparent',
|
gutterBackground: "transparent",
|
||||||
// gutterForeground: '#8a919966',
|
gutterForeground: "#8a919966"
|
||||||
gutterForeground: '#676e95',
|
|
||||||
},
|
},
|
||||||
styles: [
|
styles: [
|
||||||
{ tag: tags.keyword, color: '#c792ea' },
|
{ tag: r.keyword, color: "#c792ea" },
|
||||||
{ tag: tags.operator, color: '#89ddff' },
|
{ tag: r.operator, color: "#89ddff" },
|
||||||
{ tag: tags.special(tags.variableName), color: '#eeffff' },
|
{ tag: r.special(r.variableName), color: "#eeffff" },
|
||||||
{ tag: tags.typeName, color: '#f07178' },
|
{ tag: r.typeName, color: "#c3e88d" },
|
||||||
{ tag: tags.atom, color: '#f78c6c' },
|
{ tag: r.atom, color: "#f78c6c" },
|
||||||
{ tag: tags.number, color: '#ff5370' },
|
{ tag: r.number, color: "#c3e88d" },
|
||||||
{ tag: tags.definition(tags.variableName), color: '#82aaff' },
|
{ tag: r.definition(r.variableName), color: "#82aaff" },
|
||||||
{ tag: tags.string, color: '#c3e88d' },
|
{ tag: r.string, color: "#c3e88d" },
|
||||||
{ tag: tags.special(tags.string), color: '#f07178' },
|
{ tag: r.special(r.string), color: "#c3e88d" },
|
||||||
{ tag: tags.comment, color: '#7d8799' },
|
{ tag: r.comment, color: "#7d8799" },
|
||||||
{ tag: tags.variableName, color: '#f07178' },
|
{ tag: r.variableName, color: "#c792ea" },
|
||||||
{ tag: tags.tagName, color: '#ff5370' },
|
{ tag: r.tagName, color: "#c3e88d" },
|
||||||
{ tag: tags.bracket, color: '#a2a1a4' },
|
{ tag: r.bracket, color: "#525154" },
|
||||||
{ tag: tags.meta, color: '#ffcb6b' },
|
{ tag: r.meta, color: "#ffcb6b" },
|
||||||
{ tag: tags.attributeName, color: '#c792ea' },
|
{ tag: r.attributeName, color: "#c792ea" },
|
||||||
{ tag: tags.propertyName, color: '#c792ea' },
|
{ tag: r.propertyName, color: "#c792ea" },
|
||||||
{ tag: tags.className, color: '#decb6b' },
|
{ tag: r.className, color: "#decb6b" },
|
||||||
{ tag: tags.invalid, color: '#ffffff' },
|
{ tag: r.invalid, color: "#ffffff" }
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
|
const T = j.define(), ne = U.define({
|
||||||
var style = '';
|
|
||||||
|
|
||||||
const setFlash = StateEffect.define();
|
|
||||||
const flashField = StateField.define({
|
|
||||||
create() {
|
create() {
|
||||||
return Decoration.none;
|
return E.none;
|
||||||
},
|
},
|
||||||
update(flash2, tr) {
|
update(e, t) {
|
||||||
try {
|
try {
|
||||||
for (let e of tr.effects) {
|
for (let o of t.effects)
|
||||||
if (e.is(setFlash)) {
|
if (o.is(T))
|
||||||
if (e.value) {
|
if (o.value) {
|
||||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } });
|
||||||
flash2 = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
e = E.set([a.range(0, t.newDoc.length)]);
|
||||||
} else {
|
} else
|
||||||
flash2 = Decoration.set([]);
|
e = E.set([]);
|
||||||
}
|
return e;
|
||||||
}
|
} catch (o) {
|
||||||
}
|
return console.warn("flash error", o), e;
|
||||||
return flash2;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("flash error", err);
|
|
||||||
return flash2;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
provide: (f) => EditorView.decorations.from(f)
|
provide: (e) => O.decorations.from(e)
|
||||||
});
|
}), ae = (e) => {
|
||||||
const flash = (view) => {
|
e.dispatch({ effects: T.of(!0) }), setTimeout(() => {
|
||||||
view.dispatch({ effects: setFlash.of(true) });
|
e.dispatch({ effects: T.of(!1) });
|
||||||
setTimeout(() => {
|
|
||||||
view.dispatch({ effects: setFlash.of(false) });
|
|
||||||
}, 200);
|
}, 200);
|
||||||
};
|
}, A = j.define(), se = U.define({
|
||||||
const setHighlights = StateEffect.define();
|
|
||||||
const highlightField = StateField.define({
|
|
||||||
create() {
|
create() {
|
||||||
return Decoration.none;
|
return E.none;
|
||||||
},
|
},
|
||||||
update(highlights, tr) {
|
update(e, t) {
|
||||||
try {
|
try {
|
||||||
for (let e of tr.effects) {
|
for (let o of t.effects)
|
||||||
if (e.is(setHighlights)) {
|
if (o.is(A)) {
|
||||||
const marks = e.value.map(
|
const a = o.value.map(
|
||||||
(hap) => (hap.context.locations || []).map(({ start, end }) => {
|
(s) => (s.context.locations || []).map(({ start: m, end: l }) => {
|
||||||
const color = hap.context.color || "#FFCA28";
|
const d = s.context.color || "#FFCA28";
|
||||||
let from = tr.newDoc.line(start.line).from + start.column;
|
let c = t.newDoc.line(m.line).from + m.column, i = t.newDoc.line(l.line).from + l.column;
|
||||||
let to = tr.newDoc.line(end.line).from + end.column;
|
const g = t.newDoc.length;
|
||||||
const l = tr.newDoc.length;
|
return c > g || i > g ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${d};` } }).range(c, i);
|
||||||
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) || [];
|
||||||
highlights = Decoration.set(marks, true);
|
e = E.set(a, !0);
|
||||||
}
|
}
|
||||||
}
|
return e;
|
||||||
return highlights;
|
} catch {
|
||||||
} catch (err) {
|
return E.set([]);
|
||||||
return Decoration.set([]);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
provide: (f) => EditorView.decorations.from(f)
|
provide: (e) => O.decorations.from(e)
|
||||||
});
|
}), ce = [W(), oe, se, ne];
|
||||||
const extensions = [javascript(), strudelTheme, highlightField, flashField];
|
function ie({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: m }) {
|
||||||
function CodeMirror({ value, onChange, onViewChanged, onSelectionChange, options, editorDidMount }) {
|
const l = N(
|
||||||
const handleOnChange = useCallback(
|
(i) => {
|
||||||
(value2) => {
|
t?.(i);
|
||||||
onChange?.(value2);
|
|
||||||
},
|
},
|
||||||
[onChange]
|
[t]
|
||||||
);
|
), d = N(
|
||||||
const handleOnCreateEditor = useCallback(
|
(i) => {
|
||||||
(view) => {
|
o?.(i);
|
||||||
onViewChanged?.(view);
|
|
||||||
},
|
},
|
||||||
[onViewChanged]
|
[o]
|
||||||
);
|
), c = N(
|
||||||
const handleOnUpdate = useCallback(
|
(i) => {
|
||||||
(viewUpdate) => {
|
i.selectionSet && a && a?.(i.state.selection);
|
||||||
if (viewUpdate.selectionSet && onSelectionChange) {
|
|
||||||
onSelectionChange?.(viewUpdate.state.selection);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[onSelectionChange]
|
[a]
|
||||||
);
|
);
|
||||||
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(_CodeMirror, {
|
return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(Q, {
|
||||||
value,
|
value: e,
|
||||||
onChange: handleOnChange,
|
onChange: l,
|
||||||
onCreateEditor: handleOnCreateEditor,
|
onCreateEditor: d,
|
||||||
onUpdate: handleOnUpdate,
|
onUpdate: c,
|
||||||
extensions
|
extensions: ce
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
function B(...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 le({ view: e, pattern: t, active: o, getTime: a }) {
|
||||||
/*
|
const s = x([]), m = x();
|
||||||
usePostMessage.mjs - <short description TODO>
|
R(() => {
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/usePostMessage.mjs>
|
if (e)
|
||||||
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/>.
|
if (t && o) {
|
||||||
*/
|
let d = function() {
|
||||||
|
|
||||||
function usePostMessage(listener) {
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener('message', listener);
|
|
||||||
return () => window.removeEventListener('message', listener);
|
|
||||||
}, [listener]);
|
|
||||||
return useCallback((data) => window.postMessage(data, '*'), []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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), []),
|
|
||||||
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 {
|
try {
|
||||||
const audioTime = Tone.getTransport().seconds;
|
const c = a(), g = [Math.max(m.current || c, c - 1 / 10, 0), c + 1 / 60];
|
||||||
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
|
m.current = g[1], s.current = s.current.filter((p) => p.whole.end > c);
|
||||||
// see https://github.com/tidalcycles/strudel/issues/108
|
const v = t.queryArc(...g).filter((p) => p.hasOnset());
|
||||||
const begin = Math.max(lastEnd || audioTime, audioTime - 1 / 10);
|
s.current = s.current.concat(v), e.dispatch({ effects: A.of(s.current) });
|
||||||
const span = [begin, audioTime + 1 / 60];
|
} catch {
|
||||||
lastEnd = audioTime + 1 / 60;
|
e.dispatch({ effects: A.of([]) });
|
||||||
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);
|
l = requestAnimationFrame(d);
|
||||||
}
|
}, l = requestAnimationFrame(d);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(frame);
|
cancelAnimationFrame(l);
|
||||||
};
|
};
|
||||||
} else {
|
} else
|
||||||
highlights = [];
|
s.current = [], e.dispatch({ effects: A.of([]) });
|
||||||
view.dispatch({ effects: setHighlights.of([]) });
|
}, [t, o, e]);
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [pattern, active, view]);
|
|
||||||
}
|
}
|
||||||
|
const de = "_container_3i85k_1", ue = "_header_3i85k_5", fe = "_buttons_3i85k_9", me = "_button_3i85k_9", ge = "_buttonDisabled_3i85k_17", pe = "_error_3i85k_21", he = "_body_3i85k_25", b = {
|
||||||
var tailwind = '';
|
container: de,
|
||||||
|
header: ue,
|
||||||
const container = "_container_3i85k_1";
|
buttons: fe,
|
||||||
const header = "_header_3i85k_5";
|
button: me,
|
||||||
const buttons = "_buttons_3i85k_9";
|
buttonDisabled: ge,
|
||||||
const button = "_button_3i85k_9";
|
error: pe,
|
||||||
const buttonDisabled = "_buttonDisabled_3i85k_17";
|
body: he
|
||||||
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 q({ type: e }) {
|
||||||
function Icon({ type }) {
|
return /* @__PURE__ */ n.createElement("svg", {
|
||||||
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__ */ React.createElement("path", {
|
refresh: /* @__PURE__ */ n.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__ */ React.createElement("path", {
|
play: /* @__PURE__ */ n.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__ */ React.createElement("path", {
|
pause: /* @__PURE__ */ n.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"
|
||||||
})
|
})
|
||||||
}[type]);
|
}[e]);
|
||||||
}
|
}
|
||||||
|
function ve({
|
||||||
function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableKeyboard }) {
|
defaultOutput: e,
|
||||||
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } = useRepl({
|
interval: t,
|
||||||
tune,
|
getTime: o,
|
||||||
autolink: false,
|
evalOnMount: a = !1,
|
||||||
onEvent
|
initialCode: s = "",
|
||||||
});
|
autolink: m = !1,
|
||||||
useEffect(() => {
|
beforeEval: l,
|
||||||
init && evaluateOnly();
|
afterEval: d,
|
||||||
}, [tune, init]);
|
onEvalError: c,
|
||||||
const [view, setView] = useState();
|
onToggle: i
|
||||||
const [ref, isVisible] = useInView({
|
}) {
|
||||||
|
const [g, v] = w(), [p, D] = w(), [h, k] = w(s), [y, P] = w(h), [z, _] = w(), [C, H] = w(!1), F = h !== y, { scheduler: u, evaluate: L, start: $, stop: G, pause: J } = I(
|
||||||
|
() => te({
|
||||||
|
interval: t,
|
||||||
|
defaultOutput: e,
|
||||||
|
onSchedulerError: v,
|
||||||
|
onEvalError: (f) => {
|
||||||
|
D(f), c?.(f);
|
||||||
|
},
|
||||||
|
getTime: o,
|
||||||
|
transpiler: re,
|
||||||
|
beforeEval: ({ code: f }) => {
|
||||||
|
k(f), l?.();
|
||||||
|
},
|
||||||
|
afterEval: ({ pattern: f, code: S }) => {
|
||||||
|
P(S), _(f), D(), v(), m && (window.location.hash = "#" + encodeURIComponent(btoa(S))), d?.();
|
||||||
|
},
|
||||||
|
onToggle: (f) => {
|
||||||
|
H(f), i?.(f);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[e, t, o]
|
||||||
|
), M = N(async (f = !0) => L(h, f), [L, h]), V = x();
|
||||||
|
return R(() => {
|
||||||
|
!V.current && a && h && (V.current = !0, M());
|
||||||
|
}, [M, a, h]), R(() => () => {
|
||||||
|
u.stop();
|
||||||
|
}, [u]), {
|
||||||
|
code: h,
|
||||||
|
setCode: k,
|
||||||
|
error: g || p,
|
||||||
|
schedulerError: g,
|
||||||
|
scheduler: u,
|
||||||
|
evalError: p,
|
||||||
|
evaluate: L,
|
||||||
|
activateCode: M,
|
||||||
|
activeCode: y,
|
||||||
|
isDirty: F,
|
||||||
|
pattern: z,
|
||||||
|
started: C,
|
||||||
|
start: $,
|
||||||
|
stop: G,
|
||||||
|
pause: J,
|
||||||
|
togglePlay: async () => {
|
||||||
|
C ? u.pause() : await M();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const be = () => ee().currentTime;
|
||||||
|
function Pe({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) {
|
||||||
|
const {
|
||||||
|
code: s,
|
||||||
|
setCode: m,
|
||||||
|
evaluate: l,
|
||||||
|
activateCode: d,
|
||||||
|
error: c,
|
||||||
|
isDirty: i,
|
||||||
|
activeCode: g,
|
||||||
|
pattern: v,
|
||||||
|
started: p,
|
||||||
|
scheduler: D,
|
||||||
|
togglePlay: h,
|
||||||
|
stop: k
|
||||||
|
} = ve({
|
||||||
|
initialCode: e,
|
||||||
|
defaultOutput: Z,
|
||||||
|
getTime: be
|
||||||
|
}), [y, P] = w(), [z, _] = Y({
|
||||||
threshold: 0.01
|
threshold: 0.01
|
||||||
});
|
}), C = x(), H = I(() => ((_ || !t) && (C.current = !0), _ || C.current), [_, t]);
|
||||||
const wasVisible = useRef();
|
return le({
|
||||||
const show = useMemo(() => {
|
view: y,
|
||||||
if (isVisible || !hideOutsideView) {
|
pattern: v,
|
||||||
wasVisible.current = true;
|
active: p && !g?.includes("strudel disable-highlighting"),
|
||||||
}
|
getTime: () => D.getPhase()
|
||||||
return isVisible || wasVisible.current;
|
}), K(() => {
|
||||||
}, [isVisible, hideOutsideView]);
|
if (a) {
|
||||||
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes("strudel disable-highlighting") });
|
const F = async (u) => {
|
||||||
useLayoutEffect(() => {
|
(u.ctrlKey || u.altKey) && (u.code === "Enter" ? (u.preventDefault(), ae(y), await d()) : u.code === "Period" && (k(), u.preventDefault()));
|
||||||
if (enableKeyboard) {
|
|
||||||
const handleKeyPress = async (e) => {
|
|
||||||
if (e.ctrlKey || e.altKey) {
|
|
||||||
if (e.code === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
flash(view);
|
|
||||||
await activateCode();
|
|
||||||
} else if (e.code === "Period") {
|
|
||||||
cycle.stop();
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
window.addEventListener("keydown", handleKeyPress, true);
|
return window.addEventListener("keydown", F, !0), () => window.removeEventListener("keydown", F, !0);
|
||||||
return () => window.removeEventListener("keydown", handleKeyPress, true);
|
|
||||||
}
|
}
|
||||||
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
|
}, [a, v, s, l, k, y]), /* @__PURE__ */ n.createElement("div", {
|
||||||
return /* @__PURE__ */ React.createElement("div", {
|
className: b.container,
|
||||||
className: styles.container,
|
ref: z
|
||||||
ref
|
}, /* @__PURE__ */ n.createElement("div", {
|
||||||
}, /* @__PURE__ */ React.createElement("div", {
|
className: b.header
|
||||||
className: styles.header
|
}, /* @__PURE__ */ n.createElement("div", {
|
||||||
}, /* @__PURE__ */ React.createElement("div", {
|
className: b.buttons
|
||||||
className: styles.buttons
|
}, /* @__PURE__ */ n.createElement("button", {
|
||||||
}, /* @__PURE__ */ React.createElement("button", {
|
className: B(b.button, p ? "sc-animate-pulse" : ""),
|
||||||
className: cx(styles.button, cycle.started ? "sc-animate-pulse" : ""),
|
onClick: () => h()
|
||||||
onClick: () => togglePlay()
|
}, /* @__PURE__ */ n.createElement(q, {
|
||||||
}, /* @__PURE__ */ React.createElement(Icon, {
|
type: p ? "pause" : "play"
|
||||||
type: cycle.started ? "pause" : "play"
|
})), /* @__PURE__ */ n.createElement("button", {
|
||||||
})), /* @__PURE__ */ React.createElement("button", {
|
className: B(i ? b.button : b.buttonDisabled),
|
||||||
className: cx(dirty ? styles.button : styles.buttonDisabled),
|
onClick: () => d()
|
||||||
onClick: () => activateCode()
|
}, /* @__PURE__ */ n.createElement(q, {
|
||||||
}, /* @__PURE__ */ React.createElement(Icon, {
|
|
||||||
type: "refresh"
|
type: "refresh"
|
||||||
}))), error && /* @__PURE__ */ React.createElement("div", {
|
}))), c && /* @__PURE__ */ n.createElement("div", {
|
||||||
className: styles.error
|
className: b.error
|
||||||
}, error.message)), /* @__PURE__ */ React.createElement("div", {
|
}, c.message)), /* @__PURE__ */ n.createElement("div", {
|
||||||
className: styles.body
|
className: b.body
|
||||||
}, show && /* @__PURE__ */ React.createElement(CodeMirror, {
|
}, H && /* @__PURE__ */ n.createElement(ie, {
|
||||||
value: code,
|
value: s,
|
||||||
onChange: setCode,
|
onChange: m,
|
||||||
onViewChanged: setView
|
onViewChanged: P
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
function ze(e) {
|
||||||
/*
|
return R(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((t) => window.postMessage(t, "*"), []);
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
const He = (e) => K(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
|
||||||
export { CodeMirror, MiniRepl, cx, flash, useCycle, useHighlighting, usePostMessage, useRepl, useWebMidi };
|
export {
|
||||||
|
ie as CodeMirror,
|
||||||
|
Pe as MiniRepl,
|
||||||
|
B as cx,
|
||||||
|
ae as flash,
|
||||||
|
le as useHighlighting,
|
||||||
|
He as useKeydown,
|
||||||
|
ze as usePostMessage,
|
||||||
|
ve as useStrudel
|
||||||
|
};
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
.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}
|
.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}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# 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
@@ -0,0 +1 @@
|
|||||||
|
.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
@@ -0,0 +1 @@
|
|||||||
|
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
@@ -0,0 +1 @@
|
|||||||
|
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};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!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>
|
||||||
+3520
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #123;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
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: [],
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()]
|
||||||
|
})
|
||||||
+12
-10
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/react",
|
"name": "@strudel.cycles/react",
|
||||||
"version": "0.2.0",
|
"version": "0.4.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,6 +17,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"watch": "vite build --watch",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -37,12 +38,13 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-javascript": "^6.0.2",
|
"@codemirror/lang-javascript": "^6.1.1",
|
||||||
"@strudel.cycles/core": "^0.2.0",
|
"@strudel.cycles/core": "^0.4.0",
|
||||||
"@strudel.cycles/eval": "^0.2.0",
|
"@strudel.cycles/tone": "^0.4.0",
|
||||||
"@strudel.cycles/tone": "^0.2.0",
|
"@strudel.cycles/transpiler": "^0.4.0",
|
||||||
"@uiw/codemirror-themes": "^4.11.4",
|
"@strudel.cycles/webaudio": "^0.4.0",
|
||||||
"@uiw/react-codemirror": "^4.11.4",
|
"@uiw/codemirror-themes": "^4.12.4",
|
||||||
|
"@uiw/react-codemirror": "^4.12.4",
|
||||||
"react-hook-inview": "^4.5.0"
|
"react-hook-inview": "^4.5.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -52,12 +54,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": "^1.3.0",
|
"@vitejs/plugin-react": "^2.2.0",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"postcss": "^8.4.13",
|
"postcss": "^8.4.18",
|
||||||
"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": "^2.9.9"
|
"vite": "^3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
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 { evalScope } from '@strudel.cycles/eval';
|
import { controls, evalScope } from '@strudel.cycles/core';
|
||||||
|
|
||||||
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'),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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';
|
||||||
@@ -8,17 +7,33 @@ 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';
|
||||||
|
|
||||||
export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableKeyboard }) {
|
const getTime = () => getAudioContext().currentTime;
|
||||||
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
|
|
||||||
useRepl({
|
export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }) {
|
||||||
tune,
|
const {
|
||||||
autolink: false,
|
code,
|
||||||
onEvent,
|
setCode,
|
||||||
});
|
evaluate,
|
||||||
useEffect(() => {
|
activateCode,
|
||||||
init && evaluateOnly();
|
error,
|
||||||
}, [tune, init]);
|
isDirty,
|
||||||
|
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,
|
||||||
@@ -30,7 +45,12 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
|||||||
}
|
}
|
||||||
return isVisible || wasVisible.current;
|
return isVisible || wasVisible.current;
|
||||||
}, [isVisible, hideOutsideView]);
|
}, [isVisible, hideOutsideView]);
|
||||||
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes('strudel disable-highlighting') });
|
useHighlighting({
|
||||||
|
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(() => {
|
||||||
@@ -42,7 +62,7 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
|||||||
flash(view);
|
flash(view);
|
||||||
await activateCode();
|
await activateCode();
|
||||||
} else if (e.code === 'Period') {
|
} else if (e.code === 'Period') {
|
||||||
cycle.stop();
|
stop();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,16 +70,16 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
|||||||
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, activateCode, cycle, view]);
|
}, [enableKeyboard, pattern, code, evaluate, stop, 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, cycle.started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
|
<button className={cx(styles.button, started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
|
||||||
<Icon type={cycle.started ? 'pause' : 'play'} />
|
<Icon type={started ? 'pause' : 'play'} />
|
||||||
</button>
|
</button>
|
||||||
<button className={cx(dirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
||||||
<Icon type="refresh" />
|
<Icon type="refresh" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-theme-light {
|
.cm-theme-light {
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
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;
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { setHighlights } from '../components/CodeMirror6';
|
import { setHighlights } from '../components/CodeMirror6';
|
||||||
import { Tone } from '@strudel.cycles/tone';
|
|
||||||
|
|
||||||
let highlights = []; // actively highlighted events
|
function useHighlighting({ view, pattern, active, getTime }) {
|
||||||
let lastEnd;
|
const highlights = useRef([]);
|
||||||
|
const lastEnd = useRef();
|
||||||
function useHighlighting({ view, pattern, active }) {
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (view) {
|
if (view) {
|
||||||
if (pattern && active) {
|
if (pattern && active) {
|
||||||
@@ -13,18 +11,17 @@ function useHighlighting({ view, pattern, active }) {
|
|||||||
|
|
||||||
function updateHighlights() {
|
function updateHighlights() {
|
||||||
try {
|
try {
|
||||||
const audioTime = Tone.getTransport().seconds;
|
const audioTime = getTime();
|
||||||
// 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 || audioTime, audioTime - 1 / 10);
|
const begin = Math.max(lastEnd.current || audioTime, audioTime - 1 / 10, 0); // negative time seems buggy
|
||||||
const span = [begin, audioTime + 1 / 60];
|
const span = [begin, audioTime + 1 / 60];
|
||||||
lastEnd = audioTime + 1 / 60;
|
lastEnd.current = span[1];
|
||||||
highlights = highlights.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
|
highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
|
||||||
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
||||||
highlights = highlights.concat(haps); // add potential new onsets
|
highlights.current = highlights.current.concat(haps); // add potential new onsets
|
||||||
view.dispatch({ effects: setHighlights.of(highlights) }); // highlight all still active + new active haps
|
view.dispatch({ effects: setHighlights.of(highlights.current) }); // 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);
|
||||||
@@ -34,7 +31,7 @@ function useHighlighting({ view, pattern, active }) {
|
|||||||
cancelAnimationFrame(frame);
|
cancelAnimationFrame(frame);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
highlights = [];
|
highlights.current = [];
|
||||||
view.dispatch({ effects: setHighlights.of([]) });
|
view.dispatch({ effects: setHighlights.of([]) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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;
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
/*
|
|
||||||
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;
|
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { repl } from '@strudel.cycles/core';
|
||||||
|
import { transpiler } from '@strudel.cycles/transpiler';
|
||||||
|
|
||||||
|
function useStrudel({
|
||||||
|
defaultOutput,
|
||||||
|
interval,
|
||||||
|
getTime,
|
||||||
|
evalOnMount = false,
|
||||||
|
initialCode = '',
|
||||||
|
autolink = false,
|
||||||
|
beforeEval,
|
||||||
|
afterEval,
|
||||||
|
onEvalError,
|
||||||
|
onToggle,
|
||||||
|
}) {
|
||||||
|
// scheduler
|
||||||
|
const [schedulerError, setSchedulerError] = useState();
|
||||||
|
const [evalError, setEvalError] = useState();
|
||||||
|
const [code, setCode] = useState(initialCode);
|
||||||
|
const [activeCode, setActiveCode] = useState(code);
|
||||||
|
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 activateCode = useCallback(async (autostart = true) => evaluate(code, autostart), [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;
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,8 @@
|
|||||||
|
|
||||||
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 useRepl } from './hooks/useRepl';
|
export { default as useStrudel } from './hooks/useStrudel';
|
||||||
|
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,28 +8,36 @@ 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',
|
// lineHighlight: '#8a91991a', // original
|
||||||
|
lineHighlight: '#00000050',
|
||||||
gutterBackground: 'transparent',
|
gutterBackground: 'transparent',
|
||||||
// gutterForeground: '#8a919966',
|
// gutterForeground: '#8a919966',
|
||||||
gutterForeground: '#676e95',
|
gutterForeground: '#8a919966',
|
||||||
},
|
},
|
||||||
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' },
|
// { tag: t.typeName, color: '#f07178' }, // original
|
||||||
|
{ tag: t.typeName, color: '#c3e88d' },
|
||||||
{ tag: t.atom, color: '#f78c6c' },
|
{ tag: t.atom, color: '#f78c6c' },
|
||||||
{ tag: t.number, color: '#ff5370' },
|
// { tag: t.number, color: '#ff5370' }, // original
|
||||||
|
{ 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' },
|
// { tag: t.special(t.string), color: '#f07178' }, // original
|
||||||
|
{ tag: t.special(t.string), color: '#c3e88d' },
|
||||||
{ tag: t.comment, color: '#7d8799' },
|
{ tag: t.comment, color: '#7d8799' },
|
||||||
{ tag: t.variableName, color: '#f07178' },
|
// { tag: t.variableName, color: '#f07178' }, // original
|
||||||
{ tag: t.tagName, color: '#ff5370' },
|
{ tag: t.variableName, color: '#c792ea' },
|
||||||
{ tag: t.bracket, color: '#a2a1a4' },
|
// { tag: t.tagName, color: '#ff5370' }, // original
|
||||||
|
{ 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' },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ export default defineConfig({
|
|||||||
// TODO: find out which of below names are obsolete now
|
// TODO: find out which of below names are obsolete now
|
||||||
'@strudel.cycles/tone',
|
'@strudel.cycles/tone',
|
||||||
'@strudel.cycles/eval',
|
'@strudel.cycles/eval',
|
||||||
|
'@strudel.cycles/transpiler',
|
||||||
|
'acorn',
|
||||||
'@strudel.cycles/core',
|
'@strudel.cycles/core',
|
||||||
'@strudel.cycles/core/util.mjs',
|
|
||||||
'@strudel.cycles/mini',
|
'@strudel.cycles/mini',
|
||||||
'@strudel.cycles/tonal',
|
'@strudel.cycles/tonal',
|
||||||
'@strudel.cycles/midi',
|
'@strudel.cycles/midi',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/serial",
|
"name": "@strudel.cycles/serial",
|
||||||
"version": "0.1.3",
|
"version": "0.2.0",
|
||||||
"description": "Webserial API for strudel",
|
"description": "Webserial API for strudel",
|
||||||
"main": "serial.mjs",
|
"main": "serial.mjs",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
+13
-17
@@ -9,7 +9,7 @@ import { Pattern, isPattern } from '@strudel.cycles/core';
|
|||||||
var serialWriter;
|
var serialWriter;
|
||||||
var choosing = false;
|
var choosing = false;
|
||||||
|
|
||||||
export async function getWriter(br=38400) {
|
export async function getWriter(br = 38400) {
|
||||||
if (choosing) {
|
if (choosing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -24,11 +24,10 @@ export async function getWriter(br=38400) {
|
|||||||
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
|
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
|
||||||
const writer = textEncoder.writable.getWriter();
|
const writer = textEncoder.writable.getWriter();
|
||||||
serialWriter = function (message) {
|
serialWriter = function (message) {
|
||||||
writer.write(message)
|
writer.write(message);
|
||||||
}
|
};
|
||||||
}
|
} else {
|
||||||
else {
|
throw 'Webserial is not available in this browser.';
|
||||||
throw('Webserial is not available in this browser.')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ Pattern.prototype.serial = function (...args) {
|
|||||||
getWriter(...args);
|
getWriter(...args);
|
||||||
}
|
}
|
||||||
const onTrigger = (time, hap, currentTime) => {
|
const onTrigger = (time, hap, currentTime) => {
|
||||||
var message = "";
|
var message = '';
|
||||||
if (typeof hap.value === 'object') {
|
if (typeof hap.value === 'object') {
|
||||||
if ('action' in hap.value) {
|
if ('action' in hap.value) {
|
||||||
message += hap.value['action'] + '(';
|
message += hap.value['action'] + '(';
|
||||||
@@ -51,26 +50,23 @@ Pattern.prototype.serial = function (...args) {
|
|||||||
}
|
}
|
||||||
if (first) {
|
if (first) {
|
||||||
first = false;
|
first = false;
|
||||||
|
} else {
|
||||||
|
message += ',';
|
||||||
}
|
}
|
||||||
else {
|
message += `${key}:${val}`;
|
||||||
message +=',';
|
|
||||||
}
|
|
||||||
message += `${key}:${val}`
|
|
||||||
}
|
}
|
||||||
message += ')';
|
message += ')';
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
for (const [key, val] of Object.entries(hap.value)) {
|
for (const [key, val] of Object.entries(hap.value)) {
|
||||||
message += `${key}:${val};`
|
message += `${key}:${val};`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
message = hap.value;
|
message = hap.value;
|
||||||
}
|
}
|
||||||
const offset = (time - currentTime + latency) * 1000;
|
const offset = (time - currentTime + latency) * 1000;
|
||||||
window.setTimeout(serialWriter, offset, message);
|
window.setTimeout(serialWriter, offset, message);
|
||||||
};
|
};
|
||||||
return hap.setContext({ ...hap.context, onTrigger });
|
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/soundfonts",
|
"name": "@strudel.cycles/soundfonts",
|
||||||
"version": "0.2.0",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/soundfonts",
|
"name": "@strudel.cycles/soundfonts",
|
||||||
"version": "0.2.1",
|
"version": "0.4.0",
|
||||||
"description": "Soundsfont support for strudel",
|
"description": "Soundsfont support for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.2.0",
|
"@strudel.cycles/core": "^0.4.0",
|
||||||
"@strudel.cycles/webaudio": "^0.2.0",
|
"@strudel.cycles/webaudio": "^0.4.0",
|
||||||
"sfumato": "^0.1.2",
|
"sfumato": "^0.1.2",
|
||||||
"soundfont2": "^0.4.0"
|
"soundfont2": "^0.4.0"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/tonal",
|
"name": "@strudel.cycles/tonal",
|
||||||
"version": "0.2.0",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/tonal",
|
"name": "@strudel.cycles/tonal",
|
||||||
"version": "0.2.0",
|
"version": "0.4.0",
|
||||||
"description": "Tonal functions for strudel",
|
"description": "Tonal functions for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -25,8 +25,9 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.2.0",
|
"@strudel.cycles/core": "^0.4.0",
|
||||||
"@tonaljs/tonal": "^4.6.5",
|
"@tonaljs/tonal": "^4.6.5",
|
||||||
"webmidi": "^3.0.15"
|
"chord-voicings": "^0.0.1",
|
||||||
|
"webmidi": "^3.0.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
import './pianoroll.mjs';
|
|
||||||
|
|
||||||
export * from './tone.mjs';
|
export * from './tone.mjs';
|
||||||
export * from './draw.mjs';
|
|
||||||
export * from './ui.mjs';
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/tone",
|
"name": "@strudel.cycles/tone",
|
||||||
"version": "0.2.0",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/tone",
|
"name": "@strudel.cycles/tone",
|
||||||
"version": "0.2.0",
|
"version": "0.4.0",
|
||||||
"description": "Tone.js API for strudel",
|
"description": "Tone.js API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -22,9 +22,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.2.0",
|
"@strudel.cycles/core": "^0.4.0",
|
||||||
"@tonejs/piano": "^0.2.1",
|
|
||||||
"chord-voicings": "^0.0.1",
|
|
||||||
"tone": "^14.7.77"
|
"tone": "^14.7.77"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user