synthwave-surfer¶
Deterministic generative synthwave in the browser — eight genre engines, one seed, no build step. Pick a genre, hit play, get a full arrangement.
A single HTML file that composes and plays multi-genre electronic music in the browser. You give it a seed and a genre engine; it deterministically generates a complete arrangement — bass, lead, pads, drums, builds and drops — and plays it back through Tone.js. Same seed plus same genre always yields the same track, so a composition is just two numbers you can share.
It started as a way to generate John-Carpenter-style soundtracks for 3D animations (the Kuro screensaver), and is on its way to becoming a standalone Obsidian plugin — the audio layer of the Kuro universe.
Quickstart¶
The app needs an HTTP origin to make sound — Tone.js does not initialise on
a file:// page, so double-clicking the HTML will show the UI but stay silent.
Serve it locally:
git clone https://git.jkaindl.de/jkaindl/synthwave-surfer.git
cd synthwave-surfer
python3 -m http.server 8745
# → open http://localhost:8745/synthwave_surfer.html
Or just use the hosted build: pages.jkaindl.de/synthwave-surfer.
Then: click a genre card, press Generate, hit ▶ Play. Change the seed for a different composition in the same style; tweak any slider to re-voice it live.
Features¶
- Eight genre engines, each with its own composition algorithm and its own synth voicing — Outrun, Noir, Dreamwave, Carpenter, Techno, Prog House, Trance, Acid House (see the genre table). A ninth, Dubstep, is parked.
- Deterministic —
seed + genrefully determines the output. Reproducible, shareable, diff-able. No two seeds sound alike; the same seed never drifts. - Motif-based composition — a melody engine that states a motif and develops it (transpose / invert / ornament) instead of random-walking, plus a root-anchored bass-riff engine with turnaround fills.
- Six synth voices, shared across the engines — a DX7-style FM bell, an MS-20 distorted bass, a TB-303 (saw + resonant filter envelope + Chebyshev squelch), two Juno-106-style detuned-saw voices (a washed pad and a brighter lead), and a 7-oscillator supersaw anthem lead.
- Four classic drum kits — LinnDrum-style gated, TR-909, TR-808, and a heavy Dubstep kit — with live per-voice knobs (kick decay/boom, snare whip/tone, hat decay, gated-snare toggle).
- EDM arrangement engine — a 32-bar form with two drops and a breakdown, noise risers and crashes into each drop, and a pre-drop kick cut so the drop slams.
- Real-time editing — per-track instrument panels (model, ADSR, filter, LFO) and a master FX bus (reverb, feedback delay, sidechain, tape saturation).
- Export — MIDI (all tracks),
.swmd(a Markdown music format that opens in Obsidian), WAV (offline render), and full state as JSON. - Presets — every genre card is a curated preset; save your own tweaks to
localStorageand export/import them as JSON. - External API — a small frozen
window.synthwaveSurferfor driving phase changes from outside (e.g. syncing an animation to the music). - Zero build, one dependency — a single ~3.9k-line HTML file; Tone.js loads from a CDN. Nothing to install, nothing to compile.
Genres¶
Each genre is both a composition engine (the notes and structure) and a voicing (the sound). Clicking a card loads its curated seed, BPM, scale, drum kit, and voicing in one go.
| Genre | Character | BPM | Key | Seed |
|---|---|---|---|---|
| Outrun | Driving 8th-note bass, pumping sidechain, bright lead hooks | 120 | A minor pentatonic | 1986 |
| Noir | Sparse Blade Runner atmosphere, sustained Vangelis-style leads, no sidechain | 85 | A aeolian | 2019 |
| Dreamwave | 16th-note arpeggio bass, shuffled hats, washed pads | 100 | A aeolian | 1985 |
| Carpenter | Relentless phrygian ostinato, continuous "Halloween" arp, no hats | 92 | A phrygian | 1978 |
| Techno | Four-on-the-floor, offbeat open hats, two drops + breakdown | 128 | A aeolian | 808 |
| Prog House | Four-on-the-floor, supersaw plucks, pumping pads | 126 | A aeolian | 909 |
| Trance | Euphoric supersaw anthem leads, full 32-bar arrangement | 138 | A aeolian | 138 |
| Acid House | TB-303 squelch line (per-step slide + accent), hypnotic single key | 124 | A phrygian | 303 |
A ninth engine, Dubstep, is currently parked (hidden from the selector) — its wobble needs per-bar bass-filter modulation that isn't wired yet. The code is still in the repo; see the roadmap.
The seeds are deliberate flavour, not arbitrary: birth years of the style
(Outrun 1986, Halloween 1978), Roland model numbers (808, 909, TB-303),
and signature tempos (Trance 138).
Usage¶
Full reference of every control: docs/USAGE.md.
The essentials:
| Element | Function |
|---|---|
| Genre cards | Load a genre's preset (seed, BPM, scale, drum kit, voicing) and regenerate |
| Seed / Scale / BPM | The composition inputs — change any, then Generate |
| ▶ Play / ⏹ Stop / Loop | Transport |
| Tracks & Loop | Per-track mute (bass/lead/pad/drums), loop, harmonic mode |
| Instrument panels | Live model + ADSR + filter + LFO per track |
| Drums · Kit & Voicing | Drum-kit selector + live per-voice knobs + snare-gate toggle |
| Master · FX & Bus | Reverb, delay, sidechain, tape saturation, master gain |
| Export | WAV · MIDI · .swmd · State JSON |
The .swmd format¶
.swmd is a Markdown music format — a human-readable, Obsidian-compatible
serialisation of a composition. It stores YAML frontmatter (genre, BPM, mode),
one block per phase with per-track settings, note patterns as piano-roll tables,
drum grids as fenced ```swdrum blocks, and an FX-bus table. Files are written
with a .md extension so they render natively in Obsidian, and the importer
accepts both .swmd and .md. It is the didactic bridge format between the
generator and the planned Obsidian plugin. See
docs/ARCHITECTURE.md for the schema.
Architecture in one picture¶
seed + genre
│
├── ALGORITHMS[genre] ──► composition: form · bass · lead · pad ·
│ (what notes/structure) drums · swing · sidechain · arrangement
│
└── ENGINE_VOICINGS[genre] ─► voicing: synth model + ADSR + filter +
(what it sounds like) LFO per track · master FX (mergeVoicing)
│
▼
generate() ──► currentState.form (note events)
│
▼
Tone.Transport schedules parts
│
per-track bus ──► dry ─┐ drum voices ─┐
(vol·sidechain·LFO) ├─► compressor ─► tape sat ─► limiter ─► master ─► out
└─► sends ─► reverb / delay ─┘
Engine (composition) and voicing (sound) are deliberately decoupled, so a
genre's algorithm and its tone evolve independently. Full technical docs —
synth models, drum-kit graph, arrangement engine, the .swmd codec, the
public API — are in docs/ARCHITECTURE.md.
No build step¶
Synthwave Surfer is a single self-contained HTML file — the UI, all eight
engines, the voicings, the audio graph, and the in-browser tests all live in
synthwave_surfer.html. There is no bundler, no transpiler, and no
node_modules: the only runtime dependency, Tone.js,
loads from a CDN.
That is deliberate. The app stays inspectable straight from source, is trivially
hostable as a static page (any static host, python3 -m http.server, any CDN), and
diffs cleanly. The only reason it needs an HTTP origin at all is Tone.js's
autoplay-policy initialisation (see Quickstart) — not a build.
Browser compatibility¶
Needs the Web Audio API and a modern evergreen browser. Audio starts only after a user gesture (click / key), per browser autoplay policy.
| Browser | Status |
|---|---|
| Chrome / Edge (Chromium) ≥ 90 | ✅ full |
| Firefox ≥ 90 | ✅ full |
| Safari ≥ 14 | ✅ full (first interaction unlocks audio) |
| Mobile Safari / Chrome Mobile | ✅ plays; the dense desktop UI is cramped on small screens |
In all cases the page must be served over HTTP/HTTPS, not opened from
file:// (Tone.js will not initialise otherwise).
Project layout¶
.
├── synthwave_surfer.html # the entire app (UI + engines + voicings + audio graph + tests)
├── index.html # redirect to synthwave_surfer.html (for the hosted root URL)
├── scripts/ # headless test runners (check-syntax, fullsuite)
├── docs/
│ ├── ARCHITECTURE.md # how the engines, audio graph, .swmd codec, and API work
│ ├── USAGE.md # full user guide
│ ├── images/ # README hero (scripts/shoot-hero.mjs)
│ └── superpowers/ # internal design specs / plans / research (process docs)
├── LICENSE # AGPL-3.0-or-later
├── LICENSE-DOCS # CC BY-SA 4.0 (docs)
├── LICENSING.md # dual-licensing model (AGPL + optional commercial)
├── CLA.md # contributor license agreement
├── CONTRIBUTING.md # developer workflow + test procedures
├── CHANGELOG.md # Keep a Changelog
├── SECURITY.md # threat model + reporting
├── .editorconfig
└── AGENTS.md # orientation for AI assistants (humans can skip it)
Testing¶
No build, three test layers (details in CONTRIBUTING.md):
node scripts/check-syntax.mjs # 1. syntax gate (parses the inline <script>)
node scripts/fullsuite.mjs # 2. headless logic suite → real: 47 passed, 0 failed
# 3. in-browser: open http://localhost:8745/synthwave_surfer.html?test=1 (51 tests → console)
A Playwright + headless-Chrome audio harness (measuring master-output RMS) is used to verify that engines actually produce sound, not just that the logic passes.
Contributing¶
Issues and PRs welcome — developer quickstart in CONTRIBUTING.md. Security-relevant reports: please don't file them publicly, see SECURITY.md. AI assistants get their orientation in AGENTS.md.
Roadmap¶
Synthwave Surfer is the audio layer of the Kuro universe — a theme, a companion plugin, and this generator forming one Obsidian ecosystem.
Shipped:
- [x] Multi-genre engines with decoupled composition + voicing
- [x] Motif-based melody engine and bass-riff engine
- [x] Selectable drum kits with live per-voice editing
- [x] EDM arrangement engine (builds, drops, breakdown, risers)
- [x]
.swmdMarkdown codec (import / export)
Capability-layer expansion (in progress):
- [ ] Dubstep wobble — per-bar bass-filter modulation via a dedicated filter node in the bass bus (the bus LFO can't reach the PolySynth's per-voice filter); Dubstep is parked until then.
Beyond the generator:
- [ ] Obsidian plugin (TypeScript port +
swdrumrenderer) — also resolves thefile://limitation - [ ] Kuro integration — drive soundtracks from companion-plugin events
License¶
Synthwave Surfer is dual-licensed — see LICENSING.md for the full picture:
- Open source (default): GNU AGPL-3.0-or-later — copyleft with the network-use clause. If you host the tool (or a fork) so others can use it over a network, your variant's source must also be available under the AGPL.
- Commercial license (on request): for uses the AGPL does not fit — e.g. a proprietary product or an Apple App Store build. Details in LICENSING.md; contributions are governed by the CLA.
Documentation (README.md, CHANGELOG.md, CONTRIBUTING.md, SECURITY.md, AGENTS.md, docs/) is licensed under CC BY-SA 4.0.
Tone.js is © its authors, MIT-licensed, loaded from a CDN and not redistributed here.
Arbeitsregeln (AGENTS.md)¶
Workspace-Standards (maintainer-lokal): Die verbindliche Leitkonvention steht in
_docs/CONVENTIONS.mdim Multi-Projekt-Workspace des Maintainers,../_docsrelativ zu diesem Repo — nicht Teil dieses Repos, ignorieren falls im Klon nicht vorhanden. Modell comply-or-explain. Offene Punkte fuer dieses Repo siehe Abschnitt "Offene Konventions-Punkte".
Conventions for AI assistants working in this repo.
What this is¶
Synthwave Surfer — a single-file generative music app (synthwave_surfer.html,
~3.9k lines, HTML/JS + Tone.js 14 from CDN). It generates multi-genre electronic
music in the browser: pick a genre "engine" (Outrun, Noir, Dreamwave, Carpenter,
Techno, Prog House, Trance, Dubstep, Acid House), it composes + plays deterministically
from a seed. Long-term goal: an Obsidian plugin (the Kuro universe's audio layer).
Workflow conventions¶
- Edit
synthwave_surfer.htmldirectly — it's the whole app (no build step). - Tests:
- Headless logic suite (committed, canonical):
node scripts/fullsuite.mjs(extracts the in-filetestsarray + pure functions, runs with Node — expect "real: 47 passed, 0 failed"). A.remember/tmp/fullsuite.mjscopy may also exist. - Syntax gate (committed):
node scripts/check-syntax.mjsbefore committing. - Headless AUDIO verification (Claude can self-check sound, not just the user's ear):
NODE_PATH=~/.npm/_npx/e41f203b7505f1fb/node_modules node .remember/tmp/verify_*.cjs(Playwright + cached Chrome) — loads the app, captures console errors, measures master-output RMS via an analyser/offline render. Build newverify_*.cjsper task. - In-browser:
synthwave_surfer.html?test=1runs the test harness in the console. - Audio needs an HTTP server (Tone.js won't load on
file://):python3 -m http.server 8745→ http://localhost:8745/synthwave_surfer.html - Commit style: Conventional Commits; AI-pair commits get a
Co-Authored-Bytrailer. This is a solo single-file project — committing directly tomainis the norm, but large multi-edit features go on a feature branch + merge back. - Tone.js gotchas (learned the hard way): cutoff ≤ ~0.04 ≈ 140 Hz lowpass mutes a
bass; new synth models need a
MDL_LABELSentry (elsepadEndcrash); schedule one-shots viamakePart(transport-synced + cleaned up), not rawtriggerAttack; for the offline render (WAV export) useTone.getDestination()/Tone.getTransport()+setContext()+OfflineContext.render(), NOT the frozen globalTone.Destination/Tone.Transport(cross-context connect error — fixed in v0.1.2).
Memory + logs¶
- Vault memory (tracked, primary):
claude/memory/—MEMORY.mdis the index,project_synthwave_surfer.mdis the live project state. READ THIS FIRST each session. - Global memory:
~/.claude/projects/-Users-johannes-Projects-jkaindl-apps-synthwave-surfer/memory/(bis zum Umzug 2026-10:-Users-Shared-code-synthwave-surfer). - Session logs:
claude/logs/YYYY-MM-DD-<topic>.md. (Therememberplugin also keeps daily scratch in.remember/— gitignored.) - SDD artifacts (since 2026-07-16): Cockpit, not repo — specs/plans/task reports live in
the maintainer's Coding-Cockpit (
$VAULT/25_Coding/synthwave-surfer/_SDD/, CORE-META-14, maintainer-local). They carry working context (vault paths, sister-repo internals) that is of no use to anyone in a public repo. The repo keeps the design essence in this file +CHANGELOG.md. - Legacy stock:
docs/superpowers/{specs,plans,research}/is frozen — do not add anything new there (the existing files stay as historical record). - Never in the repo: absolute paths outside the repo (
/Users/…, vault paths) — use placeholders ($VAULT/…,~/…, repo-relative). Provenance as repo name +file:lineis welcome, though.
Architecture notes¶
- Genre = Engine + Voicing.
ALGORITHMS[engine]= composition (form builder, bass/lead generators, drum pattern, padProg, arrangement).ENGINE_VOICINGS[engine]= canonical synth voicing;mergeVoicingapplies sparse deltas.DRUM_KITS+drumParams= the drum voicing (kit + live-editable knobs via the Drums panel). - Each genre card carries a curated seed (
GENRE_SEED); clicking it = loading "its preset". (The oldFACTORY_PRESETS/applyPreset/renderFactoryRowsystem was removed 2026-06-06.) - Synth models live in
buildSynthModel; drum voices inbuildAudioGraph(kit-driven). - Arrangement (
buildArrangedForm+arrangement{dropBars,breakdownStart/End}): multi-section EDM structure (build→drop→main→breakdown→drop2→main) with riser+crash. - Current state lives in
claude/memory/project_synthwave_surfer.md— including the active branch and the open next-steps. Always reconcile against it.
Offene Konventions-Punkte¶
- [x] CORE-META-03 — Hero liegt unter
docs/images/hero.pngund entsteht reproduzierbar perscripts/shoot-hero.mjs(NODE_PATH=<node_modules mit playwright> node scripts/shoot-hero.mjs) — erledigt 2026-11-03. - [x] CORE-META-08 — Doc-Lizenz
LICENSE-DOCS(CC BY-SA 4.0) angelegt (Text aus yijing) und im README verlinkt (Badge, Lizenz-Abschnitt, Baum) — erledigt 2026-11-03. - [x] CORE-META-10 — Beschreibung + Topics auf Forgejo (git.jkaindl.de)/GitHub setzen — erledigt 2026-06-08 (Description + Homepage=Live-Demo + 15 Topics auf beiden via API; manuell gepflegt).
- [x] CORE-AGENT-01 — Abschnitt "Abweichungen von der Leitkonvention" ergänzt (siehe unten) — erledigt 2026-11-03.
- [x] CORE-AGENT-05 —
.claude/*gitignored (!.claude/rules/bleibt versionierbar);.claude/launch.jsonund.claude/settings.local.jsonaus dem Index genommen — erledigt 2026-11-03.
Crew-Ordner¶
Dieses Projekt hat einen Crew-Ordner (.crew/ im Repo, _Crew/ im Vault) für Aufträge an lokale Worker. Worker lesen RULES.md und ihren Auftrag unter tasks/, arbeiten nur in den dort erlaubten Pfaden und schreiben einen Report unter reports/. Worker committen nie. Der Teamleader (Claude-Session, Skill team-lead) integriert.
Abweichungen von der Leitkonvention¶
Comply-or-explain (CORE-AGENT-01): Diese Abweichungen sind bekannt und gewollt oder noch offen.
- Sektionsnamen (CORE-AGENT-01): Das Skelett steht unter eigenen Überschriften: „What this is“ = Project character, „Architecture notes“ = Architecture principles, „Workflow conventions“ = Commands + Conventions + Gotchas (die Tone.js-Gotchas stehen dort), „Memory + logs“ = Memory. Einen eigenen Context index (CORE-AGENT-15) gibt es nicht: die Datei ist kurz, und es existiert kein
.claude/rules/. - Memory im Repo (CORE-AGENT-02): Neben dem Daemon-Memory unter
~/.claude/projects/…liegt ein getracktes Vault-Memory inclaude/memory/. Das ist der Altbestand aus der Zeit vor dem Cockpit und bleibt der primäre Projektstand, bis er ins Cockpit überführt ist. - Specs und Pläne (CORE-AGENT-04): Der Altbestand
docs/superpowers/{specs,plans,research}/bleibt als eingefrorenes Archiv im Repo. Neues entsteht im Cockpit.