diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index b9147226..39429432 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -5,6 +5,7 @@ import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language' import { javascript } from '@codemirror/lang-javascript'; import { StateField, StateEffect } from '@codemirror/state'; 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 }) { @@ -127,22 +128,72 @@ export const flash = (view, ms = 200) => { }; export class StrudelMirror { - constructor({ root, initialCode = '', onEvaluate, onStop }) { + constructor(options) { + const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options; this.code = initialCode; - this.view = initEditor({ + + this.drawer = new Drawer((haps, time) => { + const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); + this.highlight(currentFrame); + onDraw?.(haps, time, currentFrame); + }, drawTime); + + const prebaked = prebake(); + prebaked.then(async () => { + if (!onDraw) { + return; + } + const { scheduler, evaluate } = await this.repl; + // draw first frame instantly + prebaked.then(async () => { + await evaluate(this.code, false); + this.drawer.invalidate(scheduler); + onDraw?.(this.drawer.visibleHaps, 0, []); + }); + }); + + this.repl = repl({ + ...replOptions, + onToggle: async (started) => { + replOptions?.onToggle?.(started); + const { scheduler } = await this.repl; + if (started) { + this.drawer.start(scheduler); + } else { + this.drawer.stop(); + } + }, + beforeEval: async () => { + await prebaked; + }, + afterEval: (options) => { + replOptions?.afterEval?.(options); + this.drawer.invalidate(); + }, + }); + this.editor = initEditor({ root, initialCode, onChange: (v) => { this.code = v.state.doc.toString(); }, - onEvaluate, - onStop, + onEvaluate: () => this.evaluate(), + onStop: () => this.stop(), }); } + async evaluate() { + const { evaluate } = await this.repl; + this.flash(); + await evaluate(this.code); + } + async stop() { + const { scheduler } = await this.repl; + scheduler.stop(); + } flash(ms) { - flash(this.view, ms); + flash(this.editor, ms); } highlight(haps) { - highlightHaps(this.view, haps); + highlightHaps(this.editor, haps); } } diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 6ea2ac25..a4ebf03a 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -38,7 +38,8 @@ "@codemirror/language": "^6.6.0", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.10.0", - "@lezer/highlight": "^1.1.4" + "@lezer/highlight": "^1.1.4", + "@strudel.cycles/core": "workspace:*" }, "devDependencies": { "vite": "^4.3.3" diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index bc6d3cd8..58b14040 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -97,19 +97,18 @@ export class Drawer { }, ); } - check() { - if (!this.scheduler) { - throw new Error('no scheduler set..'); + invalidate(scheduler = this.scheduler) { + if (!scheduler) { + return; } - } - invalidate() { - this.check(); - const t = this.scheduler.now(); + this.scheduler = scheduler; + const t = scheduler.now(); let [_, lookahead] = this.drawTime; + const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1]; // remove all future haps this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); // query future haps - const futureHaps = this.scheduler.pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +0.1 = workaround for weird holes in query.. + const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query.. // append future haps this.visibleHaps = this.visibleHaps.concat(futureHaps); } diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index a8a1c754..425799be 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -1,69 +1,39 @@ -// moved from sandbox: https://codesandbox.io/s/vanilla-codemirror-strudel-2wb7yw?file=/index.html:114-186 - import { StrudelMirror } from '@strudel/codemirror'; -import { initStrudel } from './strudel'; import { funk42 } from './tunes'; -import { pianoroll, getDrawOptions, Drawer } from '@strudel.cycles/core'; +import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core'; import './style.css'; import { initAudioOnFirstClick } from '@strudel.cycles/webaudio'; +import { transpiler } from '@strudel.cycles/transpiler'; +import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio'; +import { registerSoundfonts } from '@strudel.cycles/soundfonts'; -const initAudio = initAudioOnFirstClick(); - -const editor = new StrudelMirror({ - root: document.getElementById('editor'), - initialCode: funk42, - onEvaluate, - onStop, -}); - -async function drawFirstFrame(editor, repl) { - const { evaluate } = repl; - const pattern = await evaluate(editor.code, false); - const initialHaps = pattern.queryArc(0, drawTime[1]); - drawPianoroll(initialHaps, 0); - return repl; -} - -const repl = initStrudel().then(async (repl) => { - await drawFirstFrame(editor, repl); - return repl; -}); - +// init canvas const canvas = document.getElementById('roll'); canvas.width = canvas.width * 2; canvas.height = canvas.height * 2; +const drawContext = canvas.getContext('2d'); +const drawTime = [-2, 2]; // time window of drawn haps -async function onEvaluate() { - const { evaluate, scheduler } = await repl; - await initAudio; - editor.flash(); - if (!scheduler.started) { - scheduler.stop(); - await evaluate(editor.code); - drawer.start(scheduler); - } else { - await evaluate(editor.code); - drawer.invalidate(); // this is a bit mystic - } -} +const editor = new StrudelMirror({ + defaultOutput: webaudioOutput, + getTime: () => getAudioContext().currentTime, + transpiler, + root: document.getElementById('editor'), + initialCode: funk42, + drawTime, + onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }), + prebake: async () => { + initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..) + const loadModules = evalScope( + controls, + import('@strudel.cycles/core'), + import('@strudel.cycles/mini'), + import('@strudel.cycles/tonal'), + import('@strudel.cycles/webaudio'), + ); + await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]); + }, +}); -async function onStop() { - const { scheduler } = await repl; - scheduler.stop(); - drawer.stop(); -} - -const drawTime = [-2, 2]; -function drawPianoroll(haps, time) { - pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0 }) }); -} - -const ctx = canvas.getContext('2d'); -let drawer = new Drawer((haps, time) => { - const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); - editor.highlight(currentFrame); - drawPianoroll(haps, time); -}, drawTime); - -document.getElementById('play').addEventListener('click', () => onEvaluate()); -document.getElementById('stop').addEventListener('click', async () => onStop()); +document.getElementById('play').addEventListener('click', () => editor.evaluate()); +document.getElementById('stop').addEventListener('click', () => editor.stop()); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js deleted file mode 100644 index de9b2cd7..00000000 --- a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js +++ /dev/null @@ -1,28 +0,0 @@ -import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio'; - -const ctx = getAudioContext(); - -export async function initStrudel(options = {}) { - const [{ controls, repl, evalScope }, { registerSoundfonts }, { transpiler }] = await Promise.all([ - import('@strudel.cycles/core'), - import('@strudel.cycles/soundfonts'), - import('@strudel.cycles/transpiler'), - ]); - - const loadModules = evalScope( - controls, - import('@strudel.cycles/core'), - import('@strudel.cycles/mini'), - import('@strudel.cycles/tonal'), - import('@strudel.cycles/webaudio'), - ); - - await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]); - - return repl({ - defaultOutput: webaudioOutput, - getTime: () => ctx.currentTime, - transpiler, - ...options, - }); -} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs b/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs index b235aa95..242a0d4b 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs +++ b/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs @@ -78,7 +78,7 @@ let drums = stack( s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"), s("hh*4").begin(.2).release(.02).end(.25).release(.02) .gain(.3).bank('RolandTR707').late(.02).room(.5), - s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02) + //s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02) ).fast(2) let wurli = note(\`< diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index b0f4c074..336bd428 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -175,3 +175,8 @@ Pattern.prototype.pianoroll = function (options) { pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }), ); }; + +export function drawPianoroll(options) { + const { drawTime, ...rest } = options; + pianoroll({ ...getDrawOptions(drawTime), ...rest }); +}