Everything — your hand, the room, a satellite, the markets, the sky — becomes an OSC control signal fused through a modulation matrix. Includes: - hub + normalize (One-Euro) + matrix (curves/gates/quantize) + transform (tweaks/groups) - 20+ workers: sim sensors, 8 keyless world feeds, ephemeris/almanac/clock (computed), time-warp replay (quakes/weather/db), and the dealgod market warehouse feed - planetary orbital LFOs, SQLite recorder, city targeting - live browser console: mute, group macros, drag-to-patch, inspector, Web MIDI Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
176 lines
6.5 KiB
Python
176 lines
6.5 KiB
Python
"""
|
|
matrix.py — the modulation matrix. The actual instrument.
|
|
|
|
Everything upstream turns the world into a flat bag of normalized 0..1 signals:
|
|
|
|
sources = {"sun.speed": 0.62, "crypto.vel": 0.11, "tof.cx": 0.8, ...}
|
|
|
|
The matrix is a patchbay: N sources x M destinations, each connection a "cable"
|
|
with an amount, a response curve, and a polarity. Cables can optionally:
|
|
|
|
* gate — only pass when the source crosses a threshold (an event fires
|
|
a sample only when a real quake is big enough).
|
|
* quantize — snap a continuous source to notes in a musical scale, so the
|
|
price of bitcoin plays a melody in D-dorian instead of a
|
|
formless wobble.
|
|
|
|
A source and a satellite are the same kind of thing here. That is the whole
|
|
point of the Godstrument.
|
|
|
|
Pure stdlib.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Musical scales (semitone offsets within an octave)
|
|
# ---------------------------------------------------------------------------
|
|
SCALES: dict[str, list[int]] = {
|
|
"chromatic": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
|
"major": [0, 2, 4, 5, 7, 9, 11],
|
|
"minor": [0, 2, 3, 5, 7, 8, 10],
|
|
"dorian": [0, 2, 3, 5, 7, 9, 10],
|
|
"phrygian": [0, 1, 3, 5, 7, 8, 10],
|
|
"lydian": [0, 2, 4, 6, 7, 9, 11],
|
|
"mixolydian": [0, 2, 4, 5, 7, 9, 10],
|
|
"pentatonic": [0, 2, 4, 7, 9],
|
|
"minor_pent": [0, 3, 5, 7, 10],
|
|
"wholetone": [0, 2, 4, 6, 8, 10],
|
|
"hirajoshi": [0, 2, 3, 7, 8],
|
|
}
|
|
|
|
|
|
def quantize_to_scale(x01: float, scale: str = "minor_pent",
|
|
root: int = 48, octaves: int = 3) -> int:
|
|
"""Map 0..1 to a MIDI note constrained to `scale`, starting at `root`."""
|
|
steps = SCALES.get(scale, SCALES["chromatic"])
|
|
n = len(steps) * octaves
|
|
idx = int(_clamp01(x01) * (n - 1) + 0.5)
|
|
octave, degree = divmod(idx, len(steps))
|
|
return root + 12 * octave + steps[degree]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Response curves
|
|
# ---------------------------------------------------------------------------
|
|
def apply_curve(x: float, curve: str = "lin") -> float:
|
|
x = _clamp01(x)
|
|
if curve == "lin":
|
|
return x
|
|
if curve == "exp": # slow start, fast finish (perceptual filter sweep)
|
|
return x * x
|
|
if curve == "exp3":
|
|
return x * x * x
|
|
if curve == "log": # fast start, slow finish
|
|
return x ** 0.5
|
|
if curve == "scurve": # ease-in-out
|
|
return x * x * (3 - 2 * x)
|
|
if curve == "inv":
|
|
return 1.0 - x
|
|
return x
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The matrix
|
|
# ---------------------------------------------------------------------------
|
|
class ModMatrix:
|
|
"""
|
|
routes: list of dicts, each a cable:
|
|
{
|
|
"source": "sun.speed", # source signal name (dotted)
|
|
"dest": "filter.cutoff", # destination parameter name
|
|
"amount": 0.9, # -1..1 depth
|
|
"curve": "exp", # response curve (see apply_curve)
|
|
"gate": 0.7, # optional: only pass if source >= gate
|
|
"quantize": {"scale": "dorian", # optional: snap to notes -> writes
|
|
"root": 48, # dest as a MIDI note number instead
|
|
"octaves": 2}, # of a 0..1 value
|
|
"label": "the sun opens the filter" # optional, for the visualizer
|
|
}
|
|
"""
|
|
|
|
def __init__(self, routes: list[dict]):
|
|
self.routes = [dict(r) for r in routes]
|
|
|
|
@classmethod
|
|
def from_config(cls, cfg: dict) -> "ModMatrix":
|
|
return cls(cfg.get("routes", []))
|
|
|
|
# ---- live editing (browser -> hub) --------------------------------
|
|
def add_route(self, source, dest, amount=0.5, curve="lin", label=""):
|
|
if not source or not dest:
|
|
return
|
|
if any(r.get("source") == source and r.get("dest") == dest
|
|
for r in self.routes):
|
|
return # already patched
|
|
self.routes.append({"source": source, "dest": dest,
|
|
"amount": float(amount), "curve": curve,
|
|
"label": label or (source + " → " + dest)})
|
|
|
|
def remove_route(self, source, dest):
|
|
self.routes = [r for r in self.routes
|
|
if not (r.get("source") == source and r.get("dest") == dest)]
|
|
|
|
def set_route_amount(self, source, dest, amount):
|
|
for r in self.routes:
|
|
if r.get("source") == source and r.get("dest") == dest:
|
|
r["amount"] = float(amount)
|
|
|
|
def evaluate(self, sources: dict[str, float]):
|
|
"""Returns (dests, active) where:
|
|
dests = {dest_name: value} summed & clamped contributions
|
|
active = {route_index: 0..1} how 'lit' each cable is (for viz)
|
|
"""
|
|
dests: dict[str, float] = {}
|
|
notes: dict[str, int] = {}
|
|
active: dict[int, float] = {}
|
|
|
|
for i, r in enumerate(self.routes):
|
|
src = r.get("source")
|
|
dst = r.get("dest")
|
|
if src is None or dst is None:
|
|
active[i] = 0.0
|
|
continue
|
|
raw = sources.get(src)
|
|
if raw is None:
|
|
active[i] = 0.0
|
|
continue
|
|
|
|
# gate: a threshold the source must cross to pass at all
|
|
gate = r.get("gate")
|
|
if gate is not None and raw < gate:
|
|
active[i] = 0.0
|
|
continue
|
|
|
|
shaped = apply_curve(raw, r.get("curve", "lin"))
|
|
amount = float(r.get("amount", 1.0))
|
|
|
|
q = r.get("quantize")
|
|
if q:
|
|
note = quantize_to_scale(
|
|
shaped,
|
|
q.get("scale", "minor_pent"),
|
|
q.get("root", 48),
|
|
q.get("octaves", 3),
|
|
)
|
|
notes[dst] = note
|
|
active[i] = abs(amount) * shaped
|
|
continue
|
|
|
|
contrib = amount * shaped
|
|
dests[dst] = dests.get(dst, 0.0) + contrib
|
|
active[i] = abs(contrib)
|
|
|
|
# clamp summed continuous destinations
|
|
for k in list(dests.keys()):
|
|
dests[k] = _clamp01(dests[k])
|
|
# note destinations override as integers
|
|
for k, v in notes.items():
|
|
dests[k] = v
|
|
|
|
return dests, active
|
|
|
|
|
|
def _clamp01(x: float) -> float:
|
|
return 0.0 if x < 0.0 else 1.0 if x > 1.0 else x
|