Commit Graph

73 Commits

Author SHA1 Message Date
fa5aa434d1 🥁 godrum: integrate — merge A+B, review fixes R1–R7, mix pass + kick-duck
Stage 2 (Agent C). Real merge of godrum/a-engine + godrum/b-beat-panel onto
godrum/integrate (zero conflicts; extracted app script passes node --check),
then the seven review findings:

R1  kick tail — stop the osc at t+0.01+8τ (τ computed once); no click at max decay.
R2  seed — clear closed hats on steps 14/15 so the seeded open hat rings into the downbeat.
R3  stale paint — paintGrid now drives paintExpr (lit-bars track the grid); groove-slider drags relight the preset buttons.
R4  groove — add the fourth "velocity" slider (groove.velo), matching the arranger's grooves tab.
R5  moods — applyMood's storm + clearGrooves loops skip drum.* lanes; a mood never silences the beat.
R6  kick-duck — duck param on the kick (default .35); pad+drone reroute through a new duckG; fireKick dips it and lets it flood back ("the kick parts the sea").
R7  mix — closed/open hat internal peak scalars 0.70/0.80 → 0.35/0.45 so the hats sit at/just under the ambient perc.density shimmer (§0.6 contract defaults unchanged).

Verified end-to-end on a static serve via offline-render measurements plus a live
analyser tap on the real graph: first-press wake with ♪ off, every voice audible,
choke (95% tail cut), duck −3.07 dB → recover, chance/ratchet, R5 through a storm,
kit+beat persistence incl. duck, zero-mode round-trip, arranger↔beat shared groove,
mutual-exclusion, zero console errors. Temporary verify hooks removed (grep-clean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:44:41 +10:00
4990e9244f Merge branch 'godrum/b-beat-panel' into godrum/integrate 2026-07-12 22:02:54 +10:00
9435c78feb 🥁 godrum: drum lanes + the 🥁 beat panel
Stage 1 / Agent B. Drum lanes fire from seqTick under "drum.*" seq keys
(gate + chance + ratchet + velocity, inheriting swing/humanize from the
groove clock), guarded Synth.drum + GM MIDI on channel 9. New #beat panel
modelled on the arranger: master play, shared groove, 5×16 grid with
playhead, vel/chance/ratchet expression lane, per-lane mute + euclid,
guarded kit drawer. Kit rides serializePatch/loadPatch; drum patterns ride
seqs. B key + sky menu + launcher button. Nothing touched inside the Synth
IIFE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 21:47:57 +10:00
28be1de3b8 🥁 godrum: five synthesized drum voices in the Synth engine
Kick / snare / clap / closed-hat / open-hat as one-shot node graphs
summed onto a drumBus → preSat, so drums ride the world's saturation →
crush → chorus/reverb/echo like every other voice. Each voice keeps a
persistent level→pan chain; hits connect into it. TR-style choke: a
closed hat OR a new open hat cuts the ringing open hat.

Frozen API (§0.6): drum(name,vel), drumParam get/set, drumParamList,
drumKit / drumKitLoad. All params normalized 0..1 at the boundary and
mapped to real units inside each voice; non-finite inputs rejected.
Entirely inside the Synth IIFE — no other territory touched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 21:34:19 +10:00
type-two
4e72592cd3 🧹 no wordmark inside the instrument
The app no longer announces GODSTRUMENT to someone already playing it —
the landing page and auth modal keep the brand; the console keeps only
the counts and the header line. The grimoire trigger slides up into the
vacated corner, and drawSpacedText goes with the wordmark it existed for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:57:13 +10:00
type-two
232475e0ce 🌱 lifestrument v1.0.1 — post-ship review fixes (fable)
Three finds Opus's pre-ship review missed, all verified in-browser:
the heart-connect button was unclickable (pointer-events inherited
none from #readout), orb taps rang the 2.5s Moment bell per tap (now
a short pluck via the newly exposed Synth.pluck), and Game Day's
asleep copy leaked --sports jargon to civilians. Plus the Grimoire
now names the gentle door, and the brief gains §14: review verdict
and the v1.1 punch list (hub Cache-Control for .html, manual section,
knob double-tap, per-vibe info sheets, PWA manifest, wake lock).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:52:04 +10:00
type-two
350ad72782 🌱 LIFESTRUMENT — the gentle client (viz/life.html)
A second, self-contained client for the same hub, mobile-first: only
the sources about living (bodies, births, weather, sun & moon, play),
eight one-tap Vibes instead of a matrix, two macro knobs (ENERGY /
DREAM), a breathing orb, and dopamine Moments (somewhere a baby, the
sun where you stand, a kickoff). Its own compact Web Audio synth, all
routing client-side so it plays against the read-only public hub.
Reuses the invite-only /api auth; local sensors (heart BT, device tilt,
shake, mic, tap) never touch the hub. No money, no machinery, no
cables. Links back to the full instrument; added a "🌱 lifestrument"
item to godstrument's sky menu. Built understand→build→review with an
adversarial pass; 21 findings fixed (a11y sliders, zoom, contrast,
warm-dawn background, reachable heart connect, pluck de-click,
scheduler catch-up cap, moment tuning). test_lifestrument.py guards the
whitelist and the money/machinery ban.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 13:43:48 +10:00
type-two
9ab1b0f5ae 🧹 health-sweep tidy-up: 5 low-severity loose ends (0 broken found)
A 13-agent adversarial whole-app sweep (client controls / runtime / server-auth /
new features / feed-config-honesty, each finding reproduced-or-refuted) found ZERO
broken functions — verdict healthy. Fixed the reachable low-severity edges it did
confirm:

- viz/index.html:1804 — arranger 'grooves' tab threw TypeError (nSel.isNote) when
  dests is empty in the cold-connect window (post-sign-in, pre-WS-hello). Guard:
  dests.get(arrSelKey) || {isNote:false} (the 'clips' tab already tolerates empty).
- viz/index.html — picking a skin from the sky menu / F4 next-skin / F4 prev while
  in ZERO mode force-enabled an invisible, un-toggleable chakraMode (every clear
  path is zeroMode-guarded). Added the same !zeroMode guard to all three sites
  (byte-identical outside zero — no regression).
- hub.py:117 — GODSTRUMENT_READONLY parse treated any non-empty non-'0' value as
  true (so 'false'/'no'/'off' enabled read-only). Now an explicit truthy set
  {1,true,yes,on}; prod uses '1' → stays read-only (verified before deploy).
- viz/index.html:2996 — removed a dead else branch in zero 'save as vibe' (its
  'vibes land in the next update' placeholder was unreachable; saveVibe is a real
  sibling method) → call this.saveVibe(gk) directly.
- transform.py:40 — collapsed a no-op ternary (both branches float(v)).
- viz/manual.html — regenerated (was stale vs build_manual.py; picked up the
  admin-panel CSS added in 764e28b; grimoire prose was already in sync).

Left as documented cosmetic: sanitizeVibe caps emoji at 2 code points (can split a
ZWJ/flag glyph on a shared vibe — display only, no XSS). Verified: node --check,
all .py compile, auth --selftest green, app boots into zero clean, console clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 10:21:03 +10:00
type-two
764e28b1c3 👤 admin user management — list / edit / delete accounts + reset passwords
The admin panel could mint invite codes and read stats but not manage the people.
Adds, all under the existing admin-gated /api/admin/ block (default-closed:
no session → 401, non-admin → 403):

- GET  /api/admin/users        — every account: id, email, admin flag, patch count, joined
- POST /api/admin/user/<id>    — rename / change email / grant-or-revoke admin
                                 (validated like signup: format + uniqueness)
- POST /api/admin/user/<id>/reset — set a fresh random temp password, returned ONCE
                                 (never stored plaintext or logged) to relay to the user
- DELETE /api/admin/user/<id>  — delete the account + its saved patches (feedback +
                                 used invite kept as history)

Self-lockout guards: you can't delete your own account or revoke your own admin.
Admin granted via the GODSTRUMENT_ADMIN env is marked env_admin and its toggle is
hidden (can't be removed in the db). auth.py migration-free — uses existing tables;
the is_admin column was already migrated.

Client: the ⚙-account admin panel now lists every user with inline edit (username +
email), 🔑 reset-pw (shows a copyable temp), ★ make/revoke admin, and 🗑 delete
(confirm); the panel widens for the table. Names/emails escaped on render.

Verified: auth.py --selftest (list/edit/reset/delete + endpoint guards); live curl
against the local dev db (rename, bad-email/dup rejected, admin toggle, self-guards
400, non-admin 403, delete cascades presets); and the UI end-to-end (rows render
with correct per-row actions, edit expands, reset shows a temp password). Console
clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 07:01:58 +10:00
type-two
fb32d10991 🐛 seed the synth idle (not 0) for cableless tweaked voices — fix-verify regression
The fix-verify pass flagged one CONFIRMED regression in the #1 floor loop: it
pinned any tweaked cableless non-note dest to a hard 0, but two dests have a
non-zero synth idle — filter.cutoff (d(...,0.3)) and tempo.nudge (d(...,0.5)).
So touching the filter knob in zero (or, before this, merely opening OMNI, which
created a neutral tweak) pinned filter.cutoff to 0 → 250 Hz, darkening the lead.

- new DEST_IDLE map; the floor loop seeds destVals[k]=DEST_IDLE[k]||0, so a
  neutral/reset knob returns a voice to its idle (filter 0.3), an offset rides
  from the idle, and mute still → 0. (verified: reset filter → 0.3 not 0;
  drag up → 0.767; mute → 0)
- OMNI update() + knob pointerdown now read tweaks[k] directly instead of tw(k),
  so rendering/opening the panel no longer creates phantom neutral tweaks (which
  also kept the tweaks map — and saved dimensions — clean). (verified: open OMNI
  → filter has no tweak)

A clean instrument (no tweaks) stays byte-identical; the public house patch was
already masked (filter.cutoff is cabled to sun.speed). Deploy gate: GO.
Console clean, syntax clean, 21 knobs, field healthy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 01:26:01 +10:00
type-two
fa948823a7 🐛 fix 8 adversarial-review findings (zero/vibes/OMNI)
A 30-agent adversarial review (5 dimensions × find→refute×2) confirmed 8 defects;
all fixed and live-verified:

BLOCKERS
- #1 unwired OMNI trim couldn't be silenced: the offset-floor only seeded a dest
  while offset>0.0015, so reset/mute/lower stopped emitting the key and the last
  floored value stuck in the synth (Synth.update merges). Now ANY tweaked cableless
  non-note dest is seeded 0 so the processing loop writes its real value, incl. 0.
  (verified: crush norm 0.5 on drag → 0 on reset)
- #2 leaving zero with an OMNI/offset-only build (no cables) was silently discarded
  — exitZeroToMood gated on routesArr.length===0. New zeroHasBuild() also counts
  picked orbs + any non-neutral tweak, so the keep/old/weave card always shows.
  (verified: offset-only build → 'you shaped this instrument by hand. keep it?')
- #3 vibe scale whitelist used truthiness (SCALES[q.scale]) → 'constructor'/'__proto__'
  passed → NaN into a Web Audio AudioParam → threw ~40×/s (remotely triggerable via
  public #vibe= links). Now Array.isArray(SCALES[...]) + quantize only on note dests;
  mxQuantize fallback hardened too. (verified: fx quantize dropped, note scale→minor_pent)

SHOULD-FIX
- #4 finishZeroExit keep/weave clobbered the kept mix (applyMood resets MOOD_KEYS):
  re-lay the build's tweaks after applyMood; weave merges build tweaks over prezero.
- #5 crush knob was fully dead (never in dests): OMNI.render ensureDest()s every
  voice/FX key → arc lives, floor works, right-click opens the menu. (verified)
- #6 OMNI drags in zero weren't persisted: knob pointerup/dblclick call zeroUI.persist()
  when zeroMode → survives reload. (verified: offset 0.467 in gs_zerobuild)
- #7 reloaded vibes showed OFF: persist activeVibes + per-route _vibes/_base to
  gs_zerovibes, restoreVibeState() on enterZero(true). (verified: vibe restores ◉)
- #8 GODSONIQ chip lit ~3s late: update() re-lights engine/♪ chips each frame.

Console clean, syntax clean, 21 knobs, field healthy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 01:10:30 +10:00
type-two
fc1bb307bc 🔒 harden vibe import trust boundary — strip markup from name/emoji (ship-check)
The vibe name + emoji arrive from user-pasted GSV1 codes and #vibe= URL fragments
and are rendered via innerHTML in the vibes panel + the shared-vibe offer card.
name was <-escaped but emoji was raw. sanitizeVibe now strips [<>&"'`] from both
at the trust boundary, so nothing dangerous reaches any render path.

Verified live: a crafted code with name='<img onerror=…>' + emoji='<svg onload=…>'
imported via #vibe= → no <img>/<svg> in the DOM, no script fired, both rendered as
inert text. (ship-check item 3: user strings → HTML must be escaped.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 00:41:40 +10:00
type-two
93c5e20ccf 📖 Z6 — grimoire + manual: zero-build, OMNI panel, dimensions & vibes
Canon (viz/index.html grimoire) is truth; manual synced to match.

- grimoire: two new tech-manual sections in the OMNI/Earth-Echo/GODSONIQ style
  (numbered how-to + spec table) — 'Building from zero — the empty instrument'
  (rings → orbs → drag/tap routing → synth-roll → keep/weave exit → persistence)
  and 'OMNI — the synth panel' (live arcs, trim-riding knobs, engine selector,
  earth echo, transport). Rewrote 'Saving your instrument' as dimensions & vibes.
- manual (had drifted — still said MY PATCHES): added Building-from-zero + the
  🎹 OMNI panel; 'Saving' → dimensions & vibes; keymap now lists F11 zero + O.

Verified: grimoire renders all three new h2 sections, no broken markup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 00:37:09 +10:00
type-two
5e6e257ec0 🎹 Z5 — OMNI: a NICE synth panel over the dests + tweaks
A 🎹 OMNI button (sibling of 🎛 tracks, key O) opens a floating panel — the
planet's synth, laid out like a synth. NO new audio paths: a view over the same
dests + tweaks the field already drives.

- knobs: each knob's ARC is the dest's live value (n.normDisp, the same number
  the field meters — it breathes with the world); dragging rides that dest's
  tweak OFFSET (setParamLocal) and a dot marks your hand; double-click resets to
  neutral; right-click opens the node's full context menu. Note voices (lead/bass)
  show a live pitch arc — display-only (their character is the engine selector).
- modules: VOICES (lead/bass/pad/drone/perc), FX RACK (filter/reverb/delay/
  saturation/glitch/granular/crush/morph/lfo/space), 🌀 EARTH ECHO (a toggle;
  its six echo.* knobs appear only when it's on).
- header: voice-engine selector (FM tine / 🎸 string / 🎛 GODSONIQ — the pluck's
  character, via Synth.stringVoice/godsoniq/sampling), ♪ start-stop, live BPM,
  🔒 godtime lock.

Matrix honesty fix (computeDests): a hand-set positive offset now gives a voice a
floor even with no cable — so an OMNI (or the existing 'trim') knob can make a
voice audible from nothing. Only affects dests a user explicitly offset; a clean
instrument (all offsets 0) is bit-identical to before.

Verified live (z0test, via a manual matrix pump since the headless preview's rAF
is paused): panel opens with 21 knobs + 3 modules; idle arcs track live data
(filter 0.41); dragging reverb up floored its value 0.09→0.59 with the tweak dot
showing; double-click reset it; engine selector swapped FM→string; earth-echo
toggle revealed its knobs; right-click opened the 'filter cutoff' node menu.
Tweaks persist in the saved dimension (serializePatch carries tweaks). Console
clean, syntax clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 00:32:59 +10:00
type-two
3654b2f6ec 🌌 Z4 — dimensions: MY PATCHES → DIMENSIONS (framing) + dimension zero
A dimension is everything (the whole scene: routes, tweaks, grooves, tempo);
a vibe is a feeling you can hand a friend. Same /api/presets storage — this is
a framing rename, existing saves appear unchanged.

- ⚙ panel: 'MY PATCHES' → 'DIMENSIONS', copy reworked; input placeholder
  'name this dimension…'.
- the load dropdown always leads with '✦ dimension zero — the empty field';
  loading it calls enterZero() (the place you build from nothing).
- grimoire 'Saving your instrument' rewritten (canon = truth): the dimension /
  vibe distinction, the GSV1 share code + #vibe= link, 'build a vibe, save a
  vibe, share a vibe'.

Verified live (z0test): panel shows DIMENSIONS +  VIBES, MY PATCHES gone,
dropdown leads with dimension zero. Console clean, syntax clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 00:11:59 +10:00
type-two
55defaa695 Z3 — vibes: build a feeling, save it, share it
A vibe = a small bundle of cables {v:1,name,emoji,routes[≤32]}. Distinct from a
dimension (the whole scene): a vibe is a feeling you can hand a friend.

- apply is additive + reversible: routes a vibe creates are tagged (route._vibes)
  so toggling it off removes exactly those; a route two vibes share survives until
  both are off; a hand-built cable (route._base) a vibe merely rides is NEVER
  deleted. Tags live in-memory only — they don't leak into saved patches.
- save from a zero orb (right-click → 💾), from any node's right-click in the full
  field (💾 save these as a vibe), or 'save current wiring' in the panel (≤32 cap).
-  vibes panel (zero pill + ⚙ gear): list, toggle on/off, ⧉ copy share code,
  × delete, paste-to-import. localStorage library (gs_vibes).
- share: GSV1.<base64url(json)> clipboard code + godstrument.pro/#vibe=… fragment
  that OFFERS on load (add it / just save) and never auto-applies.
- trust boundary: sanitizeVibe hard-validates every import — unknown source/dest
  skipped (not thrown), amounts/roots/octaves clamped, curves/scales whitelisted,
  32-route cap, try/catch the base64/JSON. Garbage codes are rejected cleanly.
- per-account cloud storage deferred: the code + local library already deliver
  build/save/share across devices and friends. (add a vibes table if users ask.)

Verified live (z0test): built a 2-cable orb → saved 'sunrise duo' (quantize
preserved) → panel listed it ◉ → share code round-tripped through import →
garbage code rejected → hard-reload kept the library → toggle ON created exactly
2 routes (lead.note lit), toggle OFF removed them → #vibe= fragment offered
'shared gift' in the full app, hash cleared, not auto-applied. Console clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 00:08:36 +10:00
type-two
00503b14c3 🌱 C1-C3 — keep-your-build exit, reload persistence, orb tap/menu/storm
C1: leaving zero with a build no longer silently discards it — a compact
    three-way card (keep my build / bring the old world back / weave them
    together); esc stays in zero. 0-route exits stay quiet (restore prezero).
C2: the zero build survives a reload — picks + cables persist to
    gs_zeronodes/gs_zerobuild; enterZero(true) on the reload path restores them.
C3: tap-to-patch (arm an orb, tap a voice) as a touch fallback for drag;
    right-click an orb → member list (× to drop a feed + its cables),
    unpatch-this-orb, and a save-as-vibe stub (Z3); a ⛈ enter-the-storm pill.
Nit: the chakra-view key/menu is a no-op in zero (nothing to draw there).

Verified live (z0test): pick→orb→tap-patch lit lead.note; hard reload restored
the ☀ orb + lit socket; the exit card's keep-my-build landed in a healthy full
field on the storm mood with zero-storage cleared; orb menu drops members.
Console clean, syntax clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 23:57:14 +10:00
type-two
9a7f94759c 🌱 Z2 — zero mode routing: drag an orb to a voice, right-click to assign
The functional heart — build the instrument by hand from zero.

- Orbs are draggable; the output-rack sockets are drop targets. Drop an orb on a
  voice/FX socket → lays a cable per member feed (registerRoute), auto-adding a
  default quantize (minor_pent / root 60 or 36 for bass) when the target is a
  note voice. The socket lights green when it carries routes; a ticker line
  confirms the patch and points to ♪.
- Right-click a socket → the "synth roll" assign menu: for note voices a scale
  (the 11 SCALES) / root / octaves picker; for CC voices an amount slider + curve
  picker (lin/exp/exp3/log/scurve/inv). Plus "× unpatch all". Edits the live routes.
- refreshSockets() reflects existing routes on open; hover-highlight on drag.

Z-AC verified live: pick sun.speed (cosmos ring) → drop on filter · pick
crypto.price (wealth ring) → drop on lead → both sockets light · right-click lead
→ scale picker (11 scales, root, octaves) · press ♪ → OMNI plays (button green).
Bitcoin sings through a sun-opened filter, built from nothing. Console clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 23:38:03 +10:00
type-two
4e44533cdd 🌱 Z1.5 — stage nodes are spinning orbs (John's vision)
Each built group becomes a spinning orb on the stage, not a flat chip — a glowing
core in the group's hue with its picked feeds as petals orbiting around it (CSS
zspin/zpulse). They accumulate as you build: pick from fire → one orb; add cosmos
→ two; and on. Orbs breathe live — the core glows by the group's average value,
each petal brightens with its own feed. More feeds widen the orbit ring.

Verified live: fire (3 petals) + cosmos (2 petals) render as two spinning glowing
orbs; console clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 23:33:43 +10:00
type-two
d30cea469c 🌱 Z1 — zero mode: the calm 7+7 field (rings, picker drawer, stage)
The overlay that replaces the sensory flood. In zero mode the node field is not
drawn (frame() skips drawCables/drawSourceNode/drawDestNode); a DOM overlay
takes its place over the starfield:

- 7 vibe-rings down each side — the 14 config groups (left = elements/feelings:
  light·dark·fire·water·earth·air·spirit; right = domains: planet·cosmos·heavens·
  human·market·wealth·summer). Each: a hued dot (GROUP_META — taste consts,
  retune freely), label, live feed count, and a breathing glow scaled by the
  group's live average value. Members come from groupsInfo (the hub's config).
- Picker drawer: click a ring → it slides in from that side listing the group's
  feeds (name, key, live value bar, ⊕ that toggles in place). Pick 1+ → a stage
  node (chip, colored by group) lands on the center stage.
- Output rack along the bottom: OMNI voices (lead·bass·pad·drone·perc) + space &
  grit (filter·reverb·delay·…) as labeled sockets. Visual only — routing is Z2.

Veil (z-8) sits below the ♪/tracks/grimoire controls (z10-13) so they stay
usable. Verified live: rings render with hues + live glow, heavens drawer opens
with its 5 feeds + value bars, picking lands stage chips, ⊕ toggles in place,
console clean. Routing to the rack + right-click assign = Z2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 23:29:19 +10:00
type-two
2a705e3a35 🌱 Z0 — zero mode: blank dimension + first-visit choice card
The calm front door. Zero is not a muted world (moods do that) — it's an EMPTY
one you build by hand, the same matrix started from nothing.

- enterZero(): snapshots the current patch → gs_prezero, then loadPatch({}) empties
  the field; sets clientMatrix so the hub can't re-populate the cables; gs_mood="0 zero".
- exitZeroToMood(name): restores gs_prezero (the full factory) then applies the mood —
  leaving zero brings the whole world back.
- First-visit choice card ("one world, three doors"): 🌱 start from zero · 🌅 first
  light · ⛈ the full storm. Replaces the silent default-into-first-light; backdrop/esc =
  first light. Returning users keep their last mood; a returning zero-user re-enters zero.
- Mood submenu gains "0 zero — build from nothing"; F11 toggles zero (KEY_ACTIONS).

Verified live (logged-in preview): card renders with 3 doors; "start from zero" empties
the field (56 factory cables cleared, snapshotted); F11 restores all 56 + first light
(92 BPM locked); returning-zero reload re-enters zero with no card; console clean.

The 7+7 calm slot UI is Z1. Not deployed yet (deploy is Z6, post-review).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 22:53:14 +10:00
type-two
5db568d9da 📖 name the softsynth OMNI + tech-manual sections (OMNI / Earth Echo / GODSONIQ)
Names the built-in browser synth OMNI (♪ tooltip, intro prose) and adds three
scannable "tech manual" reference sections to the grimoire — OMNI (full voice
roster + how-to), the Earth Echo (six world-driven tape knobs + Schumann), and
GODSONIQ (resample + tab feed) — each a numbered how-to plus a two-column
control/dest spec table (new .spec / ol.steps CSS; auto-indexed in the TOC).

Every label, dest key and world→voice mapping extracted from code and verified.
Fixes two drifted claims in the old Earth Echo prose the audit caught: tape
wear drives hiss only (grit/saturation is static, not world-driven), and the
Schumann sub is ×4 = two octaves (not "four times").

Manual: sections flow into PART I (extractor taught .spec/.row → markdown
bullets) + an OMNI umbrella entry in Appendix J.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:02:40 +10:00
type-two
78718e9459 🌐 feed a tab into the world — external audio via getDisplayMedia
Right-click sky → "🌐 feed a tab": getDisplayMedia({video:true, audio:{…}})
lets the user share another tab's audio (a YouTube tab, a stream). Wired via
createMediaStreamSource into preSat → the full FX chain + master, so it's
audible live, crush-able, and resampleable by GODSONIQ from one wiring. Video
track stopped immediately; a track 'ended' listener handles Stop-sharing.

Audio constraints (verified against MDN/W3C/Chrome docs): suppressLocalAudio
Playback so the tab isn't heard twice, and echoCancellation/noiseSuppression/
autoGainControl off so the WebRTC voice-DSP doesn't mangle music. Chromium-only
(feature-detected; graceful ticker hints for unsupported / no-audio-track);
macOS grabs a tab not all-system; DRM services capture silent, YouTube fine.

Grimoire (truth) + manual Appendix J updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:38:33 +10:00
type-two
5ffa3a4bfb 🎛 GODSONIQ — resample the world onto the keys (ASR-10 nod)
Record ~3s of the live master mix into a buffer via a recorder AudioWorklet
tapping the full post-FX bus, then replay it pitched across the keyboard
(playbackRate = 2^((midi-60)/12), middle C = unity). Runs through the same
FX chain as every voice, so `crush` gives it the classic 12-bit sampler grit.
Right-click sky → 🎛 GODSONIQ to sample / re-sample; 🎸 string voice exits
back to the FM tine. Capture path verified: full-amplitude sawtooth recorded
clean (peak 0.98, rms 0.49) over the realtime port round-trip.

Grimoire (truth) + manual Appendix J updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:30:25 +10:00
type-two
b884cf9642 🎛 make wavetable.morph & granular.density literal (honesty fix)
The last two names that lied are now true:
- wavetable.morph — was a filter-Q sweep. Now a REAL wavetable: the lead is two
  band-limited PeriodicWaves (a mellow one + a bright saw-rich one) crossfaded by
  morph. Spectrally verified: smooth brightness sweep (centroid 462→880→1174
  across morph 0→0.5→1), no aliasing.
- granular.density — was one occasional noise burst every 4th step. Now a REAL
  grain cloud: a density-scaled scatter of short (12–40ms) pitch-varied grains
  per step (0 at rest → a dense cloud when the world writes). noiseHit takes a
  scheduled start time so grains scatter within the step.

The grimoire's existing claims ("sweeps the oscillator through its bank of
waveforms" / "a cloud of tiny grains") were aspirational — now they're accurate,
so no doc softening needed. Every clickable/routable thing now does what it says.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:13:40 +10:00
type-two
adde3bc326 🎸🔊 worklet batch — Karplus string + 12-bit bitcrush (spectrally verified)
The two voices native Web Audio can't do safely, done as one AudioWorklet module
(loaded from a Blob URL, so still single-file):
- 🎸 Karplus-Strong string — a sample-accurate 8-voice plucked-string worklet
  (noise burst → damped delay loop). Right-click sky → "string voice" swaps the
  FM tine for it. Verified offline: STABLE (rms 0.04, not the billions the native
  version exploded to), in-tune (A3→221, A4→436), harmonic.
- 🔊 12-bit bitcrush — a world-drivable lo-fi stage on the master bus (true
  bit-depth quantise + sample-rate reduction). Route anything to `crush`; clean
  by default (16-bit → 4-bit + downsample). Verified: transparent at 16-bit,
  hi-freq grit 0.18 at 4-bit.

Loaded async on synth start, spliced glitch→crusher→out and karplus→preSat.
Verified live: both load with no errors, string-voice toggle flips correctly.
Grimoire + manual document the whole synth-voice suite.

This closes the timbre work: FM tine · supersaw · chorus · string · bitcrush —
every one tuned by the offline-render + FFT harness before it made a sound.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 07:37:31 +10:00
type-two
2db0d5945f 🔍 pinch/scroll zoom — get right into the orbits
The whole visual now zooms: scroll or trackpad-pinch to zoom the canvas toward
the cursor (1×–6×), drag empty space to pan when zoomed, double-click to snap
home. Implemented as a CSS transform on #stage (smooth, no draw-loop surgery);
all four hit-test chokepoints (mousedown/move/up/contextmenu) invert the zoom so
clicking/dragging/right-clicking nodes still lands exactly — provably correct
(the inverse of the transform). Pan clamps so the canvas always covers the view.
Grimoire + manual note the gesture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 03:01:06 +10:00
type-two
d1fa247ad8 🎹 supersaw pad + brighter FM tine (spectrally verified)
Supersaw: the pad's 3 chord tones each become a detuned saw cluster (9 saws,
±14¢ spread) — the lush JP-8000 wall. Verified offline: lush (133 partials),
no clip (peak 0.29), no aliasing, no DC; padAmp trimmed 0.10→0.07 for the extra
voices. Retuning loop + level updated for the new {o,iv} structure.

FM finish: bumped FM_RATIO 2→3 (the classic bright DX7 tine). Verified alias-free
across the whole register — even ratio 4 at A6/hard aliases <0.2% — so brightness
is pure taste, not a correctness risk. One-line const if you want warmer (2) or
glassier (4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 02:41:15 +10:00
type-two
41d2c762a9 🔔 FM pluck — DX7-style tine on every plucked note (spectrally verified)
pluck() (used by the sequencer, hand notes, and mind mode) is now a sine carrier
phase-modulated by a sine operator (ratio 2), with the mod index enveloped —
a bright "tine" attack decaying to a mellow tail — and scaled by velocity, so
harder = brighter (the DX7 magic). PM not FM, for stable pitch.

Built via the new spectral-verification method: rendered offline in an
OfflineAudioContext and FFT-analysed BEFORE shipping. Verified: in-tune
(A4→439Hz, A2→110Hz), harder-is-brighter (centroid 570→858), zero aliasing,
zero DC, stable. The harness earned its keep immediately — it caught a
Karplus-Strong feedback explosion (rms in the billions) that would've shipped
blind; KS + bitcrush deferred to an AudioWorklet batch (native feedback + filters
is unstable under Web Audio block-processing).

Taste (the FM ratio/index) is the one thing left for real ears. FM_RATIO is a
one-line const to tune.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 02:30:03 +10:00
type-two
9e5de4f55f 📖 grimoire front door — quick-start card + clickable contents
The grimoire got big, so give it a proper entrance:
-  Quick-start card up top: a 7-step "make it sing in a minute" on-ramp for the
  generative sequencer (scale → euclid → Play One → ratchet → p-lock → play).
- ☰ contents: a floating, jump-anywhere table of contents built from the 22 h2
  sections (delta-based scroll so it lands reliably in any layout). Sits beside
  the × close, drops a clean index panel, closes on pick or outside-click.
Verified: quick-start renders (7 steps), TOC lists all 22 sections and jumps
distinctly (Getting in→top, Playing it→far). Manual regenerated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 01:33:16 +10:00
type-two
c5997163b5 🎨 hand-tuned skin home keys — 14 deliberate, distinct moods
Replace the auto-derived skin scale (which gave 3× C wholetone and a sparse
hirajoshi default) with a curated SKIN_HOME map — a considered {root,scale} per
cosmology, every pair unique so turning the wheel always changes the key.
Verified: 14/14 distinct. Default (chakra) is now warm C minor pentatonic.

chakra C min-pent · planes A lydian · tree D minor · lodge G major ·
ladder E phrygian · yggdrasil F# minor · lataif B dorian · wuxing F# pentatonic ·
tarot A minor · zodiac C mixolydian · futhark G dorian · wheelyear F major ·
artist C wholetone · bagua E min-pent. (Fallback to derived scale for any future skin.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 01:14:39 +10:00
type-two
8dd551017a ⬗ Parameter locks — the Pocket-Operator move on the texture voices
Steal the one clever idea from the PO/Elektron world: p-locks. A CC/texture
voice (filter, glitch, reverb, drive) now gets a per-step LOCK lane — pin its
value to a blocky level on any step, overriding the world there; drag a step
below the lane to unlock it and hand it back to the data. The filter jumping to
a different cutoff on every hit, disjointed and alive — a full groovebox.

- applyGroove(sq, step, world) unifies the gate + p-lock; computeDests calls it.
- lock[] rides on seqs (serialised, migrated). Note voices keep velocity/chance/
  ratchet; CC voices get the p-lock lane (amber).
- self-check guards all four cases (no-groove / lit-unlocked / dark / locked).
- grimoire + manual updated (also documents the Juno chorus).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 01:02:22 +10:00
type-two
1938874707 🌊 Juno-106 chorus — the whole synth, warmer and wider
The safest, highest-value vintage borrow: an always-on subtle BBD-style chorus
on the master bus — two short delays each warbled by its own slow LFO, panned
wide, ~0.3 wet. Universally warms and widens the dry oscillators; can't sound
bad, can't error. Parallel send off `post`, chorusWet exposed for future
world-modulation. Verified: graph builds clean on synth start, no errors.

(FM mode, supersaw, and the 12-bit bitcrush are the next step — deferred to a
listening pass since they reshape core timbre and want real ears to tune, not
a backgrounded preview tab.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 00:48:33 +10:00
type-two
75dbe81749 🎚🌀 skins re-tune the world — Source → Spirit → Matter, made true
The flagship fix from the wired audit: skins were visual-only (the mix was
identical across all 14). Now the active Spirit re-tunes the Matter — each
skin's home tonality (its first station's scale) becomes the global scale, so
BOTH the sequencer and the ambient data-melodies re-harmonise when you turn the
wheel of cosmologies. Verified: 10 distinct tonalities across 14 skins
(chakra→B hirajoshi, wuxing→A dorian, wheelyear→F major, futhark→G# pentatonic…).
- skinHomeScale() derives {root,scale} from the skin's own station data (no
  hardcoded map); applySkinScale() sets gScale on skin change + at init.
- computeDests overrides the ambient note quantize to follow gScale.
- the sequencer already followed gScale, so it re-tunes for free.
- grimoire updated: "choose your spirit and the same planet plays through it"
  is now literally true. Manual regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 00:42:36 +10:00
type-two
e2170060c7 🔌 honesty pass — every control now does what it says
Retire the wired-audit debt so nothing is decorative-pretending-to-be-wired:
- FREEZE now works: a group freeze snapshots each member and holds it (was a
  set-but-never-read flag). Verified: holds 0.2 while the source moves to 0.9.
- .mid export is full-fidelity: real per-step velocity + ratchets expanded to
  sub-notes (was hardcoded vel 100, single hits). Chance/Play-One stay live-only
  — honest for a deterministic file.
- Hide the level/trim sliders on note voices (they never applied to a pitch —
  no more dead sliders).
- Grimoire honesty: soften "every source is a real measurement" to name the
  honest models (demographic tide, room-sensor sim); drop the phantom ⚙ SPELLS
  panel claim. Manual regenerated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 00:34:57 +10:00
type-two
74df16c90f Ratchets — per-step sub-hits, and the glitch loop is closed
The last piece from the glitch-drum workflow: a per-step ratchet lane (1–4) —
each step can re-fire that many sub-hits within its slot, for instant rolls and
32nd-note glitch from a single step. Fired via short setTimeouts inside seqTick;
combined with Play One, every sub-hit re-rolls the note. Verified live:
rat3→3 hits, rat4→4, rat1→1, empty step→0. Self-check guards the rat default.

Now the generative loop is complete: Euclidean picks the rhythm, Play One the
pitch, ratchets the subdivision, the Groove the feel, follow-actions the
structure — and the global Scale keeps it all in key. Grimoire + manual synced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:43:37 +10:00
type-two
f65598a988 Global Scale + Play One — one key for the planet, glitch by chance
Global Scale (Live 12 Scale Awareness §3.13): a root + scale the whole groove
editor sings in. The piano-roll rows ARE the scale degrees (always folded to
scale), so every melody is in key; change the scale and drawn patterns
re-harmonise in place. Travels with the patch.

Play One (adapted from Ableton's Group Notes → Play One): a piano-roll column
can hold several candidate degrees — stack them (they turn blue) and the
sequencer picks one at random each fire. Stack in-scale notes under a fast
Euclidean fill and the line glitches itself, always in key. Applied in both
grid editors, seqTick, and .mid export (prints the first candidate).

Self-check guards scaleRows + the stack helpers. Grimoire + manual in sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:32:40 +10:00
type-two
d4a1b71427 ⬢ Euclidean fills — the world's oldest rhythms, from the arithmetic
Live 12's Euclidean generator (manual §11.3.5) on the groove grid: spread N
pulses as evenly as possible over the pattern, with adjustable steps (cycle
length) and rotation, tiled into the 16-step grid. E(3,8) tresillo, E(5,8)
cinquillo, E(4,16) four-on-the-floor — one control fills any groove or gate
lane (note lanes fill the root). Verified live: E(4,16)→[0,4,8,12],
E(3,8)→[0,3,6,8,11,14], E(5,16)→[0,4,7,10,13]. Self-check guards the
canonical patterns. Grimoire + integrated manual updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 19:40:10 +10:00
type-two
18e03d3c82 📖 grimoire: document the Groove and Follow Actions
Keep the source-of-truth grimoire honest — describe the swing/humanize/
velocity feel with presets, the per-step velocity & chance lanes, and the
⤳ follow-action clip hand-offs. (GODSTRUMENT_MANUAL_SOURCE.md mirror pending.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 17:26:06 +10:00
type-two
0a89080e8d ⤳ Follow Actions — the arrangement composes itself
Ableton's Follow Actions (Live manual §16.7) on the arranger's clips: after a
clip plays its bar, it can hand off to another by chance — again / next / any /
any-other / stop — so an 8-bar loop stops being static. The manual's own words:
"structures that repeat but can also be surprising… never exactly repeat."
- per clip-letter action + chance, edited in the grooves tab.
- a live overlay (arr.now) diverts playback without rewriting the authored grid;
  with no follow actions set, playback is byte-identical to before (no regression).
- ⤳ marks clips that can divert; the feel travels with the patch.
- self-check now guards the follow-action decision logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 17:24:22 +10:00
type-two
b248d9e629 🎚 the Groove — swing, humanize, per-step velocity & chance
Ableton's Groove Pool (Live manual §14.1) reduced to the fixed 1/16 grid:
one "feel" applied to every playing pattern instead of scattered sliders.
- swing delays the off-beats; humanize = the manual's Random timing jitter;
  velocity = per-note Velocity Deviation; amount = the Global Amount master.
- named presets: straight / swing 16 / mpc 8-4 / drunk.
- per-step Velocity + Chance editors on the grid (§10.5.11-13): each step
  gets a loudness and a probability it fires.
- the feel + per-step expression travel with the patch; old patches migrate.
- gitignore the two large Live PDFs (local reference only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 17:17:46 +10:00
type-two
ea572e8493 🏟 the stadium — the world at play (opt-in)
TheSportsDB's free tier, invited with run.py --sports and off otherwise
(a shared, rate-limited key — 30/min — so the worker is polite by
design: the day's fixtures in a few calls per hour, everything else
computed at home). sport.events_today counts the planet's fixtures,
sport.inplay is the stadium lights climbing through the evenings, and
sport.kickoff fires the instant any match begins — swelling when many
kick off together. Routed where play belongs: Netzach the Victory,
Leo's fifth house of games, Lughnasadh's funeral games, wunjo the
joy-rune, dui the joyous lake.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:43:50 +10:00
type-two
56a775fcf8 🎭 performance mode goes pure — nothing on stage but the world
P (or F7) now removes everything that is not the world: the wordmark,
the header and its counts, the godtime readout, the ticker, every
button and chip. Only the visuals and the rotating dispatch line
remain for the audience. Z stays zen for the fully wordless sky.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 23:30:26 +10:00
type-two
aa81535379 📈 the trading desk — see the data, pin the ticker, hear the market think
Right-click any source: its actual number, live, with a two-minute
sparkline right in the menu — and 📌 pin it to the ticker, where your
watchlist scrolls ahead of the news with live values. The Binance tap
widens to ETH, SOL and the euro itself (EUR/USD on a 1s miniTicker,
since the pair trades thin), the ECB's daily fixes arrive as glacial
drones (GBP/USD, USD/JPY, AUD/USD), and Bitcoin speaks its own
technical analysis on 5s closes: the MACD line, Wilder's RSI(14), and
cross bells that ring once at every golden and death cross. All of it
routed through the Spirits like any money — to the solar plexus, to
Hod, to Nidavellir, to fehu.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:29:08 +10:00
type-two
d19618083b the Schumann resonance — the Earth's heartbeat under everything
The planet-ionosphere cavity rings at 7.83 Hz (Schumann, 1952), pumped
by every lightning strike on the globe. Below hearing, so it plays the
machine: a 7.83 Hz pulse forever breathing the Earth Echo's tone and
laying a planetary wow under the moon's — and octaved ×4 to 31.32 Hz,
a true sub humming beneath the drone voice. The one dial the world
never turns: it is the size of the Earth, and that does not change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:13:05 +10:00
type-two
c6083fb2b7 ☯ the bagua — the I Ching joins the Spirits, and the world casts itself
Eight trigrams in the King Wen Later Heaven ring, south at the top as
the compasses have it, every feed seated by the Shuogua's attributions:
quakes at zhen the thunder-shock, the word at dui the mouth, the moon
at kan, the sun at li, your own hands at gen — "Gen is the hand." At
the centre the two loudest trigrams stack into a live hexagram, named
from all 64 by the King Wen table; silence rests at 11, Peace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:05:48 +10:00
type-two
f0e5b28aef 🎨 the artist — Kandinsky's palette joins the Spirits
Concerning the Spiritual in Art (1911) taken at its word: six pigments
round a wireframe palette, weiß the canvas on the easel, schwarz the
final pause, his four great contrasts strung dashed between the drops —
and every feed seated by the feeling of its colour, each colour keyed
to the register of its instrument. Paint strokes itself onto the canvas
as the data sings. Plus GODSTRUMENT_MANUAL_SOURCE.md (the manual's
source doc) and its Appendix E entry for the new skin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:50:31 +10:00
type-two
d0a9345cb2 ⌨ global keymap — F1-F10 defaults + fully remappable keyboard
Capture-phase keydown runs bound actions (keymap wins over built-ins, skips
inputs and the mind-mode console). Defaults F1-F10: grimoire, play/stop,
skin view, next skin, cycle mood, tracks, performance, godtime lock, earth
echo, stage. Bind any key to any action from the ⌨ keys panel (sky menu), or
"map a key" in a voice's right-click menu — including targeted nudge-up /
nudge-down (a & s as a live fader on a drone) and mute/solo. keymap persists
(gs_keys); reset-to-defaults button. Synth.toggle() exposed for the play key.

Verified: F1 opens the grimoire, F5 cycles the mood (first light->ambient,
bpm->70 locked) — the engine and defaults fire; node-bind uses the same
capture flow. Zero console errors. Grimoire section added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:42:30 +10:00
type-two
7a444c3965 🌀 Earth Echo — a Space Echo whose tape is driven by the planet
A tape-delay effect in the built-in synth, RE-201 flavoured, with its knobs
exposed as matrix voices so any feed can drive them (and they groove/CV like
any voice). Audio chain: a delay with a feedback loop (lowpass tone roll +
highpass + tanh tape saturation), an LFO on delayTime for wow&flutter, and a
white-noise hiss bed for wear; the mix taps post -> echoIn -> wet -> out.
params() maps: echo.time -> delay 60-500ms with a LONG glide time-constant
(tape-motor pitch-bend), echo.feedback -> feedback HARD-CAPPED at 0.97
(self-oscillates, never runs away), echo.tone -> loop lowpass 6800->500Hz,
echo.flutter -> LFO depth, echo.wear -> hiss, echo.wet gated by earthEchoOn.

Sky menu "🌀 earth echo" toggles it (gs_echo persists); enabling installs the
earthy default cables: wind->time, volatility->feedback, air.pm25->tone,
moon->flutter, fire.count->wear, quake->wet. Verified: 6 echo voices appear in
the matrix with cables, zero console errors; param math unit-tested (feedback
capped, all ranges bounded, safe). Grimoire section added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:22:59 +10:00
type-two
c6dd2adcee control voltage — the world as raw electricity for modular gear
Two paths, both documented in the grimoire:
- MIDI-to-CV (already live, no new code): the note voices are 1V/oct pitch +
  gate, CCs are modulation, 24ppqn clock is analog sync — any MIDI-CV module
  (Befaco, CM1A, FH-2, Yarns) is driven today.
- NEW: true DC control voltage from Web Audio. Sky menu ->  control voltage.
  A CV panel maps any source/voice to an output channel of a DC-coupled
  interface (ES-8/ES-9) via ConstantSourceNode -> ChannelMerger -> multichannel
  destination (setSinkId device pick). Modes: unipolar / bipolar / 1V-oct
  (semitone-accurate), per-channel scale trim (real gear never tracks on
  paper). Config persists (gs_cv). Output CLAMPED to +/-1 sample-units so it
  can never send a runaway/damaging value.

Verified: voltage math unit-tested (monotonic, 1V/oct = 0.1/oct, clamped);
panel opens, device enumeration, channel add + persist, and the audio graph
starts cleanly (no exceptions). Physical voltage needs DC-coupled hardware to
confirm (documented). Zero console errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:12:55 +10:00