Zum Inhalt

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.

License: AGPL-3.0 Docs: CC BY-SA 4.0 No build step Single file Powered by Tone.js Runtime: Browser

Launch the web app

Synthwave Surfer — the Outrun engine with the genre selector and instrument panels

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 + genre fully 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 localStorage and export/import them as JSON.
  • External API — a small frozen window.synthwaveSurfer for 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] .swmd Markdown 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 + swdrum renderer) — also resolves the file:// 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.md im Multi-Projekt-Workspace des Maintainers, ../_docs relativ 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.html directly — it's the whole app (no build step).
  • Tests:
  • Headless logic suite (committed, canonical): node scripts/fullsuite.mjs (extracts the in-file tests array + pure functions, runs with Node — expect "real: 47 passed, 0 failed"). A .remember/tmp/fullsuite.mjs copy may also exist.
  • Syntax gate (committed): node scripts/check-syntax.mjs before 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 new verify_*.cjs per task.
  • In-browser: synthwave_surfer.html?test=1 runs 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-By trailer. This is a solo single-file project — committing directly to main is 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_LABELS entry (else padEnd crash); schedule one-shots via makePart (transport-synced + cleaned up), not raw triggerAttack; for the offline render (WAV export) use Tone.getDestination()/Tone.getTransport() + setContext()+OfflineContext.render(), NOT the frozen global Tone.Destination/Tone.Transport (cross-context connect error — fixed in v0.1.2).

Memory + logs

  • Vault memory (tracked, primary): claude/memory/ — MEMORY.md is the index, project_synthwave_surfer.md is 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. (The remember plugin 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:line is 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; mergeVoicing applies 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 old FACTORY_PRESETS/applyPreset/renderFactoryRow system was removed 2026-06-06.)
  • Synth models live in buildSynthModel; drum voices in buildAudioGraph (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.png und entsteht reproduzierbar per scripts/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.json und .claude/settings.local.json aus 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 in claude/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.

Quelldateien