mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-19 00:56:43 -04:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b765085e0 | |||
| 28b978f95f | |||
| cdf5456b75 | |||
| f40b57ee48 | |||
| d47fd0cf18 | |||
| 40117fbe54 | |||
| 7466d8a1de | |||
| ce09443e48 | |||
| 1718dbe94e | |||
| 69894db206 | |||
| 6e4c873248 | |||
| 64693ffd26 | |||
| 66f8ca72c1 | |||
| c82f7bd8fe | |||
| 34176ab5f8 | |||
| 7f12ce9b45 | |||
| 9e1b9f7f5c | |||
| 23e949de1c | |||
| 00bd60a855 | |||
| dacd9afac0 | |||
| 3fc5bb31d0 | |||
| 78770888a5 | |||
| f5b092acf2 | |||
| ba9562f000 | |||
| aded178ab7 | |||
| 0b5d905120 | |||
| 08abec8fd5 | |||
| 63c23736ad | |||
| f7bd373ce6 | |||
| 8e717d2ea1 | |||
| 5f271ed127 | |||
| d686b65dbe | |||
| c7e882e001 | |||
| f07859996e | |||
| fbc73bc78a | |||
| bd8ad1ed1b | |||
| dc2ff83fa5 | |||
| 13545d147b | |||
| 1b85aa713b |
@@ -24,8 +24,8 @@ There are multiple npm packages you can use to use strudel, or only parts of it,
|
||||
|
||||
- [`core`](./packages/core/): tidal pattern engine
|
||||
- [`mini`](./packages/mini): mini notation parser + core binding
|
||||
- [`eval`](./packages/eval): user code evaluator. syntax sugar + highlighting
|
||||
- [`tone`](./packages/tone): bindings for Tone.js instruments and effects
|
||||
- [`transpiler`](./packages/transpiler): user code transpiler
|
||||
- [`webaudio`](./packages/webaudio): webaudio output
|
||||
- [`osc`](./packages/osc): bindings to communicate via OSC
|
||||
- [`midi`](./packages/midi): webmidi bindings
|
||||
- [`serial`](./packages/serial): webserial bindings
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
export * from './packages/core/index.mjs';
|
||||
export * from './packages/csound/index.mjs';
|
||||
export * from './packages/embed/index.mjs';
|
||||
export * from './packages/eval/index.mjs';
|
||||
export * from './packages/midi/index.mjs';
|
||||
export * from './packages/mini/index.mjs';
|
||||
export * from './packages/osc/index.mjs';
|
||||
@@ -10,8 +9,6 @@ export * from './packages/react/index.mjs';
|
||||
export * from './packages/serial/index.mjs';
|
||||
export * from './packages/soundfonts/index.mjs';
|
||||
export * from './packages/tonal/index.mjs';
|
||||
export * from './packages/tone/index.mjs';
|
||||
export * from './packages/transpiler/index.mjs';
|
||||
export * from './packages/webaudio/index.mjs';
|
||||
export * from './packages/webdirt/index.mjs';
|
||||
export * from './packages/xen/index.mjs';
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { EditorView, keymap, Decoration, lineNumbers, highlightActiveLineGutter } from '@codemirror/view';
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { StateField, StateEffect } from '@codemirror/state';
|
||||
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from '@codemirror/view';
|
||||
import { Drawer, repl } from '@strudel.cycles/core';
|
||||
import { flashField, flash } from './flash.mjs';
|
||||
import { highlightExtension, highlightMiniLocations } from './highlight.mjs';
|
||||
import { oneDark } from './themes/one-dark';
|
||||
import { repl, Drawer } from '@strudel.cycles/core';
|
||||
|
||||
// https://codemirror.net/docs/guide/
|
||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
|
||||
@@ -15,7 +16,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, the
|
||||
theme,
|
||||
javascript(),
|
||||
lineNumbers(),
|
||||
highlightField,
|
||||
highlightExtension,
|
||||
highlightActiveLineGutter(),
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
keymap.of(defaultKeymap),
|
||||
@@ -40,93 +41,6 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, the
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// highlighting
|
||||
//
|
||||
|
||||
export const setHighlights = StateEffect.define();
|
||||
export const highlightField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(highlights, tr) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setHighlights)) {
|
||||
const { haps } = e.value;
|
||||
const marks =
|
||||
haps
|
||||
.map((hap) =>
|
||||
(hap.context.locations || []).map(({ start, end }) => {
|
||||
// const color = hap.context.color || e.value.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; // dont mark outside of range, as it will throw an error
|
||||
}
|
||||
const mark = Decoration.mark({
|
||||
attributes: { style: `outline: 2px solid #FFCA28;` },
|
||||
});
|
||||
return mark.range(from, to);
|
||||
}),
|
||||
)
|
||||
.flat()
|
||||
.filter(Boolean) || [];
|
||||
highlights = Decoration.set(marks, true);
|
||||
}
|
||||
}
|
||||
return highlights;
|
||||
} catch (err) {
|
||||
// console.warn('highlighting error', err);
|
||||
return Decoration.set([]);
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
// helper to simply trigger highlighting for given haps
|
||||
export function highlightHaps(view, haps) {
|
||||
view.dispatch({ effects: setHighlights.of({ haps }) });
|
||||
}
|
||||
|
||||
//
|
||||
// flash
|
||||
//
|
||||
|
||||
export const setFlash = StateEffect.define();
|
||||
const flashField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(flash, tr) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setFlash)) {
|
||||
if (e.value) {
|
||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||
} else {
|
||||
flash = Decoration.set([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flash;
|
||||
} catch (err) {
|
||||
console.warn('flash error', err);
|
||||
return flash;
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
export const flash = (view, ms = 200) => {
|
||||
view.dispatch({ effects: setFlash.of(true) });
|
||||
setTimeout(() => {
|
||||
view.dispatch({ effects: setFlash.of(false) });
|
||||
}, ms);
|
||||
};
|
||||
|
||||
export class StrudelMirror {
|
||||
constructor(options) {
|
||||
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
|
||||
@@ -134,7 +48,7 @@ export class StrudelMirror {
|
||||
|
||||
this.drawer = new Drawer((haps, time) => {
|
||||
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
|
||||
this.highlight(currentFrame);
|
||||
this.highlight(currentFrame, time);
|
||||
onDraw?.(haps, time, currentFrame);
|
||||
}, drawTime);
|
||||
|
||||
@@ -193,7 +107,7 @@ export class StrudelMirror {
|
||||
flash(ms) {
|
||||
flash(this.editor, ms);
|
||||
}
|
||||
highlight(haps) {
|
||||
highlightHaps(this.editor, haps);
|
||||
highlight(haps, time) {
|
||||
highlightMiniLocations(this.editor.view, time, haps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { StateEffect, StateField } from '@codemirror/state';
|
||||
import { Decoration, EditorView } from '@codemirror/view';
|
||||
|
||||
export const setFlash = StateEffect.define();
|
||||
export const flashField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(flash, tr) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setFlash)) {
|
||||
if (e.value && tr.newDoc.length > 0) {
|
||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||
} else {
|
||||
flash = Decoration.set([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flash;
|
||||
} catch (err) {
|
||||
console.warn('flash error', err);
|
||||
return flash;
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
export const flash = (view, ms = 200) => {
|
||||
view.dispatch({ effects: setFlash.of(true) });
|
||||
setTimeout(() => {
|
||||
view.dispatch({ effects: setFlash.of(false) });
|
||||
}, ms);
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
||||
import { Decoration, EditorView } from '@codemirror/view';
|
||||
|
||||
export const setMiniLocations = StateEffect.define();
|
||||
export const showMiniLocations = StateEffect.define();
|
||||
export const updateMiniLocations = (view, locations) => {
|
||||
view.dispatch({ effects: setMiniLocations.of(locations) });
|
||||
};
|
||||
export const highlightMiniLocations = (view, atTime, haps) => {
|
||||
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
|
||||
};
|
||||
|
||||
const miniLocations = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(locations, tr) {
|
||||
if (tr.docChanged) {
|
||||
locations = locations.map(tr.changes);
|
||||
}
|
||||
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setMiniLocations)) {
|
||||
// this is called on eval, with the mini locations obtained from the transpiler
|
||||
// codemirror will automatically remap the marks when the document is edited
|
||||
// create a mark for each mini location, adding the range to the spec to find it later
|
||||
const marks = e.value
|
||||
.filter(([from]) => from < tr.newDoc.length)
|
||||
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
|
||||
.map(
|
||||
(range) =>
|
||||
Decoration.mark({
|
||||
id: range.join(':'),
|
||||
// this green is only to verify that the decoration moves when the document is edited
|
||||
// it will be removed later, so the mark is not visible by default
|
||||
attributes: { style: `background-color: #00CA2880` },
|
||||
}).range(...range), // -> Decoration
|
||||
);
|
||||
|
||||
locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<Decoration>
|
||||
}
|
||||
}
|
||||
|
||||
return locations;
|
||||
},
|
||||
});
|
||||
|
||||
const visibleMiniLocations = StateField.define({
|
||||
create() {
|
||||
return { atTime: 0, haps: new Map() };
|
||||
},
|
||||
update(visible, tr) {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(showMiniLocations)) {
|
||||
// this is called every frame to show the locations that are currently active
|
||||
// we can NOT create new marks because the context.locations haven't changed since eval time
|
||||
// this is why we need to find a way to update the existing decorations, showing the ones that have an active range
|
||||
const haps = new Map();
|
||||
for (let hap of e.value.haps) {
|
||||
for (let { start, end } of hap.context.locations) {
|
||||
let id = `${start}:${end}`;
|
||||
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
|
||||
haps.set(id, hap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visible = { atTime: e.value.atTime, haps };
|
||||
}
|
||||
}
|
||||
|
||||
return visible;
|
||||
},
|
||||
});
|
||||
|
||||
// // Derive the set of decorations from the miniLocations and visibleLocations
|
||||
const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
|
||||
const iterator = state.field(miniLocations).iter();
|
||||
const { haps } = state.field(visibleMiniLocations);
|
||||
const builder = new RangeSetBuilder();
|
||||
|
||||
while (iterator.value) {
|
||||
const {
|
||||
from,
|
||||
to,
|
||||
value: {
|
||||
spec: { id },
|
||||
},
|
||||
} = iterator;
|
||||
|
||||
if (haps.has(id)) {
|
||||
const hap = haps.get(id);
|
||||
const color = hap.context.color ?? 'var(--foreground)';
|
||||
// Get explicit channels for color values
|
||||
/*
|
||||
const swatch = document.createElement('div');
|
||||
swatch.style.color = color;
|
||||
document.body.appendChild(swatch);
|
||||
let channels = getComputedStyle(swatch)
|
||||
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
|
||||
.slice(1)
|
||||
.map((c) => parseFloat(c || 1));
|
||||
document.body.removeChild(swatch);
|
||||
|
||||
// Get percentage of event
|
||||
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
|
||||
channels[3] *= percent;
|
||||
*/
|
||||
|
||||
builder.add(
|
||||
from,
|
||||
to,
|
||||
Decoration.mark({
|
||||
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
|
||||
attributes: { style: `outline: solid 2px ${color}` },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
iterator.next();
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
});
|
||||
|
||||
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './codemirror.mjs';
|
||||
export * from './highlight.mjs';
|
||||
export * from './flash.mjs';
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@strudel/codemirror",
|
||||
"version": "0.8.4",
|
||||
"description": "Codemirror Extensions for Strudel",
|
||||
"main": "codemirror.mjs",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs"
|
||||
|
||||
@@ -7,7 +7,7 @@ export default defineConfig({
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'codemirror.mjs'),
|
||||
entry: resolve(__dirname, 'index.mjs'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||
},
|
||||
|
||||
@@ -15,6 +15,14 @@ export const getDrawContext = (id = 'test-canvas') => {
|
||||
canvas.height = window.innerHeight;
|
||||
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
|
||||
document.body.prepend(canvas);
|
||||
let timeout;
|
||||
window.addEventListener('resize', () => {
|
||||
timeout && clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
return canvas.getContext('2d');
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
evaluate.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/evaluate.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@@ -37,8 +37,12 @@ function safeEval(str, options = {}) {
|
||||
}
|
||||
|
||||
export const evaluate = async (code, transpiler) => {
|
||||
let meta = {};
|
||||
if (transpiler) {
|
||||
code = transpiler(code); // transform syntactically correct js code to semantically usable code
|
||||
// transform syntactically correct js code to semantically usable code
|
||||
const transpiled = transpiler(code);
|
||||
code = transpiled.output;
|
||||
meta = transpiled;
|
||||
}
|
||||
// if no transpiler is given, we expect a single instruction (!wrapExpression)
|
||||
const options = { wrapExpression: !!transpiler };
|
||||
@@ -48,5 +52,5 @@ export const evaluate = async (code, transpiler) => {
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||
}
|
||||
return { mode: 'javascript', pattern: evaluated };
|
||||
return { mode: 'javascript', pattern: evaluated, meta };
|
||||
};
|
||||
|
||||
@@ -472,15 +472,15 @@ export class Pattern {
|
||||
/**
|
||||
* Returns a new pattern with the given location information added to the
|
||||
* context of every hap.
|
||||
* @param {Number} start
|
||||
* @param {Number} end
|
||||
* @param {Number} start start offset
|
||||
* @param {Number} end end offset
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
withLocation(start, end) {
|
||||
withLoc(start, end) {
|
||||
const location = {
|
||||
start: { line: start[0], column: start[1], offset: start[2] },
|
||||
end: { line: end[0], column: end[1], offset: end[2] },
|
||||
start,
|
||||
end,
|
||||
};
|
||||
return this.withContext((context) => {
|
||||
const locations = (context.locations || []).concat([location]);
|
||||
@@ -488,32 +488,6 @@ export class Pattern {
|
||||
});
|
||||
}
|
||||
|
||||
withMiniLocation(start, end) {
|
||||
const offset = {
|
||||
start: { line: start[0], column: start[1], offset: start[2] },
|
||||
end: { line: end[0], column: end[1], offset: end[2] },
|
||||
};
|
||||
return this.withContext((context) => {
|
||||
let locations = context.locations || [];
|
||||
locations = locations.map(({ start, end }) => {
|
||||
const colOffset = start.line === 1 ? offset.start.column : 0;
|
||||
return {
|
||||
start: {
|
||||
...start,
|
||||
line: start.line - 1 + (offset.start.line - 1) + 1,
|
||||
column: start.column - 1 + colOffset,
|
||||
},
|
||||
end: {
|
||||
...end,
|
||||
line: end.line - 1 + (offset.start.line - 1) + 1,
|
||||
column: end.column - 1 + colOffset,
|
||||
},
|
||||
};
|
||||
});
|
||||
return { ...context, locations };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new Pattern, which only returns haps that meet the given test.
|
||||
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
pianoroll.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/pianoroll.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pianoroll.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@@ -198,6 +198,8 @@ export function pianoroll({
|
||||
let from = -cycles * playhead;
|
||||
let to = cycles * (1 - playhead);
|
||||
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
if (timeframeProp) {
|
||||
console.warn('timeframe is deprecated! use from/to instead');
|
||||
from = 0;
|
||||
|
||||
@@ -35,11 +35,10 @@ export function repl({
|
||||
}
|
||||
try {
|
||||
await beforeEval?.({ code });
|
||||
let { pattern } = await _evaluate(code, transpiler);
|
||||
|
||||
let { pattern, meta } = await _evaluate(code, transpiler);
|
||||
logger(`[eval] code updated`);
|
||||
setPattern(pattern, autostart);
|
||||
afterEval?.({ code, pattern });
|
||||
afterEval?.({ code, pattern, meta });
|
||||
return pattern;
|
||||
} catch (err) {
|
||||
// console.warn(`[repl] eval error: ${err.message}`);
|
||||
|
||||
@@ -68,8 +68,9 @@ Pattern.prototype.spiral = function (options = {}) {
|
||||
} = options;
|
||||
|
||||
function spiral({ ctx, time, haps, drawTime }) {
|
||||
ctx.clearRect(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight);
|
||||
const [cx, cy] = [ctx.canvas.width / 2, ctx.canvas.height / 2];
|
||||
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
|
||||
ctx.clearRect(0, 0, w * 2, h * 2);
|
||||
const [cx, cy] = [w / 2, h / 2];
|
||||
const settings = {
|
||||
margin: size / stretch,
|
||||
cx,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
ui.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/ui.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/ui.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/>.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
shift-parser
|
||||
shift-reducer
|
||||
!shift-traverser
|
||||
@@ -1,50 +0,0 @@
|
||||
# @strudel.cycles/eval
|
||||
|
||||
This package contains the strudel code transformer and evaluator.
|
||||
It allows creating strudel patterns from input code that is optimized for minimal keystrokes and human readability.
|
||||
|
||||
## Deprecation Note
|
||||
|
||||
This package will not be developed further. Consider using `@strudel.cycles/transpiler` as a replacement.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm i @strudel.cycles/eval --save
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import { evalScope } from '@strudel.cycles/core';
|
||||
import { evaluate } from '@strudel.cycles/eval';
|
||||
|
||||
evalScope(
|
||||
import('@strudel.cycles/core'),
|
||||
// import other strudel packages here
|
||||
); // add strudel to eval scope
|
||||
|
||||
async function run(code) {
|
||||
const { pattern } = await evaluate(code);
|
||||
const events = pattern.firstCycle();
|
||||
console.log(events.map((e) => e.show()).join('\n'));
|
||||
}
|
||||
|
||||
run('sequence([a3, [b3, c4]])');
|
||||
```
|
||||
|
||||
yields:
|
||||
|
||||
```js
|
||||
(0/1 -> 1/2, 0/1 -> 1/2, a3)
|
||||
(1/2 -> 3/4, 1/2 -> 3/4, b3)
|
||||
(3/4 -> 1/1, 3/4 -> 1/1, c4)
|
||||
```
|
||||
|
||||
[play with @strudel.cycles/eval on codesandbox](https://codesandbox.io/s/strudel-eval-example-ndz1d8?file=/src/index.js)
|
||||
|
||||
## Dev Notes
|
||||
|
||||
shift-traverser is currently monkey patched because its package.json uses estraverse@^4.2.0,
|
||||
which does not support the spread operator (Error: Unknown node type SpreadProperty.).
|
||||
By monkey patched, I mean I copied the source of shift-traverser to a subfolder and installed the dependencies (shift-spec + estraverse@^5.3.0)
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
evaluate.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { evaluate as _evaluate } from '@strudel.cycles/core';
|
||||
import shapeshifter from './shapeshifter.mjs';
|
||||
|
||||
export const evaluate = async (code) => {
|
||||
return _evaluate(code, shapeshifter);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './evaluate.mjs';
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.8.0",
|
||||
"description": "Code evaluator for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "vitest run",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"type": "module",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"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": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"estraverse": "^5.3.0",
|
||||
"shift-ast": "^7.0.0",
|
||||
"shift-codegen": "^8.1.0",
|
||||
"shift-parser": "^8.0.0",
|
||||
"shift-spec": "^2019.0.0",
|
||||
"shift-traverser": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@strudel.cycles/mini": "workspace:*",
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.28.0"
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
/*
|
||||
shapeshifter.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/shapeshifter.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 { parseScriptWithLocation } from './shift-parser/index.js'; // npm module does not work in the browser
|
||||
import traverser from './shift-traverser/index.js'; // npm module does not work in the browser */
|
||||
import { parseScriptWithLocation } from 'shift-parser';
|
||||
import traverser from './shift-traverser/index.js';
|
||||
const { replace } = traverser;
|
||||
import {
|
||||
LiteralStringExpression,
|
||||
IdentifierExpression,
|
||||
CallExpression,
|
||||
StaticMemberExpression,
|
||||
ReturnStatement,
|
||||
ArrayExpression,
|
||||
LiteralNumericExpression,
|
||||
} from 'shift-ast';
|
||||
import shiftCodegen from 'shift-codegen';
|
||||
const codegen = shiftCodegen.default || shiftCodegen; // parcel module resolution fuckup
|
||||
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
const { Pattern } = strudel;
|
||||
|
||||
const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
|
||||
|
||||
const addLocations = true;
|
||||
export const addMiniLocations = true;
|
||||
export const minifyStrings = true;
|
||||
export const wrappedAsync = false; // this is now handled by core evaluate by default
|
||||
export const shouldAddReturn = true;
|
||||
|
||||
export default (_code) => {
|
||||
const { code, addReturn } = wrapAsync(_code);
|
||||
const ast = parseScriptWithLocation(disguiseImports(code));
|
||||
const artificialNodes = [];
|
||||
const parents = [];
|
||||
const shifted = replace(ast.tree, {
|
||||
enter(node, parent) {
|
||||
parents.push(parent);
|
||||
const isSynthetic = parents.some((p) => artificialNodes.includes(p));
|
||||
if (isSynthetic) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// replace template string `xxx` with mini(`xxx`)
|
||||
if (minifyStrings && isBackTickString(node)) {
|
||||
return minifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||
}
|
||||
// allows to use top level strings, which are normally directives... but we don't need directives
|
||||
if (minifyStrings && node.directives?.length === 1 && !node.statements?.length) {
|
||||
const str = new LiteralStringExpression({ value: node.directives[0].rawValue });
|
||||
const wrapped = minifyWithLocation(str, node.directives[0], ast.locations, artificialNodes);
|
||||
return { ...node, directives: [], statements: [wrapped] };
|
||||
}
|
||||
|
||||
// replace double quote string "xxx" with mini('xxx')
|
||||
if (minifyStrings && isStringWithDoubleQuotes(node, ast.locations, code)) {
|
||||
return minifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||
}
|
||||
|
||||
// operator overloading => still not done
|
||||
const operators = {
|
||||
'*': 'fast',
|
||||
'/': 'slow',
|
||||
'&': 'stack',
|
||||
'&&': 'append',
|
||||
};
|
||||
if (
|
||||
node.type === 'BinaryExpression' &&
|
||||
operators[node.operator] &&
|
||||
['LiteralNumericExpression', 'LiteralStringExpression', 'IdentifierExpression'].includes(node.right?.type) &&
|
||||
canBeOverloaded(node.left)
|
||||
) {
|
||||
let arg = node.left;
|
||||
if (node.left.type === 'IdentifierExpression') {
|
||||
arg = wrapFunction('reify', node.left);
|
||||
}
|
||||
return new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
property: operators[node.operator],
|
||||
object: wrapFunction('reify', arg),
|
||||
}),
|
||||
arguments: [node.right],
|
||||
});
|
||||
}
|
||||
|
||||
const isMarkable = isPatternArg(parents) || hasModifierCall(parent);
|
||||
// add to location to pure(x) calls
|
||||
if (node.type === 'CallExpression' && node.callee.name === 'pure') {
|
||||
const literal = node.arguments[0];
|
||||
// const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
|
||||
return reifyWithLocation(literal, node.arguments[0], ast.locations, artificialNodes);
|
||||
}
|
||||
// replace pseudo note variables
|
||||
if (node.type === 'IdentifierExpression') {
|
||||
if (isNote(node.name)) {
|
||||
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
|
||||
if (addLocations && isMarkable) {
|
||||
return reifyWithLocation(new LiteralStringExpression({ value }), node, ast.locations, artificialNodes);
|
||||
}
|
||||
return new LiteralStringExpression({ value });
|
||||
}
|
||||
if (node.name === 'r') {
|
||||
return new IdentifierExpression({ name: 'silence' });
|
||||
}
|
||||
}
|
||||
if (
|
||||
addLocations &&
|
||||
['LiteralStringExpression' /* , 'LiteralNumericExpression' */].includes(node.type) &&
|
||||
isMarkable
|
||||
) {
|
||||
// TODO: to make LiteralNumericExpression work, we need to make sure we're not inside timeCat...
|
||||
return reifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||
}
|
||||
if (addMiniLocations) {
|
||||
return addMiniNotationLocations(node, ast.locations, artificialNodes);
|
||||
}
|
||||
return node;
|
||||
},
|
||||
leave() {
|
||||
parents.pop();
|
||||
},
|
||||
});
|
||||
// add return to last statement (because it's wrapped in an async function artificially)
|
||||
if (shouldAddReturn) {
|
||||
addReturn(shifted);
|
||||
}
|
||||
const generated = undisguiseImports(codegen(shifted));
|
||||
return generated;
|
||||
};
|
||||
|
||||
// renames all import statements to "_mport" as Shift doesn't support dynamic import.
|
||||
// there shouldn't be any side-effects from this as this change does not affect
|
||||
// the syntax & will be undone by the equivalent replace in "undisguiseImports".
|
||||
function disguiseImports(code) {
|
||||
return code.replaceAll('import', '_mport'); // Must be the same length!
|
||||
}
|
||||
|
||||
// Rename the renamed import statements back to "import"
|
||||
function undisguiseImports(code) {
|
||||
return code.replaceAll('_mport', 'import');
|
||||
}
|
||||
|
||||
function wrapAsync(code) {
|
||||
// wrap code in async to make await work on top level => this will create 1 line offset to locations
|
||||
// this is why line offset is -1 in getLocationObject calls below
|
||||
if (wrappedAsync) {
|
||||
code = `(async () => {
|
||||
${code}
|
||||
})()`;
|
||||
}
|
||||
const addReturn = (ast) => {
|
||||
const body = wrappedAsync ? ast.statements[0].expression.callee.body : ast;
|
||||
body.statements = body.statements
|
||||
.slice(0, -1)
|
||||
.concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]);
|
||||
};
|
||||
return {
|
||||
code,
|
||||
addReturn,
|
||||
};
|
||||
}
|
||||
|
||||
function addMiniNotationLocations(node, locations, artificialNodes) {
|
||||
const miniFunctions = ['mini', 'm'];
|
||||
// const isAlreadyWrapped = parent?.type === 'CallExpression' && parent.callee.name === 'withLocationOffset';
|
||||
if (node.type === 'CallExpression' && miniFunctions.includes(node.callee.name)) {
|
||||
// mini('c3')
|
||||
if (node.arguments.length > 1) {
|
||||
// TODO: transform mini(...args) to cat(...args.map(mini)) ?
|
||||
console.warn('multi arg mini locations not supported yet...');
|
||||
return node;
|
||||
}
|
||||
const str = node.arguments[0];
|
||||
return minifyWithLocation(str, str, locations, artificialNodes);
|
||||
}
|
||||
if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property)) {
|
||||
// 'c3'.mini or 'c3'.m
|
||||
return minifyWithLocation(node.object, node, locations, artificialNodes);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function wrapFunction(name, ...args) {
|
||||
return new CallExpression({
|
||||
callee: new IdentifierExpression({ name }),
|
||||
arguments: args,
|
||||
});
|
||||
}
|
||||
|
||||
function isBackTickString(node) {
|
||||
return node.type === 'TemplateExpression' && node.elements.length === 1;
|
||||
}
|
||||
|
||||
function isStringWithDoubleQuotes(node, locations, code) {
|
||||
if (node.type !== 'LiteralStringExpression') {
|
||||
return false;
|
||||
}
|
||||
const loc = locations.get(node);
|
||||
const snippet = code.slice(loc.start.offset, loc.end.offset);
|
||||
return snippet[0] === '"'; // we can trust the end is also ", as the parsing did not fail
|
||||
}
|
||||
|
||||
// returns true if the given parents belong to a pattern argument node
|
||||
// this is used to check if a node should receive a location for highlighting
|
||||
function isPatternArg(parents) {
|
||||
if (!parents.length) {
|
||||
return false;
|
||||
}
|
||||
const ancestors = parents.slice(0, -1);
|
||||
const parent = parents[parents.length - 1];
|
||||
if (isPatternFactory(parent)) {
|
||||
return true;
|
||||
}
|
||||
if (parent?.type === 'ArrayExpression') {
|
||||
return isPatternArg(ancestors);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasModifierCall(parent) {
|
||||
// TODO: modifiers are more than composables, for example every is not composable but should be seen as modifier..
|
||||
// need all prototypes of Pattern
|
||||
return parent?.type === 'StaticMemberExpression';
|
||||
// && Object.keys(Pattern.prototype.composable).includes(parent.property)
|
||||
}
|
||||
const factories = Object.keys(Pattern.prototype.factories).concat(['mini']);
|
||||
|
||||
function isPatternFactory(node) {
|
||||
return node?.type === 'CallExpression' && factories.includes(node.callee.name);
|
||||
}
|
||||
|
||||
function canBeOverloaded(node) {
|
||||
return (node.type === 'IdentifierExpression' && isNote(node.name)) || isPatternFactory(node);
|
||||
// TODO: support sequence(c3).transpose(3).x.y.z
|
||||
}
|
||||
|
||||
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
|
||||
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
|
||||
function reifyWithLocation(literalNode, node, locations, artificialNodes) {
|
||||
const args = getLocationArguments(node, locations);
|
||||
const withLocation = new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
object: wrapFunction('reify', literalNode),
|
||||
property: 'withLocation',
|
||||
}),
|
||||
arguments: args,
|
||||
});
|
||||
artificialNodes.push(withLocation);
|
||||
return withLocation;
|
||||
}
|
||||
|
||||
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
|
||||
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
|
||||
function minifyWithLocation(literalNode, node, locations, artificialNodes) {
|
||||
const args = getLocationArguments(node, locations);
|
||||
const wrapped = wrapFunction('mini', literalNode);
|
||||
if (!addMiniLocations) {
|
||||
artificialNodes.push(wrapped);
|
||||
return wrapped;
|
||||
}
|
||||
const withLocation = new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
object: wrapped,
|
||||
property: 'withMiniLocation',
|
||||
}),
|
||||
arguments: args,
|
||||
});
|
||||
artificialNodes.push(withLocation);
|
||||
return withLocation;
|
||||
}
|
||||
|
||||
function getLocationArguments(node, locations) {
|
||||
const loc = locations.get(node);
|
||||
const lineOffset = wrappedAsync ? -1 : 0;
|
||||
return [
|
||||
new ArrayExpression({
|
||||
elements: [
|
||||
new LiteralNumericExpression({ value: loc.start.line + lineOffset }), // the minus 1 assumes the code has been wrapped in async iife
|
||||
new LiteralNumericExpression({ value: loc.start.column }),
|
||||
new LiteralNumericExpression({ value: loc.start.offset }),
|
||||
],
|
||||
}),
|
||||
new ArrayExpression({
|
||||
elements: [
|
||||
new LiteralNumericExpression({ value: loc.end.line + lineOffset }), // the minus 1 assumes the code has been wrapped in async iife
|
||||
new LiteralNumericExpression({ value: loc.end.column }),
|
||||
new LiteralNumericExpression({ value: loc.end.offset }),
|
||||
],
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
index.js - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/shift-traverser/index.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/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
Copyright (C) 2014 Yusuke Suzuki <utatane.tea@gmail.com>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import _Spec from 'shift-spec';
|
||||
const Spec = _Spec.default || _Spec; // parcel module resolution fuckup
|
||||
// import { version } from '../package.json'
|
||||
|
||||
// Loading uncached estraverse for changing estraverse.Syntax.
|
||||
import _estraverse from 'estraverse';
|
||||
|
||||
const estraverse = _estraverse.cloneEnvironment();
|
||||
|
||||
// Adjust estraverse members.
|
||||
|
||||
Object.keys(estraverse.Syntax)
|
||||
.filter((key) => key !== 'Property')
|
||||
.forEach((key) => {
|
||||
delete estraverse.Syntax[key];
|
||||
delete estraverse.VisitorKeys[key];
|
||||
});
|
||||
|
||||
Object.assign(
|
||||
estraverse.Syntax,
|
||||
Object.keys(Spec).reduce((result, key) => {
|
||||
result[key] = key;
|
||||
return result;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
Object.assign(
|
||||
estraverse.VisitorKeys,
|
||||
Object.keys(Spec).reduce((result, key) => {
|
||||
result[key] = Spec[key].fields.map((field) => field.name);
|
||||
return result;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
// estraverse.version = version;
|
||||
export default estraverse;
|
||||
|
||||
/* vim: set sw=4 ts=4 et tw=80 : */
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
evaluate.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/test/evaluate.test.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 { expect, describe, it } from 'vitest';
|
||||
|
||||
import { evaluate } from '../evaluate.mjs';
|
||||
import { mini } from '@strudel.cycles/mini';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
const { fastcat, evalScope } = strudel;
|
||||
|
||||
describe('evaluate', async () => {
|
||||
await evalScope({ mini }, strudel);
|
||||
const ev = async (code) => (await evaluate(code)).pattern.firstCycleValues;
|
||||
it('Should evaluate strudel functions', async () => {
|
||||
expect(await ev('pure("c3")')).toEqual(['c3']);
|
||||
expect(await ev('cat("c3")')).toEqual(['c3']);
|
||||
expect(await ev('fastcat("c3", "d3")')).toEqual(['c3', 'd3']);
|
||||
expect(await ev('slowcat("c3", "d3")')).toEqual(['c3']);
|
||||
});
|
||||
it('Scope should be extendable', async () => {
|
||||
await evalScope({ myFunction: (...x) => fastcat(...x) });
|
||||
expect(await ev('myFunction("c3", "d3")')).toEqual(['c3', 'd3']);
|
||||
});
|
||||
it('Should evaluate simple double quoted mini notation', async () => {
|
||||
expect(await ev('"c3"')).toEqual(['c3']);
|
||||
expect(await ev('"c3 d3"')).toEqual(['c3', 'd3']);
|
||||
expect(await ev('"<c3 d3>"')).toEqual(['c3']);
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
shapeshifter.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/test/shapeshifter.test.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 { describe, it, expect } from 'vitest';
|
||||
import shapeshifter, { wrappedAsync } from '../shapeshifter.mjs';
|
||||
|
||||
describe('shapeshifter', () => {
|
||||
it('Should shift simple double quote string', () => {
|
||||
if (wrappedAsync) {
|
||||
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
|
||||
} else {
|
||||
expect(shapeshifter('"c3"')).toEqual('return mini("c3").withMiniLocation([1,0,0],[1,4,4])');
|
||||
}
|
||||
});
|
||||
if (wrappedAsync) {
|
||||
it('Should handle dynamic imports', () => {
|
||||
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
|
||||
'const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])',
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { dependencies } from './package.json';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'index.mjs'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
+68
-53
@@ -9,7 +9,7 @@ import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
const randOffset = 0.0003;
|
||||
|
||||
const applyOptions = (parent, code) => (pat, i) => {
|
||||
const applyOptions = (parent, enter) => (pat, i) => {
|
||||
const ast = parent.source_[i];
|
||||
const options = ast.options_;
|
||||
const ops = options?.ops;
|
||||
@@ -23,18 +23,14 @@ const applyOptions = (parent, code) => (pat, i) => {
|
||||
if (!legalTypes.includes(type)) {
|
||||
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
|
||||
}
|
||||
pat = strudel.reify(pat)[type](patternifyAST(amount, code));
|
||||
pat = strudel.reify(pat)[type](enter(amount));
|
||||
break;
|
||||
}
|
||||
case 'bjorklund': {
|
||||
if (op.arguments_.rotation) {
|
||||
pat = pat.euclidRot(
|
||||
patternifyAST(op.arguments_.pulse, code),
|
||||
patternifyAST(op.arguments_.step, code),
|
||||
patternifyAST(op.arguments_.rotation, code),
|
||||
);
|
||||
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
|
||||
} else {
|
||||
pat = pat.euclid(patternifyAST(op.arguments_.pulse, code), patternifyAST(op.arguments_.step, code));
|
||||
pat = pat.euclid(enter(op.arguments_.pulse), enter(op.arguments_.step));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -45,7 +41,7 @@ const applyOptions = (parent, code) => (pat, i) => {
|
||||
break;
|
||||
}
|
||||
case 'tail': {
|
||||
const friend = patternifyAST(op.arguments_.element, code);
|
||||
const friend = enter(op.arguments_.element);
|
||||
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
|
||||
break;
|
||||
}
|
||||
@@ -72,11 +68,14 @@ function resolveReplications(ast) {
|
||||
);
|
||||
}
|
||||
|
||||
export function patternifyAST(ast, code) {
|
||||
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
|
||||
export function patternifyAST(ast, code, onEnter, offset = 0) {
|
||||
onEnter?.(ast);
|
||||
const enter = (node) => patternifyAST(node, code, onEnter, offset);
|
||||
switch (ast.type_) {
|
||||
case 'pattern': {
|
||||
resolveReplications(ast);
|
||||
const children = ast.source_.map((child) => patternifyAST(child, code)).map(applyOptions(ast, code));
|
||||
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
|
||||
const alignment = ast.arguments_.alignment;
|
||||
if (alignment === 'stack') {
|
||||
return strudel.stack(...children);
|
||||
@@ -84,7 +83,7 @@ export function patternifyAST(ast, code) {
|
||||
if (alignment === 'polymeter') {
|
||||
// polymeter
|
||||
const stepsPerCycle = ast.arguments_.stepsPerCycle
|
||||
? patternifyAST(ast.arguments_.stepsPerCycle, code).fmap((x) => strudel.Fraction(x))
|
||||
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
|
||||
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
|
||||
|
||||
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
|
||||
@@ -111,7 +110,7 @@ export function patternifyAST(ast, code) {
|
||||
return pat;
|
||||
}
|
||||
case 'element': {
|
||||
return patternifyAST(ast.source_, code);
|
||||
return enter(ast.source_);
|
||||
}
|
||||
case 'atom': {
|
||||
if (ast.source_ === '~') {
|
||||
@@ -121,66 +120,82 @@ export function patternifyAST(ast, code) {
|
||||
console.warn('no location for', ast);
|
||||
return ast.source_;
|
||||
}
|
||||
const { start, end } = ast.location_;
|
||||
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
|
||||
// the following line expects the shapeshifter append .withMiniLocation
|
||||
// because location_ is only relative to the mini string, but we need it relative to whole code
|
||||
// make sure whitespaces are not part of the highlight:
|
||||
const actual = code?.split('').slice(start.offset, end.offset).join('');
|
||||
const [offsetStart = 0, offsetEnd = 0] = actual
|
||||
? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length)
|
||||
: [];
|
||||
return strudel
|
||||
.pure(value)
|
||||
.withLocation(
|
||||
[start.line, start.column + offsetStart, start.offset + offsetStart],
|
||||
[start.line, end.column - offsetEnd, end.offset - offsetEnd],
|
||||
);
|
||||
if (offset === -1) {
|
||||
// skip location handling (used when getting leaves to avoid confusion)
|
||||
return strudel.pure(value);
|
||||
}
|
||||
const [from, to] = getLeafLocation(code, ast, offset);
|
||||
return strudel.pure(value).withLoc(from, to);
|
||||
}
|
||||
case 'stretch':
|
||||
return patternifyAST(ast.source_, code).slow(patternifyAST(ast.arguments_.amount, code));
|
||||
/* case 'scale':
|
||||
let [tonic, scale] = Scale.tokenize(ast.arguments_.scale);
|
||||
const intervals = Scale.get(scale).intervals;
|
||||
const pattern = patternifyAST(ast.source_);
|
||||
tonic = tonic || 'C4';
|
||||
// console.log('scale', ast, pattern, tonic, scale);
|
||||
console.log('tonic', tonic);
|
||||
return pattern.fmap((step: any) => {
|
||||
step = Number(step);
|
||||
if (isNaN(step)) {
|
||||
console.warn(`scale step "${step}" not a number`);
|
||||
return step;
|
||||
}
|
||||
const octaves = Math.floor(step / intervals.length);
|
||||
const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
|
||||
const index = mod(step, intervals.length); // % with negative numbers. e.g. -1 % 3 = 2
|
||||
const interval = Interval.add(intervals[index], Interval.fromSemitones(octaves * 12));
|
||||
return Note.transpose(tonic, interval || '1P');
|
||||
}); */
|
||||
/* case 'struct':
|
||||
// TODO:
|
||||
return strudel.silence; */
|
||||
return enter(ast.source_).slow(enter(ast.arguments_.amount));
|
||||
default:
|
||||
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
|
||||
return strudel.silence;
|
||||
}
|
||||
}
|
||||
|
||||
// takes quoted mini string + leaf node within, returns source location of node (whitespace corrected)
|
||||
export const getLeafLocation = (code, leaf, globalOffset = 0) => {
|
||||
// value is expected without quotes!
|
||||
const { start, end } = leaf.location_;
|
||||
const actual = code?.split('').slice(start.offset, end.offset).join('');
|
||||
// make sure whitespaces are not part of the highlight
|
||||
const [offsetStart = 0, offsetEnd = 0] = actual
|
||||
? actual.split(leaf.source_).map((p) => p.split('').filter((c) => c === ' ').length)
|
||||
: [];
|
||||
return [start.offset + offsetStart + globalOffset, end.offset - offsetEnd + globalOffset];
|
||||
};
|
||||
|
||||
// takes quoted mini string, returns ast
|
||||
export const mini2ast = (code) => krill.parse(code);
|
||||
|
||||
// takes quoted mini string, returns all nodes that are leaves
|
||||
export const getLeaves = (code) => {
|
||||
const ast = mini2ast(code);
|
||||
let leaves = [];
|
||||
patternifyAST(
|
||||
ast,
|
||||
code,
|
||||
(node) => {
|
||||
if (node.type_ === 'atom') {
|
||||
leaves.push(node);
|
||||
}
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return leaves;
|
||||
};
|
||||
|
||||
// takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes
|
||||
export const getLeafLocations = (code, offset = 0) => {
|
||||
return getLeaves(code).map((l) => getLeafLocation(code, l, offset));
|
||||
};
|
||||
|
||||
// mini notation only (wraps in "")
|
||||
export const mini = (...strings) => {
|
||||
const pats = strings.map((str) => {
|
||||
const code = `"${str}"`;
|
||||
const ast = krill.parse(code);
|
||||
const ast = mini2ast(code);
|
||||
return patternifyAST(ast, code);
|
||||
});
|
||||
return strudel.sequence(...pats);
|
||||
};
|
||||
|
||||
// turns str mini string (without quotes) into pattern
|
||||
// offset is the position of the mini string in the JS code
|
||||
// each leaf node will get .withLoc added
|
||||
// this function is used by the transpiler for double quoted strings
|
||||
export const m = (str, offset) => {
|
||||
const code = `"${str}"`;
|
||||
const ast = mini2ast(code);
|
||||
return patternifyAST(ast, code, null, offset);
|
||||
};
|
||||
|
||||
// includes haskell style (raw krill parsing)
|
||||
export const h = (string) => {
|
||||
const ast = krill.parse(string);
|
||||
// console.log('ast', ast);
|
||||
const ast = mini2ast(string);
|
||||
return patternifyAST(ast, string);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { mini } from '../mini.mjs';
|
||||
import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs';
|
||||
import '@strudel.cycles/core/euclid.mjs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
@@ -185,3 +185,36 @@ describe('mini', () => {
|
||||
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeafLocation', () => {
|
||||
it('gets location of leaf nodes', () => {
|
||||
const code = '"bd sd"';
|
||||
const ast = mini2ast(code);
|
||||
|
||||
const bd = ast.source_[0].source_;
|
||||
expect(getLeafLocation(code, bd)).toEqual([1, 3]);
|
||||
|
||||
const sd = ast.source_[1].source_;
|
||||
expect(getLeafLocation(code, sd)).toEqual([4, 6]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeafLocations', () => {
|
||||
it('gets locations of leaf nodes', () => {
|
||||
expect(getLeafLocations('"bd sd"')).toEqual([
|
||||
[1, 3], // bd columns
|
||||
[4, 6], // sd columns
|
||||
]);
|
||||
expect(getLeafLocations('"bd*2 [sd cp]"')).toEqual([
|
||||
[1, 3], // bd columns
|
||||
[7, 9], // sd columns
|
||||
[10, 12], // cp columns
|
||||
[4, 5], // "2" columns
|
||||
]);
|
||||
expect(getLeafLocations('"bd*<2 3>"')).toEqual([
|
||||
[1, 3], // bd columns
|
||||
[5, 6], // "2" columns
|
||||
[7, 8], // "3" columns
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,9 +82,10 @@ function App() {
|
||||
code,
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime,
|
||||
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
|
||||
});
|
||||
|
||||
useHighlighting({
|
||||
const { setMiniLocations } = useHighlighting({
|
||||
view,
|
||||
pattern,
|
||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/transpiler": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"@strudel/codemirror": "workspace:*",
|
||||
"@uiw/codemirror-themes": "^4.19.16",
|
||||
"@uiw/react-codemirror": "^4.19.16",
|
||||
"react-hook-inview": "^4.5.0"
|
||||
|
||||
@@ -1,100 +1,31 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import _CodeMirror from '@uiw/react-codemirror';
|
||||
import { EditorView, Decoration } from '@codemirror/view';
|
||||
import { StateField, StateEffect } from '@codemirror/state';
|
||||
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
||||
import strudelTheme from '../themes/strudel-theme';
|
||||
import './style.css';
|
||||
import { useCallback } from 'react';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { strudelAutocomplete } from './Autocomplete';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
import _CodeMirror from '@uiw/react-codemirror';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import strudelTheme from '../themes/strudel-theme';
|
||||
import { strudelAutocomplete } from './Autocomplete';
|
||||
import {
|
||||
highlightExtension,
|
||||
flashField,
|
||||
flash,
|
||||
highlightMiniLocations,
|
||||
updateMiniLocations,
|
||||
} from '@strudel/codemirror';
|
||||
import './style.css';
|
||||
|
||||
export const setFlash = StateEffect.define();
|
||||
const flashField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(flash, tr) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setFlash)) {
|
||||
if (e.value) {
|
||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||
} else {
|
||||
flash = Decoration.set([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flash;
|
||||
} catch (err) {
|
||||
console.warn('flash error', err);
|
||||
return flash;
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
export { flash, highlightMiniLocations, updateMiniLocations };
|
||||
|
||||
export const flash = (view) => {
|
||||
view.dispatch({ effects: setFlash.of(true) });
|
||||
setTimeout(() => {
|
||||
view.dispatch({ effects: setFlash.of(false) });
|
||||
}, 200);
|
||||
};
|
||||
|
||||
export 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)) {
|
||||
const { haps } = e.value;
|
||||
const marks =
|
||||
haps
|
||||
.map((hap) =>
|
||||
(hap.context.locations || []).map(({ start, end }) => {
|
||||
const color = hap.context.color || e.value.color;
|
||||
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; // dont mark outside of range, as it will throw an error
|
||||
}
|
||||
let mark;
|
||||
if (color) {
|
||||
mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
|
||||
} else {
|
||||
mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
|
||||
}
|
||||
return mark.range(from, to);
|
||||
}),
|
||||
)
|
||||
.flat()
|
||||
.filter(Boolean) || [];
|
||||
highlights = Decoration.set(marks, true);
|
||||
}
|
||||
}
|
||||
return highlights;
|
||||
} catch (err) {
|
||||
// console.warn('highlighting error', err);
|
||||
return Decoration.set([]);
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
const staticExtensions = [javascript(), highlightField, flashField];
|
||||
const staticExtensions = [javascript(), flashField, highlightExtension];
|
||||
|
||||
export default function CodeMirror({
|
||||
value,
|
||||
onChange,
|
||||
onViewChanged,
|
||||
onSelectionChange,
|
||||
onDocChange,
|
||||
theme,
|
||||
keybindings,
|
||||
isLineNumbersDisplayed,
|
||||
@@ -102,8 +33,6 @@ export default function CodeMirror({
|
||||
isLineWrappingEnabled,
|
||||
fontSize = 18,
|
||||
fontFamily = 'monospace',
|
||||
options,
|
||||
editorDidMount,
|
||||
}) {
|
||||
const handleOnChange = useCallback(
|
||||
(value) => {
|
||||
@@ -121,6 +50,9 @@ export default function CodeMirror({
|
||||
|
||||
const handleOnUpdate = useCallback(
|
||||
(viewUpdate) => {
|
||||
if (viewUpdate.docChanged && onDocChange) {
|
||||
onDocChange?.(viewUpdate);
|
||||
}
|
||||
if (viewUpdate.selectionSet && onSelectionChange) {
|
||||
onSelectionChange?.(viewUpdate.state.selection);
|
||||
}
|
||||
@@ -152,6 +84,8 @@ export default function CodeMirror({
|
||||
return _extensions;
|
||||
}, [keybindings, isAutoCompletionEnabled, isLineWrappingEnabled]);
|
||||
|
||||
const basicSetup = useMemo(() => ({ lineNumbers: isLineNumbersDisplayed }), [isLineNumbersDisplayed]);
|
||||
|
||||
return (
|
||||
<div style={{ fontSize, fontFamily }} className="w-full">
|
||||
<_CodeMirror
|
||||
@@ -161,108 +95,8 @@ export default function CodeMirror({
|
||||
onCreateEditor={handleOnCreateEditor}
|
||||
onUpdate={handleOnUpdate}
|
||||
extensions={extensions}
|
||||
basicSetup={{ lineNumbers: isLineNumbersDisplayed }}
|
||||
basicSetup={basicSetup}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let parenMark;
|
||||
export const markParens = (editor, data) => {
|
||||
const v = editor.getDoc().getValue();
|
||||
const marked = getCurrentParenArea(v, data);
|
||||
parenMark?.clear();
|
||||
parenMark = editor.getDoc().markText(...marked, { css: 'background-color: #00007720' }); //
|
||||
};
|
||||
|
||||
// returns { line, ch } from absolute character offset
|
||||
export function offsetToPosition(offset, code) {
|
||||
const lines = code.split('\n');
|
||||
let line = 0;
|
||||
let ch = 0;
|
||||
for (let i = 0; i < offset; i++) {
|
||||
if (ch === lines[line].length) {
|
||||
line++;
|
||||
ch = 0;
|
||||
} else {
|
||||
ch++;
|
||||
}
|
||||
}
|
||||
return { line, ch };
|
||||
}
|
||||
|
||||
// returns absolute character offset from { line, ch }
|
||||
export function positionToOffset(position, code) {
|
||||
const lines = code.split('\n');
|
||||
if (position.line > lines.length) {
|
||||
// throw new Error('positionToOffset: position.line > lines.length');
|
||||
return 0;
|
||||
}
|
||||
let offset = 0;
|
||||
for (let i = 0; i < position.line; i++) {
|
||||
offset += lines[i].length + 1;
|
||||
}
|
||||
offset += position.ch;
|
||||
return offset;
|
||||
}
|
||||
|
||||
// given code and caret position, the functions returns the indices of the parens we are in
|
||||
export function getCurrentParenArea(code, caretPosition) {
|
||||
const caret = positionToOffset(caretPosition, code);
|
||||
let open, i, begin, end;
|
||||
// walk left
|
||||
i = caret;
|
||||
open = 0;
|
||||
while (i > 0) {
|
||||
if (code[i - 1] === '(') {
|
||||
open--;
|
||||
} else if (code[i - 1] === ')') {
|
||||
open++;
|
||||
}
|
||||
if (open === -1) {
|
||||
break;
|
||||
}
|
||||
i--;
|
||||
}
|
||||
begin = i;
|
||||
// walk right
|
||||
i = caret;
|
||||
open = 0;
|
||||
while (i < code.length) {
|
||||
if (code[i] === '(') {
|
||||
open--;
|
||||
} else if (code[i] === ')') {
|
||||
open++;
|
||||
}
|
||||
if (open === 1) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
end = i;
|
||||
return [begin, end].map((o) => offsetToPosition(o, code));
|
||||
}
|
||||
|
||||
/*
|
||||
export const markEvent = (editor) => (time, event) => {
|
||||
const locs = event.context.locations;
|
||||
if (!locs || !editor) {
|
||||
return;
|
||||
}
|
||||
const col = event.context?.color || '#FFCA28';
|
||||
// mark active event
|
||||
const marks = locs.map(({ start, end }) =>
|
||||
editor.getDoc().markText(
|
||||
{ line: start.line - 1, ch: start.column },
|
||||
{ line: end.line - 1, ch: end.column },
|
||||
//{ css: 'background-color: #FFCA28; color: black' } // background-color is now used by parent marking
|
||||
{ css: 'outline: 1px solid ' + col + '; box-sizing:border-box' },
|
||||
//{ css: `background-color: ${col};border-radius:5px` },
|
||||
),
|
||||
);
|
||||
//Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler...
|
||||
setTimeout(() => {
|
||||
marks.forEach((mark) => mark.clear());
|
||||
// }, '+' + event.duration * 0.5);
|
||||
}, event.duration * 1000);
|
||||
}; */
|
||||
|
||||
@@ -71,6 +71,7 @@ export function MiniRepl({
|
||||
evalOnMount,
|
||||
drawContext,
|
||||
drawTime,
|
||||
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
|
||||
});
|
||||
|
||||
const [view, setView] = useState();
|
||||
@@ -84,7 +85,7 @@ export function MiniRepl({
|
||||
}
|
||||
return isVisible || wasVisible.current;
|
||||
}, [isVisible, hideOutsideView]);
|
||||
useHighlighting({
|
||||
const { setMiniLocations } = useHighlighting({
|
||||
view,
|
||||
pattern,
|
||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { setHighlights } from '../components/CodeMirror6';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { highlightMiniLocations, updateMiniLocations } from '../components/CodeMirror6';
|
||||
const round = (x) => Math.round(x * 1000) / 1000;
|
||||
|
||||
function useHighlighting({ view, pattern, active, getTime }) {
|
||||
const highlights = useRef([]);
|
||||
const lastEnd = useRef(0);
|
||||
|
||||
const [miniLocations, setMiniLocations] = useState([]);
|
||||
useEffect(() => {
|
||||
if (view) {
|
||||
updateMiniLocations(view, miniLocations);
|
||||
}
|
||||
}, [view, miniLocations]);
|
||||
|
||||
useEffect(() => {
|
||||
if (view) {
|
||||
if (pattern && active) {
|
||||
@@ -20,9 +28,9 @@ function useHighlighting({ view, pattern, active, getTime }) {
|
||||
highlights.current = highlights.current.filter((hap) => hap.endClipped > audioTime); // keep only highlights that are still active
|
||||
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
||||
highlights.current = highlights.current.concat(haps); // add potential new onsets
|
||||
view.dispatch({ effects: setHighlights.of({ haps: highlights.current }) }); // highlight all still active + new active haps
|
||||
highlightMiniLocations(view, begin, highlights.current);
|
||||
} catch (err) {
|
||||
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
|
||||
highlightMiniLocations(view, 0, []);
|
||||
}
|
||||
frame = requestAnimationFrame(updateHighlights);
|
||||
});
|
||||
@@ -31,10 +39,12 @@ function useHighlighting({ view, pattern, active, getTime }) {
|
||||
};
|
||||
} else {
|
||||
highlights.current = [];
|
||||
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
|
||||
highlightMiniLocations(view, 0, highlights.current);
|
||||
}
|
||||
}
|
||||
}, [pattern, active, view]);
|
||||
|
||||
return { setMiniLocations };
|
||||
}
|
||||
|
||||
export default useHighlighting;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// import 'tailwindcss/tailwind.css';
|
||||
|
||||
export { default as CodeMirror, flash } from './components/CodeMirror6'; // !SSR
|
||||
export { default as CodeMirror, flash, updateMiniLocations, highlightMiniLocations } from './components/CodeMirror6'; // !SSR
|
||||
export * from './components/MiniRepl'; // !SSR
|
||||
export { default as useHighlighting } from './hooks/useHighlighting'; // !SSR
|
||||
export { default as useStrudel } from './hooks/useStrudel'; // !SSR
|
||||
|
||||
@@ -22,8 +22,6 @@ export default defineConfig({
|
||||
...Object.keys(peerDependencies),
|
||||
...Object.keys(dependencies),
|
||||
// TODO: find out which of below names are obsolete now
|
||||
'@strudel.cycles/tone',
|
||||
'@strudel.cycles/eval',
|
||||
'@strudel.cycles/transpiler',
|
||||
'acorn',
|
||||
'@strudel.cycles/core',
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# @strudel.cycles/tone
|
||||
|
||||
This package adds Tone.js functions to strudel Patterns.
|
||||
|
||||
## Deprecation Note
|
||||
|
||||
This package will not be developed further. Consider using `@strudel.cycles/webaudio` as a replacement.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm i @strudel.cycles/tone --save
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
The following example will create a pattern and play it back with tone.js:
|
||||
|
||||
```js
|
||||
import { sequence, stack, State, TimeSpan } from '@strudel.cycles/core';
|
||||
import { Tone, polysynth, osc, out } from '@strudel.cycles/tone';
|
||||
|
||||
const pattern = sequence('c3', ['eb3', stack('g3', 'bb3')]).tone(polysynth().set(osc('sawtooth4')).chain(out()));
|
||||
|
||||
document.getElementById('play').addEventListener('click', async () => {
|
||||
await Tone.start();
|
||||
Tone.getTransport().stop();
|
||||
const events = pattern.query(new State(new TimeSpan(0, 4))).filter((e) => e.whole.begin.equals(e.part.begin));
|
||||
events.forEach((event) =>
|
||||
Tone.getTransport().schedule((time) => event.context.onTrigger(time, event), event.whole.begin.valueOf()),
|
||||
);
|
||||
Tone.getTransport().start('+0.1');
|
||||
});
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
export * from './tone.mjs';
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.8.0",
|
||||
"description": "Tone.js API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"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": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"tone": "^14.7.77"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.28.0"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
tone.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/test/tone.test.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.mjs';
|
||||
import { pure } from '@strudel.cycles/core';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('tone', () => {
|
||||
it('Should have working tone function', () => {
|
||||
// const s = synth().chain(out()); // TODO: mock audio context?
|
||||
// assert.deepStrictEqual(s, new Tone.Synth().chain(out()));
|
||||
const s = {};
|
||||
expect(pure('c3').tone(s).firstCycleValues).toEqual(['c3']);
|
||||
});
|
||||
});
|
||||
@@ -1,106 +0,0 @@
|
||||
/*
|
||||
tone.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/tone.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 { register } from '@strudel.cycles/core';
|
||||
import * as _Tone from 'tone';
|
||||
|
||||
// import Tone from here, to make sure to get the same AudioContext
|
||||
export const Tone = _Tone;
|
||||
|
||||
const {
|
||||
Filter,
|
||||
Gain,
|
||||
Synth,
|
||||
PolySynth,
|
||||
MembraneSynth,
|
||||
MetalSynth,
|
||||
MonoSynth,
|
||||
AMSynth,
|
||||
DuoSynth,
|
||||
FMSynth,
|
||||
NoiseSynth,
|
||||
PluckSynth,
|
||||
Sampler,
|
||||
getDestination,
|
||||
Players,
|
||||
} = Tone;
|
||||
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
|
||||
|
||||
// "balanced" | "interactive" | "playback";
|
||||
// Tone.setContext(new Tone.Context({ latencyHint: 'playback', lookAhead: 1 }));
|
||||
export const getDefaultSynth = () => {
|
||||
const s = new PolySynth().chain(new Gain(0.5), getDestination());
|
||||
s.set({
|
||||
oscillator: { type: 'triangle' },
|
||||
envelope: {
|
||||
release: 0.01,
|
||||
},
|
||||
});
|
||||
return s;
|
||||
};
|
||||
|
||||
// what about
|
||||
// https://www.charlie-roberts.com/gibberish/playground/
|
||||
|
||||
// with this function, you can play the pattern with any tone synth
|
||||
export const tone = register('tone', function (instrument, pat) {
|
||||
return pat.onTrigger((time, hap) => {
|
||||
let note;
|
||||
let velocity = hap.context?.velocity ?? 0.75;
|
||||
if (instrument instanceof PluckSynth) {
|
||||
note = getPlayableNoteValue(hap);
|
||||
instrument.triggerAttack(note, time);
|
||||
} else if (instrument instanceof NoiseSynth) {
|
||||
instrument.triggerAttackRelease(hap.duration.valueOf(), time); // noise has no value
|
||||
} else if (instrument instanceof Sampler) {
|
||||
note = getPlayableNoteValue(hap);
|
||||
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
|
||||
} else if (instrument instanceof Players) {
|
||||
if (!instrument.has(hap.value)) {
|
||||
throw new Error(`name "${hap.value}" not defined for players`);
|
||||
}
|
||||
const player = instrument.player(hap.value);
|
||||
// velocity ?
|
||||
player.start(time);
|
||||
player.stop(time + hap.duration.valueOf());
|
||||
} else {
|
||||
note = getPlayableNoteValue(hap);
|
||||
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// synth helpers
|
||||
export const amsynth = (options) => new AMSynth(options);
|
||||
export const duosynth = (options) => new DuoSynth(options);
|
||||
export const fmsynth = (options) => new FMSynth(options);
|
||||
export const membrane = (options) => new MembraneSynth(options);
|
||||
export const metal = (options) => new MetalSynth(options);
|
||||
export const monosynth = (options) => new MonoSynth(options);
|
||||
export const noise = (options) => new NoiseSynth(options);
|
||||
export const pluck = (options) => new PluckSynth(options);
|
||||
export const polysynth = (options) => new PolySynth(options);
|
||||
export const sampler = (options, baseUrl) =>
|
||||
new Promise((resolve) => {
|
||||
const s = new Sampler(options, () => resolve(s), baseUrl);
|
||||
});
|
||||
export const players = (options, baseUrl = '') => {
|
||||
options = !baseUrl
|
||||
? options
|
||||
: Object.fromEntries(Object.entries(options).map(([key, value]) => [key, baseUrl + value]));
|
||||
return new Promise((resolve) => {
|
||||
const s = new Players(options, () => resolve(s));
|
||||
});
|
||||
};
|
||||
export const synth = (options) => new Synth(options);
|
||||
|
||||
// effect helpers
|
||||
export const vol = (v) => new Gain(v);
|
||||
export const lowpass = (v) => new Filter(v, 'lowpass');
|
||||
export const highpass = (v) => new Filter(v, 'highpass');
|
||||
export const adsr = (a, d = 0.1, s = 0.4, r = 0.01) => ({ envelope: { attack: a, decay: d, sustain: s, release: r } });
|
||||
export const osc = (type) => ({ oscillator: { type } });
|
||||
export const out = () => getDestination();
|
||||
@@ -1,19 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { dependencies } from './package.json';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'index.mjs'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
@@ -31,6 +31,7 @@
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/mini": "workspace:*",
|
||||
"acorn": "^8.8.1",
|
||||
"escodegen": "^2.0.0",
|
||||
"estree-walker": "^3.0.1"
|
||||
|
||||
@@ -11,22 +11,20 @@ const simple = { wrapAsync: false, addReturn: false, simpleLocs: true };
|
||||
|
||||
describe('transpiler', () => {
|
||||
it('wraps double quote string with mini and adds location', () => {
|
||||
expect(transpiler('"c3"', simple)).toEqual("mini('c3').withMiniLocation(0, 4);");
|
||||
expect(transpiler('stack("c3","bd sd")', simple)).toEqual(
|
||||
"stack(mini('c3').withMiniLocation(6, 10), mini('bd sd').withMiniLocation(11, 18));",
|
||||
);
|
||||
expect(transpiler('"c3"', simple).output).toEqual("m('c3', 0);");
|
||||
expect(transpiler('stack("c3","bd sd")', simple).output).toEqual("stack(m('c3', 6), m('bd sd', 11));");
|
||||
});
|
||||
it('wraps backtick string with mini and adds location', () => {
|
||||
expect(transpiler('`c3`', simple)).toEqual("mini('c3').withMiniLocation(0, 4);");
|
||||
expect(transpiler('`c3`', simple).output).toEqual("m('c3', 0);");
|
||||
});
|
||||
it('replaces note variables with note strings', () => {
|
||||
expect(transpiler('seq(c3, d3)', simple)).toEqual("seq('c3', 'd3');");
|
||||
expect(transpiler('seq(c3, d3)', simple).output).toEqual("seq('c3', 'd3');");
|
||||
});
|
||||
it('keeps tagged template literal as is', () => {
|
||||
expect(transpiler('xxx`c3`', simple)).toEqual('xxx`c3`;');
|
||||
expect(transpiler('xxx`c3`', simple).output).toEqual('xxx`c3`;');
|
||||
});
|
||||
it('supports top level await', () => {
|
||||
expect(transpiler("await samples('xxx');", simple)).toEqual("await samples('xxx');");
|
||||
expect(transpiler("await samples('xxx');", simple).output).toEqual("await samples('xxx');");
|
||||
});
|
||||
/* it('parses dynamic imports', () => {
|
||||
expect(
|
||||
@@ -36,4 +34,12 @@ describe('transpiler', () => {
|
||||
}),
|
||||
).toEqual("const {default: foo} = await import('https://bar.com/foo.js');");
|
||||
}); */
|
||||
it('collections locations', () => {
|
||||
const { miniLocations } = transpiler(`s("bd", "hh oh")`, { ...simple, emitMiniLocations: true });
|
||||
expect(miniLocations).toEqual([
|
||||
[3, 5],
|
||||
[9, 11],
|
||||
[12, 14],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,9 +2,10 @@ import escodegen from 'escodegen';
|
||||
import { parse } from 'acorn';
|
||||
import { walk } from 'estree-walker';
|
||||
import { isNoteWithOctave } from '@strudel.cycles/core';
|
||||
import { getLeafLocations } from '@strudel.cycles/mini';
|
||||
|
||||
export function transpiler(input, options = {}) {
|
||||
const { wrapAsync = false, addReturn = true, simpleLocs = false } = options;
|
||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true } = options;
|
||||
|
||||
let ast = parse(input, {
|
||||
ecmaVersion: 2022,
|
||||
@@ -12,18 +13,27 @@ export function transpiler(input, options = {}) {
|
||||
locations: true,
|
||||
});
|
||||
|
||||
let miniLocations = [];
|
||||
const collectMiniLocations = (value, node) => {
|
||||
const leafLocs = getLeafLocations(`"${value}"`, node.start); // stimmt!
|
||||
//const withOffset = leafLocs.map((offsets) => offsets.map((o) => o + node.start));
|
||||
miniLocations = miniLocations.concat(leafLocs);
|
||||
};
|
||||
|
||||
walk(ast, {
|
||||
enter(node, parent, prop, index) {
|
||||
enter(node, parent /* , prop, index */) {
|
||||
if (isBackTickString(node, parent)) {
|
||||
const { quasis, start, end } = node;
|
||||
const { quasis } = node;
|
||||
const { raw } = quasis[0].value;
|
||||
this.skip();
|
||||
return this.replace(miniWithLocation(raw, node, simpleLocs));
|
||||
emitMiniLocations && collectMiniLocations(raw, node);
|
||||
return this.replace(miniWithLocation(raw, node));
|
||||
}
|
||||
if (isStringWithDoubleQuotes(node)) {
|
||||
const { value, start, end } = node;
|
||||
const { value } = node;
|
||||
this.skip();
|
||||
return this.replace(miniWithLocation(value, node, simpleLocs));
|
||||
emitMiniLocations && collectMiniLocations(value, node);
|
||||
return this.replace(miniWithLocation(value, node));
|
||||
}
|
||||
// TODO: remove pseudo note variables?
|
||||
if (node.type === 'Identifier' && isNoteWithOctave(node.name)) {
|
||||
@@ -47,11 +57,14 @@ export function transpiler(input, options = {}) {
|
||||
argument: expression,
|
||||
};
|
||||
}
|
||||
const output = escodegen.generate(ast);
|
||||
let output = escodegen.generate(ast);
|
||||
if (wrapAsync) {
|
||||
return `(async ()=>{${output}})()`;
|
||||
output = `(async ()=>{${output}})()`;
|
||||
}
|
||||
return output;
|
||||
if (!emitMiniLocations) {
|
||||
return { output };
|
||||
}
|
||||
return { output, miniLocations };
|
||||
}
|
||||
|
||||
function isStringWithDoubleQuotes(node, locations, code) {
|
||||
@@ -66,64 +79,18 @@ function isBackTickString(node, parent) {
|
||||
return node.type === 'TemplateLiteral' && parent.type !== 'TaggedTemplateExpression';
|
||||
}
|
||||
|
||||
function miniWithLocation(value, node, simpleLocs) {
|
||||
let locs;
|
||||
const { start: fromOffset, end: toOffset } = node;
|
||||
if (simpleLocs) {
|
||||
locs = [
|
||||
{
|
||||
type: 'Literal',
|
||||
value: fromOffset,
|
||||
},
|
||||
{
|
||||
type: 'Literal',
|
||||
value: toOffset,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
const {
|
||||
loc: {
|
||||
start: { line: fromLine, column: fromColumn },
|
||||
end: { line: toLine, column: toColumn },
|
||||
},
|
||||
} = node;
|
||||
locs = [
|
||||
{
|
||||
type: 'ArrayExpression',
|
||||
elements: [fromLine, fromColumn, fromOffset].map((value) => ({
|
||||
type: 'Literal',
|
||||
value,
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'ArrayExpression',
|
||||
elements: [toLine, toColumn, toOffset].map((value) => ({
|
||||
type: 'Literal',
|
||||
value,
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
// with location
|
||||
function miniWithLocation(value, node) {
|
||||
const { start: fromOffset } = node;
|
||||
return {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'MemberExpression',
|
||||
object: {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
name: 'mini',
|
||||
},
|
||||
arguments: [{ type: 'Literal', value }],
|
||||
optional: false,
|
||||
},
|
||||
property: {
|
||||
type: 'Identifier',
|
||||
name: 'withMiniLocation',
|
||||
},
|
||||
type: 'Identifier',
|
||||
name: 'm',
|
||||
},
|
||||
arguments: locs,
|
||||
arguments: [
|
||||
{ type: 'Literal', value },
|
||||
{ type: 'Literal', value: fromOffset },
|
||||
],
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="https://strudel.tidalcycles.org/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@strudel/web REPL Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<canvas style="background: #000" width="320" height="320" id="canvas"></canvas><br />
|
||||
<button id="a">A</button>
|
||||
<button id="b">B</button>
|
||||
<button id="c">C</button>
|
||||
<button id="stop">stop</button>
|
||||
<script type="module">
|
||||
import { initStrudel } from '@strudel/web';
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let drawer;
|
||||
let init = initStrudel({
|
||||
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
|
||||
onToggle: async (started) => {
|
||||
const { scheduler } = await init;
|
||||
if (started) {
|
||||
drawer.start(scheduler);
|
||||
} else {
|
||||
drawer.stop();
|
||||
}
|
||||
},
|
||||
}).then(({ scheduler }) => {
|
||||
drawer = new Drawer(
|
||||
(haps, time, { drawTime }) => {
|
||||
scheduler.pattern.context?.onPaint?.(ctx, time, haps, drawTime);
|
||||
},
|
||||
[-2, 2],
|
||||
);
|
||||
return { scheduler };
|
||||
});
|
||||
|
||||
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
|
||||
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev).spiral({size:20})`));
|
||||
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).spiral({ size: 20, steady: 0.9 }).play());
|
||||
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).punchcard().play());
|
||||
click('stop', () => hush());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -29,7 +29,7 @@ export async function defaultPrebake() {
|
||||
let initDone;
|
||||
|
||||
let scheduler;
|
||||
export function initStrudel(options = {}) {
|
||||
export async function initStrudel(options = {}) {
|
||||
initAudioOnFirstClick();
|
||||
miniAllStrings();
|
||||
const { prebake, ...schedulerOptions } = options;
|
||||
@@ -39,6 +39,8 @@ export function initStrudel(options = {}) {
|
||||
await prebake?.();
|
||||
})();
|
||||
scheduler = webaudioScheduler(schedulerOptions);
|
||||
await initDone;
|
||||
return { scheduler };
|
||||
}
|
||||
|
||||
window.initStrudel = initStrudel;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# @strudel.cycles/webdirt
|
||||
|
||||
This package adds [webdirt](https://github.com/dktr0/WebDirt) support to strudel!
|
||||
|
||||
## Deprecation Note
|
||||
|
||||
This package will not be developed further. Consider using `@strudel.cycles/webaudio` as a replacement.
|
||||
@@ -1 +0,0 @@
|
||||
export * from './webdirt.mjs';
|
||||
@@ -1,32 +0,0 @@
|
||||
#/bin/sh
|
||||
printf "{\n"
|
||||
dircount=0
|
||||
# for d in $searchRoot/*; do
|
||||
find $1 -mindepth 1 -maxdepth 1 -iname "*" | sort | while read d; do
|
||||
if [ -d "$d" ]
|
||||
then
|
||||
if [ $dircount -ne 0 ]
|
||||
then
|
||||
printf ",\n"
|
||||
fi
|
||||
(( dircount++ ))
|
||||
dirname=`basename $d`
|
||||
printf "\"%s\": [" "$dirname"
|
||||
search2=$searchRoot/$dirname/*.WAV
|
||||
filecount=0
|
||||
find "$d" -iname "*.wav" | sort | while read f; do
|
||||
# for f in $search2; do
|
||||
filename=$(printf %q "$f")
|
||||
basename=${f##*/}
|
||||
if [[ ${basename:0:1} != "." ]]; then
|
||||
if [ $filecount -ne 0 ]; then
|
||||
printf ","
|
||||
fi
|
||||
(( filecount++ ))
|
||||
printf "\"%s/%s\"" "$dirname" "$basename"
|
||||
fi
|
||||
done
|
||||
printf "]"
|
||||
fi
|
||||
done
|
||||
printf "\n}\n"
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webdirt",
|
||||
"version": "0.8.0",
|
||||
"description": "WebDirt integration for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"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": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"WebDirt": "github:dktr0/WebDirt"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.28.0"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { dependencies } from './package.json';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'index.mjs'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
const { Pattern } = strudel;
|
||||
import * as WebDirt from 'WebDirt';
|
||||
//import { loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio';
|
||||
|
||||
let webDirt;
|
||||
|
||||
/*
|
||||
example config:
|
||||
{
|
||||
sampleMapUrl: 'EmuSP12.json',
|
||||
sampleFolder: 'EmuSP12',
|
||||
}
|
||||
*/
|
||||
export function loadWebDirt(config) {
|
||||
webDirt = new WebDirt.WebDirt(config);
|
||||
webDirt.initializeWebAudio();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Uses [webdirt](https://github.com/dktr0/WebDirt) as output.
|
||||
*
|
||||
* <details>
|
||||
* <summary>show supported Webdirt controls</summary>
|
||||
*
|
||||
* - s :: String, -- name of sample bank
|
||||
* - n :: Int, -- number of sample within a bank
|
||||
* - {@link gain} :: Number, -- clamped from 0 to 2; 1 is default and full-scale
|
||||
* - overgain :: Number, -- additional gain added to gain to go past clamp at 2
|
||||
* - {@link pan} :: Number, -- range: 0 to 1
|
||||
* - nudge :: Number, -- nudge the time of the sample forwards/backwards in seconds
|
||||
* - {@link speed} :: Number, -- speed / pitch of the sample
|
||||
* - {@link unit} :: String
|
||||
* - note :: Number, -- pitch offset in semitones
|
||||
* - {@link begin} :: Number, -- cut from sample start, normalized
|
||||
* - {@link end} :: Number, -- cut from sample end, normalized
|
||||
* - {@link cut} :: Int, -- samples with same cut number will interupt each other
|
||||
* - {@link cutoff} :: Number, -- lowpass filter frequency
|
||||
* - {@link resonance} :: Number, -- lowpass filter resonance
|
||||
* - {@link hcutoff} :: Number, -- highpass filter frequency
|
||||
* - {@link hresonance} :: Number, -- highpass filter resonance
|
||||
* - {@link bandf} :: Number, -- bandpass filter frequency
|
||||
* - {@link bandq} :: Number, -- bandpass filter resonance
|
||||
* - {@link vowel} :: String, -- name of vowel ('a' | 'e' | 'i' | 'o' | 'u')
|
||||
* - delay :: Number, -- delay wet/dry mix
|
||||
* - delaytime :: Number, -- delay time in seconds
|
||||
* - delayfeedback :: Number, -- delay feedback
|
||||
* - {@link loop} :: Number, -- loop sample n times (relative to sample length)
|
||||
* - {@link crush} :: Number, -- bitcrusher (currently not working)
|
||||
* - {@link coarse} :: Number, -- coarse effect (currently not working)
|
||||
* - {@link shape} :: Number, -- (currently not working)
|
||||
|
||||
*
|
||||
* </details>
|
||||
*
|
||||
* @name webdirt
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("bd*2 hh sd hh").n("<0 1>").webdirt()
|
||||
* @noAutocomplete
|
||||
*/
|
||||
Pattern.prototype.webdirt = function () {
|
||||
throw new Error('webdirt support has been dropped..');
|
||||
// create a WebDirt object and initialize Web Audio context
|
||||
/* return this.onTrigger(async (time, e, currentTime) => {
|
||||
if (!webDirt) {
|
||||
throw new Error('WebDirt not initialized!');
|
||||
}
|
||||
const deadline = time - currentTime;
|
||||
const { s, n = 0, ...rest } = e.value || {};
|
||||
if (!s) {
|
||||
console.warn('Pattern.webdirt: no "s" was set!');
|
||||
}
|
||||
const samples = getLoadedSamples();
|
||||
if (!samples?.[s]) {
|
||||
// try default samples
|
||||
webDirt.playSample({ s, n, ...rest }, deadline);
|
||||
return;
|
||||
}
|
||||
if (!samples?.[s]) {
|
||||
console.warn(`Pattern.webdirt: sample "${s}" not found in loaded samples`, samples);
|
||||
} else {
|
||||
const bank = samples[s];
|
||||
const sampleUrl = bank[n % bank.length];
|
||||
const buffer = getLoadedBuffer(sampleUrl);
|
||||
if (!buffer) {
|
||||
console.log(`Pattern.webdirt: load ${s}:${n} from ${sampleUrl}`);
|
||||
loadBuffer(sampleUrl, webDirt.ac);
|
||||
} else {
|
||||
const msg = { buffer: { buffer }, ...rest };
|
||||
webDirt.playSample(msg, deadline);
|
||||
}
|
||||
}
|
||||
}); */
|
||||
};
|
||||
Generated
+7
-197
@@ -178,40 +178,6 @@ importers:
|
||||
|
||||
packages/embed: {}
|
||||
|
||||
packages/eval:
|
||||
dependencies:
|
||||
'@strudel.cycles/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
estraverse:
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
shift-ast:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
shift-codegen:
|
||||
specifier: ^8.1.0
|
||||
version: 8.1.0
|
||||
shift-parser:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0
|
||||
shift-spec:
|
||||
specifier: ^2019.0.0
|
||||
version: 2019.0.0
|
||||
shift-traverser:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
devDependencies:
|
||||
'@strudel.cycles/mini':
|
||||
specifier: workspace:*
|
||||
version: link:../mini
|
||||
vite:
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3(@types/node@18.16.3)
|
||||
vitest:
|
||||
specifier: ^0.28.0
|
||||
version: 0.28.0(@vitest/ui@0.28.0)
|
||||
|
||||
packages/midi:
|
||||
dependencies:
|
||||
'@strudel.cycles/core':
|
||||
@@ -292,6 +258,9 @@ importers:
|
||||
'@strudel.cycles/webaudio':
|
||||
specifier: workspace:*
|
||||
version: link:../webaudio
|
||||
'@strudel/codemirror':
|
||||
specifier: workspace:*
|
||||
version: link:../codemirror
|
||||
'@uiw/codemirror-themes':
|
||||
specifier: ^4.19.16
|
||||
version: 4.19.16(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
|
||||
@@ -439,27 +408,14 @@ importers:
|
||||
specifier: ^0.28.0
|
||||
version: 0.28.0(@vitest/ui@0.28.0)
|
||||
|
||||
packages/tone:
|
||||
dependencies:
|
||||
'@strudel.cycles/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
tone:
|
||||
specifier: ^14.7.77
|
||||
version: 14.7.77
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3(@types/node@18.16.3)
|
||||
vitest:
|
||||
specifier: ^0.28.0
|
||||
version: 0.28.0(@vitest/ui@0.28.0)
|
||||
|
||||
packages/transpiler:
|
||||
dependencies:
|
||||
'@strudel.cycles/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@strudel.cycles/mini':
|
||||
specifier: workspace:*
|
||||
version: link:../mini
|
||||
acorn:
|
||||
specifier: ^8.8.1
|
||||
version: 8.8.2
|
||||
@@ -522,25 +478,6 @@ importers:
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3(@types/node@18.16.3)
|
||||
|
||||
packages/webdirt:
|
||||
dependencies:
|
||||
'@strudel.cycles/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@strudel.cycles/webaudio':
|
||||
specifier: workspace:*
|
||||
version: link:../webaudio
|
||||
WebDirt:
|
||||
specifier: github:dktr0/WebDirt
|
||||
version: github.com/dktr0/WebDirt/d2ce441e82a11036455e93643af5938a844eee27
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3(@types/node@18.16.3)
|
||||
vitest:
|
||||
specifier: ^0.28.0
|
||||
version: 0.28.0(@vitest/ui@0.28.0)
|
||||
|
||||
packages/xen:
|
||||
dependencies:
|
||||
'@strudel.cycles/core':
|
||||
@@ -4619,7 +4556,7 @@ packages:
|
||||
'@babel/plugin-transform-react-jsx-self': 7.21.0(@babel/core@7.21.5)
|
||||
'@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.21.5)
|
||||
react-refresh: 0.14.0
|
||||
vite: 4.3.3
|
||||
vite: 4.3.3(@types/node@18.16.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -6777,11 +6714,6 @@ packages:
|
||||
dependencies:
|
||||
estraverse: 5.3.0
|
||||
|
||||
/estraverse@4.2.0:
|
||||
resolution: {integrity: sha512-VHvyaGnJy+FuGfcfaM7W7OZw4mQiKW73jPHwQXx2VnMSUBajYmytOT5sKEfsBvNPtGX6YDwcrGDz2eocoHg0JA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/estraverse@5.3.0:
|
||||
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
||||
engines: {node: '>=4.0'}
|
||||
@@ -9785,10 +9717,6 @@ packages:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
dev: true
|
||||
|
||||
/multimap@1.1.0:
|
||||
resolution: {integrity: sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw==}
|
||||
dev: false
|
||||
|
||||
/multimatch@5.0.0:
|
||||
resolution: {integrity: sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -11836,56 +11764,6 @@ packages:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
/shift-ast@7.0.0:
|
||||
resolution: {integrity: sha512-O0INwsZa1XH/lMSf52udGnjNOxKBLxFiZHt0Ys3i6bqtwuGEA3eDR4+e0qJELIsCy8+BiTtlTgQzP76K1ehipQ==}
|
||||
dev: false
|
||||
|
||||
/shift-codegen@8.1.0:
|
||||
resolution: {integrity: sha512-hV48SiFM0pgTLCueh0iwqbvqElXPtZL69nb+3eXOU3iZZnLP+AlBQSLKKvHSPr/Onmk0lcUEkAM7RA6V6Wj1GQ==}
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
object-assign: 4.1.1
|
||||
shift-reducer: 7.0.0
|
||||
dev: false
|
||||
|
||||
/shift-parser@8.0.0:
|
||||
resolution: {integrity: sha512-IShW1wGhvA5e+SPNVQ+Dwi/Be6651F2jZc6wwYHbYW7PiswAYfvR/v3Q+CjjxsVCna5L6J5OtR6y+tkkCzvCfw==}
|
||||
dependencies:
|
||||
multimap: 1.1.0
|
||||
shift-ast: 7.0.0
|
||||
shift-reducer: 7.0.0
|
||||
shift-regexp-acceptor: 3.0.0
|
||||
dev: false
|
||||
|
||||
/shift-reducer@7.0.0:
|
||||
resolution: {integrity: sha512-9igIDMHzp1+CkQZITGHM1sAd9jqMPV0vhqHuh8jlYumHSMIwsYcrDeo1tlpzNRUnfbEq1nLyh8Bf1YU8HGUE7g==}
|
||||
dependencies:
|
||||
shift-ast: 7.0.0
|
||||
dev: false
|
||||
|
||||
/shift-regexp-acceptor@3.0.0:
|
||||
resolution: {integrity: sha512-98UKizBjHY6SjjLUr51YYw4rtR+vxjGFm8znqNsoahesAI8Y9+WVAyiBCxxkov1KSDhW0Wz8FwwUqHnlFnjdUg==}
|
||||
dependencies:
|
||||
unicode-match-property-ecmascript: 1.0.4
|
||||
unicode-match-property-value-ecmascript: 1.0.2
|
||||
unicode-property-aliases-ecmascript: 1.0.4
|
||||
dev: false
|
||||
|
||||
/shift-spec@2018.0.0:
|
||||
resolution: {integrity: sha512-/aiPOkj7dbe+CV2VZhIMTHQToZmgniofpRG7Yr7x2/0sO6CSVC++py1Wzf+s+rWSTDHKcLvziVAxjRRV4i4EoQ==}
|
||||
dev: false
|
||||
|
||||
/shift-spec@2019.0.0:
|
||||
resolution: {integrity: sha512-vYfKl+afWPUj/wfr5T/+mdYvWx0nn8LY6hVdfZmFENdGEBpAfQyOTo4/5i+rs8mj+Jz4+0MnsP4vXagjEoHfEw==}
|
||||
dev: false
|
||||
|
||||
/shift-traverser@1.0.0:
|
||||
resolution: {integrity: sha512-DMY3512wJbdC+IC+nhLH3/Stgr2BbxbNcg7qyZ6+e5qNnNs8TBQJWdMsRgHlX1JXwF4C0ONKS8VUxsPT0Tf7aw==}
|
||||
dependencies:
|
||||
estraverse: 4.2.0
|
||||
shift-spec: 2018.0.0
|
||||
dev: false
|
||||
|
||||
/shiki@0.11.1:
|
||||
resolution: {integrity: sha512-EugY9VASFuDqOexOgXR18ZV+TbFrQHeCpEYaXamO+SZlsnT/2LxuLBX25GGtIrwaEVFXUAbUQ601SWE2rMwWHA==}
|
||||
dependencies:
|
||||
@@ -12618,13 +12496,6 @@ packages:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
|
||||
/tone@14.7.77:
|
||||
resolution: {integrity: sha512-tCfK73IkLHyzoKUvGq47gyDyxiKLFvKiVCOobynGgBB9Dl0NkxTM2p+eRJXyCYrjJwy9Y0XCMqD3uOYsYt2Fdg==}
|
||||
dependencies:
|
||||
standardized-audio-context: 25.3.37
|
||||
tslib: 2.5.0
|
||||
dev: false
|
||||
|
||||
/totalist@3.0.0:
|
||||
resolution: {integrity: sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -12874,24 +12745,11 @@ packages:
|
||||
/unherit@3.0.1:
|
||||
resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==}
|
||||
|
||||
/unicode-canonical-property-names-ecmascript@1.0.4:
|
||||
resolution: {integrity: sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/unicode-canonical-property-names-ecmascript@2.0.0:
|
||||
resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/unicode-match-property-ecmascript@1.0.4:
|
||||
resolution: {integrity: sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
unicode-canonical-property-names-ecmascript: 1.0.4
|
||||
unicode-property-aliases-ecmascript: 1.0.4
|
||||
dev: false
|
||||
|
||||
/unicode-match-property-ecmascript@2.0.0:
|
||||
resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -12900,21 +12758,11 @@ packages:
|
||||
unicode-property-aliases-ecmascript: 2.1.0
|
||||
dev: true
|
||||
|
||||
/unicode-match-property-value-ecmascript@1.0.2:
|
||||
resolution: {integrity: sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/unicode-match-property-value-ecmascript@2.1.0:
|
||||
resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==}
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/unicode-property-aliases-ecmascript@1.0.4:
|
||||
resolution: {integrity: sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/unicode-property-aliases-ecmascript@2.1.0:
|
||||
resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -13233,38 +13081,6 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/vite@4.3.3:
|
||||
resolution: {integrity: sha512-MwFlLBO4udZXd+VBcezo3u8mC77YQk+ik+fbc0GZWGgzfbPP+8Kf0fldhARqvSYmtIWoAJ5BXPClUbMTlqFxrA==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': '>= 14'
|
||||
less: '*'
|
||||
sass: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.4.0
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
dependencies:
|
||||
esbuild: 0.17.18
|
||||
postcss: 8.4.23
|
||||
rollup: 3.21.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/vite@4.3.3(@types/node@18.11.18):
|
||||
resolution: {integrity: sha512-MwFlLBO4udZXd+VBcezo3u8mC77YQk+ik+fbc0GZWGgzfbPP+8Kf0fldhARqvSYmtIWoAJ5BXPClUbMTlqFxrA==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
@@ -13923,9 +13739,3 @@ packages:
|
||||
|
||||
/zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
|
||||
github.com/dktr0/WebDirt/d2ce441e82a11036455e93643af5938a844eee27:
|
||||
resolution: {tarball: https://codeload.github.com/dktr0/WebDirt/tar.gz/d2ce441e82a11036455e93643af5938a844eee27}
|
||||
name: webdirt
|
||||
version: 1.0.1
|
||||
dev: false
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+4421
-4421
File diff suppressed because it is too large
Load Diff
+3
-9
@@ -3,28 +3,21 @@
|
||||
// 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 { evaluate } from '@strudel.cycles/transpiler';
|
||||
import { evalScope } from '@strudel.cycles/core';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
import * as webaudio from '@strudel.cycles/webaudio';
|
||||
import controls from '@strudel.cycles/core/controls.mjs';
|
||||
// import gist from '@strudel.cycles/core/gist.js';
|
||||
import { mini } from '@strudel.cycles/mini/mini.mjs';
|
||||
// import * as toneHelpers from '@strudel.cycles/tone/tone.mjs';
|
||||
import { mini, m } from '@strudel.cycles/mini/mini.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 * as tonalHelpers from '@strudel.cycles/tonal';
|
||||
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';
|
||||
@@ -174,6 +167,7 @@ evalScope(
|
||||
csound: id,
|
||||
loadOrc: id,
|
||||
mini,
|
||||
m,
|
||||
getDrawContext,
|
||||
getAudioContext,
|
||||
loadSoundfont,
|
||||
@@ -187,7 +181,7 @@ evalScope(
|
||||
|
||||
export const queryCode = async (code, cycles = 1) => {
|
||||
const { pattern } = await evaluate(code);
|
||||
const haps = pattern.queryArc(0, cycles);
|
||||
const haps = pattern.sortHapsByPart().queryArc(0, cycles);
|
||||
return haps.map((h) => h.show(true));
|
||||
};
|
||||
|
||||
|
||||
@@ -156,17 +156,6 @@
|
||||
],
|
||||
"/home/felix/projects/strudel/packages/core/gist.js": [],
|
||||
"/home/felix/projects/strudel/packages/core/index.mjs": [],
|
||||
"/home/felix/projects/strudel/packages/eval/shift-traverser/index.js": [],
|
||||
"/home/felix/projects/strudel/packages/eval/shapeshifter.mjs": [
|
||||
"addMiniLocations",
|
||||
"minifyStrings",
|
||||
"wrappedAsync",
|
||||
"shouldAddReturn"
|
||||
],
|
||||
"/home/felix/projects/strudel/packages/eval/evaluate.mjs": [
|
||||
"evaluate"
|
||||
],
|
||||
"/home/felix/projects/strudel/packages/eval/index.mjs": [],
|
||||
"/home/felix/projects/strudel/packages/midi/midi.mjs": [
|
||||
"WebMidi",
|
||||
"enableWebMidi"
|
||||
@@ -199,29 +188,6 @@
|
||||
"setVoicingRange"
|
||||
],
|
||||
"/home/felix/projects/strudel/packages/tonal/index.mjs": [],
|
||||
"/home/felix/projects/strudel/packages/tone/tone.mjs": [
|
||||
"Tone",
|
||||
"getDefaultSynth",
|
||||
"tone",
|
||||
"amsynth",
|
||||
"duosynth",
|
||||
"fmsynth",
|
||||
"membrane",
|
||||
"metal",
|
||||
"monosynth",
|
||||
"noise",
|
||||
"pluck",
|
||||
"polysynth",
|
||||
"sampler",
|
||||
"players",
|
||||
"synth",
|
||||
"vol",
|
||||
"lowpass",
|
||||
"highpass",
|
||||
"adsr",
|
||||
"out"
|
||||
],
|
||||
"/home/felix/projects/strudel/packages/tone/index.mjs": [],
|
||||
"/home/felix/projects/strudel/packages/transpiler/transpiler.mjs": [
|
||||
"transpiler"
|
||||
],
|
||||
@@ -251,10 +217,6 @@
|
||||
"webaudioOutputTrigger"
|
||||
],
|
||||
"/home/felix/projects/strudel/packages/webaudio/index.mjs": [],
|
||||
"/home/felix/projects/strudel/packages/webdirt/webdirt.mjs": [
|
||||
"loadWebDirt"
|
||||
],
|
||||
"/home/felix/projects/strudel/packages/webdirt/index.mjs": [],
|
||||
"/home/felix/projects/strudel/packages/xen/xen.mjs": [
|
||||
"edo",
|
||||
"xen",
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
# Old APIs
|
||||
|
||||
These APIs are outdated and might break in the future.
|
||||
|
||||
## Webdirt API (deprecated)
|
||||
|
||||
You can use the powerful sampling engine [Webdirt](https://github.com/dktr0/WebDirt) with Strudel.
|
||||
|
||||
{{ 'Pattern.webdirt' | jsdoc }}
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## Tone API (deprecated)
|
||||
|
||||
The Tone API uses Tone.js instruments ands effects to create sounds.
|
||||
|
||||
<MiniRepl
|
||||
tune={`stack(
|
||||
"[c5 c5 bb4 c5] [~ g4 ~ g4] [c5 f5 e5 c5] ~"
|
||||
.tone(synth(adsr(0,.1,0,0)).chain(out())),
|
||||
"[c2 c3]*8"
|
||||
.tone(synth({
|
||||
...osc('sawtooth'),
|
||||
...adsr(0,.1,0.4,0)
|
||||
}).chain(lowpass(300), out()))
|
||||
).slow(4)`}
|
||||
/>
|
||||
|
||||
### tone(instrument)
|
||||
|
||||
To change the instrument of a pattern, you can pass any [Tone.js Source](https://tonejs.github.io/docs/14.7.77/index.html) to .tone:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
|
||||
.tone(new FMSynth().toDestination())`}
|
||||
/>
|
||||
|
||||
While this works, it is a little bit verbose. To simplify things, all Tone Synths have a shortcut:
|
||||
|
||||
```js
|
||||
const amsynth = (options) => new AMSynth(options);
|
||||
const duosynth = (options) => new DuoSynth(options);
|
||||
const fmsynth = (options) => new FMSynth(options);
|
||||
const membrane = (options) => new MembraneSynth(options);
|
||||
const metal = (options) => new MetalSynth(options);
|
||||
const monosynth = (options) => new MonoSynth(options);
|
||||
const noise = (options) => new NoiseSynth(options);
|
||||
const pluck = (options) => new PluckSynth(options);
|
||||
const polysynth = (options) => new PolySynth(options);
|
||||
const synth = (options) => new Synth(options);
|
||||
const sampler = (options, baseUrl?) => new Sampler(options); // promisified, see below
|
||||
const players = (options, baseUrl?) => new Sampler(options); // promisified, see below
|
||||
```
|
||||
|
||||
### sampler
|
||||
|
||||
With sampler, you can create tonal instruments from samples:
|
||||
|
||||
<MiniRepl
|
||||
tune={`sampler({
|
||||
C5: 'https://freesound.org/data/previews/536/536549_11935698-lq.mp3'
|
||||
}).then(kalimba =>
|
||||
saw.struct("x*8").mul(16).round()
|
||||
.legato(4).scale('D dorian').slow(2)
|
||||
.tone(kalimba.toDestination())
|
||||
)`}
|
||||
/>
|
||||
|
||||
The sampler function promisifies [Tone.js Sampler](https://tonejs.github.io/docs/14.7.77/Sampler).
|
||||
|
||||
Note that this function currently only works with this promise notation, but in the future,
|
||||
it will be possible to use async instruments in a synchronous fashion.
|
||||
|
||||
### players
|
||||
|
||||
With players, you can create sound banks:
|
||||
|
||||
<MiniRepl
|
||||
tune={`players({
|
||||
bd: 'samples/tidal/bd/BT0A0D0.wav',
|
||||
sn: 'samples/tidal/sn/ST0T0S3.wav',
|
||||
hh: 'samples/tidal/hh/000_hh3closedhh.wav'
|
||||
}, 'https://loophole-letters.vercel.app/')
|
||||
.then(drums=>
|
||||
"bd hh sn hh".tone(drums.toDestination())
|
||||
)
|
||||
`}
|
||||
/>
|
||||
|
||||
The sampler function promisifies [Tone.js Players](https://tonejs.github.io/docs/14.7.77/Players).
|
||||
|
||||
Note that this function currently only works with this promise notation, but in the future,
|
||||
it will be possible to use async instruments in a synchronous fashion.
|
||||
|
||||
### out
|
||||
|
||||
Shortcut for Tone.Destination. Intended to be used with Tone's .chain:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
|
||||
.tone(membrane().chain(out()))`}
|
||||
/>
|
||||
|
||||
This alone is not really useful, so read on..
|
||||
|
||||
### vol(volume)
|
||||
|
||||
Helper that returns a Gain Node with the given volume. Intended to be used with Tone's .chain:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
|
||||
.tone(noise().chain(vol(0.5), out()))`}
|
||||
/>
|
||||
|
||||
### osc(type)
|
||||
|
||||
Helper to set the waveform of a synth, monosynth or polysynth:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
|
||||
.tone(synth(osc('sawtooth4')).chain(out()))`}
|
||||
/>
|
||||
|
||||
The base types are `sine`, `square`, `sawtooth`, `triangle`. You can also append a number between 1 and 32 to reduce the harmonic partials.
|
||||
|
||||
### lowpass(cutoff)
|
||||
|
||||
Helper that returns a Filter Node of type lowpass with the given cutoff. Intended to be used with Tone's .chain:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
|
||||
.tone(synth(osc('sawtooth')).chain(lowpass(800), out()))`}
|
||||
/>
|
||||
|
||||
### highpass(cutoff)
|
||||
|
||||
Helper that returns a Filter Node of type highpass with the given cutoff. Intended to be used with Tone's .chain:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
|
||||
.tone(synth(osc('sawtooth')).chain(highpass(2000), out()))`}
|
||||
/>
|
||||
|
||||
### adsr
|
||||
|
||||
Helper to set the envelope of a Tone.js instrument. Intended to be used with Tone's .set:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
|
||||
.tone(synth(adsr(0,.1,0,0)).chain(out()))`}
|
||||
/>
|
||||
@@ -126,13 +126,9 @@ These functions are more low level, probably not needed by the live coder.
|
||||
|
||||
<JsDoc client:idle name="Pattern#stripContext" h={0} />
|
||||
|
||||
## withLocation
|
||||
## withLoc
|
||||
|
||||
<JsDoc client:idle name="Pattern#withLocation" h={0} />
|
||||
|
||||
## withMiniLocation
|
||||
|
||||
<JsDoc client:idle name="Pattern#withMiniLocation" h={0} />
|
||||
<JsDoc client:idle name="Pattern#withLoc" h={0} />
|
||||
|
||||
## filterHaps
|
||||
|
||||
|
||||
@@ -52,9 +52,9 @@ These packages provide bindings for different ways to output strudel patterns:
|
||||
|
||||
### No Longer Maintained
|
||||
|
||||
- [eval](https://github.com/tidalcycles/strudel/tree/main/packages/eval): old code transpiler
|
||||
- [tone](https://github.com/tidalcycles/strudel/tree/main/packages/tone#strudelcyclestone): bindings for Tone.js instruments and effects
|
||||
- [webdirt](https://github.com/tidalcycles/strudel/tree/main/packages/webdirt): webdirt bindings, replaced by webaudio package
|
||||
- [eval](https://www.npmjs.com/package/@strudel.cycles/eval): old code transpiler
|
||||
- [tone](https://www.npmjs.com/package/@strudel.cycles/tone): bindings for Tone.js instruments and effects
|
||||
- [webdirt](https://www.npmjs.com/package/@strudel.cycles/webdirt): webdirt bindings, replaced by webaudio package
|
||||
|
||||
## Tools
|
||||
|
||||
|
||||
@@ -32,19 +32,17 @@ In the JavaScript world, using transpilation is a common practise to be able to
|
||||
|
||||
In the same tradition, Strudel can add a transpilation step to simplify the user code in the context of live coding. For example, the Strudel REPL lets the user create mini-notation patterns using just double quoted strings, while single quoted strings remain what they are:
|
||||
|
||||
```js
|
||||
'c3 [e3 g3]*2';
|
||||
```strudel
|
||||
note("c3 [e3 g3]*2")
|
||||
```
|
||||
|
||||
is transpiled to:
|
||||
|
||||
```js
|
||||
mini('c3 [e3 g3]*2').withMiniLocation([1, 0, 0], [1, 14, 14]);
|
||||
```strudel
|
||||
note(m('c3 [e3 g3]', 5))
|
||||
```
|
||||
|
||||
Here, the string is wrapped in `mini`, which will create a pattern from a mini-notation string. Additionally, the `withMiniLocation` method passes the original source code location of the string to the pattern, which enables highlighting active events.
|
||||
|
||||
Other convenient features like pseudo variables, operator overloading and top level await are possible with transpilation.
|
||||
Here, the string is wrapped in `m`, which will create a pattern from a mini-notation string. As the second parameter, it gets passed source code location of the string, which enables highlighting active events later.
|
||||
|
||||
After the transpilation, the code is ready to be evaluated into a `Pattern`.
|
||||
|
||||
@@ -56,16 +54,22 @@ While the transpilation allows JavaScript to express Patterns in a less verbose
|
||||
|
||||
The mini-notation parser is implemented using `peggy`, which allows generating performant parsers for Domain Specific Languages (DSLs) using a concise grammar notation. The generated parser turns the mini-notation string into an AST which is used to call the respective Strudel functions with the given structure. For example, `"c3 [e3 g3]*2"` will result in the following calls:
|
||||
|
||||
```js
|
||||
```strudel
|
||||
seq(
|
||||
reify('c3').withLocation([1, 1, 1], [1, 4, 4]),
|
||||
seq(reify('e3').withLocation([1, 5, 5], [1, 8, 8]), reify('g3').withLocation([1, 8, 8], [1, 10, 10])).fast(2),
|
||||
);
|
||||
reify('c3').withLoc(6, 9),
|
||||
seq(reify('e3').withLoc(10, 12), reify('g3',).withLoc(13, 15))
|
||||
)
|
||||
```
|
||||
|
||||
### Highlighting Locations
|
||||
|
||||
As seen in the examples above, both the JS and the mini-notation parser add source code locations using `withMiniLocation` and `withLocation` methods. While the JS parser adds locations relative to the user code as a whole, the mini-notation adds locations relative to the position of the mini-notation string. The absolute location of elements within mini-notation can be calculated by simply adding both locations together. This absolute location can be used to highlight active events in real time.
|
||||
As seen in the examples above, both the mini-notation parser adds the source code locations using `withLoc`.
|
||||
This location is calculated inside the `m` function, as the sum of 2 locations:
|
||||
|
||||
1. the location where the mini notation string begins, as obtained from the JS parser
|
||||
2. the location of the substring inside the mini notation, as obtained from the mini notation parser
|
||||
|
||||
The sum of both is passed to `withLoc` to tell each element its location, which can be later used for highlighting when it's active.
|
||||
|
||||
### Mini Notation
|
||||
|
||||
|
||||
+21
-10
@@ -5,11 +5,19 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core';
|
||||
import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react';
|
||||
import {
|
||||
CodeMirror,
|
||||
cx,
|
||||
flash,
|
||||
useHighlighting,
|
||||
useStrudel,
|
||||
useKeydown,
|
||||
updateMiniLocations,
|
||||
} from '@strudel.cycles/react';
|
||||
import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import React, { createContext, useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import './Repl.css';
|
||||
import { Footer } from './Footer';
|
||||
import { Header } from './Header';
|
||||
@@ -106,7 +114,6 @@ export function Repl({ embedded = false }) {
|
||||
const [view, setView] = useState(); // codemirror view
|
||||
const [lastShared, setLastShared] = useState();
|
||||
const [pending, setPending] = useState(true);
|
||||
|
||||
const {
|
||||
theme,
|
||||
keybindings,
|
||||
@@ -128,7 +135,8 @@ export function Repl({ embedded = false }) {
|
||||
cleanupUi();
|
||||
cleanupDraw();
|
||||
},
|
||||
afterEval: ({ code }) => {
|
||||
afterEval: ({ code, meta }) => {
|
||||
setMiniLocations(meta.miniLocations);
|
||||
setPending(false);
|
||||
setLatestCode(code);
|
||||
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
||||
@@ -178,7 +186,7 @@ export function Repl({ embedded = false }) {
|
||||
);
|
||||
|
||||
// highlighting
|
||||
useHighlighting({
|
||||
const { setMiniLocations } = useHighlighting({
|
||||
view,
|
||||
pattern,
|
||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
@@ -200,6 +208,7 @@ export function Repl({ embedded = false }) {
|
||||
// TODO: scroll to selected function in reference
|
||||
// console.log('selectino change', selection.ranges[0].from);
|
||||
}, []);
|
||||
|
||||
const handleTogglePlay = async () => {
|
||||
await getAudioContext().resume(); // fixes no sound in ios webkit
|
||||
if (!started) {
|
||||
@@ -264,6 +273,11 @@ export function Repl({ embedded = false }) {
|
||||
handleShuffle,
|
||||
handleShare,
|
||||
};
|
||||
const currentTheme = useMemo(() => themes[theme] || themes.strudelTheme, [theme]);
|
||||
const handleViewChanged = useCallback((v) => {
|
||||
setView(v);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
// bg-gradient-to-t from-blue-900 to-slate-900
|
||||
// bg-gradient-to-t from-green-900 to-slate-900
|
||||
@@ -278,7 +292,7 @@ export function Repl({ embedded = false }) {
|
||||
<Header context={context} />
|
||||
<section className="grow flex text-gray-100 relative overflow-auto cursor-text pb-0" id="code">
|
||||
<CodeMirror
|
||||
theme={themes[theme] || themes.strudelTheme}
|
||||
theme={currentTheme}
|
||||
value={code}
|
||||
keybindings={keybindings}
|
||||
isLineNumbersDisplayed={isLineNumbersDisplayed}
|
||||
@@ -287,10 +301,7 @@ export function Repl({ embedded = false }) {
|
||||
fontSize={fontSize}
|
||||
fontFamily={fontFamily}
|
||||
onChange={handleChangeCode}
|
||||
onViewChanged={(v) => {
|
||||
setView(v);
|
||||
// window.editorView = v;
|
||||
}}
|
||||
onViewChanged={handleViewChanged}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
/>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user