anvilsign in

collin/strudel-claude

1// Headless Strudel: the strudel.cc REPL with the browser taken out from under it.
2//
3// Strudel is layered more cleanly than it looks. The pattern algebra, the
4// mini-notation parser, the transpiler and the scheduler are all pure JS with no
5// DOM dependency whatsoever — the only thing that ever needed a browser is the
6// output stage, superdough, which speaks Web Audio. `repl()` takes both the
7// clock (`getTime`) and the output (`defaultOutput`) as constructor arguments,
8// so pointing them at a native AudioContext is enough to run the whole thing in
9// Node. See browser-shim.ts and loader-hooks.ts for the glue that gets
10// superdough itself to import.
11//
12// Import order matters and is load-bearing: the shim must be fully evaluated
13// before anything reaches for `AudioContext` or `window` at module scope. Static
14// imports are hoisted in source order, so keeping the shim first is sufficient —
15// do not reorder these.
16import './browser-shim.ts';
17
18import { repl, evalScope, controls, silence } from '@strudel/core';
19import type { Hap, Repl, ReplState } from '@strudel/core';
20import { transpiler } from '@strudel/transpiler';
21import * as core from '@strudel/core';
22
23import {
24 setAudioContext,
25 getAudioContext,
26 loadWorklets,
27 samples,
28 aliasBank,
29 registerSynthSounds,
30 registerZZFXSounds,
31 superdough,
32} from 'superdough';
33
34const DOUGH = 'https://raw.githubusercontent.com/felixroos/dough-samples/main';
35const UZU = 'https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main';
36const TODEPOND = 'https://raw.githubusercontent.com/todepond/samples/main';
37
38// superdough's own webaudioOutput lives in @strudel/webaudio, which pulls in a
39// pile of browser-only siblings we don't want. The output contract is small
40// enough to just implement: turn a hap into a superdough call at a wall-clock
41// deadline. This mirrors @strudel/webaudio's webaudioOutput.
42function nodeAudioOutput(hap: Hap, deadline: number, duration: number, cps: number): void {
43 const { s, note, n } = hap.value;
44 if (s === undefined && note === undefined && n === undefined) {
45 // Nothing to sound — a control-only hap (e.g. a bare `.gain()` with no source).
46 return;
47 }
48 const ctx = getAudioContext();
49 superdough({ ...hap.value, cps }, ctx.currentTime + deadline, duration, cps);
50}
51
52export interface EngineHandlers {
53 onEval?: (state: ReplState) => void;
54 onError?: (err: Error) => void;
55 onToggle?: (started: boolean) => void;
56}
57
58/**
59 * Boot the audio stack: native AudioContext, superdough's worklets, and the
60 * default strudel.cc sound library.
61 *
62 * `onProgress` is called with short status strings so a UI can show what's
63 * happening — the sample banks are a few MB of JSON and take a moment.
64 */
65export async function initAudio(
66 { onProgress = (): void => {} }: { onProgress?: (msg: string) => void } = {},
67): Promise<AudioContext> {
68 const ctx = new globalThis.AudioContext();
69 setAudioContext(ctx);
70
71 onProgress('loading worklets');
72 await loadWorklets();
73
74 onProgress('loading sounds');
75 await Promise.all([
76 registerSynthSounds(),
77 registerZZFXSounds(),
78 samples(`${DOUGH}/tidal-drum-machines.json`),
79 samples(`${DOUGH}/piano.json`),
80 samples(`${DOUGH}/Dirt-Samples.json`),
81 samples(`${DOUGH}/vcsl.json`),
82 samples(`${DOUGH}/mridangam.json`),
83 samples(`${UZU}/strudel.json`),
84 ]);
85 await aliasBank(`${TODEPOND}/tidal-drum-machines-alias.json`);
86
87 onProgress('ready');
88 return ctx;
89}
90
91/**
92 * Build the REPL. Returns the same surface StrudelMirror drives in the browser —
93 * evaluate/start/stop/toggle plus the live scheduler — so a terminal frontend
94 * and a web frontend are interchangeable consumers.
95 */
96export async function createEngine(handlers: EngineHandlers = {}): Promise<Repl> {
97 const ctx = getAudioContext();
98
99 // Everything the user's code can call by bare name — `s`, `note`, `stack`,
100 // mini-notation, the tonal helpers. Without this, evaluate() throws
101 // ReferenceError on the first `s("bd")`.
102 await evalScope(core, controls, import('@strudel/mini'), import('@strudel/tonal'));
103
104 return repl({
105 defaultOutput: nodeAudioOutput,
106 getTime: () => ctx.currentTime,
107 transpiler,
108 afterEval: (state) => handlers.onEval?.(state),
109 onEvalError: (err) => handlers.onError?.(err),
110 onToggle: (started) => handlers.onToggle?.(started),
111 });
112}
113
114export { silence, getAudioContext };
115export type { Repl, ReplState, Hap };