Merge remote-tracking branch 'origin/main' into in-source-doc

This commit is contained in:
Felix Roos 2022-05-20 21:46:14 +02:00
commit b50daed0d0
112 changed files with 16311 additions and 41178 deletions

4
.gitignore vendored
View File

@ -1,4 +1,5 @@
dist
out
!docs/dist
dist-*
cabal-dev
@ -28,4 +29,5 @@ repl-parcel
mytunes.ts
doc
out
.parcel-cache
.parcel-cache
repl_old

View File

View File

@ -1 +0,0 @@
strudel.tidalcycles.org

View File

@ -1,16 +0,0 @@
{
"files": {
"main.css": "/static/css/main.35f2ca4c.css",
"main.js": "/static/js/main.f848d51e.js",
"static/js/787.1c52cb78.chunk.js": "/static/js/787.1c52cb78.chunk.js",
"static/media/logo.svg": "/static/media/logo.ac95051720b3dccfe511e0e02d8e1029.svg",
"index.html": "/index.html",
"main.35f2ca4c.css.map": "/static/css/main.35f2ca4c.css.map",
"main.f848d51e.js.map": "/static/js/main.f848d51e.js.map",
"787.1c52cb78.chunk.js.map": "/static/js/787.1c52cb78.chunk.js.map"
},
"entrypoints": [
"static/css/main.35f2ca4c.css",
"static/js/main.f848d51e.js"
]
}

View File

@ -1 +0,0 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Strudel REPL"/><title>Strudel REPL</title><script defer="defer" src="/static/js/main.f848d51e.js"></script><link href="/static/css/main.35f2ca4c.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

View File

@ -1,15 +0,0 @@
{
"short_name": "Strudel REPL",
"name": "Strudel REPL - Tidal Patterns in JavaScript",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +0,0 @@
"use strict";(self.webpackChunk_strudel_cycles_repl=self.webpackChunk_strudel_cycles_repl||[]).push([[787],{787:function(e,t,n){n.r(t),n.d(t,{getCLS:function(){return y},getFCP:function(){return g},getFID:function(){return C},getLCP:function(){return P},getTTFB:function(){return D}});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,p=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=p(),d(),s((function(){setTimeout((function(){v=p(),d()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime<i.firstHiddenTime&&(r.value=e.startTime,r.entries.push(e),n(!0)))},o=window.performance&&performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint")[0],f=o?null:c("paint",a);(o||f)&&(n=m(e,r,t),o&&a(o),s((function(i){r=u("FCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,n(!0)}))}))})))},h=!1,T=-1,y=function(e,t){h||(g((function(e){T=e.value})),h=!0);var n,i=function(t){T>-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},p=c("layout-shift",v);p&&(n=m(i,r,t),f((function(){p.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r<a-w){var e={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+r};o.forEach((function(t){t(e)})),o=[]}},b=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),p=function(e){e.startTime<a.firstHiddenTime&&(v.value=e.processingStart-e.startTime,v.entries.push(e),n(!0))},d=c("first-input",p);n=m(e,v,t),d&&f((function(){d.takeRecords().map(p),d.disconnect()}),!0),d&&s((function(){var a;v=u("FID"),n=m(e,v,t),o=[],r=-1,i=null,F(addEventListener),a=p,o.push(a),S()}))},k={},P=function(e,t){var n,i=l(),r=u("LCP"),a=function(e){var t=e.startTime;t<i.firstHiddenTime&&(r.value=t,r.entries.push(e),n())},o=c("largest-contentful-paint",a);if(o){n=m(e,r,t);var v=function(){k[r.id]||(o.takeRecords().map(a),o.disconnect(),k[r.id]=!0,n(!0))};["keydown","click"].forEach((function(e){addEventListener(e,v,{once:!0,capture:!0})})),f(v,!0),s((function(i){r=u("LCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,k[r.id]=!0,n(!0)}))}))}))}},D=function(e){var t,n=u("TTFB");t=function(){try{var t=performance.getEntriesByType("navigation")[0]||function(){var e=performance.timing,t={entryType:"navigation",startTime:0};for(var n in e)"navigationStart"!==n&&"toJSON"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]);
//# sourceMappingURL=787.1c52cb78.chunk.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,96 +0,0 @@
/*
Strudel - javascript-based environment for live coding algorithmic (musical) patterns
https://strudel.tidalcycles.org / https://github.com/tidalcycles/strudel/
Copyright (C) Strudel contributors
https://github.com/tidalcycles/strudel/graphs/contributors
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/>.
*/
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/*! @license ReactCodeMirror - MIT License - Tom Golden (8162045+tbjgolden@users.noreply.github.com) */
/**
* @license Fraction.js v4.2.0 05/03/2022
* https://www.xarg.org/2014/03/rational-numbers-in-javascript/
*
* Copyright (c) 2021, Robert Eisele (robert@xarg.org)
* Dual licensed under the MIT or GPL Version 2 licenses.
**/
/**
* Tone.js
* @author Yotam Mann
* @license http://opensource.org/licenses/MIT MIT License
* @copyright 2014-2019 Yotam Mann
*/
/** @license React v0.20.2
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 42 KiB

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

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><link rel="icon" href="/tutorial/favicon.e3ab9dd9.ico"><link rel="stylesheet" type="text/css" href="/tutorial/index.a3acddda.css"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content="Strudel REPL"><title>Strudel Tutorial</title></head><body> <div id="root"></div> <noscript>You need to enable JavaScript to run this app.</noscript> <script src="/tutorial/index.55306425.js" defer></script> </body></html>

3148
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,9 +7,11 @@
"test": "npm run test --workspaces --if-present && cd repl && npm run test",
"bootstrap": "lerna bootstrap",
"setup": "npm i && npm run bootstrap && cd repl && npm i",
"repl": "cd repl && npm run start",
"repl": "cd repl && npm run dev",
"osc": "cd packages/osc && npm run server",
"build": "cd repl && npm run build",
"build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build",
"preview": "npx serve ./out",
"deploy": "gh-pages -d out",
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json"
},
@ -35,6 +37,7 @@
"homepage": "https://strudel.tidalcycles.org",
"devDependencies": {
"events": "^3.3.0",
"gh-pages": "^4.0.0",
"jsdoc": "^3.6.10",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^7.1.1",

View File

@ -14,4 +14,6 @@ export * from './signal.mjs';
export * from './state.mjs';
export * from './timespan.mjs';
export * from './util.mjs';
export * from './speak.mjs';
export * as gist from './gist.js';
// export * from './value.mjs';

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.0.5",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.0.5",
"version": "0.1.0",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",

View File

@ -450,7 +450,7 @@ export class Pattern {
// set context type to midi to let the player know its meant as midi number and not as frequency
return new Hap(hap.whole, hap.part, toMidi(hap.value), { ...hap.context, type: 'midi' });
}
if (dropfail) {
if (dropfails) {
// return 'nothing'
return undefined;
}
@ -461,10 +461,6 @@ export class Pattern {
throw new Error('cannot parse as number: "' + hap.value + '"');
return hap;
});
if (dropfail) {
return result._removeUndefineds();
}
return result;
}
/**
@ -744,6 +740,14 @@ export class Pattern {
return this._fast(Fraction(1).div(factor));
}
_inside(factor, f) {
return f(this._slow(factor))._fast(factor);
}
_outside(factor, f) {
return f(this._fast(factor))._slow(factor);
}
_ply(factor) {
return this.fmap((x) => pure(x)._fast(factor))._squeezeJoin();
}
@ -870,6 +874,16 @@ export class Pattern {
return slowcatPrime(...pats);
}
/**
* Returns a new pattern where every other cycle is played once, twice as
* fast, and offset in time by one quarter of a cycle. Creates a kind of
* breakbeat feel.
* @returns Pattern
*/
brak() {
return this.when(slowcat(false, true), (x) => fastcat(x, silence)._late(0.25));
}
rev() {
const pat = this;
const query = function (state) {
@ -1007,6 +1021,10 @@ export class Pattern {
_velocity(velocity) {
return this._withContext((context) => ({ ...context, velocity: (context.velocity || 1) * velocity }));
}
_loopAt(factor,cps=1) {
return this.speed((1/factor)*cps).unit("c").slow(factor)
}
}
// TODO - adopt value.mjs fully..
@ -1430,6 +1448,10 @@ Pattern.prototype.chunkBack = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._chunkBack)(...args, this);
};
Pattern.prototype.loopAt = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._loopAt)(...args, this);
};
Pattern.prototype.zoom = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._zoom)(...args, this);
@ -1438,6 +1460,14 @@ Pattern.prototype.compress = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._compress)(...args, this);
};
Pattern.prototype.outside = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._outside)(...args, this);
};
Pattern.prototype.inside = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._inside)(...args, this);
};
// call this after all Patter.prototype.define calls have been executed! (right before evaluate)
Pattern.prototype.bootstrap = function () {

View File

@ -6,7 +6,13 @@ This program is free software: you can redistribute it and/or modify it under th
import { Pattern, patternify2 } from './index.mjs';
const synth = window?.speechSynthesis;
let synth;
try {
synth = window?.speechSynthesis;
} catch (err) {
console.warn('cannot use window: not in browser?');
}
let allVoices = synth?.getVoices();
// console.log('voices', allVoices);

View File

@ -41,6 +41,7 @@ import {
tri2,
id,
ply,
rev
} from '../index.mjs';
import { steady } from '../signal.mjs';
@ -277,10 +278,7 @@ describe('Pattern', function () {
);
});
it('can SqueezeOut() structure', () => {
sameFirst(
sequence(1, [2, 3]).keepifSqueezeOut(true, true, false),
sequence([1, [2, 3]], [1, [2, 3]], silence),
);
sameFirst(sequence(1, [2, 3]).keepifSqueezeOut(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
});
});
describe('sub()', function () {
@ -439,6 +437,20 @@ describe('Pattern', function () {
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
});
});
describe('inside', () => {
it('can rev inside a cycle', () => {
sameFirst(sequence('a', 'b', 'c', 'd').inside(2, rev),
sequence('b', 'a', 'd', 'c')
);
});
});
describe('outside', () => {
it('can rev outside a cycle', () => {
sameFirst(sequence('a', 'b', 'c', 'd')._slow(2).outside(2, rev),
sequence('d', 'c')
);
});
});
describe('_filterValues()', function () {
it('Filters true', function () {
assert.equal(
@ -589,6 +601,11 @@ describe('Pattern', function () {
);
});
});
describe('brak()', () => {
it('Can make something a bit breakbeaty', () => {
sameFirst(sequence('a', 'b').brak()._fast(2), sequence('a', 'b', fastcat(silence, 'a'), fastcat('b', silence)));
});
});
describe('timeCat()', function () {
it('Can concatenate patterns with different relative durations', function () {
assert.deepStrictEqual(

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/embed",
"version": "0.0.2",
"version": "0.1.0",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js",
"type": "module",
@ -20,6 +20,5 @@
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {}
"homepage": "https://github.com/tidalcycles/strudel#readme"
}

View File

@ -11,6 +11,8 @@ npm i @strudel.cycles/eval --save
## Example
<!-- TODO: -extend +evalScope -->
```js
import { evaluate, extend } from '@strudel.cycles/eval';
import * as strudel from '@strudel.cycles/core';

View File

@ -7,17 +7,39 @@ This program is free software: you can redistribute it and/or modify it under th
import shapeshifter from './shapeshifter.mjs';
import * as strudel from '@strudel.cycles/core';
const { isPattern } = strudel;
const { isPattern, Pattern } = strudel;
export const extend = (...args) => {
// TODO: find a way to make args available to eval without adding it to global scope...
// sadly, "with" does not work in strict mode
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) => {
if (!scoped) {
await evalScope(); // at least scope Pattern.prototype.boostrap
}
const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code
let evaluated = await eval(shapeshifted);
let evaluated = await safeEval(shapeshifted);
if (!isPattern(evaluated)) {
console.log('evaluated', evaluated);
const message = `got "${typeof evaluated}" instead of pattern`;

1
packages/eval/index.mjs Normal file
View File

@ -0,0 +1 @@
export * from './evaluate.mjs';

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.0.5",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -1,8 +1,9 @@
{
"name": "@strudel.cycles/eval",
"version": "0.0.5",
"version": "0.1.1",
"description": "Code evaluator for strudel",
"main": "evaluate.mjs",
"main": "index.mjs",
"type": "module",
"directories": {
"test": "test"
},
@ -13,7 +14,6 @@
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"type": "module",
"keywords": [
"tidalcycles",
"strudel",
@ -28,7 +28,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.5",
"@strudel.cycles/core": "^0.1.0",
"estraverse": "^5.3.0",
"shift-ast": "^6.1.0",
"shift-codegen": "^7.0.3",

View File

@ -12,6 +12,7 @@ import * as strudel from '@strudel.cycles/core';
const { fastcat } = strudel;
extend({ mini }, strudel);
// TODO: test evalScope
describe('evaluate', () => {
const ev = async (code) => (await evaluate(code)).pattern._firstCycleValues;

3
packages/midi/index.mjs Normal file
View File

@ -0,0 +1,3 @@
import './midi.mjs';
export * from './midi.mjs';

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.0.6",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -1,8 +1,8 @@
{
"name": "@strudel.cycles/midi",
"version": "0.0.6",
"version": "0.1.1",
"description": "Midi API for strudel",
"main": "midi.mjs",
"main": "index.mjs",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
@ -21,7 +21,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/tone": "^0.0.6",
"@strudel.cycles/tone": "^0.1.1",
"tone": "^14.7.77",
"webmidi": "^2.5.2"
}

2
packages/mini/index.mjs Normal file
View File

@ -0,0 +1,2 @@
export * from './mini.mjs';
export * from './krill-parser.js';

View File

@ -1,8 +1,8 @@
{
"name": "@strudel.cycles/mini",
"version": "0.0.7",
"version": "0.1.1",
"description": "Mini notation for strudel",
"main": "mini.mjs",
"main": "index.mjs",
"type": "module",
"scripts": {
"test": "mocha --colors"
@ -25,8 +25,8 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.5",
"@strudel.cycles/eval": "^0.0.5",
"@strudel.cycles/tone": "^0.0.6"
"@strudel.cycles/core": "^0.1.0",
"@strudel.cycles/eval": "^0.1.1",
"@strudel.cycles/tone": "^0.1.1"
}
}

View File

@ -10,15 +10,19 @@ import { Pattern } from '@strudel.cycles/core';
const comm = new OSC();
comm.open();
const latency = 0.1;
let startedAt = -1;
Pattern.prototype.osc = function () {
return this._withHap((hap) => {
const onTrigger = (time, hap, currentTime) => {
const onTrigger = (time, hap, currentTime, cps, cycle, delta) => {
// time should be audio time of onset
// currentTime should be current time of audio context (slightly before time)
const keyvals = Object.entries(hap.value).flat();
const offset = (time - currentTime + latency) * 1000;
const ts = Math.floor(Date.now() + offset);
if (startedAt < 0) {
startedAt = Date.now() - (currentTime * 1000);
}
const controls = Object.assign({}, { cps: cps, cycle: cycle, delta: delta }, hap.value);
const keyvals = Object.entries(controls).flat();
const ts = Math.floor(startedAt + ((time + latency) * 1000));
const message = new OSC.Message('/dirt/play', ...keyvals);
const bundle = new OSC.Bundle([message], ts);
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.0.2",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.0.2",
"version": "0.1.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"scripts": {
@ -21,7 +21,9 @@
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": ["Alex McLean <alex@slab.org>"],
"contributors": [
"Alex McLean <alex@slab.org>"
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"

25
packages/react/.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist-ssr
*.local
!dist
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

4
packages/react/README.md Normal file
View File

@ -0,0 +1,4 @@
# @strudel.cycles/react
This package contains react hooks and components for strudel.
Example coming soon

3
packages/react/dist/index.cjs.js vendored Normal file

File diff suppressed because one or more lines are too long

620
packages/react/dist/index.es.js vendored Normal file
View File

@ -0,0 +1,620 @@
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { CodeMirror as CodeMirror$1 } from 'react-codemirror6';
import { EditorView, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import { HighlightStyle, tags } from '@codemirror/highlight';
import { useInView } from 'react-hook-inview';
import { evaluate } from '@strudel.cycles/eval';
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
import { Tone } from '@strudel.cycles/tone';
import { TimeSpan, State } from '@strudel.cycles/core';
import { WebMidi, enableWebMidi } from '@strudel.cycles/midi';
/*
Credits for color palette:
Author: Mattia Astorino (http://github.com/equinusocio)
Website: https://material-theme.site/
*/
const ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
invalid = '#ffffff',
darkBackground = '#21252b',
highlightBackground = 'rgba(0, 0, 0, 0.5)',
// background = '#292d3e',
background = 'transparent',
tooltipBackground = '#353a42',
selection = 'rgba(128, 203, 196, 0.2)',
cursor = '#ffcc00';
/// The editor theme styles for Material Palenight.
const materialPalenightTheme = EditorView.theme(
{
// done
'&': {
color: '#ffffff',
backgroundColor: background,
fontSize: '15px',
'z-index': 11,
},
// done
'.cm-content': {
caretColor: cursor,
lineHeight: '22px',
},
'.cm-line': {
background: '#2C323699',
},
// done
'&.cm-focused .cm-cursor': {
borderLeftColor: cursor,
},
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
backgroundColor: selection,
},
'.cm-panels': { backgroundColor: darkBackground, color: '#ffffff' },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
// done, use onedarktheme
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
outline: '1px solid #515a6b',
},
// done
'.cm-gutters': {
background: '#2C323699',
color: '#676e95',
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: true },
);
/// The highlighting style for code in the Material Palenight theme.
const materialPalenightHighlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#c792ea' },
{ tag: tags.operator, color: '#89ddff' },
{ tag: tags.special(tags.variableName), color: '#eeffff' },
{ tag: tags.typeName, color: '#f07178' },
{ tag: tags.atom, color: '#f78c6c' },
{ tag: tags.number, color: '#ff5370' },
{ tag: tags.definition(tags.variableName), color: '#82aaff' },
{ tag: tags.string, color: '#c3e88d' },
{ tag: tags.special(tags.string), color: '#f07178' },
{ tag: tags.comment, color: stone },
{ tag: tags.variableName, color: '#f07178' },
{ tag: tags.tagName, color: '#ff5370' },
{ tag: tags.bracket, color: '#a2a1a4' },
{ tag: tags.meta, color: '#ffcb6b' },
{ tag: tags.attributeName, color: '#c792ea' },
{ tag: tags.propertyName, color: '#c792ea' },
{ tag: tags.className, color: '#decb6b' },
{ tag: tags.invalid, color: invalid },
]);
/// Extension to enable the Material Palenight theme (both the editor theme and
/// the highlight style).
// : Extension
const materialPalenight = [materialPalenightTheme, materialPalenightHighlightStyle];
const setHighlights = StateEffect.define();
const highlightField = StateField.define({
create() {
return Decoration.none;
},
update(highlights, tr) {
try {
for (let e of tr.effects) {
if (e.is(setHighlights)) {
highlights = Decoration.set(e.value.flatMap((hap) => (hap.context.locations || []).map(({ start, end }) => {
const color = hap.context.color || "#FFCA28";
let from = tr.newDoc.line(start.line).from + start.column;
let to = tr.newDoc.line(end.line).from + end.column;
const l = tr.newDoc.length;
if (from > l || to > l) {
return;
}
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
return mark.range(from, to);
})).filter(Boolean), true);
}
}
return highlights;
} catch (err) {
return highlights;
}
},
provide: (f) => EditorView.decorations.from(f)
});
function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(CodeMirror$1, {
onViewChange: onViewChanged,
style: {
display: "flex",
flexDirection: "column",
flex: "1 0 auto"
},
value,
onChange,
extensions: [
javascript(),
materialPalenight,
highlightField
]
}));
}
/*
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,
};
}
/*
usePostMessage.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/usePostMessage.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/>.
*/
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, defaultSynth, 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]);
// 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, velocity } = event.context;
if (!onTrigger) {
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
} else {
throw new Error('no defaultSynth passed to useRepl.');
}
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
} else {
onTrigger(
time,
event,
currentTime,
1 /* cps */,
event.wholeOrPart().begin.valueOf(),
event.duration.valueOf(),
);
}
} 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, defaultSynth],
),
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) ;
};
const togglePlay = () => {
if (!cycle.started) {
activateCode();
} else {
cycle.stop();
}
};
return {
pending,
code,
setCode,
pattern,
error,
cycle,
setPattern,
dirty,
log,
togglePlay,
setActiveCode,
activateCode,
activeCode,
pushLog,
hash,
};
}
/*
cx.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/cx.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function cx(...classes) {
// : Array<string | undefined>
return classes.filter(Boolean).join(' ');
}
let highlights = []; // actively highlighted events
let lastEnd;
function useHighlighting({ view, pattern, active }) {
useEffect(() => {
if (view) {
if (pattern && active) {
let frame = requestAnimationFrame(updateHighlights);
function updateHighlights() {
try {
const audioTime = Tone.getTransport().seconds;
const span = [lastEnd || audioTime, audioTime + 1 / 60];
lastEnd = audioTime + 1 / 60;
highlights = highlights.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
highlights = highlights.concat(haps); // add potential new onsets
view.dispatch({ effects: setHighlights.of(highlights) }); // highlight all still active + new active haps
} catch (err) {
// console.log('error in updateHighlights', err);
view.dispatch({ effects: setHighlights.of([]) });
}
frame = requestAnimationFrame(updateHighlights);
}
return () => {
cancelAnimationFrame(frame);
};
} else {
highlights = [];
view.dispatch({ effects: setHighlights.of([]) });
}
}
}, [pattern, active, view]);
}
var tailwind = '';
const container = "_container_10e1g_1";
const header = "_header_10e1g_5";
const buttons = "_buttons_10e1g_9";
const button = "_button_10e1g_9";
const buttonDisabled = "_buttonDisabled_10e1g_17";
const error = "_error_10e1g_21";
const body = "_body_10e1g_25";
var styles = {
container: container,
header: header,
buttons: buttons,
button: button,
buttonDisabled: buttonDisabled,
error: error,
body: body
};
function Icon({ type }) {
return /* @__PURE__ */ React.createElement("svg", {
xmlns: "http://www.w3.org/2000/svg",
className: "sc-h-5 sc-w-5",
viewBox: "0 0 20 20",
fill: "currentColor"
}, {
refresh: /* @__PURE__ */ React.createElement("path", {
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",
clipRule: "evenodd"
}),
play: /* @__PURE__ */ React.createElement("path", {
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",
clipRule: "evenodd"
}),
pause: /* @__PURE__ */ React.createElement("path", {
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",
clipRule: "evenodd"
})
}[type]);
}
function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
tune,
defaultSynth,
autolink: false
});
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01
});
const wasVisible = useRef();
const show = useMemo(() => {
if (isVisible || !hideOutsideView) {
wasVisible.current = true;
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
useHighlighting({ view, pattern, active: cycle.started });
return /* @__PURE__ */ React.createElement("div", {
className: styles.container,
ref
}, /* @__PURE__ */ React.createElement("div", {
className: styles.header
}, /* @__PURE__ */ React.createElement("div", {
className: styles.buttons
}, /* @__PURE__ */ React.createElement("button", {
className: cx(styles.button, cycle.started ? "sc-animate-pulse" : ""),
onClick: () => togglePlay()
}, /* @__PURE__ */ React.createElement(Icon, {
type: cycle.started ? "pause" : "play"
})), /* @__PURE__ */ React.createElement("button", {
className: cx(dirty ? styles.button : styles.buttonDisabled),
onClick: () => activateCode()
}, /* @__PURE__ */ React.createElement(Icon, {
type: "refresh"
}))), error && /* @__PURE__ */ React.createElement("div", {
className: styles.error
}, error.message)), /* @__PURE__ */ React.createElement("div", {
className: styles.body
}, show && /* @__PURE__ */ React.createElement(CodeMirror, {
value: code,
onChange: setCode,
onViewChanged: setView
})));
}
/*
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 };
}
export { CodeMirror, MiniRepl, cx, useCycle, useHighlighting, usePostMessage, useRepl, useWebMidi };

1
packages/react/dist/style.css vendored Normal file
View File

@ -0,0 +1 @@
*,: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_10e1g_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(68 76 87 / var(--tw-bg-opacity))}._header_10e1g_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_10e1g_9{display:flex}._button_10e1g_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_10e1g_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_10e1g_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_10e1g_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_10e1g_25{position:relative;overflow:auto}

13
packages/react/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Strudel React Components</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@ -0,0 +1,62 @@
{
"name": "@strudel.cycles/react",
"version": "0.1.2",
"description": "React components for strudel",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
"exports": {
".": {
"require": "./dist/index.cjs.js",
"import": "./dist/index.es.js"
},
"./dist/style.css": "./dist/style.css"
},
"files": [
"dist"
],
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/lang-javascript": "^0.19.0",
"@strudel.cycles/core": "*",
"@strudel.cycles/eval": "^0.1.1",
"@strudel.cycles/tone": "^0.1.1",
"react-codemirror6": "^1.1.0",
"react-hook-inview": "^4.5.0"
},
"peerDependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2",
"@vitejs/plugin-react": "^1.3.0",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.13",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"tailwindcss": "^3.0.24",
"vite": "^2.9.9"
}
}

View File

@ -1,11 +1,12 @@
/*
setupTests.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/setupTests.js>
postcss.config.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/postcss.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/>.
*/
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1,28 @@
import React from 'react';
import { MiniRepl } from './components/MiniRepl';
import 'tailwindcss/tailwind.css';
import { Tone, getDefaultSynth } from '@strudel.cycles/tone';
import { evalScope } from '@strudel.cycles/eval';
const defaultSynth = getDefaultSynth();
evalScope(
Tone,
import('@strudel.cycles/core'),
import('@strudel.cycles/tone'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
);
function App() {
return (
<div>
<MiniRepl tune={`"c3"`} defaultSynth={defaultSynth} />
</div>
);
}
export default App;

View File

@ -1,10 +1,11 @@
import React from 'react';
import { CodeMirror as _CodeMirror } from 'react-codemirror6';
// import { CodeMirrorLite as _CodeMirror } from 'react-codemirror6/dist/lite';
import { EditorView, Decoration } from '@codemirror/view';
import { StateField, StateEffect } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
// import { materialPalenight } from 'codemirror6-themes';
import { materialPalenight } from './themes/material-palenight';
import { materialPalenight } from '../themes/material-palenight';
export const setHighlights = StateEffect.define();
const highlightField = StateField.define({

View File

@ -0,0 +1,33 @@
import React from 'react';
export function Icon({ type }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" className="sc-h-5 sc-w-5" viewBox="0 0 20 20" fill="currentColor">
{
{
refresh: (
<path
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"
clipRule="evenodd"
/>
),
play: (
<path
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"
clipRule="evenodd"
/>
),
pause: (
<path
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"
clipRule="evenodd"
/>
),
}[type]
}
</svg>
);
}

View File

@ -0,0 +1,48 @@
import React, { useState, useMemo, useRef } from 'react';
import { useInView } from 'react-hook-inview';
import useRepl from '../hooks/useRepl.mjs';
import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs';
import CodeMirror6 from './CodeMirror6';
import 'tailwindcss/tailwind.css';
import './style.css';
import styles from './MiniRepl.module.css';
import { Icon } from './Icon';
export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
tune,
defaultSynth,
autolink: false,
});
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01,
});
const wasVisible = useRef();
const show = useMemo(() => {
if (isVisible || !hideOutsideView) {
wasVisible.current = true;
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
useHighlighting({ view, pattern, active: cycle.started });
return (
<div className={styles.container} ref={ref}>
<div className={styles.header}>
<div className={styles.buttons}>
<button className={cx(styles.button, cycle.started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
<Icon type={cycle.started ? 'pause' : 'play'} />
</button>
<button className={cx(dirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
<Icon type="refresh" />
</button>
</div>
{error && <div className={styles.error}>{error.message}</div>}
</div>
<div className={styles.body} >
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</div>
</div>
);
}

View File

@ -0,0 +1,27 @@
.container {
@apply sc-rounded-md sc-overflow-hidden sc-bg-[#444C57];
}
.header {
@apply sc-flex sc-justify-between sc-bg-slate-700 sc-border-t sc-border-slate-500;
}
.buttons {
@apply sc-flex;
}
.button {
@apply sc-cursor-pointer sc-w-16 sc-flex sc-items-center sc-justify-center sc-p-1 sc-bg-slate-700 sc-border-r sc-border-slate-500 sc-text-white hover:sc-bg-slate-600;
}
.buttonDisabled {
@apply sc-cursor-pointer sc-w-16 sc-flex sc-items-center sc-justify-center sc-p-1 sc-bg-slate-600 sc-text-slate-400 sc-cursor-not-allowed;
}
.error {
@apply sc-text-right sc-p-1 sc-text-sm sc-text-red-200;
}
.body {
@apply sc-overflow-auto sc-relative;
}

View File

@ -0,0 +1,3 @@
.cm-editor {
background-color: transparent !important;
}

View File

@ -4,6 +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/>.
*/
export default function cx(...classes) { // : Array<string | undefined>
export default function cx(...classes) {
// : Array<string | undefined>
return classes.filter(Boolean).join(' ');
}

View File

@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { setHighlights } from './CodeMirror6';
import { setHighlights } from '../components/CodeMirror6';
import { Tone } from '@strudel.cycles/tone';
let highlights = []; // actively highlighted events

View File

@ -57,7 +57,14 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
} else {
onTrigger(time, event, currentTime);
onTrigger(
time,
event,
currentTime,
1 /* cps */,
event.wholeOrPart().begin.valueOf(),
event.duration.valueOf(),
);
}
} catch (err) {
console.warn(err);

View File

@ -0,0 +1,10 @@
// import 'tailwindcss/tailwind.css';
export { default as CodeMirror } from './components/CodeMirror6';
export * from './components/MiniRepl';
export { default as useCycle } from './hooks/useCycle';
export { default as useHighlighting } from './hooks/useHighlighting';
export { default as usePostMessage } from './hooks/usePostMessage';
export { default as useRepl } from './hooks/useRepl';
export { default as cx } from './cx';
export { useWebMidi } from './hooks/useWebMidi';

View File

@ -0,0 +1,5 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(<React.StrictMode><App /></React.StrictMode>,document.getElementById('root'))

View File

@ -0,0 +1,11 @@
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
corePlugins: {
preflight: false,
},
prefix: 'sc-',
};

View File

@ -0,0 +1,42 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { peerDependencies, dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react({
jsxRuntime: 'classic',
}),
],
build: {
lib: {
entry: resolve(__dirname, 'src', 'index.js'),
formats: ['es', 'cjs'],
fileName: (ext) => `index.${ext}.js`,
// for UMD name: 'GlobalName'
},
rollupOptions: {
external: [
...Object.keys(peerDependencies),
...Object.keys(dependencies),
// TODO: find out which of below names are obsolete now
'@strudel.cycles/tone',
'@strudel.cycles/eval',
'@strudel.cycles/core',
'@strudel.cycles/core/util.mjs',
'@strudel.cycles/mini',
'@strudel.cycles/tonal',
'@strudel.cycles/midi',
'@strudel.cycles/xen',
'@strudel.cycles/serial',
'@strudel.cycles/webaudio',
'@codemirror/view',
'@codemirror/highlight',
'@codemirror/state'
],
},
target: 'esnext',
},
});

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/serial",
"version": "0.0.6",
"version": "0.1.0",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"repository": {
@ -19,6 +19,5 @@
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {}
"homepage": "https://github.com/tidalcycles/strudel#readme"
}

2
packages/tonal/index.mjs Normal file
View File

@ -0,0 +1,2 @@
import './tonal.mjs';
import './voicings.mjs';

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.0.5",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -1,8 +1,8 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.0.5",
"version": "0.1.1",
"description": "Tonal functions for strudel",
"main": "tonal.mjs",
"main": "index.mjs",
"type": "module",
"scripts": {
"test": "mocha --colors"
@ -25,7 +25,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.5",
"@strudel.cycles/core": "^0.1.0",
"@tonaljs/tonal": "^4.6.5",
"webmidi": "^3.0.15"
}

View File

@ -8,8 +8,8 @@ import { Note, Interval, Scale } from '@tonaljs/tonal';
import { Pattern, mod } from '@strudel.cycles/core';
// transpose note inside scale by offset steps
// function scaleTranspose(scale: string, offset: number, note: string) {
export function scaleTranspose(scale, offset, note) {
// function scaleOffset(scale: string, offset: number, note: string) {
function scaleOffset(scale, offset, note) {
let [tonic, scaleName] = Scale.tokenize(scale);
let { notes } = Scale.get(`${tonic} ${scaleName}`);
notes = notes.map((note) => Note.get(note).pc); // use only pc!
@ -71,7 +71,7 @@ Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
if (typeof hap.value !== 'string') {
throw new Error('can only use scaleTranspose with notes');
}
return hap.withValue(() => scaleTranspose(hap.context.scale, Number(offset), hap.value));
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
});
};
Pattern.prototype._scale = function (scale /* : string */) {
@ -81,7 +81,7 @@ Pattern.prototype._scale = function (scale /* : string */) {
if (!isNaN(asNumber)) {
let [tonic, scaleName] = Scale.tokenize(scale);
const { pc, oct = 3 } = Note.get(tonic);
note = scaleTranspose(pc + ' ' + scaleName, asNumber, pc + oct);
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
}
return hap.withValue(() => note).setContext({ ...hap.context, scale });
});

View File

@ -37,6 +37,7 @@ Pattern.prototype.draw = function (callback, cycleSpan, lookaheadCycles = 1) {
const end = (currentCycle + lookaheadCycles) * cycleSpan;
events = this._asNumber(true) // true = silent error
.query(new State(new TimeSpan(begin, end)))
.filter(Boolean)
.filter((event) => event.part.begin.equals(event.whole.begin));
}
}
@ -47,7 +48,7 @@ Pattern.prototype.draw = function (callback, cycleSpan, lookaheadCycles = 1) {
return this;
};
export const cleanup = () => {
export const cleanupDraw = () => {
const ctx = getDrawContext();
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
if (window.strudelAnimation) {

5
packages/tone/index.mjs Normal file
View File

@ -0,0 +1,5 @@
import './pianoroll.mjs';
export * from './tone.mjs';
export * from './draw.mjs';
export * from './ui.mjs';

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.0.6",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -1,8 +1,8 @@
{
"name": "@strudel.cycles/tone",
"version": "0.0.6",
"version": "0.1.1",
"description": "Tone.js API for strudel",
"main": "tone.mjs",
"main": "index.mjs",
"type": "module",
"repository": {
"type": "git",
@ -22,7 +22,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.5",
"@strudel.cycles/core": "^0.1.0",
"@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1",
"tone": "^14.7.77"

View File

@ -47,7 +47,7 @@ export const backgroundImage = function (src, animateOptions = {}) {
);
};
export const cleanup = () => {
export const cleanupUi = () => {
const container = document.getElementById('code');
if (container) {
container.style = '';

View File

@ -10,7 +10,7 @@ const urlifyFunction = (func) => URL.createObjectURL(new Blob([stringifyFunction
const createWorker = (func) => new Worker(urlifyFunction(func));
// this class is basically the tale of two clocks
class ClockWorker {
export class ClockWorker {
worker;
audioContext;
interval = 0.2; // query span
@ -72,4 +72,3 @@ class ClockWorker {
}
}
export default ClockWorker;

View File

@ -4,6 +4,6 @@ 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/>.
*/
export { default as ClockWorker } from './clockworker.mjs';
export { default as Scheduler } from './scheduler.mjs';
export * from './clockworker.mjs';
export * from './scheduler.mjs';
export * from './webaudio.mjs';

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.0.5",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -1,8 +1,9 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.0.6",
"version": "0.1.1",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
"directories": {
"example": "examples"
},
@ -27,6 +28,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.5"
"@strudel.cycles/core": "^0.1.0"
}
}

View File

@ -4,10 +4,10 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ClockWorker from './clockworker.mjs';
import { ClockWorker } from './clockworker.mjs';
import { State, TimeSpan } from '@strudel.cycles/core';
class Scheduler {
export class Scheduler {
worker;
pattern;
constructor({ audioContext, interval = 0.2, onEvent }) {
@ -41,5 +41,3 @@ class Scheduler {
this.pattern = pat;
}
}
export default Scheduler;

View File

@ -4,16 +4,17 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
// import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
import * as strudel from '@strudel.cycles/core';
import { Tone } from '@strudel.cycles/tone';
const { Pattern, getFrequency, patternify2 } = strudel;
// let audioContext;
let audioContext;
export const getAudioContext = () => {
return Tone.getContext().rawContext;
/* if (!audioContext) {
if (!audioContext) {
audioContext = new AudioContext();
}
return audioContext; */
return audioContext;
};
const lookahead = 0.2;

2
packages/xen/index.mjs Normal file
View File

@ -0,0 +1,2 @@
import './xen.mjs';
import './tune.mjs';

View File

@ -1,8 +1,8 @@
{
"name": "@strudel.cycles/xen",
"version": "0.0.5",
"version": "0.1.1",
"description": "Xenharmonic API for strudel",
"main": "xen.mjs",
"main": "index.mjs",
"scripts": {
"test": "mocha --colors"
},
@ -24,6 +24,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.5"
"@strudel.cycles/core": "^0.1.0"
}
}

41
repl/.gitignore vendored
View File

@ -1,25 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.parcel-cache
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,9 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/src/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Strudel REPL" />
<!-- TODO: add manifest images -->
@ -13,5 +13,6 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

41271
repl/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,65 +1,29 @@
{
"name": "@strudel.cycles/repl",
"version": "0.1.0",
"private": true,
"homepage": "https://strudel.tidalcycles.org",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@codemirror/lang-javascript": "^0.19.0",
"@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^13.5.0",
"codemirror6-themes": "^0.1.2",
"events": "^3.3.0",
"gh-pages": "^3.2.3",
"react": "^17.0.2",
"react-codemirror6": "^1.1.0",
"react-dom": "^17.0.2",
"react-hook-inview": "^4.4.1",
"react-scripts": "5.0.0",
"tone": "^14.7.77",
"web-vitals": "^2.1.4"
},
"version": "0.0.0",
"scripts": {
"start": "react-scripts start",
"build": "BUILD_PATH='../docs' react-scripts build && npm run build-tutorial && npm run add-license",
"dev": "vite",
"start": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "mocha ./src/test --colors",
"snapshot": "cd ./src/ && rm -f ./tunes.snapshot.mjs && node ./shoot.mjs > ./tunes.snapshot.mjs",
"eject": "react-scripts eject",
"tutorial": "parcel src/tutorial/index.html --no-cache",
"build-tutorial": "rm -rf ../docs/tutorial && parcel build src/tutorial/index.html --dist-dir ../docs/tutorial --public-url /tutorial --no-scope-hoist --no-cache",
"add-license": "cat etc/agpl-header.txt ../docs/static/js/*LICENSE.txt > /tmp/strudel-license.txt && cp /tmp/strudel-license.txt ../docs/static/js/*LICENSE.txt",
"predeploy": "npm run build",
"deploy": "gh-pages -d ../docs",
"static": "npx serve ../docs"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@mdx-js/react": "^1.6.22",
"@parcel/transformer-mdx": "^2.3.1",
"@tailwindcss/typography": "^0.5.2",
"autoprefixer": "^10.4.4",
"parcel": "^2.3.1",
"postcss": "^8.4.12",
"process": "^0.11.10",
"serve": "^13.0.2",
"tailwindcss": "^3.0.23"
"@vitejs/plugin-react": "^1.3.0",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.13",
"tailwindcss": "^3.0.24",
"vite": "^2.9.9"
}
}

View File

@ -4,58 +4,24 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import CodeMirror6, { setHighlights } from './CodeMirror6';
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import cx from './cx';
import logo from './logo.svg';
import playStatic from './static.mjs';
import { getDefaultSynth } from '@strudel.cycles/tone';
import * as tunes from './tunes.mjs';
import useRepl from './useRepl.mjs';
import { useWebMidi } from './useWebMidi';
import './App.css';
// eval stuff start
import { evaluate, extend } from '@strudel.cycles/eval';
import * as strudel from '@strudel.cycles/core';
import gist from '@strudel.cycles/core/gist.js';
import { mini } from '@strudel.cycles/mini/mini.mjs';
import { Tone } from '@strudel.cycles/tone';
import * as toneHelpers from '@strudel.cycles/tone/tone.mjs';
import * as voicingHelpers from '@strudel.cycles/tonal/voicings.mjs';
import * as uiHelpers from '@strudel.cycles/tone/ui.mjs';
import * as drawHelpers from '@strudel.cycles/tone/draw.mjs';
import euclid from '@strudel.cycles/core/euclid.mjs';
import '@strudel.cycles/tone/tone.mjs';
import '@strudel.cycles/midi/midi.mjs';
import '@strudel.cycles/tonal/voicings.mjs';
import '@strudel.cycles/tonal/tonal.mjs';
import '@strudel.cycles/xen/xen.mjs';
import '@strudel.cycles/xen/tune.mjs';
import '@strudel.cycles/core/euclid.mjs';
import '@strudel.cycles/core/speak.mjs';
import '@strudel.cycles/tone/pianoroll.mjs';
import '@strudel.cycles/tone/draw.mjs';
import '@strudel.cycles/osc/osc.mjs';
import '@strudel.cycles/webaudio/webaudio.mjs';
import '@strudel.cycles/serial/serial.mjs';
import controls from '@strudel.cycles/core/controls.mjs';
import useHighlighting from './useHighlighting';
extend(
import { evalScope, evaluate } from '@strudel.cycles/eval';
import { CodeMirror, cx, useHighlighting, useRepl, useWebMidi } from '@strudel.cycles/react';
import { getDefaultSynth, cleanupDraw, cleanupUi, Tone } from '@strudel.cycles/tone';
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import './App.css';
import logo from './logo.svg';
import * as tunes from './tunes.mjs';
evalScope(
Tone,
strudel,
strudel.Pattern.prototype.bootstrap(),
controls,
toneHelpers,
voicingHelpers,
drawHelpers,
uiHelpers,
{
gist,
euclid,
mini,
Tone,
},
import('@strudel.cycles/core'),
import('@strudel.cycles/tone'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
);
const initialUrl = window.location.href;
@ -194,8 +160,8 @@ function App() {
const _code = getRandomTune();
console.log('tune', _code); // uncomment this to debug when random code fails
setCode(_code);
drawHelpers.cleanup();
uiHelpers.cleanup();
cleanupDraw();
cleanupUi();
const parsed = await evaluate(_code);
setPattern(parsed.pattern);
setActiveCode(_code);
@ -234,7 +200,7 @@ function App() {
<section className="grow flex flex-col text-gray-100">
<div className="grow relative flex overflow-auto" id="code">
{/* onCursor={markParens} */}
<CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
<span className="z-[20] py-1 px-2 absolute top-0 right-0 text-xs whitespace-pre text-right pointer-events-none">
{!cycle.started ? `press ctrl+enter to play\n` : dirty ? `ctrl+enter to update\n` : 'no changes\n'}
</span>
@ -254,11 +220,11 @@ function App() {
/>
)}
</section>
{!isEmbedded && (
{/* !isEmbedded && (
<button className="fixed right-4 bottom-2 z-[11]" onClick={() => playStatic(code)}>
static
</button>
)}
) */}
</div>
);
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

11
repl/src/main.jsx Normal file
View File

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root'),
);

View File

@ -1,170 +0,0 @@
/*
oldtunes.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/oldtunes.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 const timeCatMini = `stack(
"c3@3 [eb3, g3, [c4 d4]/2]",
"c2 g2",
"[eb4@5 [f4 eb4 d4]@3] [eb4 c4]/2".slow(8)
)`;
export const timeCat = `stack(
timeCat([3, c3], [1, stack(eb3, g3, cat(c4, d4).slow(2))]),
cat(c2, g2),
sequence(
timeCat([5, eb4], [3, cat(f4, eb4, d4)]),
cat(eb4, c4).slow(2)
).slow(4)
)`;
export const shapeShifted = `stack(
sequence(
e5, [b4, c5], d5, [c5, b4],
a4, [a4, c5], e5, [d5, c5],
b4, [r, c5], d5, e5,
c5, a4, a4, r,
[r, d5], [r, f5], a5, [g5, f5],
e5, [r, c5], e5, [d5, c5],
b4, [b4, c5], d5, e5,
c5, a4, a4, r,
).rev(),
sequence(
e2, e3, e2, e3, e2, e3, e2, e3,
a2, a3, a2, a3, a2, a3, a2, a3,
gs2, gs3, gs2, gs3, e2, e3, e2, e3,
a2, a3, a2, a3, a2, a3, b1, c2,
d2, d3, d2, d3, d2, d3, d2, d3,
c2, c3, c2, c3, c2, c3, c2, c3,
b1, b2, b1, b2, e2, e3, e2, e3,
a1, a2, a1, a2, a1, a2, a1, a2,
).rev()
).slow(16)`;
export const tetrisWithFunctions = `stack(sequence(
'e5', sequence('b4', 'c5'), 'd5', sequence('c5', 'b4'),
'a4', sequence('a4', 'c5'), 'e5', sequence('d5', 'c5'),
'b4', sequence(silence, 'c5'), 'd5', 'e5',
'c5', 'a4', 'a4', silence,
sequence(silence, 'd5'), sequence(silence, 'f5'), 'a5', sequence('g5', 'f5'),
'e5', sequence(silence, 'c5'), 'e5', sequence('d5', 'c5'),
'b4', sequence('b4', 'c5'), 'd5', 'e5',
'c5', 'a4', 'a4', silence),
sequence(
'e2', 'e3', 'e2', 'e3', 'e2', 'e3', 'e2', 'e3',
'a2', 'a3', 'a2', 'a3', 'a2', 'a3', 'a2', 'a3',
'g#2', 'g#3', 'g#2', 'g#3', 'e2', 'e3', 'e2', 'e3',
'a2', 'a3', 'a2', 'a3', 'a2', 'a3', 'b1', 'c2',
'd2', 'd3', 'd2', 'd3', 'd2', 'd3', 'd2', 'd3',
'c2', 'c3', 'c2', 'c3', 'c2', 'c3', 'c2', 'c3',
'b1', 'b2', 'b1', 'b2', 'e2', 'e3', 'e2', 'e3',
'a1', 'a2', 'a1', 'a2', 'a1', 'a2', 'a1', 'a2',
)
).slow(16)`;
export const tetris = `stack(
cat(
"e5 [b4 c5] d5 [c5 b4]",
"a4 [a4 c5] e5 [d5 c5]",
"b4 [~ c5] d5 e5",
"c5 a4 a4 ~",
"[~ d5] [~ f5] a5 [g5 f5]",
"e5 [~ c5] e5 [d5 c5]",
"b4 [b4 c5] d5 e5",
"c5 a4 a4 ~"
),
cat(
"e2 e3 e2 e3 e2 e3 e2 e3",
"a2 a3 a2 a3 a2 a3 a2 a3",
"g#2 g#3 g#2 g#3 e2 e3 e2 e3",
"a2 a3 a2 a3 a2 a3 b1 c2",
"d2 d3 d2 d3 d2 d3 d2 d3",
"c2 c3 c2 c3 c2 c3 c2 c3",
"b1 b2 b1 b2 e2 e3 e2 e3",
"a1 a2 a1 a2 a1 a2 a1 a2",
)
).slow(16)`;
export const tetrisMini = `\`[[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5]
[c5 a4 a4 ~]
[[~ d5] [~ f5] a5 [g5 f5]]
[e5 [~ c5] e5 [d5 c5]]
[b4 [b4 c5] d5 e5]
[c5 a4 a4 ~]],
[[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]\`.slow(16)
`;
export const whirlyStrudel = `sequence(e4, [b2, b3], c4)
.every(4, fast(2))
.every(3, slow(1.5))
.fast(slowcat(1.25, 1, 1.5))
.every(2, _ => sequence(e4, r, e3, d4, r))`;
export const giantSteps = `stack(
// melody
cat(
"[F#5 D5] [B4 G4] Bb4 [B4 A4]",
"[D5 Bb4] [G4 Eb4] F#4 [G4 F4]",
"Bb4 [B4 A4] D5 [D#5 C#5]",
"F#5 [G5 F5] Bb5 [F#5 F#5]",
),
// chords
cat(
"[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]",
"[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]",
"Eb^7 [Am7 D7] G^7 [C#m7 F#7]",
"B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]"
).voicings(['E3', 'G4']),
// bass
cat(
"[B2 D2] [G2 Bb2] [Eb2 Bb3] [A2 D2]",
"[G2 Bb2] [Eb2 F#2] [B2 F#2] [F2 Bb2]",
"[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]",
"[B2 F#2] [F2 Bb2] [Eb2 Bb3] [C#2 F#2]"
)
).slow(20);`;
export const transposedChordsHacked = `stack(
"c2 eb2 g2",
"Cm7".voicings(['g2','c4']).slow(2)
).transpose(
slowcat(1, 2, 3, 2).slow(2)
).transpose(5)`;
export const scaleTranspose = `stack(f2, f3, c4, ab4)
.scale(sequence('F minor', 'F harmonic minor').slow(4))
.scaleTranspose(sequence(0, -1, -2, -3).slow(4))
.transpose(sequence(0, 1).slow(16))`;
export const struct = `stack(
"c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]",
"[C^7 A7] [Dm7 G7]".struct("[x@2 x] [~@2 x] [~ x@2]@2 [x ~@2] ~ [~@2 x@4]@2")
.voicings(['G3','A4'])
).slow(4)`;
export const magicSofa = `stack(
"<C^7 F^7 ~> <Dm7 G7 A7 ~>"
.every(2, fast(2))
.voicings(),
"<c2 f2 g2> <d2 g2 a2 e2>"
).slow(1).transpose.slowcat(0, 2, 3, 4)`;
export const primalEnemy = `()=>{
const f = fast("<1 <2 [4 8]>>");
return stack(
"c3,g3,c4".struct("[x ~]*2").apply(f).transpose("<0 <3 [5 [7 [9 [11 13]]]]>>"),
"c2 [c2 ~]*2".tone(synth(osc('sawtooth8')).chain(vol(0.8),out())),
"c1*2".tone(membrane().chain(vol(0.8),out()))
).slow(1)
}`;

View File

@ -116,6 +116,7 @@ const uiHelpersMocked = {
backgroundImage: id,
};
// TODO: refactor to evalScope
extend(
// Tone,
strudel,

View File

@ -1,124 +0,0 @@
import React, { useState } from 'react';
import { Tone } from '@strudel.cycles/tone';
import useRepl from '../useRepl.mjs';
import cx from '../cx';
import useHighlighting from '../useHighlighting';
import { useInView } from 'react-hook-inview';
// eval stuff start
import { extend } from '@strudel.cycles/eval';
import * as strudel from '@strudel.cycles/core';
import gist from '@strudel.cycles/core/gist.js';
import { mini } from '@strudel.cycles/mini/mini.mjs';
import { Tone } from '@strudel.cycles/tone';
import * as toneHelpers from '@strudel.cycles/tone/tone.mjs';
import * as voicingHelpers from '@strudel.cycles/tonal/voicings.mjs';
import * as uiHelpers from '@strudel.cycles/tone/ui.mjs';
import * as drawHelpers from '@strudel.cycles/tone/draw.mjs';
import euclid from '@strudel.cycles/core/euclid.mjs';
import '@strudel.cycles/tone/tone.mjs';
import '@strudel.cycles/midi/midi.mjs';
import '@strudel.cycles/tonal/voicings.mjs';
import '@strudel.cycles/tonal/tonal.mjs';
import '@strudel.cycles/xen/xen.mjs';
import '@strudel.cycles/xen/tune.mjs';
import '@strudel.cycles/core/euclid.mjs';
import '@strudel.cycles/tone/pianoroll.mjs';
import '@strudel.cycles/tone/draw.mjs';
import CodeMirror6 from '../CodeMirror6';
extend(Tone, strudel, strudel.Pattern.prototype.bootstrap(), toneHelpers, voicingHelpers, drawHelpers, uiHelpers, {
gist,
euclid,
mini,
Tone,
});
// eval stuff end
const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.Destination).set({
oscillator: { type: 'triangle' },
envelope: {
release: 0.01,
},
});
// "balanced" | "interactive" | "playback";
// Tone.setContext(new Tone.Context({ latencyHint: 'playback', lookAhead: 1 }));
function MiniRepl({ tune, maxHeight = 500 }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
tune,
defaultSynth,
autolink: false,
});
const lines = code.split('\n').length;
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01,
});
useHighlighting({ view, pattern, active: cycle.started });
return (
<div className="rounded-md overflow-hidden bg-[#444C57]" ref={ref}>
<div className="flex justify-between bg-slate-700 border-t border-slate-500">
<div className="flex">
<button
className={cx(
'w-16 flex items-center justify-center p-1 bg-slate-700 border-r border-slate-500 text-white hover:bg-slate-600',
cycle.started ? 'animate-pulse' : '',
)}
onClick={() => togglePlay()}
>
{!cycle.started ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
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"
clipRule="evenodd"
/>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
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"
clipRule="evenodd"
/>
</svg>
)}
</button>
<button
className={cx(
'w-16 flex items-center justify-center p-1 border-slate-500 hover:bg-slate-600',
dirty
? 'bg-slate-700 border-r border-slate-500 text-white'
: 'bg-slate-600 text-slate-400 cursor-not-allowed',
)}
onClick={() => activateCode()}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
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"
clipRule="evenodd"
/>
</svg>
</button>
</div>
<div className="text-right p-1 text-sm">{error && <span className="text-red-200">{error.message}</span>}</div>{' '}
</div>
<div className="flex space-y-0 overflow-auto relative">
{isVisible && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</div>
{/* <div className="bg-slate-700 border-t border-slate-500 content-right pr-2 text-right">
<a href={`https://strudel.tidalcycles.org/#${hash}`} className="text-white items-center inline-flex">
<span>open in REPL</span>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
</a>
</div> */}
</div>
);
}
export default MiniRepl;

View File

@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="../../public/favicon.ico" />
<link rel="stylesheet" type="text/css" href="./style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Strudel REPL" />
<title>Strudel Tutorial</title>
</head>
<body>
<div id="root"></div>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="Tutorial.js"></script>
</body>
</html>

View File

@ -1,11 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.cm-editor {
background-color: transparent !important;
}
main {
margin: 0 auto;
}

View File

@ -5,7 +5,8 @@ This program is free software: you can redistribute it and/or modify it under th
*/
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
// TODO: find out if leaving out tutorial path works now
content: ['./src/**/*.{js,jsx,ts,tsx}','./tutorial/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
},

10
repl/vite.config.js Normal file
View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
build: {
outDir: '../out',
},
});

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