add basic dough repl
This commit is contained in:
parent
ea7dc7a732
commit
48407c309c
5 changed files with 279 additions and 3 deletions
|
|
@ -27,7 +27,7 @@ import { updateWidgets, widgetPlugin } from './widget.mjs';
|
||||||
|
|
||||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||||
|
|
||||||
const extensions = {
|
export const extensions = {
|
||||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||||
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
|
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
|
||||||
isBracketClosingEnabled: (on) => (on ? closeBrackets() : []),
|
isBracketClosingEnabled: (on) => (on ? closeBrackets() : []),
|
||||||
|
|
@ -48,7 +48,7 @@ const extensions = {
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
export const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||||
|
|
||||||
export const defaultSettings = {
|
export const defaultSettings = {
|
||||||
keybindings: 'codemirror',
|
keybindings: 'codemirror',
|
||||||
|
|
@ -411,7 +411,7 @@ export class StrudelMirror {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBooleans(value) {
|
export function parseBooleans(value) {
|
||||||
return { true: true, false: false }[value] ?? value;
|
return { true: true, false: false }[value] ?? value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
17
website/src/components/Dough/Dough.astro
Normal file
17
website/src/components/Dough/Dough.astro
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
import '../../repl/Repl.css';
|
||||||
|
---
|
||||||
|
|
||||||
|
<div id="code"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { DoughMirror } from './dough-mirror.mjs';
|
||||||
|
const root = document.getElementById('code');
|
||||||
|
const doughmirror = new DoughMirror({ root });
|
||||||
|
globalThis.settings = doughmirror.settings.bind(doughmirror);
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
#code {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
139
website/src/components/Dough/dough-mirror.mjs
Normal file
139
website/src/components/Dough/dough-mirror.mjs
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
import {
|
||||||
|
initEditor,
|
||||||
|
codemirrorSettings,
|
||||||
|
flash,
|
||||||
|
compartments,
|
||||||
|
extensions,
|
||||||
|
parseBooleans,
|
||||||
|
activateTheme,
|
||||||
|
updateMiniLocations,
|
||||||
|
highlightMiniLocations,
|
||||||
|
} from '@strudel/codemirror';
|
||||||
|
import { evalScope } from '@strudel/core';
|
||||||
|
import { Framer } from '@strudel/draw';
|
||||||
|
import { persistentAtom } from '@nanostores/persistent';
|
||||||
|
import { DoughRepl } from './dough-repl.mjs';
|
||||||
|
|
||||||
|
const initialCode = '$: note("c a f e")';
|
||||||
|
export const code = persistentAtom('vanilla-repl-code', initialCode, {
|
||||||
|
encode: JSON.stringify,
|
||||||
|
decode: JSON.parse,
|
||||||
|
});
|
||||||
|
|
||||||
|
export class DoughMirror {
|
||||||
|
constructor(options) {
|
||||||
|
const { root, initialCode = code.get(), bgFill = true } = options;
|
||||||
|
this.root = root;
|
||||||
|
this.code = initialCode;
|
||||||
|
this.repl = new DoughRepl();
|
||||||
|
this.prebaked = this.prebake();
|
||||||
|
|
||||||
|
// init codemirror
|
||||||
|
this.editor = initEditor({
|
||||||
|
root,
|
||||||
|
initialCode: this.code,
|
||||||
|
onChange: (v) => {
|
||||||
|
if (v.docChanged) {
|
||||||
|
this.code = v.state.doc.toString();
|
||||||
|
code.set(this.code);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEvaluate: this.evaluate.bind(this),
|
||||||
|
onStop: () => this.stop(),
|
||||||
|
mondo: false,
|
||||||
|
});
|
||||||
|
const settings = codemirrorSettings.get();
|
||||||
|
this.setFontSize(settings.fontSize);
|
||||||
|
this.setFontFamily(settings.fontFamily);
|
||||||
|
|
||||||
|
// init event highlighting
|
||||||
|
this.framer = new Framer(
|
||||||
|
() => {
|
||||||
|
const frameHaps = this.repl.processHaps();
|
||||||
|
highlightMiniLocations(this.editor, time, frameHaps);
|
||||||
|
},
|
||||||
|
(err) => console.log('Framer error', err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
prebake() {
|
||||||
|
const modulesLoading = evalScope(import('@strudel/core'), import('@strudel/tonal'), import('@strudel/mini'));
|
||||||
|
return Promise.all([modulesLoading, this.repl.prebake()]);
|
||||||
|
}
|
||||||
|
async evaluate() {
|
||||||
|
this.framer.start();
|
||||||
|
this.flash();
|
||||||
|
await this.prebaked;
|
||||||
|
const { miniLocations } = await this.repl.evaluate(this.code);
|
||||||
|
updateMiniLocations(this.editor, miniLocations);
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
this.repl.stop();
|
||||||
|
this.framer.stop();
|
||||||
|
highlightMiniLocations(this.editor, 0, []);
|
||||||
|
}
|
||||||
|
// added synonym (compared to StrudelMirror)
|
||||||
|
settings(settings = {}) {
|
||||||
|
this.updateSettings(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the rest is copy pasted from StrudelMirror:
|
||||||
|
|
||||||
|
updateSettings(settings = {}) {
|
||||||
|
settings.fontSize && this.setFontSize(settings.fontSize);
|
||||||
|
settings.fontFamily && this.setFontFamily(settings.fontFamily);
|
||||||
|
for (let key in extensions) {
|
||||||
|
if (key in settings) {
|
||||||
|
this.reconfigureExtension(key, settings[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updated = { ...codemirrorSettings.get(), ...settings };
|
||||||
|
// console.log(updated);
|
||||||
|
codemirrorSettings.set(updated);
|
||||||
|
}
|
||||||
|
reconfigureExtension(key, value) {
|
||||||
|
if (!extensions[key]) {
|
||||||
|
console.warn(`extension ${key} is not known`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
value = parseBooleans(value);
|
||||||
|
const newValue = extensions[key](value, this);
|
||||||
|
this.editor.dispatch({
|
||||||
|
effects: compartments[key].reconfigure(newValue),
|
||||||
|
});
|
||||||
|
if (key === 'theme') {
|
||||||
|
activateTheme(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flash(ms) {
|
||||||
|
flash(this.editor, ms);
|
||||||
|
}
|
||||||
|
setFontSize(size) {
|
||||||
|
this.root.style.fontSize = size + 'px';
|
||||||
|
}
|
||||||
|
setFontFamily(family) {
|
||||||
|
this.root.style.fontFamily = family;
|
||||||
|
const scroller = this.root.querySelector('.cm-scroller');
|
||||||
|
if (scroller) {
|
||||||
|
scroller.style.fontFamily = family;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCode(code, offset = 0) {
|
||||||
|
const changes = {
|
||||||
|
from: 0,
|
||||||
|
to: this.editor.state.doc.length + offset,
|
||||||
|
insert: code,
|
||||||
|
};
|
||||||
|
this.editor.dispatch({ changes });
|
||||||
|
}
|
||||||
|
getCursorLocation() {
|
||||||
|
return this.editor.state.selection.main.head;
|
||||||
|
}
|
||||||
|
setCursorLocation(col) {
|
||||||
|
return this.editor.dispatch({ selection: { anchor: col } });
|
||||||
|
}
|
||||||
|
appendCode(code) {
|
||||||
|
const cursor = this.getCursorLocation();
|
||||||
|
this.setCode(this.code + code);
|
||||||
|
this.setCursorLocation(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
105
website/src/components/Dough/dough-repl.mjs
Normal file
105
website/src/components/Dough/dough-repl.mjs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
// import { Dough, doughsamples } from 'dough-synth';
|
||||||
|
import { Dough, doughsamples } from 'https://unpkg.com/dough-synth@0.1.9/dough.js';
|
||||||
|
import { Pattern, noteToMidi, evaluate } from '@strudel/core';
|
||||||
|
// import doughUrl from 'dough-synth?url';
|
||||||
|
import { transpiler } from '@strudel/transpiler';
|
||||||
|
//const doughBaseUrl = doughUrl.split('/').slice(0, -1).join('/') + '/';
|
||||||
|
const doughBaseUrl = 'https://unpkg.com/dough-synth@0.1.9/';
|
||||||
|
|
||||||
|
export class DoughRepl {
|
||||||
|
pattern;
|
||||||
|
latency = 0.1;
|
||||||
|
cps = 0.5;
|
||||||
|
origin;
|
||||||
|
t0;
|
||||||
|
lasttime;
|
||||||
|
strudel;
|
||||||
|
q = [];
|
||||||
|
constructor() {
|
||||||
|
this.ready = this.init();
|
||||||
|
}
|
||||||
|
async init() {
|
||||||
|
// init dough immediately, so that it can attach the document click event to initAudio immediately
|
||||||
|
this.dough = new Dough({
|
||||||
|
base: doughBaseUrl,
|
||||||
|
//base: "../", // local dev
|
||||||
|
onTick: ({ t0, t1 }) => {
|
||||||
|
if (!this.pattern) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.origin ??= t0;
|
||||||
|
this.t0 = t0;
|
||||||
|
this.lasttime = performance.now();
|
||||||
|
const a = (t0 - this.origin) * this.cps;
|
||||||
|
const b = (t1 - this.origin) * this.cps;
|
||||||
|
|
||||||
|
const haps = this.pattern.queryArc(a, b).filter((hap) => hap.hasOnset());
|
||||||
|
if (!haps.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
haps.forEach((hap) => {
|
||||||
|
const time = hap.whole.begin.valueOf() / this.cps + this.origin + this.latency;
|
||||||
|
const duration = hap.duration.valueOf() / this.cps;
|
||||||
|
const event = {
|
||||||
|
dough: 'play',
|
||||||
|
...hap.value,
|
||||||
|
time,
|
||||||
|
duration,
|
||||||
|
};
|
||||||
|
if (event.note && typeof event.note === 'string') {
|
||||||
|
event.note = noteToMidi(event.note);
|
||||||
|
}
|
||||||
|
//console.log("event", JSON.stringify(Object.entries(event)));
|
||||||
|
this.dough.evaluate(event);
|
||||||
|
this.q.push({ event, hap });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// miniAllStrings();
|
||||||
|
const setcps = (cps) => (this.cps = cps);
|
||||||
|
const setcpm = (cpm) => setcps(cpm / 60);
|
||||||
|
const replScope = { setcps, setcpm };
|
||||||
|
Object.assign(globalThis, replScope);
|
||||||
|
}
|
||||||
|
async evaluate(code) {
|
||||||
|
await this.ready;
|
||||||
|
let patterns = [];
|
||||||
|
Pattern.prototype.p = function (id) {
|
||||||
|
if (!id.startsWith('_')) {
|
||||||
|
patterns.push(this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let { meta } = await evaluate(code, transpiler, { addReturn: false, wrapAsync: true, emitWidgets: false });
|
||||||
|
const { miniLocations } = meta;
|
||||||
|
|
||||||
|
this.pattern = stack(...patterns);
|
||||||
|
return { miniLocations, pattern: this.pattern };
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
this.pattern = undefined;
|
||||||
|
this.origin = undefined;
|
||||||
|
}
|
||||||
|
prebake() {
|
||||||
|
return Promise.all([doughsamples('github:eddyflux/crate')]);
|
||||||
|
}
|
||||||
|
// tbd: move this to dough-synth
|
||||||
|
get time() {
|
||||||
|
return this.t0 + (performance.now() - this.lasttime) / 1000 + this.latency;
|
||||||
|
}
|
||||||
|
processHaps() {
|
||||||
|
const currentHaps = [];
|
||||||
|
const time = this.time;
|
||||||
|
this.q = this.q.filter(({ event, hap }) => {
|
||||||
|
const end = event.time + event.duration;
|
||||||
|
const isActive = time >= event.time && time <= end;
|
||||||
|
if (isActive) {
|
||||||
|
currentHaps.push(hap);
|
||||||
|
}
|
||||||
|
return end > time; // delete old events
|
||||||
|
// we do NOT return !isActive, because a frame might miss an event, which would cause a leak
|
||||||
|
});
|
||||||
|
// console.log(this.q.length); // to check for leaks
|
||||||
|
return currentHaps;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
website/src/pages/dough/index.astro
Normal file
15
website/src/pages/dough/index.astro
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
import HeadCommon from '../../components/HeadCommon.astro';
|
||||||
|
import Dough from '../../components/Dough/Dough.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en" class="m-0 dark">
|
||||||
|
<head>
|
||||||
|
<HeadCommon />
|
||||||
|
<title>Strudel Dough REPL</title>
|
||||||
|
</head>
|
||||||
|
<body class="h-app-height bg-background m-0">
|
||||||
|
<Dough />
|
||||||
|
<a rel="me" href="https://social.toplap.org/@strudel" target="_blank" class="hidden">mastodon</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue