anvilsign in

collin/strudel-claude

1// Builds the *real* Strudel editor — the same StrudelMirror that powers
2// strudel.cc. Unlike a plain CodeMirror, it wires the audio scheduler to the
3// source: while a pattern plays, each active event flashes the characters that
4// produced it (the "cursor follow-along" highlighting).
5import { silence } from '@strudel/core';
6import { getDrawContext } from '@strudel/draw';
7import { transpiler } from '@strudel/transpiler';
8import {
9 getAudioContext,
10 webaudioOutput,
11 initAudioOnFirstClick,
12} from '@strudel/webaudio';
13import { StrudelMirror } from '@strudel/codemirror';
14import { prebake } from '@strudel/repl/prebake.mjs';
15
16// Browsers only allow audio to start after a user gesture; this arms it.
17initAudioOnFirstClick();
18
19// Resume the AudioContext and wait until it is actually running. Call this
20// from a click handler *before* evaluate(), otherwise the very first play can
21// fire while the context is still suspended and the scheduler won't start.
22export async function resumeAudio() {
23 const ctx = getAudioContext();
24 if (ctx.state !== 'running') {
25 try {
26 await ctx.resume();
27 } catch {
28 /* ignore — will retry on the next gesture */
29 }
30 }
31 return ctx.state === 'running';
32}
33
34export function createEditor(root, initialCode, handlers = {}) {
35 const editor = new StrudelMirror({
36 defaultOutput: webaudioOutput,
37 getTime: () => getAudioContext().currentTime,
38 transpiler,
39 root,
40 initialCode,
41 pattern: silence,
42 // Draw a small window around "now" so highlights lead/trail naturally.
43 drawTime: [-2, 2],
44 drawContext: getDrawContext(),
45 // Loads the default strudel.cc sound set (drum machines, piano, GM, ...).
46 prebake,
47 afterEval: () => {
48 editor.__evalError = null;
49 handlers.onEval?.();
50 },
51 onEvalError: (err) => {
52 editor.__evalError = err;
53 handlers.onError?.(err);
54 },
55 });
56 editor.setCode(initialCode);
57 if (import.meta.env.DEV) {
58 window.__strudel = { editor, getAudioContext };
59 }
60 return editor;
61}
62
63// Play the editor's current code. Resumes audio first, and if the very first
64// evaluate after page load doesn't start the scheduler (a known cold-start
65// quirk), evaluates once more — the second pass reliably starts it.
66export async function playPattern(editor) {
67 await resumeAudio();
68 editor.__evalError = null;
69 await editor.evaluate();
70 if (!editor.repl?.scheduler?.started && !editor.__evalError) {
71 await editor.evaluate();
72 }
73 return !editor.__evalError; // false → the code had a syntax/eval error
74}