Merge branch 'revelation/integrate' — TESTAMENT · WHEELS · PSALM · CANON

This commit is contained in:
type-two 2026-07-14 14:16:34 +10:00
commit 7ee3c51269
8 changed files with 1664 additions and 20 deletions

46
auth.py
View File

@ -30,6 +30,8 @@ import time
HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_DB = os.path.join(HERE, "godstrument_users.db")
FACTORY_DIR = os.path.join(HERE, "factory") # ✦ CANON — git-tracked, read-only patches that ship with the instrument
FACTORY_PREFIX = "" # "✦ " — the mark of a factory dimension (also the read-only guard key)
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
USERNAME_RE = re.compile(r"^[A-Za-z0-9_.\-]{2,24}$")
MIN_PW = 8
@ -403,6 +405,42 @@ def delete_preset(user_id: int, name: str, db: str = DEFAULT_DB) -> bool:
return cur.rowcount > 0
# ---- ✦ CANON — factory dimensions (git-tracked, read-only, shared by everyone) ---
def list_factory() -> list[dict]:
"""The shipped ✦ patches, newest-name-order stable. Silent if factory/ is absent."""
out = []
try:
for fn in sorted(os.listdir(FACTORY_DIR)):
if not fn.endswith(".json"):
continue
try:
with open(os.path.join(FACTORY_DIR, fn), encoding="utf-8") as f:
name = (json.load(f).get("name") or "").strip()
if name.startswith(FACTORY_PREFIX):
out.append({"name": name, "updated": 0, "factory": True})
except (OSError, ValueError):
continue
except OSError:
pass
return out
def load_factory(name: str):
"""Load a factory patch by its ✦-prefixed name. Guards against path traversal
(the name never touches the filesystem we match on the JSON's own name field)."""
for fn in os.listdir(FACTORY_DIR) if os.path.isdir(FACTORY_DIR) else []:
if not fn.endswith(".json"):
continue
try:
with open(os.path.join(FACTORY_DIR, fn), encoding="utf-8") as f:
obj = json.load(f)
if (obj.get("name") or "").strip() == name:
return obj.get("data")
except (OSError, ValueError):
continue
return None
# ---- HTTP dispatch (called by hub.py's request handler) ---------------------
COOKIE = "gs_session"
@ -498,13 +536,17 @@ def handle_api(method: str, path: str, body: bytes, cookie_header: str,
return 404, {"error": "not found"}, None
if path == "/api/presets" and method == "GET":
return 200, {"presets": list_presets(uid, db)}, None
# ✦ CANON — the factory shelf first, then the player's own saved dimensions
return 200, {"presets": list_factory() + list_presets(uid, db)}, None
if path.startswith("/api/presets/"):
name = _unquote(path[len("/api/presets/"):])
is_factory = name.startswith(FACTORY_PREFIX)
if method == "GET":
data = load_preset(uid, name, db)
data = load_factory(name) if is_factory else load_preset(uid, name, db)
return (200, {"name": name, "data": data}, None) if data is not None else (404, {"error": "no such preset"}, None)
if is_factory: # ✦ factory dimensions are read-only — never write or delete them
return 403, {"error": "factory dimensions are read-only — save it under a new name"}, None
if method in ("PUT", "POST"):
return (200, {"ok": True}, None) if save_preset(uid, name, payload.get("data"), db) else (400, {"error": "bad preset"}, None)
if method == "DELETE":

View File

@ -0,0 +1,57 @@
{
"name": "✦ quiet arrival",
"data": {
"routes": [
{
"source": "astro.moon",
"dest": "echo.wet",
"amount": 0.6,
"curve": "lin",
"gate": null,
"quantize": null,
"label": "the moon opens the echo"
},
{
"source": "weather.wind",
"dest": "echo.time",
"amount": 0.5,
"curve": "scurve",
"gate": null,
"quantize": null,
"label": "the wind whips the tape speed"
},
{
"source": "sky.elev",
"dest": "pad.brightness",
"amount": 0.7,
"curve": "scurve",
"gate": null,
"quantize": null,
"label": "the sun brightens the pads"
},
{
"source": "sun.speed",
"dest": "reverb.size",
"amount": 0.5,
"curve": "lin",
"gate": null,
"quantize": null,
"label": "the solar wind sizes the room"
}
],
"tweaks": {},
"seqs": {},
"bpm": 64,
"groove": {
"amount": 1,
"preset": "straight",
"swing": 0,
"humanize": 0,
"velo": 0
},
"scale": {
"root": 5,
"name": "major"
}
}
}

479
factory/the_heartbeat.json Normal file
View File

@ -0,0 +1,479 @@
{
"name": "✦ the heartbeat",
"data": {
"routes": [],
"tweaks": {},
"seqs": {
"drum.kick": {
"on": true,
"steps": [
1,
0,
0,
0,
0,
0,
1,
0,
0,
0,
1,
0,
0,
0,
0,
0
],
"vel": [
0.95,
0.8,
0.8,
0.8,
0.8,
0.8,
0.7,
0.8,
0.8,
0.8,
0.75,
0.8,
0.8,
0.8,
0.8,
0.8
],
"chance": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"rat": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"lock": [
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1
],
"len": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"wlen": 16
},
"drum.snare": {
"on": true,
"steps": [
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
1
],
"vel": [
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8
],
"chance": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"rat": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"lock": [
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1
],
"len": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"wlen": 16
},
"drum.hat": {
"on": true,
"steps": [
1,
0,
1,
0,
1,
0,
1,
0,
1,
0,
1,
0,
1,
0,
1,
0
],
"vel": [
0.7,
0.4,
0.6,
0.4,
0.7,
0.4,
0.6,
0.4,
0.7,
0.4,
0.6,
0.4,
0.7,
0.4,
0.6,
0.5
],
"chance": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"rat": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"lock": [
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1
],
"len": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"wlen": 16
},
"drum.ohat": {
"on": true,
"steps": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0
],
"vel": [
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8
],
"chance": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"rat": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"lock": [
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1
],
"len": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"wlen": 16
}
},
"bpm": 88,
"groove": {
"amount": 1,
"preset": "mpc 8-4",
"swing": 0.62,
"humanize": 0.12,
"velo": 0.18
},
"scale": {
"root": 0,
"name": "minor_pent"
},
"kit": {
"kick": {
"tune": 0.44,
"punch": 0.6,
"decay": 0.55,
"duck": 0.5,
"level": 0.9,
"pan": 0.5
}
}
}
}

376
factory/the_squeeze.json Normal file
View File

@ -0,0 +1,376 @@
{
"name": "✦ the squeeze",
"data": {
"routes": [
{
"source": "crypto.vel",
"dest": "squash",
"amount": 0.9,
"curve": "lin",
"gate": null,
"quantize": null,
"label": "the market pumps the master"
},
{
"source": "quake.event",
"dest": "squash",
"amount": 0.7,
"curve": "exp3",
"gate": null,
"quantize": null,
"label": "a quake heaves the whole mix"
}
],
"tweaks": {},
"seqs": {
"drum.kick": {
"on": true,
"steps": [
1,
0,
0,
0,
1,
0,
0,
0,
1,
0,
0,
0,
1,
0,
0,
0
],
"vel": [
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8
],
"chance": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"rat": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"lock": [
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1
],
"len": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"wlen": 16
},
"drum.hat": {
"on": true,
"steps": [
0,
0,
1,
0,
0,
0,
1,
0,
0,
0,
1,
0,
0,
0,
1,
0
],
"vel": [
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8
],
"chance": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"rat": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"lock": [
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1
],
"len": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"wlen": 16
},
"drum.snare": {
"on": true,
"steps": [
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0
],
"vel": [
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8
],
"chance": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"rat": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"lock": [
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1
],
"len": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"wlen": 16
}
},
"bpm": 120,
"groove": {
"amount": 1,
"preset": "straight",
"swing": 0,
"humanize": 0.03,
"velo": 0.12
},
"scale": {
"root": 0,
"name": "minor_pent"
}
}
}

View File

@ -0,0 +1,357 @@
{
"name": "✦ the wheel within the wheel",
"data": {
"routes": [],
"tweaks": {},
"seqs": {
"drum.kick": {
"on": true,
"steps": [
1,
0,
0,
0,
1,
0,
0,
0,
1,
0,
0,
0,
1,
0,
0,
0
],
"vel": [
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8
],
"chance": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"rat": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"lock": [
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1
],
"len": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"wlen": 16
},
"drum.hat": {
"on": true,
"steps": [
1,
0,
0,
1,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0
],
"vel": [
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8
],
"chance": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"rat": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"lock": [
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1
],
"len": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"wlen": 12
},
"drum.clap": {
"on": true,
"steps": [
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"vel": [
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8,
0.8
],
"chance": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"rat": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"lock": [
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1
],
"len": [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1
],
"wlen": 10
}
},
"bpm": 96,
"groove": {
"amount": 1,
"preset": "swing 16",
"swing": 0.55,
"humanize": 0.05,
"velo": 0.1
},
"scale": {
"root": 0,
"name": "minor_pent"
}
}
}

91
test_factory.py Normal file
View File

@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""test_factory.py — validate the ✦ CANON factory patches' SHAPE.
Not a taste check (the human re-voices by ear) this asserts every factory
patch loads cleanly through the client's migrateSeq expectations: 16-step seq
arrays, wlen in 1..16, sane bpm, valid route/groove/scale shape, and a
-prefixed name. Stdlib only; excluded from the deploy by the test_*.py filter.
python3 test_factory.py
"""
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
FACTORY = os.path.join(HERE, "factory")
SCALES = {"chromatic", "major", "minor", "dorian", "phrygian", "lydian",
"mixolydian", "pentatonic", "minor_pent", "wholetone", "hirajoshi"}
SEQ_ARRAYS = ("steps", "vel", "chance", "rat", "lock", "len")
def check_seq(key, s, errs):
for a in SEQ_ARRAYS:
if a in s:
if not isinstance(s[a], list) or len(s[a]) != 16:
errs.append(f"{key}.{a} must be a 16-length array")
if "steps" not in s or not isinstance(s.get("steps"), list) or len(s["steps"]) != 16:
errs.append(f"{key}.steps missing/not-16 (migrateSeq would reject → dropped)")
if "wlen" in s and not (isinstance(s["wlen"], int) and 1 <= s["wlen"] <= 16):
errs.append(f"{key}.wlen must be an int 1..16, got {s.get('wlen')!r}")
if "on" in s and not isinstance(s["on"], (bool, int)):
errs.append(f"{key}.on must be boolean-ish")
def check_patch(name, p, errs):
if not isinstance(p.get("bpm"), (int, float)) or not (40 <= p["bpm"] <= 180):
errs.append(f"bpm must be 40..180, got {p.get('bpm')!r}")
for r in p.get("routes", []):
if not r.get("source") or not r.get("dest"):
errs.append("route missing source/dest")
if not isinstance(r.get("amount"), (int, float)):
errs.append(f"route {r.get('source')}{r.get('dest')} amount not numeric")
for key, s in (p.get("seqs") or {}).items():
check_seq(key, s, errs)
sc = p.get("scale") or {}
if sc and sc.get("name") not in SCALES:
errs.append(f"scale.name {sc.get('name')!r} not a known scale")
gr = p.get("groove") or {}
for k in ("swing", "humanize", "velo"):
if k in gr and not (0 <= gr[k] <= 1):
errs.append(f"groove.{k} out of 0..1")
def main():
if not os.path.isdir(FACTORY):
print("FAIL: no factory/ dir"); sys.exit(1)
files = sorted(f for f in os.listdir(FACTORY) if f.endswith(".json"))
if len(files) < 4:
print(f"FAIL: expected ≥4 factory patches, found {len(files)}"); sys.exit(1)
total_errs = 0
names = []
for fn in files:
with open(os.path.join(FACTORY, fn), encoding="utf-8") as f:
obj = json.load(f)
name, data = obj.get("name", ""), obj.get("data")
errs = []
if not name.startswith(""):
errs.append(f"name {name!r} must start with ''")
if not isinstance(data, dict):
errs.append("missing 'data' object")
else:
check_patch(name, data, errs)
names.append(name)
if errs:
total_errs += len(errs)
print(f"{fn}: " + "; ".join(errs))
else:
nseq = len(data.get("seqs") or {})
nroute = len(data.get("routes") or [])
print(f"{name} (bpm {data['bpm']}, {nseq} seqs, {nroute} routes)")
# the four the brief names
for want in ("✦ the heartbeat", "✦ the wheel within the wheel", "✦ the squeeze", "✦ quiet arrival"):
if want not in names:
print(f" ✗ missing canonical patch: {want}"); total_errs += 1
if total_errs:
print(f"\nFAIL: {total_errs} shape error(s)"); sys.exit(1)
print(f"\nALL {len(files)} FACTORY PATCHES VALID (shape only — beauty is the human's)")
if __name__ == "__main__":
main()

View File

@ -234,6 +234,7 @@
.seqcell:hover { background: rgba(140,170,220,0.45); }
.seqcell.on { background: #78a0ff; box-shadow: 0 0 6px rgba(120,160,255,0.6); }
.seqcell.cur { outline: 1px solid rgba(255,238,150,0.85); }
.seqcell.off { opacity: 0.3; pointer-events: none; } /* ☸ WHEELS — a step past the lane's length is outside the wheel */
/* note length — a placed note is an overlay block spanning `len` steps, drag its
right edge (the ew-resize handle) to lengthen. Sits over the lit cells; only the
handle takes the mouse, so the grid underneath still toggles notes. */
@ -378,6 +379,18 @@
#godspeakrec.on { display: flex; animation: gspk-pulse 1.1s ease-in-out infinite; }
@keyframes gspk-pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
/* 📼 TESTAMENT — the record button; red + gently pulsing while it bears witness */
#testament.on { color: #ff8a8a !important; border-color: rgba(240,110,110,0.6) !important;
background: rgba(230,90,90,0.2) !important; animation: gspk-pulse 1.5s ease-in-out infinite; font-variant-numeric: tabular-nums; }
/* 🕊 PSALM — the listening chip; shows the note you're singing, brightens on voice */
#psalmchip { position: fixed; bottom: 164px; right: 14px; z-index: 12; display: none;
align-items: center; gap: 4px; min-width: 46px; padding: 6px 12px; border-radius: 8px; font-size: 12px;
background: rgba(20,28,44,0.7); color: #8fa4c8; border: 1px solid rgba(120,150,200,0.25);
cursor: pointer; -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }
#psalmchip.open { display: inline-flex; }
#psalmchip.voiced { color: #cfe4ff; border-color: rgba(150,200,255,0.6); background: rgba(60,95,150,0.5); }
/* right-click context menu — every setting, on everything */
#ctxmenu {
position: fixed; z-index: 30; display: none; width: 240px; max-height: 72vh;
@ -1191,6 +1204,10 @@
<li><b>🫀 GODSQUASH — the master squeeze the world can pump.</b> In <b>OMNI</b>'s fx rack sits one new knob: <b>squash</b>, a compressor across the whole master bus. Turn it up and the <b>threshold dives while the ratio climbs together</b>, a matched gain riding underneath so the <i>loudness stays level while the peaks kneel</i> — the mix gets denser, not louder. And because it's a destination like any other, <b>route a feed onto it</b> — the quakes, your heartbeat, the market's velocity — and the planet <b>pumps the mix</b>, breathing the whole instrument in time with the world.</li>
<li><b>🗣 GODSPEAK — the voice of god.</b> On your own machine (<i>Apple-Silicon only</i>), run <code>run.py --godspeak</code>, then <b>hold Right ⌥ and speak</b>. Your words are heard <b>locally</b> — your own Whisper, nothing leaving the machine — and become commands: <i>"tempo ninety", "storm mood", "capture six seconds", "chop sixteen", "open the beat", "squash sixty percent", "zen"</i>, and — the voice of god undoing its own works — <i>"take it back"</i>. A 🗣 chip glows by the ♪ while it listens. Anything it doesn't recognise simply <b>arrives in the world</b>, printed to the ticker — the word is never an error, only another feed.</li>
<li><b>⏪ UNGODLY — take it back.</b> <kbd>⌘Z</kbd> undoes; <kbd>⌘⇧Z</kbd> redoes. The instrument remembers your last <b>thirty-two acts</b> — every cable patched, mood applied, groove and scale changed, kit knob turned, beat step drawn, level and mute — and steps back through them, un-doing what god (or you) did. A held drag counts as one act, not fifty. It is <b>memory-only</b>: a reload forgives everything and starts the history clean.</li>
<li><b>📼 TESTAMENT — record what god does.</b> Beside the <kbd></kbd> sits a <b></b>. Press it and the instrument records <i>itself</i> — the true final mix, taken after the squash and the limiter, exactly what you hear. The button turns red and counts the minutes; press it again and a file drops to your disk (<kbd>godstrument-testament-…webm</kbd>), and the ticker says <i>it is written</i>. Start it before the beat and it captures the session from its first breath; it needs no setup and no other app — the planet, at last, can bear witness to itself. (Also on the sky menu, near GODSONIQ.)</li>
<li><b>☸ WHEELS — a wheel within a wheel.</b> <i>"…their work was as it were a wheel in the middle of a wheel"</i> (Ezekiel 1:16). Each lane keeps its own length: set the <b>☸ wheel</b> chip in a beat lane (or the arranger's euclid row) to anything from 1 to 16, and a lane shorter than the full sixteen <b>wraps on its own turn</b>. A hat of twelve over a kick of sixteen drift apart and only realign every forty-eight steps — <b>polymeter</b>, the oldest trick for making a loop that never quite repeats. Steps past the wheel dim out; the playhead rides each lane's own length; and the pattern <b>exports to MIDI exactly as it plays</b>, twelve-wheel and all. A full wheel of sixteen behaves precisely as it always did.</li>
<li><b>🕊 PSALM — sing, and the instrument answers.</b> Right-click the sky → <b>🕊 PSALM</b>, allow the microphone, and <b>sing</b>. The pitch is heard, <b>folded to the scale you're in</b> (so you're never out of key), and played as notes — a <b>🕊</b> chip shows the note leaving your throat. And the duet with <b>⚡ GODSPEED</b>: with the arp running, each note you sing <b>latches into the held chord</b>, one at a time — hum four notes and the machine arpeggiates the chord you sang. <b>It listens locally</b>: the analysis happens in the page, in its own silence, and <i>nothing ever leaves the machine</i> — no recording, no upload, just your voice becoming the instrument's.</li>
<li><b>✦ CANON — the shelf of worked examples.</b> Open <b>⚙ → DIMENSIONS</b> and at the top of the list, above your own saved worlds, sit four <b>✦ factory dimensions</b> that ship with the instrument — <i>the heartbeat</i> (a boom-bap kit with the swing and the kick's duck), <i>the wheel within the wheel</i> (WHEELS, made audible), <i>the squeeze</i> (the market pumping GODSQUASH), and <i>quiet arrival</i> (the earth echo, slow and sparse). Load one to hear a feature in its element, then save your own version under a new name — the ✦ ones are <b>read-only</b>, the same for everyone, a place to start rather than a thing to overwrite.</li>
<li><b>📽 The stage — send the visual to any screen.</b> Right-click the sky → <b>the stage</b> and a clean black mirror window opens, fed live from the canvas. Drag it to a projector, an AirPlay display, a second monitor — macOS treats it like any window — and <b>double-click for fullscreen</b>. The stage has a <b>camera</b>: the whole view by default (whatever you're in — a skin, the wheel, mind mode's POV), or right-click any node → <b>project this node</b> and the camera glides after that one sphere through its orbits, or <b>project its station</b> to frame a whole wheel and its satellites. Change cameras live from the menus while the projector runs; the audience sees the camera breathe from subject to subject.</li>
<li><b>🕸 The commune — play together, properly.</b> Right-click the sky → <b>the commune</b>. This is for <b>signed-in players</b>: your session rides the connection, so the hub knows exactly who everyone is — no impostors, no setup, no Bluetooth. One of you <b>hosts</b>: name the room, then <b>✉ invite</b> the others by their username (they must be on the site — invitations reach the present). The invited see the invitation arrive live and <b>join or decline</b>; anyone may <b>leave</b> whenever they wish, and if the host leaves, the room closes with them. Then the sections: members <b>claim voices</b> (right-click any voice, or the panel's picker), or the <b>host allocates</b> them — this bass to her, those pads to him, <b>the godtime itself</b> to whoever conducts — and the hub enforces it: only the section's holder can speak for it. From that moment your levels, mutes, grooves and arranger lanes for what you hold follow your hands on every screen, same living room or opposite sides of the earth. The world — the Source — stays identical for everyone, because it always was. Three people, one planet, one instrument.</li></ul>
@ -1639,7 +1656,7 @@
// voice gets an 808-style gate row that rhythmically chops the world's data.
// "Quantize in time" — the rhythmic sibling of the matrix's quantize-to-scale.
const seqs = {}; // destKey -> {on, steps[16]} (notes: -1|row, CC: 0|1)
let seqPhase = 0, seqCurStep = 0, seqGridRefresh = null;
let seqPhase = 0, seqCurStep = 0, seqAbsStep = 0, seqGridRefresh = null; // seqAbsStep: monotonic 16th counter for ☸ WHEELS polymeter
// The Groove — Ableton's Groove Pool (manual §14.1), reduced to a fixed 1/16
// grid: swing delays the off-beats, humanize is the manual's "Random" timing
// jitter, velo is per-note Velocity Deviation, amount is the Global Amount
@ -1771,6 +1788,7 @@
rat: new Array(16).fill(1), // per-step ratchet count (1 = single hit)
lock: new Array(16).fill(-1), // per-step parameter lock (-1 = unlocked)
len: new Array(16).fill(1), // per-step note length, in 16ths (1 = one step) — the piano-roll "key-roll"
wlen: 16, // ☸ WHEELS — the lane's own pattern length (1..16); 16 = a full wheel (default, unchanged)
});
}
// GODRUM drum lanes — same seq shape as seqFor, but steps are 0/1 gates that fill
@ -1790,8 +1808,13 @@
rat: new Array(16).fill(1),
lock: new Array(16).fill(-1), // unused by drums, kept for seq-shape parity (migrateSeq)
len: new Array(16).fill(1), // unused by drums (they're one-shots), kept for seq-shape parity
wlen: 16, // ☸ WHEELS — per-lane pattern length; polymeter for the drums
});
}
// ☸ WHEELS — "a wheel within a wheel". A lane shorter than 16 wraps on its own
// length against the absolute clock, so kick-16 over hat-12 drift into polymeter.
// A full-length lane returns seqCurStep — bit-identical to pre-WHEELS behaviour.
const wheelStep = (sq) => (sq && sq.wlen && sq.wlen < 16) ? ((seqAbsStep % sq.wlen) + sq.wlen) % sq.wlen : seqCurStep;
// ---- the arranger — tracks & clips for the casuals -----------------------
// Ableton-shaped, not Ableton-sized: tracks are the voices, a clip is a bar
// of groove (A/B/C patterns on the existing grids), "live" means the world
@ -1948,13 +1971,12 @@
const sq = seqs["drum." + v];
if (!sq || !owned(v, sq)) continue;
const gm = DRUM_GM[v];
for (let b = 0; b < arr.bars; b++) {
for (let s2 = 0; s2 < 16; s2++) {
if (!sq.steps[s2]) continue;
const dvel = Math.round(clamp(sq.vel ? sq.vel[s2] : 0.8, 0.05, 1) * 127);
const t0 = (b * 16 + s2) * TICKS16, t1 = Math.max(t0 + 1, t0 + TICKS16 - 2); // one 16th long
dev.push([t0, 0x99, gm, dvel], [t1, 0x89, gm, 0]); // ratchet sub-hits omitted in v1
}
for (let sa = 0; sa < arr.bars * 16; sa++) { // WHEELS one absolute loop
const s2 = (sq.wlen && sq.wlen < 16) ? sa % sq.wlen : sa % 16; // a 12-wheel exports exactly as it plays
if (!sq.steps[s2]) continue;
const dvel = Math.round(clamp(sq.vel ? sq.vel[s2] : 0.8, 0.05, 1) * 127);
const t0 = sa * TICKS16, t1 = Math.max(t0 + 1, t0 + TICKS16 - 2); // one 16th long
dev.push([t0, 0x99, gm, dvel], [t1, 0x89, gm, 0]); // ratchet sub-hits omitted in v1
}
}
if (dev.length) {
@ -2179,6 +2201,16 @@
euRow.appendChild(mkEuNum("pulses", () => euclidP.pulses, v => euclidP.pulses = v, 0, 16));
euRow.appendChild(mkEuNum("steps", () => euclidP.len, v => euclidP.len = v, 2, 16));
euRow.appendChild(mkEuNum("rotate", () => euclidP.rot, v => euclidP.rot = v, 0, 15));
{ // ☸ WHEELS — the lane's own pattern length (1..16); shorter than 16 → polymeter
const wsqA = seqFor(arrSelKey, nSel.isNote);
const w = document.createElement("span"); w.className = "gctl";
const l = document.createElement("span"); l.className = "glbl"; l.style.minWidth = "auto"; l.textContent = "☸ wheel";
const n = document.createElement("input"); n.type = "number"; n.min = "1"; n.max = "16"; n.className = "eunum";
n.value = String(wsqA.wlen || 16); n.title = "the lane's own length — 12 over a 16 kick is polymeter";
n.oninput = () => { ungodlyGesture("wheel"); wsqA.wlen = clamp(Math.round(+n.value || 16), 1, 16);
euclidP.len = wsqA.wlen; paintG(); }; // a fill now spreads within the wheel (steps still overrides)
w.appendChild(l); w.appendChild(n); euRow.appendChild(w);
}
const euBtn = document.createElement("button"); euBtn.className = "midibtn"; euBtn.textContent = "fill";
euBtn.onclick = () => { ungodlyMark("euclid"); euclidFill(steps, euclidP.pulses, euclidP.len, euclidP.rot, nSel.isNote); paintG(); };
euRow.appendChild(euBtn);
@ -2212,10 +2244,12 @@
if (nSel.isNote) { const vsq = seqFor(arrSelKey, true); if (!vsq.len) vsq.len = new Array(16).fill(1);
renderNotesG = attachNoteLengths(grid, cells, () => steps, vsq.len, rows, null); }
const paintG = () => {
const wsq = seqFor(arrSelKey, nSel.isNote), wl = wsq.wlen || 16, ph = wheelStep(wsq); // ☸ WHEELS
for (const c2 of cells) {
c2.el.classList.toggle("on", nSel.isNote ? stepHas(steps[c2.stp], c2.rw) : !!steps[c2.stp]);
c2.el.classList.toggle("playone", nSel.isNote && stepStack(steps[c2.stp]) && stepHas(steps[c2.stp], c2.rw));
c2.el.classList.toggle("cur", arr.playing && c2.stp === seqCurStep);
c2.el.classList.toggle("cur", arr.playing && c2.stp === ph);
c2.el.classList.toggle("off", c2.stp >= wl); // dim the steps past the wheel
}
if (renderNotesG) renderNotesG();
};
@ -2833,7 +2867,11 @@
const eu = document.createElement("button"); eu.className = "midibtn bmini"; eu.textContent = "⬢";
eu.title = "euclid — spread N pulses evenly across the 16 steps";
eu.onclick = () => { euclidPrompt(sq, v); paintGrid(); };
lead.appendChild(name); lead.appendChild(mute); lead.appendChild(eu);
const wl = document.createElement("input"); wl.type = "number"; wl.min = "1"; wl.max = "16";
wl.className = "eunum"; wl.style.width = "36px"; wl.value = String(sq.wlen || 16); // ☸ WHEELS — this lane's length
wl.title = "☸ wheel — this lane's length (a 12 over a 16 kick is polymeter)";
wl.oninput = () => { ungodlyGesture("wheel"); sq.wlen = clamp(Math.round(+wl.value || 16), 1, 16); paintGrid(); };
lead.appendChild(name); lead.appendChild(mute); lead.appendChild(eu); lead.appendChild(wl);
lane.appendChild(lead);
const steps = document.createElement("div"); steps.className = "bsteps";
const cells = [];
@ -2853,9 +2891,11 @@
const bv = String(Math.round(godtime.bpm));
if (document.activeElement !== bpm && bpm.value !== bv) bpm.value = bv;
for (const L of lanes) { L.paintMute();
const wl2 = L.sq.wlen || 16, ph = wheelStep(L.sq); // ☸ WHEELS — this lane's wrap
for (let s = 0; s < 16; s++) {
L.cells[s].classList.toggle("on", !!L.sq.steps[s]);
L.cells[s].classList.toggle("cur", playing && s === seqCurStep);
L.cells[s].classList.toggle("cur", playing && s === ph);
L.cells[s].classList.toggle("off", s >= wl2); // dim steps past the wheel
}
}
if (paintExpr) paintExpr(); // keep the expression lane's lit-bars in step with the grid
@ -3152,6 +3192,119 @@
godspeedEl.appendChild(hint);
}
// ---- 🕊 PSALM — sing, and the instrument answers ------------------------
// Mic pitch, folded to the global scale, played as notes — or, when ⚡ GODSPEED
// is on, latched into the arp one note at a time (sing your chord in). PRIVACY:
// analysis is entirely local, in a THROWAWAY AudioContext the music graph never
// touches — zero feedback risk, and nothing ever leaves the page. This block
// only consumes Synth.pluck / arpLatch; it reaches into no engine.
const PSALM_SENS = 0.012; // RMS gate — quieter than this is "silence"
const PSALM_CLARITY = 0.6; // autocorrelation confidence floor
// Pure pitch detector over a Float32Array — unit-testable without a mic.
function psalmDetect(buf, sampleRate) {
const n = buf.length;
let rms = 0; for (let i = 0; i < n; i++) rms += buf[i] * buf[i];
rms = Math.sqrt(rms / n);
if (rms < 1e-4) return { freq: 0, clarity: 0, rms };
const minLag = Math.floor(sampleRate / 1000), maxLag = Math.floor(sampleRate / 80); // 801000 Hz
let c0 = 0; for (let i = 0; i < n; i++) c0 += buf[i] * buf[i];
const acAt = (L) => { let a = 0; for (let i = 0; i + L < n; i++) a += buf[i] * buf[i + L]; return a; };
let bestLag = -1, bestVal = 0;
for (let lag = minLag; lag <= maxLag && lag < n; lag++) { const ac = acAt(lag); if (ac > bestVal) { bestVal = ac; bestLag = lag; } }
if (bestLag < 0) return { freq: 0, clarity: 0, rms };
const clarity = c0 > 0 ? bestVal / c0 : 0;
let lag = bestLag; // parabolic interpolation for sub-sample accuracy
if (bestLag > minLag && bestLag < maxLag) {
const y0 = acAt(bestLag - 1), y1 = bestVal, y2 = acAt(bestLag + 1), den = (y0 - 2 * y1 + y2);
if (den !== 0) lag = bestLag + 0.5 * (y0 - y2) / den;
}
return { freq: sampleRate / lag, clarity, rms };
}
const freqToMidi = (f) => 69 + 12 * Math.log2(f / 440);
function psalmFold(midiFloat) { // fold to the nearest note of the global scale
const iv = SCALES[gScale.name] || SCALES.minor_pent;
const pcs = iv.map(i => (((gScale.root + i) % 12) + 12) % 12);
const c = Math.round(midiFloat); let best = c, bestD = 1e9;
for (let k = c - 7; k <= c + 7; k++) {
if (pcs.indexOf(((k % 12) + 12) % 12) < 0) continue;
const d = Math.abs(k - midiFloat); if (d < bestD) { bestD = d; best = k; }
}
return best;
}
const psalm = { on: false, ctx: null, an: null, stream: null, timer: null,
lastNote: -1, stableNote: -1, stableCount: 0, gateOpen: false, silentMs: 0, curName: "" };
// Pure note-logic state machine (testable): a NEW note fires once after 2 stable
// frames; legato re-fires on a note change; after ~150ms below the gate it closes
// and the same note may re-attack. Velocity from RMS.
function psalmStep(d, dtMs) {
const out = { fire: false, note: -1, vel: 0 };
const voiced = d.rms >= PSALM_SENS && d.clarity >= PSALM_CLARITY && d.freq > 0;
if (!voiced) {
psalm.silentMs += dtMs;
if (psalm.silentMs >= 150) { psalm.gateOpen = false; psalm.lastNote = -1; psalm.stableNote = -1; psalm.stableCount = 0; }
return out;
}
psalm.silentMs = 0;
const note = psalmFold(freqToMidi(d.freq));
if (note === psalm.stableNote) psalm.stableCount++;
else { psalm.stableNote = note; psalm.stableCount = 1; }
if (psalm.stableCount < 2) return out; // hold two frames before committing
if (note !== psalm.lastNote) { // a new note, or a legato change
psalm.lastNote = note; psalm.gateOpen = true;
out.fire = true; out.note = note; out.vel = clamp(0.3 + d.rms * 4, 0.3, 1);
}
return out;
}
let psalmChip = null;
function psalmChipEnsure() {
if (psalmChip) return psalmChip;
psalmChip = document.createElement("div"); psalmChip.id = "psalmchip";
psalmChip.title = "🕊 PSALM — singing; click to stop listening";
psalmChip.onclick = () => psalmSet(false);
document.body.appendChild(psalmChip);
return psalmChip;
}
function psalmPaint() {
if (!psalmChip) return;
psalmChip.classList.toggle("open", psalm.on);
psalmChip.classList.toggle("voiced", psalm.gateOpen);
psalmChip.textContent = "🕊 " + (psalm.on ? (psalm.curName || "…") : "");
}
async function psalmSet(v) {
if (v && psalm.on) return;
if (!v) { // stop + fully release the mic
psalm.on = false;
if (psalm.timer) { clearInterval(psalm.timer); psalm.timer = null; }
if (psalm.stream) { psalm.stream.getTracks().forEach(t => t.stop()); psalm.stream = null; }
if (psalm.ctx) { try { psalm.ctx.close(); } catch (e) {} psalm.ctx = null; }
psalm.gateOpen = false; psalm.curName = ""; psalmPaint(); return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false } });
const AC = window.AudioContext || window.webkitAudioContext;
const actx = new AC(); // OWN context — mic is analysed only, never routed to the music
const src = actx.createMediaStreamSource(stream);
const an = actx.createAnalyser(); an.fftSize = 2048; src.connect(an);
const buf = new Float32Array(an.fftSize), dtMs = 25;
Object.assign(psalm, { on: true, stream, ctx: actx, an, lastNote: -1, stableNote: -1, stableCount: 0, gateOpen: false, silentMs: 0 });
psalmChipEnsure(); psalmPaint();
psalm.timer = setInterval(() => { // setInterval, not rAF — a hidden tab shouldn't freeze the ear
an.getFloatTimeDomainData(buf);
const r = psalmStep(psalmDetect(buf, actx.sampleRate), dtMs);
if (r.fire && r.note >= 0) {
psalm.curName = midiToName(r.note);
if (arp.on) arpLatch(r.note, r.vel); else Synth.pluck(r.note, r.vel);
}
psalmPaint();
}, dtMs);
} catch (e) {
psalm.on = false;
eventTicker.unshift({ text: "🕊 the instrument cannot hear you — allow the microphone", tAdded: now(), src: "sys" });
psalmPaint();
}
}
function togglePsalm() { psalmSet(!psalm.on); }
// the one place a drum voice sounds — shared by seqTick AND the finger-drum keys (E.1),
// so the sequenced path and the played path can never drift: same guard, same GM MIDI.
function fireDrum(name, vel, durMs) {
@ -3164,7 +3317,7 @@
const sq = seqs[k];
if (!sq.on) continue;
if (k.slice(0, 5) === "drum.") { // a GODRUM lane — fire a synth drum
const cur = seqCurStep;
const cur = wheelStep(sq); // ☸ WHEELS — the lane's own wrap (16 → seqCurStep)
if (sq.steps[cur]) { // 0/1 gate
const chc = sq.chance ? sq.chance[cur] : 1; // chance: does this step fire?
if (chc >= 1 || Math.random() < chc) {
@ -3185,7 +3338,7 @@
}
const n = dests.get(k);
if (n && n.isNote) {
const cur = seqCurStep, sv = sq.steps[cur];
const cur = wheelStep(sq), sv = sq.steps[cur]; // ☸ WHEELS — one cur for both the gate and len[] (must not diverge)
const hasNote = Array.isArray(sv) ? sv.length > 0 : sv >= 0;
const ch = sq.chance ? sq.chance[cur] : 1; // chance: does this step fire?
if (hasNote && (ch >= 1 || Math.random() < ch)) {
@ -3259,7 +3412,8 @@
} else if (frozenVals[k] != null) delete frozenVals[k];
const sq = seqs[k];
if (sq && sq.on) { // sample & hold: the world only speaks on your steps
if (sq.steps[seqCurStep] && sq._sampled !== seqCurStep) { sq.held = v; sq._sampled = seqCurStep; }
const cur = wheelStep(sq); // ☸ WHEELS — a short source-wheel samples on its own turn
if (sq.steps[cur] && sq._sampled !== cur) { sq.held = v; sq._sampled = cur; }
if (sq.held != null) v = sq.held;
}
sv[k] = v;
@ -3301,7 +3455,7 @@
if (tk && !muted && !n.isNote) // per-voice output level (⇧-drag / menu)
v = clamp(v * tk.gain + tk.offset, 0, 1);
const sq = seqs[k]; // groove grid: gate + per-step p-lock in time
if (!n.isNote) v = applyGroove(sq, seqCurStep, v);
if (!n.isNote) v = applyGroove(sq, wheelStep(sq), v); // ☸ WHEELS — the gate wraps on the lane's length
destVals[k] = v;
n.raw = v; n.muted = muted;
n.norm = n.isNote ? clamp((v - 24) / (96 - 24), 0, 1) : clamp(v, 0, 1);
@ -3339,7 +3493,8 @@
chance: Array.isArray(s.chance) && s.chance.length === 16 ? s.chance.slice() : new Array(16).fill(1),
rat: Array.isArray(s.rat) && s.rat.length === 16 ? s.rat.slice() : new Array(16).fill(1),
lock: Array.isArray(s.lock) && s.lock.length === 16 ? s.lock.slice() : new Array(16).fill(-1),
len: Array.isArray(s.len) && s.len.length === 16 ? s.len.slice() : new Array(16).fill(1) };
len: Array.isArray(s.len) && s.len.length === 16 ? s.len.slice() : new Array(16).fill(1),
wlen: Number.isFinite(s.wlen) ? clamp(Math.round(s.wlen), 1, 16) : 16 }; // ☸ WHEELS — per-lane length (legacy patches → a full wheel)
}
function gsGrooveSelfCheck() { // ponytail: one runnable guard on the risky bits
const f = [];
@ -3386,6 +3541,11 @@
if (migrateSeq({ steps: new Array(16).fill(0) }).rat[0] !== 1) f.push("ratchet-default"); // rat back-fills to 1
if (migrateSeq({ steps: new Array(16).fill(0) }).len[0] !== 1) f.push("len-default"); // len back-fills to 1 (one 16th)
if (migrateSeq({ steps: new Array(16).fill(0), len: new Array(4).fill(3) }).len[0] !== 1) f.push("len-badlen"); // wrong-length len → reset
if (migrateSeq({ steps: new Array(16).fill(0) }).wlen !== 16) f.push("wlen-default"); // ☸ legacy seq → full wheel
if (migrateSeq({ steps: new Array(16).fill(0), wlen: 12 }).wlen !== 12) f.push("wlen-preserve"); // ☸ a 12-wheel round-trips
if (migrateSeq({ steps: new Array(16).fill(0), wlen: 0 }).wlen !== 1) f.push("wlen-clamp-lo"); // ☸ clamps into 1..16
if (migrateSeq({ steps: new Array(16).fill(0), wlen: 99 }).wlen !== 16) f.push("wlen-clamp-hi");
if (wheelStep({ wlen: 16 }) !== seqCurStep) f.push("wheel-full-identity"); // ☸ full wheel == pre-WHEELS
// parameter locks (applyGroove)
if (applyGroove(null, 0, 0.7) !== 0.7) f.push("plock-nogroove"); // no groove → world passes
const lsq = { on: true, steps: [1, 0, 1, ...new Array(13).fill(0)], lock: [-1, -1, 0.25, ...new Array(13).fill(-1)] };
@ -5024,7 +5184,7 @@
}
const frac = seqPhase - Math.floor(seqPhase);
const st16 = frac >= seqStepOnset ? rawStep : (rawStep + 15) % 16; // hold until the onset
if (st16 !== seqCurStep) { seqCurStep = st16; seqTick(); }
if (st16 !== seqCurStep) { seqCurStep = st16; seqAbsStep++; seqTick(); } // seqAbsStep drives WHEELS' per-lane wrap
// the arrangement advances one column per bar
const barAbs = Math.floor(seqPhase / 16);
@ -7797,7 +7957,10 @@
eventTicker.unshift({ text: ok ? "🎛 GODSONIQ — sampling the world for 3s… then every pluck replays it, pitched across the keys (🎸 string voice to exit)"
: "🎛 GODSONIQ needs the audio worklet — press ♪ first, then try again", tAdded: now(), src: "sys" }); });
ctxItem("🌾 AMPLER — there is ample (sample the world into the kit)", () => openAmpler());
ctxItem((Synth.testamentState && Synth.testamentState().on) ? "📼 TESTAMENT — stop & save the take" : "📼 TESTAMENT — record what god does",
() => { if (Synth.testament) Synth.testament(); });
ctxItem("⚡ GODSPEED — hold a chord, the machine runs", () => openGodspeed());
ctxItem((psalm.on ? "🕊 PSALM — listening… (click to stop)" : "🕊 PSALM — sing, and the instrument answers"), () => togglePsalm());
ctxItem("🌐 " + (Synth.feeding() ? "tab feed: live — click to stop" : "feed a tab (YouTube, a stream…) into the world"),
async () => { const r = await Synth.feedTab();
const msg = r.stopped ? "🌐 tab feed stopped"
@ -9202,7 +9365,7 @@
g.connect(pan); pan.connect(drumBus); drums[dv] = { g, pan };
}
return { out, squash, squashMakeup, preSat, glitch, revSend, fb, satDry, satWet, leadA, leadB, morphA, morphB, leadFilt, leadAmp,
return { out, squash, squashMakeup, lim, preSat, glitch, revSend, fb, satDry, satWet, leadA, leadB, morphA, morphB, leadFilt, leadAmp,
bassOsc, bassAmp, padOscs, padFilt, padAmp, droneOscs, droneAmp,
echoDelay, echoFilt, echoFb, echoWet, echoFlutAmt, hissGain, chorusWet,
drumBus, drums, noiseBuf, duckG };
@ -9626,7 +9789,69 @@
}
});
// ---- 📼 TESTAMENT — record what god does ("it is written") --------------
// A MediaRecorder tapped off N.lim: the TRUE final mix, post-squash and
// post-limiter — exactly what you hear. Lazy (destination + recorder built
// on first roll), timesliced so a crash loses ≤1s. The GODSONIQ worklet
// Recorder is a different machine for a different purpose — untouched.
let testDest = null, testRec = null, testChunks = [], testT0 = 0, testUiTimer = null;
function testamentStart() {
if (!ctx) start(); // wake the graph — a recording can begin the session
if (testRec) return false; // already rolling
if (!testDest) { testDest = ctx.createMediaStreamDestination(); N.lim.connect(testDest); }
const mime = ["audio/webm;codecs=opus", "audio/webm", "audio/mp4"]
.find(m => window.MediaRecorder && MediaRecorder.isTypeSupported(m));
if (!mime) return false; // no MediaRecorder / no supported container (old Safari)
testChunks = []; testRec = new MediaRecorder(testDest.stream, { mimeType: mime, audioBitsPerSecond: 256000 });
testRec.ondataavailable = (e) => { if (e.data && e.data.size) testChunks.push(e.data); };
testRec.start(1000); // 1s timeslices
testT0 = ctx.currentTime; return true;
}
function testamentStop() {
return new Promise((resolve) => {
if (!testRec) { resolve(null); return; }
const rec = testRec, secs = ctx.currentTime - testT0;
const ext = rec.mimeType.indexOf("mp4") >= 0 ? "m4a" : "webm";
rec.onstop = () => { const blob = new Blob(testChunks, { type: rec.mimeType }); testChunks = []; resolve({ blob, secs, ext }); };
testRec = null; rec.stop();
});
}
function testamentState() { return { on: !!testRec, secs: testRec ? ctx.currentTime - testT0 : 0 }; }
const testBtn = document.createElement("div");
testBtn.id = "testament"; testBtn.textContent = "⏺"; testBtn.title = "TESTAMENT — record the master mix to a file";
Object.assign(testBtn.style, { position: "fixed", top: "12px", right: "132px", zIndex: 10,
height: "30px", minWidth: "30px", padding: "0 7px", borderRadius: "8px", display: "flex",
alignItems: "center", justifyContent: "center", background: "rgba(20,28,44,0.7)", color: "#cba6a6",
cursor: "pointer", fontSize: "13px", border: "1px solid rgba(120,150,200,0.25)",
backdropFilter: "blur(6px)", WebkitBackdropFilter: "blur(6px)" });
if (!AC || !window.MediaRecorder) { testBtn.style.opacity = "0.3"; testBtn.title = "this browser cannot record"; }
document.body.appendChild(testBtn);
const testFmt = (s) => { s = Math.max(0, Math.floor(s)); return Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0"); };
function testPaint() { const st = testamentState(); testBtn.classList.toggle("on", st.on); testBtn.textContent = st.on ? "⏺ " + testFmt(st.secs) : "⏺"; }
function testamentToggle() {
if (testamentState().on) {
testamentStop().then((r) => {
if (testUiTimer) { clearInterval(testUiTimer); testUiTimer = null; }
testPaint();
if (!r || !r.blob || !r.blob.size) return;
const d = new Date(), pad = (n) => String(n).padStart(2, "0");
const stamp = "" + d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + "-" + pad(d.getHours()) + pad(d.getMinutes());
const a = document.createElement("a"); a.href = URL.createObjectURL(r.blob);
a.download = "godstrument-testament-" + stamp + "." + r.ext;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(a.href), 4000);
eventTicker.unshift({ text: "📼 testament: " + testFmt(r.secs) + " — it is written", tAdded: now(), src: "sys" });
});
} else {
if (!testamentStart()) { eventTicker.unshift({ text: "📼 this browser cannot bear witness", tAdded: now(), src: "sys" }); return; }
testPaint(); testUiTimer = setInterval(testPaint, 1000);
}
}
testBtn.addEventListener("click", testamentToggle);
return { update(dests) { if (dests) Object.assign(D, dests); }, pluck,
testamentStart, testamentStop, testamentState, testament: testamentToggle,
toggle() { on ? stop() : start(); paint(); return on; }, playing() { return on; },
stringVoice(v) { if (v === undefined) return ksMode; if (!ctx) start(); ksMode = !!v; sampleMode = false; return ksMode; },
godsoniq(secs) { return godsoniq(secs); }, sampling() { return sampleMode && !!sampleBuf; },

View File

@ -238,6 +238,7 @@
.seqcell:hover { background: rgba(140,170,220,0.45); }
.seqcell.on { background: #78a0ff; box-shadow: 0 0 6px rgba(120,160,255,0.6); }
.seqcell.cur { outline: 1px solid rgba(255,238,150,0.85); }
.seqcell.off { opacity: 0.3; pointer-events: none; } /* ☸ WHEELS — a step past the lane's length is outside the wheel */
/* note length — a placed note is an overlay block spanning `len` steps, drag its
right edge (the ew-resize handle) to lengthen. Sits over the lit cells; only the
handle takes the mouse, so the grid underneath still toggles notes. */
@ -382,6 +383,18 @@
#godspeakrec.on { display: flex; animation: gspk-pulse 1.1s ease-in-out infinite; }
@keyframes gspk-pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
/* 📼 TESTAMENT — the record button; red + gently pulsing while it bears witness */
#testament.on { color: #ff8a8a !important; border-color: rgba(240,110,110,0.6) !important;
background: rgba(230,90,90,0.2) !important; animation: gspk-pulse 1.5s ease-in-out infinite; font-variant-numeric: tabular-nums; }
/* 🕊 PSALM — the listening chip; shows the note you're singing, brightens on voice */
#psalmchip { position: fixed; bottom: 164px; right: 14px; z-index: 12; display: none;
align-items: center; gap: 4px; min-width: 46px; padding: 6px 12px; border-radius: 8px; font-size: 12px;
background: rgba(20,28,44,0.7); color: #8fa4c8; border: 1px solid rgba(120,150,200,0.25);
cursor: pointer; -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }
#psalmchip.open { display: inline-flex; }
#psalmchip.voiced { color: #cfe4ff; border-color: rgba(150,200,255,0.6); background: rgba(60,95,150,0.5); }
/* right-click context menu — every setting, on everything */
#ctxmenu {
position: fixed; z-index: 30; display: none; width: 240px; max-height: 72vh;
@ -1171,6 +1184,10 @@
<li><b>🫀 GODSQUASH — the master squeeze the world can pump.</b> In <b>OMNI</b>'s fx rack sits one new knob: <b>squash</b>, a compressor across the whole master bus. Turn it up and the <b>threshold dives while the ratio climbs together</b>, a matched gain riding underneath so the <i>loudness stays level while the peaks kneel</i> — the mix gets denser, not louder. And because it's a destination like any other, <b>route a feed onto it</b> — the quakes, your heartbeat, the market's velocity — and the planet <b>pumps the mix</b>, breathing the whole instrument in time with the world.</li>
<li><b>🗣 GODSPEAK — the voice of god.</b> On your own machine (<i>Apple-Silicon only</i>), run <code>run.py --godspeak</code>, then <b>hold Right ⌥ and speak</b>. Your words are heard <b>locally</b> — your own Whisper, nothing leaving the machine — and become commands: <i>"tempo ninety", "storm mood", "capture six seconds", "chop sixteen", "open the beat", "squash sixty percent", "zen"</i>, and — the voice of god undoing its own works — <i>"take it back"</i>. A 🗣 chip glows by the ♪ while it listens. Anything it doesn't recognise simply <b>arrives in the world</b>, printed to the ticker — the word is never an error, only another feed.</li>
<li><b>⏪ UNGODLY — take it back.</b> <kbd>⌘Z</kbd> undoes; <kbd>⌘⇧Z</kbd> redoes. The instrument remembers your last <b>thirty-two acts</b> — every cable patched, mood applied, groove and scale changed, kit knob turned, beat step drawn, level and mute — and steps back through them, un-doing what god (or you) did. A held drag counts as one act, not fifty. It is <b>memory-only</b>: a reload forgives everything and starts the history clean.</li>
<li><b>📼 TESTAMENT — record what god does.</b> Beside the <kbd></kbd> sits a <b></b>. Press it and the instrument records <i>itself</i> — the true final mix, taken after the squash and the limiter, exactly what you hear. The button turns red and counts the minutes; press it again and a file drops to your disk (<kbd>godstrument-testament-…webm</kbd>), and the ticker says <i>it is written</i>. Start it before the beat and it captures the session from its first breath; it needs no setup and no other app — the planet, at last, can bear witness to itself. (Also on the sky menu, near GODSONIQ.)</li>
<li><b>☸ WHEELS — a wheel within a wheel.</b> <i>"…their work was as it were a wheel in the middle of a wheel"</i> (Ezekiel 1:16). Each lane keeps its own length: set the <b>☸ wheel</b> chip in a beat lane (or the arranger's euclid row) to anything from 1 to 16, and a lane shorter than the full sixteen <b>wraps on its own turn</b>. A hat of twelve over a kick of sixteen drift apart and only realign every forty-eight steps — <b>polymeter</b>, the oldest trick for making a loop that never quite repeats. Steps past the wheel dim out; the playhead rides each lane's own length; and the pattern <b>exports to MIDI exactly as it plays</b>, twelve-wheel and all. A full wheel of sixteen behaves precisely as it always did.</li>
<li><b>🕊 PSALM — sing, and the instrument answers.</b> Right-click the sky → <b>🕊 PSALM</b>, allow the microphone, and <b>sing</b>. The pitch is heard, <b>folded to the scale you're in</b> (so you're never out of key), and played as notes — a <b>🕊</b> chip shows the note leaving your throat. And the duet with <b>⚡ GODSPEED</b>: with the arp running, each note you sing <b>latches into the held chord</b>, one at a time — hum four notes and the machine arpeggiates the chord you sang. <b>It listens locally</b>: the analysis happens in the page, in its own silence, and <i>nothing ever leaves the machine</i> — no recording, no upload, just your voice becoming the instrument's.</li>
<li><b>✦ CANON — the shelf of worked examples.</b> Open <b>⚙ → DIMENSIONS</b> and at the top of the list, above your own saved worlds, sit four <b>✦ factory dimensions</b> that ship with the instrument — <i>the heartbeat</i> (a boom-bap kit with the swing and the kick's duck), <i>the wheel within the wheel</i> (WHEELS, made audible), <i>the squeeze</i> (the market pumping GODSQUASH), and <i>quiet arrival</i> (the earth echo, slow and sparse). Load one to hear a feature in its element, then save your own version under a new name — the ✦ ones are <b>read-only</b>, the same for everyone, a place to start rather than a thing to overwrite.</li>
<li><b>📽 The stage — send the visual to any screen.</b> Right-click the sky → <b>the stage</b> and a clean black mirror window opens, fed live from the canvas. Drag it to a projector, an AirPlay display, a second monitor — macOS treats it like any window — and <b>double-click for fullscreen</b>. The stage has a <b>camera</b>: the whole view by default (whatever you're in — a skin, the wheel, mind mode's POV), or right-click any node → <b>project this node</b> and the camera glides after that one sphere through its orbits, or <b>project its station</b> to frame a whole wheel and its satellites. Change cameras live from the menus while the projector runs; the audience sees the camera breathe from subject to subject.</li>
<li><b>🕸 The commune — play together, properly.</b> Right-click the sky → <b>the commune</b>. This is for <b>signed-in players</b>: your session rides the connection, so the hub knows exactly who everyone is — no impostors, no setup, no Bluetooth. One of you <b>hosts</b>: name the room, then <b>✉ invite</b> the others by their username (they must be on the site — invitations reach the present). The invited see the invitation arrive live and <b>join or decline</b>; anyone may <b>leave</b> whenever they wish, and if the host leaves, the room closes with them. Then the sections: members <b>claim voices</b> (right-click any voice, or the panel's picker), or the <b>host allocates</b> them — this bass to her, those pads to him, <b>the godtime itself</b> to whoever conducts — and the hub enforces it: only the section's holder can speak for it. From that moment your levels, mutes, grooves and arranger lanes for what you hold follow your hands on every screen, same living room or opposite sides of the earth. The world — the Source — stays identical for everyone, because it always was. Three people, one planet, one instrument.</li></ul>