Synthwave Surfer — Foundations Design (Phase 1)¶
Status: Design — pending implementation plan Date: 2026-05-14 Scope: Phase 1 of 4 (see Roadmap section) Implementation target:
synthwave_surfer.html(single-file, ~1915 lines)
1. Vision & Context¶
1.1 The Kuro Universe¶
Synthwave Surfer is the audio dimension of the Kuro ecosystem — a designer-built Obsidian universe that already includes:
- Kuro Theme (v3.2.0) — visual language, ~6000 lines of CSS, signal subthemes via frontmatter
- Kuro Companion Plugin (v1.1.0) — aspect switcher, CRT effects, Hanko marker, retro 3D screensaver
- Synthwave Surfer (this project) — generative + curated synthwave music, designed to soundtrack the Kuro screensaver and other 3D animations
.swmdCodec — a Markdown-native exchange format for music patterns, editable in Obsidian, didactically valuable for learning digital music production
The original spark: the Kuro screensaver gave off such strong synthwave vibes that it called for a matching soundtrack. The generative algorithm alone wasn't compelling enough, so the .swmd codec emerged as a way to combine algorithmic generation with curated, hand-composed patterns — and as a natural bridge to Obsidian.
1.2 What Phase 1 Solves¶
Three current frictions block Synthwave Surfer from delivering Carpenter-grade synthwave soundtracks:
| Friction | Phase 1 Fix |
|---|---|
| Quality — generative output sounds random | 6 curated multi-phase presets in .swmd format |
| Length / Dramaturgy — only 16-bar loops, no narrative | Phase system: one .swmd holds N phases, switchable live |
| External control — no API for animation/screensaver sync | window.synthwaveSurfer.setPhase() API surface |
Phase 1 does not ship full song-form composition or mood macros — those are Phase 3. Phase 1 lays the architectural foundation.
2. Roadmap¶
Phase 1 is the foundation. Phases 2–4 build on it. Each phase is a separate spec/plan cycle.
Phase 1 — Foundations (this document)¶
.swmdcodec (parse + serialize)- Phase system in pattern format
- 6 curated multi-phase presets
- Phase-transition API for external triggers
- WAV export fix + State JSON export
- Stays in single-file HTML. ~3-4 days.
Phase 2 — Plugin Migration (next)¶
- New repo / vault location:
<vault>/.obsidian/plugins/synthwave-surfer/ - Audio engine refactored to TypeScript Obsidian plugin
.swmdfiles open in Obsidian with custom Pattern Editor viewswdrumMarkdown code-block renderer (interactive step sequencer in reading mode)- Plugin events:
app.workspace.trigger('synthwave:phase-change', {phase}) - HTML version remains as standalone "Composition Studio"
- ~1 week.
Phase 3 — Song-Form & Mood Control (later)¶
- Song-form sections:
## Section: intro|verse|chorus|bridge|drop|outrowith tension curves - Mood macros (UI sliders): Brooding ↔ Bright, Sparse ↔ Dense, Static ↔ Evolving
- Phase auto-sequencing for 60–90s animation soundtracks (input: target duration → output: scripted phase progression)
- Solves the remaining Length and Mood-Control frictions completely.
Phase 4 — Kuro Integration & Public Release (future)¶
- Kuro Companion listens to
synthwave:*events; screensaver visuals sync with phase changes - Pattern library with 30–50 curated patterns, organized as Genre Packs (Outrun, Noir, Dreamwave, Italo, Darksynth)
- Submission to Obsidian Community Plugin Marketplace
- Online demo (the standalone HTML) as "try before you install"
3. Phase 1 Architecture¶
3.1 Where Code Lives¶
All Phase 1 code stays in the single file synthwave_surfer.html. No build step. No npm. The plugin migration is Phase 2.
3.2 New Modules (logical, all in one file)¶
┌─────────────────────────────────────────────────────────────┐
│ synthwave_surfer.html │
│ │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ .swmd Codec │ │ Phase Engine │ │
│ │ - parser │ │ - currentPhase: string │ │
│ │ - serializer │ │ - setPhase(name) │ │
│ │ - validator │ │ - crossfade scheduling │ │
│ └────────┬─────────┘ └─────────┬────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ generate() / scheduleAll() (existing, modified) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Tone.js Audio Graph (existing) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ External API (window.synthwaveSurfer.*) │ │
│ │ setPhase(name) | getPhases() | onPhaseChange(cb) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
3.3 Data Flow (Fixed-Pattern Mode)¶
- User picks a Factory Preset →
applyFactoryPreset()parses embedded.swmdtemplate literal → fillscurrentSwmdwith{meta, phases: [...]}. currentSwmd.phases[currentPhaseIndex]is the active phase.generate()builds aformobject from this phase's bass/lead/pad/drums.- Tone.js loops the form. When
setPhase('intense')fires, the engine schedules the switch at the next bar boundary (hard cut at bar — see §5.2). - External code (Kuro screensaver in Phase 4, or just a timer in Phase 1) calls
window.synthwaveSurfer.setPhase('drop')to trigger transitions.
3.4 Generative Mode (preserved)¶
patternMode === 'generative' still uses the existing algo.riffGen / leadGenA / leadGenB / buildForm chain. No regression. The phase system is dormant in generative mode:
- getPhases() returns []
- getCurrentPhase() returns null
- setPhase(name) returns false and logs a console warning
- onPhaseChange(cb) registers the callback but it never fires until fixed mode is active
4. .swmd Format Specification (v1.0, Phase-1 extension)¶
4.1 File Conventions¶
- Codec name:
.swmd(Synthwave Markdown) - File extension on disk:
.md(so Obsidian opens them for editing) - Identification: YAML frontmatter contains
synthwave-surfer: "1.0"
4.2 Frontmatter (extended)¶
---
synthwave-surfer: "1.0"
name: Miami Nights
algo: outrun
bpm: 120
mode: aeolian
pattern-mode: fixed
bass-root: 48 # MIDI note, C3
lead-root: 60 # MIDI note, C4
mood: nostalgic # NEW: free-text tag, used as filter/metadata
energy: medium # NEW: low | medium | high
default-phase: calm # NEW: which phase plays first (defaults to first in file)
---
New fields (Phase 1 introduces, Phase 3 consumes as UI controls):
- mood: free-text mood tag (e.g. brooding, nostalgic, dreamy, tense)
- energy: enum low | medium | high (heuristic for filter UIs)
- default-phase: which phase to start with on load
4.3 Phases — Multi-Phase Pattern Sections¶
A .swmd file contains one or more phases. Each phase is its own complete arrangement (bass + lead + pad + drums). A phase is delimited by a level-2 heading of the form ## Phase: <name>.
## Phase: calm
### Bass · Stratos
[settings + pattern as in original spec]
### Lead · Crystal
[settings + pattern]
### Pad · Vapor
[settings + progression]
### Drums
[swdrum block]
## Phase: intense
### Bass · Stratos
[different pattern, possibly different settings]
...
Backward compatibility with original spec: A .swmd file without any ## Phase: heading is treated as a single implicit phase named default. The original headings (## Bass, ## Lead, etc.) at top level remain valid.
Rules:
- Each phase has its own Bass / Lead / Pad / Drums sections
- Phases share the file-level bpm, mode, bass-root, lead-root — no per-phase overrides in v1.0 (Phase 3 may add this)
- FX Bus and Routing are file-level (one per file, applies to all phases)
4.4 Per-Phase Section Structure¶
Each phase contains the same sections as a single-pattern .swmd from the original spec:
### Bass · <Model>with### Settings+### Pattern(piano-roll table)### Lead · <Model>with### Settings+### Pattern### Pad · <Model>with### Settings+### Progression### Drumswith### Pattern(swdrum code block)
The piano-roll encoding (● ON, ─ HOLD, blank rest) is unchanged from the original spec.
4.5 File-Level Sections (one per file)¶
After all phases:
## FX Bus— reverb/delay/sidechain/tape-saturation/master settings## Routing— Mermaid diagram
4.6 Minimal Multi-Phase Example¶
---
synthwave-surfer: "1.0"
name: Blade Runner Rain
algo: noir
bpm: 80
mode: aeolian
pattern-mode: fixed
bass-root: 48
lead-root: 60
mood: cinematic
energy: low
default-phase: calm
---
## Phase: calm
### Bass · Stratos
### Settings
| Param | Value |
|--------|--------|
| volume | 0.40 |
### Pattern
| Grad | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
|------|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|
| 0 | ● | ─ | ─ | ─ | ─ | ─ | ─ | ─ | ─ | ─ | ─ | ─ | ─ | ─ | ─ | ─ |
[Lead, Pad, Drums sections...]
## Phase: rising
### Bass · Stratos
[denser pattern, same settings or modified]
[Lead, Pad, Drums sections...]
## FX Bus
| Effect | Param | Value |
|-----------|-------|-------|
| reverb | wet | 0.60 |
## Routing
```mermaid
graph LR
Bass --> M[Master]
Lead --> Rev[Reverb] --> M
Pad --> Rev
M --> Out[🔊]
```
5. Phase Engine (state machine)¶
5.1 State¶
let currentSwmd = null; // parsed { meta, phases: [{name, bass, lead, pad, drums}], fxSettings }
let currentPhaseIndex = 0; // index into currentSwmd.phases
let pendingPhaseChange = null; // { targetIndex, scheduledBarTime } — for queued transitions
const phaseChangeListeners = []; // callbacks registered via onPhaseChange
5.2 Transition Behavior¶
When setPhase(name) is called:
- If the target phase doesn't exist → log warning, no-op
- If name === currentPhase → no-op
- Otherwise → schedule the transition at the next bar boundary (the natural musical seam)
- Crossfade duration: 0 by default in v1.0 (hard cut at bar boundary). Phase 3 may add per-phase crossfade configuration.
5.3 Why "next bar" not "immediately"¶
Switching mid-bar would cause audible glitches and break the harmonic context (chord changes, drum fills are bar-aligned). Bar boundaries are the musical seam where transitions feel natural without crossfade. v1.0 keeps it simple; Phase 3 can add crossfade.
6. External API Surface¶
Exposed on window.synthwaveSurfer once the app initializes:
window.synthwaveSurfer = {
setPhase(name: string): boolean, // returns true if scheduled, false if invalid
getPhases(): string[], // names of available phases in current preset
getCurrentPhase(): string | null, // name of currently-playing phase
onPhaseChange(cb: (phase: string) => void): () => void, // returns unsubscribe fn
getMeta(): { name, mood, energy, ... } | null, // current preset metadata
}
6.1 Why window.* not a Module¶
Phase 1 is single-file HTML. No module system. window.synthwaveSurfer is the simplest cross-context API and works for both:
- Browser console testing (window.synthwaveSurfer.setPhase('drop'))
- Future iframe embedding (parent page can drive the synth)
Phase 2 (plugin) will replace this with proper Obsidian plugin API: app.workspace.trigger('synthwave:phase-change', ...) and exported plugin instance methods.
7. Curated Presets (Phase 1)¶
All 6 factory presets get embedded multi-phase .swmd template literals. Each preset has 2–3 phases. All in C aeolian (C minor) for harmonic consistency across phases.
| Preset | Algorithm | BPM | Phases |
|---|---|---|---|
| Miami Nights | outrun | 120 | calm, pumping |
| Highway Cruise | outrun | 108 | cruising, breakdown, lift |
| Blade Runner Rain | noir | 80 | calm, rising, outro |
| Carpenter Synth | noir | 90 | tension, release |
| Mall Bliss | dreamwave | 100 | floating, arrival |
| VHS Sunset | dreamwave | 96 | glow, fade |
Each phase is hand-composed with the design intent that all phases of a preset can transition to each other without harmonic clash (same key, compatible tessitura).
8. UI Changes¶
Minimal additions to keep Phase 1 fast. Major UI work is Phase 2/3.
8.1 New Buttons (Export panel)¶
⬇ SWMD— export current state as.swmd(usesswmdSerialize())⬆ SWMD— import a.swmdfile (usesswmdParse())⬇ State JSON— capture full state as JSON (existing fix from original plan)
8.2 Pattern Mode Toggle (Tracks panel)¶
⚙ Generativ / ⚙ Fixed— switches between algorithmic generation and fixed-pattern playback (from original plan)
8.3 Phase Selector (NEW for Phase 1)¶
When currentSwmd has more than one phase, a small phase selector appears (e.g. dropdown or row of pill buttons). Selecting a phase calls setPhase(name). The currently-playing phase is highlighted. Hidden when only one phase exists.
8.4 No Mood/Energy UI in Phase 1¶
The mood and energy frontmatter fields are stored and displayed read-only (e.g. as a small label near the preset name) but not yet user-editable controls. UI sliders come in Phase 3.
9. Out of Scope for Phase 1¶
Explicitly not in this phase, to keep scope tight:
- Plugin migration (Phase 2)
- Obsidian view registration /
swdrumcode-block renderer (Phase 2) - Song-form sections (
## Section: intro/verse/...) — distinct concept from phases (Phase 3) - Mood macros as user-editable sliders (Phase 3)
- Tension-curve scheduling (Phase 3)
- Cross-phase crossfading (Phase 3 — Phase 1 is hard cut at bar boundary)
- Pattern library beyond 6 presets (Phase 4)
- Kuro Companion event integration (Phase 4)
- WAV cue-chunk markers for AE/DAW sync (Phase 3+)
- Polyphonic bass / lead patterns (would break the monophonic piano-roll grid)
10. Testing Strategy¶
Phase 1 is a single-file HTML app with audio side effects, so testing is mostly manual + smoke-level automated where possible.
10.1 Manual Verification Per Preset¶
For each of the 6 curated presets:
- [ ] Load preset → audio starts within 1 second
- [ ] Each phase plays (via Phase Selector UI) without audio glitches
- [ ] Phase transitions land on bar boundary, no out-of-key notes
- [ ] Export .swmd → re-import → produces same audio (round-trip integrity)
- [ ] WAV export produces a playable file matching the live audio
10.2 Codec Round-Trip Tests¶
A small inline test block (button-triggered, results in console):
- For each preset: serialize → parse → serialize must produce identical strings
- Malformed .swmd (missing frontmatter, broken phase header) → parser logs clear error, doesn't crash
10.3 API Smoke Test¶
// Run in browser console after loading a multi-phase preset:
window.synthwaveSurfer.getPhases(); // → ["calm", "rising", "outro"]
window.synthwaveSurfer.getCurrentPhase(); // → "calm"
window.synthwaveSurfer.setPhase("rising"); // → true (scheduled)
// Wait for next bar → audio shifts to rising phase
window.synthwaveSurfer.getCurrentPhase(); // → "rising"
window.synthwaveSurfer.setPhase("nonexistent"); // → false, console.warn
10.4 Browser-Compat¶
Tested in Chrome/Firefox/Safari latest. Mobile is out of scope (Tone.js + Web Audio mobile is unreliable; explicit non-goal for Phase 1).
11. Risks & Open Questions¶
11.1 Risks¶
- Phase transitions sound abrupt without crossfade. Mitigation: bar-boundary alignment + same-key constraint should make this acceptable; Phase 3 adds proper crossfade if needed.
- Multi-phase presets bloat the HTML file size. Each preset's
.swmdis ~2-3KB; 6 presets × 2-3 phases ≈ 30-50KB extra. Single-file stays under 200KB total. Acceptable. .swmdserializer drift from parser. Mitigation: round-trip tests in §10.2 catch this immediately.
11.2 Open Questions (non-blocking)¶
- Should phase transitions emit a sound effect (sweep, riser) by default? Decision: no, keep it musical and clean. User can layer that themselves.
- Should the FX Bus differ per phase? Decision: no in v1.0, file-level only. Phase 3 may revisit.
- Should the Phase Selector show energy/mood metadata per phase? Decision: phase has only
namein v1.0; metadata is at preset level. Keeps the format minimal.
12. Success Criteria¶
Phase 1 is done when:
- ✅ All 6 curated presets play with multi-phase content and sound musical (subjective, but clearly better than current generative output)
- ✅
.swmdexport → save → re-import → identical playback (round-trip works) - ✅
window.synthwaveSurfer.setPhase()works from browser console; transitions land on bar boundary - ✅ WAV export produces a playable file (fixes existing bug)
- ✅ The codebase is structured so Phase 2 (plugin migration) is an extraction, not a rewrite
13. Relation to Existing Plan¶
The existing implementation plan at docs/superpowers/plans/2026-05-14-swmd-codec-fixed-patterns.md covers Tasks 1–12 of the single-pattern version of .swmd. Phase 1 of this design supersedes that plan with extensions:
- Original Task 1–9 (codec, parsers, UI buttons, generate() integration) → still applies, with parser extended for multi-phase
- Original Task 10 (curated patterns) → expanded from 1 pattern per preset to 2–3 phases per preset
- Original Task 11 (WAV fix) → unchanged
- Original Task 12 (State JSON export) → unchanged
- NEW in Phase 1: Phase Engine, External API, Phase Selector UI, mood/energy frontmatter fields
The new implementation plan (next step) will replace the old plan file.