migrate missing files + fix tests

This commit is contained in:
Felix Roos 2022-05-14 20:51:00 +02:00
parent 645ea30b53
commit 92c17d6376
13 changed files with 416 additions and 2 deletions

View File

@ -1,2 +1,4 @@
- use react 17
- make sure @codemirror/state is installed once (single version)
- make sure @codemirror/state is installed once (single version)
"build": "BUILD_PATH='../docs' react-scripts build && npm run build-tutorial && npm run add-license",

0
repl/.nojekyll Normal file
View File

1
repl/CNAME Normal file
View File

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

30
repl/README.md Normal file
View File

@ -0,0 +1,30 @@
# Strudel REPL
This is the REPL for Strudel. REPL stands for
- Read
- Evaluate
- Play!
- Loop
The REPL is deployed at [strudel.tidalcycles.org](https://strudel.tidalcycles.org/).
## Run REPL locally
```bash
# from project root
npm run setup
npm run repl
```
## Build REPL
```bash
cd repl
npm run build # <- builds repl + tutorial to ../docs
npm run static # <- test static build
```
## TODO vite
- "build": "BUILD_PATH='../docs' react-scripts build && npm run build-tutorial && npm run add-license",

23
repl/etc/agpl-header.txt Normal file
View File

@ -0,0 +1,23 @@
/*
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/>.
*/

15
repl/manifest.json Normal file
View File

@ -0,0 +1,15 @@
{
"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

@ -4,8 +4,18 @@
"version": "0.0.0",
"scripts": {
"dev": "vite",
"start": "vite",
"build": "vite build",
"preview": "vite preview"
"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"
},
"dependencies": {
"react": "^17.0.2",

3
repl/robots.txt Normal file
View File

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

185
repl/src/runtime.mjs Normal file
View File

@ -0,0 +1,185 @@
// this file contains a runtime scope for testing all the tunes
// it mocks all the functions that won't work in node (who are not important for testing values / structure)
// it might require mocking more stuff when tunes added that use other functions
// import * as tunes from './tunes.mjs';
import { evaluate } from '@strudel.cycles/eval';
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/core/speak.mjs'; // window is not defined
// 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';
class MockedNode {
chain() {
return this;
}
connect() {
return this;
}
toDestination() {
return this;
}
set() {
return this;
}
start() {
return this;
}
}
const mockNode = () => new MockedNode();
const id = (x) => x;
const toneHelpersMocked = {
FeedbackDelay: MockedNode,
MembraneSynth: MockedNode,
NoiseSynth: MockedNode,
MetalSynth: MockedNode,
Synth: MockedNode,
PolySynth: MockedNode,
Chorus: MockedNode,
Freeverb: MockedNode,
Gain: MockedNode,
vol: mockNode,
out: id,
osc: id,
adsr: id,
getDestination: id,
players: mockNode,
sampler: mockNode,
synth: mockNode,
piano: mockNode,
polysynth: mockNode,
fmsynth: mockNode,
membrane: mockNode,
noise: mockNode,
metal: mockNode,
lowpass: mockNode,
highpass: mockNode,
};
// tone mock
strudel.Pattern.prototype.tone = function () {
return this;
};
// draw mock
strudel.Pattern.prototype.pianoroll = function () {
return this;
};
// speak mock
strudel.Pattern.prototype.speak = function () {
return this;
};
// webaudio mock
strudel.Pattern.prototype.wave = function () {
return this;
};
strudel.Pattern.prototype.filter = function () {
return this;
};
strudel.Pattern.prototype.adsr = function () {
return this;
};
strudel.Pattern.prototype.out = function () {
return this;
};
// tune mock
strudel.Pattern.prototype.tune = function () {
return this;
};
const uiHelpersMocked = {
backgroundImage: id,
};
extend(
// Tone,
strudel,
strudel.Pattern.prototype.bootstrap(),
toneHelpersMocked,
uiHelpersMocked,
/* controls,
toneHelpers,
voicingHelpers,
drawHelpers,
uiHelpers,
*/
{
// gist,
// euclid,
mini,
// Tone,
},
);
export const queryCode = async (code, cycles = 1) => {
const { pattern } = await evaluate(code);
const haps = pattern.queryArc(0, cycles);
return haps.map((h) => h.showWhole());
};
export const testCycles = {
timeCatMini: 16,
timeCat: 8,
shapeShifted: 16,
tetrisMini: 16,
whirlyStrudel: 16,
swimming: 51,
giantSteps: 20,
giantStepsReggae: 25,
transposedChordsHacked: 8,
scaleTranspose: 16,
struct: 4,
magicSofa: 8,
confusedPhone: 8,
zeldasRescue: 48,
technoDrums: 4,
caverave: 60,
callcenterhero: 22,
primalEnemy: 4,
synthDrums: 4,
sampleDrums: 4,
xylophoneCalling: 60,
sowhatelse: 60,
barryHarris: 64,
wavyKalimba: 64,
jemblung: 12,
risingEnemy: 12,
festivalOfFingers: 16,
festivalOfFingers2: 22,
undergroundPlumber: 20,
bridgeIsOver: 16,
goodTimes: 16,
echoPiano: 8,
sml1: 48,
speakerman: 48,
randomBells: 24,
waa: 16,
waar: 16,
hyperpop: 60,
festivalOfFingers3: 16,
};

11
repl/src/shoot.mjs Normal file
View File

@ -0,0 +1,11 @@
// this script will render all example tunes and log them to the console.
// it is intended to be written to tunes.snapshot.mjs using `npm run snapshot`
import * as tunes from './tunes.mjs';
import { queryCode, testCycles } from './runtime.mjs';
Object.entries(tunes).forEach(([key, code]) => {
queryCode(code, testCycles[key] || 1).then((haps) => {
console.log(`export const ${key} = ${JSON.stringify(haps)}`);
});
});

75
repl/src/static.mjs Normal file
View File

@ -0,0 +1,75 @@
/*
static.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/static.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 { Tone } from '@strudel.cycles/tone';
import { State, TimeSpan } from '@strudel.cycles/core';
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
import { evaluate } from '@strudel.cycles/eval';
import { getDefaultSynth } from '@strudel.cycles/tone';
const defaultSynth = getDefaultSynth();
// this is a test to play back events with as less runtime code as possible..
// the code asks for the number of seconds to prequery
// after the querying is done, the events are scheduled
// after the scheduling is done, the transport is started
// nothing happens while tone.js runs except the schedule callback, which is a thin wrapper around triggerAttackRelease calls
// so all glitches that appear here should have nothing to do with strudel and or the repl
async function playStatic(code) {
Tone.getTransport().cancel();
Tone.getTransport().stop();
let start, took;
const seconds = Number(prompt('How many seconds to run?')) || 60;
start = performance.now();
console.log('evaluating..');
const { pattern: pat } = await evaluate(code);
took = performance.now() - start;
console.log('evaluate took', took, 'ms');
console.log('querying..');
start = performance.now();
const events = pat
?.query(new State(new TimeSpan(0, seconds)))
?.filter((event) => event.part.begin.equals(event.whole.begin))
?.map((event) => ({
time: event.whole.begin.valueOf(),
duration: event.whole.end.sub(event.whole.begin).valueOf(),
value: event.value,
context: event.context,
}));
took = performance.now() - start;
console.log('query took', took, 'ms');
console.log('scheduling..');
start = performance.now();
events.forEach((event) => {
Tone.getTransport().schedule((time) => {
try {
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.');
}
} else {
onTrigger(time, event);
}
} catch (err) {
console.warn(err);
err.message = 'unplayable event: ' + err?.message;
console.error(err);
}
}, event.time);
});
took = performance.now() - start;
console.log('scheduling took', took, 'ms');
console.log('now starting!');
Tone.getTransport().start('+0.5');
}
export default playStatic;

View File

@ -0,0 +1,18 @@
import { queryCode, testCycles } from '../runtime.mjs';
import * as snaps from '../tunes.snapshot.mjs';
import * as tunes from '../tunes.mjs';
import { strict as assert } from 'assert';
async function testTune(key) {
// console.log('test tune', key);
const haps = await queryCode(tunes[key], testCycles[key] || 1);
assert.deepStrictEqual(haps, snaps[key]);
}
describe('renders tunes', () => {
Object.keys(tunes).forEach((key) => {
it(`tune: ${key}`, async () => {
await testTune(key);
});
});
});

File diff suppressed because one or more lines are too long