An interactive, fully offline web notebook for teaching live-coding music with Strudel.
Each cell holds editable code that plays sound directly in the browser — no internet, no
accounts, no CDN.
Install Node.js 18 or newer. It’s the only runtime you need —
everything below runs on Node builtins with no other dependencies.
Install Git and clone this repository:
git clone https://github.com/synacktraa/strudel-demo-and-player.git
cd strudel-demo-and-player
npm run setup
Downloads Strudel and its whole sample library into app/vendor/, then rewrites Strudel’s
internal URLs to point there. Needs internet — run it before you leave. Takes roughly
20–30 minutes and about 215 MB. If it’s interrupted, run it again: completed files are kept
and skipped.
npm run app
Starts the notebook and opens it in your browser. Runs with no internet at all. It
prints the URL too, and picks another port automatically if 8000 is taken.
That’s the whole workflow.
There is also an online mode:
npm run app:online
It loads Strudel, React and every sample from the CDN, needs no npm run setup, and
unlocks the sets too large to vendor — the full VCSL orchestral library, mridangam, and
helpers like samples('github:...') and shabda(...). Useful for developing on a machine
that hasn’t run setup, or for trying something the vendored library doesn’t cover.
Offline stays the default because the failure modes are not symmetric. Defaulting to
offline on a machine that happens to have internet costs nothing — it just uses local files.
Defaulting to online on a machine that doesn’t costs you the workshop. A flag you have to
remember is a flag that gets forgotten exactly once, at the worst possible moment.
For the same reason there is no automatic fallback from offline to CDN. A machine with
half-finished assets would look fine during prep on your wifi and die in the classroom.
npm run app refuses to start and tells you to run setup instead.
Only the third-party libraries move in online mode; ui/ and styles.css are always
served from disk, so what you develop is what ships.
npm run verify
Runs offline in about a second and prints a per-item report:
[ ok ] vendor manifest profile "recommended", strudel 1.3.0
[ ok ] strudel bundle 2.1 MB
[ ok ] no remote asset URLs in bundle 5 base URLs redirected to /vendor/
[ ok ] audio worklet 1 present
[ ok ] sample maps 7 present
[ ok ] sample audio 709 bank(s) spot-checked, all present
[ ok ] soundfonts 126 GM font file(s)
[ ok ] last setup run clean
[ ok ] index.html sub-resources all local
It exits non-zero if anything is missing, so you can loop it over a room of machines.
npm run app does its own quick check on startup and tells you to run setup if assets
are missing.
Nothing in app/vendor/ is machine-specific. Run npm run setup once, then copy the whole
project folder to the other machines — far faster than 25 minutes each. Every machine still
needs Node.js installed.
app/ everything that gets served — runs offline
index.html shell: one <div id="root"> and the local script tags
styles.css
server.mjs static server, Node builtins only
ui/ the React app (no build step — see below)
main.js mounts <Notebook> into #root
Notebook.js header, nav, sections, panic button
StrudelCell.js one cell + the imperatively mounted <strudel-editor>
Equalizer.js canvas visualiser driven by the master audio tap
useMasterAnalyser.js
lessons.js all lesson content, as data
Icon.js runtime.js
vendor/ downloaded assets (gitignored — a build output, not source)
setup/ everything that needs internet — never runs at the workshop
index.mjs entry point for `npm run setup`
vendor.mjs downloads and rewrites the assets
verify.mjs the offline readiness report
lib/
tests/
unit/ fast, no dependencies
e2e/ Playwright, with the internet blocked
A server is required rather than opening index.html directly: Strudel’s AudioWorklet,
SharedWorker and sample loading are all blocked on file:// URLs.
The original notebook loaded five things over the network. All of them are now local:
| Was | Now |
|---|---|
unpkg.com/@strudel/repl@1 |
app/vendor/strudel/index.js |
kit.fontawesome.com icon kit |
inline <svg> sprite in index.html |
raw.githubusercontent.com sample maps + audio |
app/vendor/samples/ |
felixroos.github.io GM soundfonts |
app/vendor/soundfonts/ |
unpkg.com/hydra-synth |
app/vendor/hydra/ |
Verified in a real browser with all non-localhost traffic blocked: the page loads, every
cell plays, and zero requests leave the machine.
bd sd hh oh cp rim mt lt ht cr rd cb sh tb brk misc).bank()casio crow insect wind jazz metal east space numbers)gm_piano, gm_acoustic_bass, gm_epiano1, …)sawtooth, triangle, sine, square) — these need no samplessamples('github:...') and shabda(...) fetch from the internet by design and will failTo include more:
npm run setup -- --profile=generous
| Profile | Size | Contents |
|---|---|---|
lean |
~25 MB | default kit, Dirt-Samples, piano, 20 drum machines, only the GM instruments the notebook names |
recommended (default) |
~215 MB | everything above plus all 683 drum machines and one variant of all 125 GM instruments |
generous |
~400 MB | adds mridangam and 3 variants per GM instrument |
full |
~2.3 GB | adds the complete VCSL orchestral library |
The notebook is bilingual: Indonesian and English shown together, Indonesian leading and
English beneath it in smaller type.
Not a language switcher, deliberately. The students read Indonesian and the teaching team
does not; the teachers read English and the students may not. A switcher means whoever is
not holding the mouse cannot read the screen — including a teacher leaning over a student’s
shoulder to help. Showing both costs a little vertical space and solves that outright.
The ID + EN / ID / EN control in the header narrows the display when you want it (handy
on a projector). It is remembered per machine, and defaults to showing both.
⚠️ The Indonesian has not been checked by a native speaker. It was written by whoever
generated this code, not by anyone on the teaching team. Please have a local reviewer
read it before teaching from it — a mistranslated instruction is worse than none.Everything is in two files, each string beside its English pair:
app/ui/lessons.js— all lesson contentapp/ui/text.js— buttons, tooltips, the header taglineThe format is
t('Indonesian', 'English'). Edit the first string only.
npm testfails if any string is missing a language, or if the code samples inside a
lesson differ between the two.
The six lesson cells ship empty on purpose — the plan is to write them live in front of
the class. Only the demo cell at the top is pre-filled, as a “here’s where this goes”
showcase.
Each cell has a Hint button showing the pattern below, so you don’t have to remember
them. They are the same strings listed here — tests/unit/hints.test.mjs fails if the two
drift apart.
1 · Rhythm
s("bd hh sd hh")
2 · Repetition
s("bd*2 hh*4 sd hh*2")
3 · Melody
note("c e g e a g e c")
4 · Instruments
note("c e g b").sound("sawtooth")
5 · Layering
stack(
s("bd*2 hh*4 sd hh*2"),
note("c e g e").sound("sawtooth")
)
6 · Effects
note("c e g b a g e c")
.sound("sawtooth")
.room(0.6)
.delay(0.4)
Every sound named above is vendored, so all of it works offline.
“The offline assets are missing” — npm run setup hasn’t finished on this machine. Run
it somewhere with internet, or copy app/vendor/ from a machine that has it.
No sound at all — browsers block audio until you interact with the page. Click any Play
button first. If it’s still silent, check the system volume and output device.
A cell says “sound not found” — that bank wasn’t vendored. Run npm run verify to see
what’s present, or re-run setup with --profile=generous.
Setup failed partway — run it again. Completed files are kept and skipped.
Want a specific port — node app/server.mjs --port=9000.
The notebook is a React app with no build step. React, ReactDOM and htm are vendored as
UMD bundles into app/vendor/ui-libs/ and loaded as plain script tags; htm provides
JSX-like syntax through tagged templates, so components read normally without a compiler.
That matters because the workshop machines have no internet and no node_modules — there
is nothing to install and nothing to compile, the browser just runs the files.
React 18 rather than 19, because 19 dropped the UMD builds.
One deliberate exception to React owning the DOM: <strudel-editor> is created imperatively
and mounted exactly once per cell. It holds a scheduler, a prebaked sample registry and an
AudioWorklet, and it inserts its own CodeMirror container as a sibling of itself — so a
React remount would cut the audio and re-run a multi-second prebake. Each editor lives in a
host <div> that React renders empty and never reconciles into.
The header equalizer reads Strudel’s master output (superdough’s destinationGain) through
an AnalyserNode, so it responds to every cell without students adding .analyze() to
their own patterns.
setup/vendor.mjs reads app/index.html to find which soundfonts and banks the lessons
actually use, so the demo cell’s gm_epiano1:1 pulls the second variant, not just the
first. setup/lib/rewrite.mjs patches the Strudel bundle’s five hardcoded remote base
URLs and throws if any expected URL is missing — a future Strudel release that moves
its URLs fails loudly at setup rather than silently phoning home at the workshop.
npm test # unit tests, no dependencies, ~1s
npm run test:e2e # Playwright: loads the page with the internet blocked
npm run test:all
npm run test:online # opt-in, needs internet: checks --online mode still works
The E2E suite hard-aborts every non-localhost request, so a regression that reintroduces a
CDN can’t pass by succeeding on a machine that happens to be online. Playwright is a
devDependency for the prep machine only — the notebook itself never needs it.
strudel.cc has the full documentation and the online REPL
(internet required, obviously).