Godstrument/timewarp.py
monsterrobotparty 8deb65b4fe Godstrument: a multi-modal sensor-fusion instrument
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>
2026-07-05 02:13:05 +10:00

130 lines
5.0 KiB
Python

"""
timewarp.py — give slow data a playhead you can scratch.
Live data is stuck in the present: you can freeze or bend it, but you can't
speed it up, because it's happening *now*. Recorded data is different — it has a
timeline. This module takes a window of history (a month of earthquakes, four
days of weather) and plays it back **compressed** — 30 days into 5 minutes — on
a loop, so glacial data finally has agency inside a track.
And because it has a playhead, you can *nudge* it: push the platter forward,
drag it back, scrub. A month of the Earth's seismicity becomes a loopable,
scratchable rhythm.
Two buffer modes:
* continuous — interpolates a value at the playhead (temperature, wind...)
* event — fires discrete hits as the playhead crosses them (quakes...)
Playhead is tracked as a phase in [0,1) over the loaded window. Pure stdlib.
"""
from __future__ import annotations
import bisect
NUDGE_GAIN = 2.0 # how hard a full nudge shoves the platter
class Jog:
"""Live playhead control, fed by the hub over OSC (/jog/rate, /jog/nudge)."""
def __init__(self, rate_min: float = 0.25, rate_max: float = 4.0):
self.rate_min, self.rate_max = rate_min, rate_max
self.rate_mult = 1.0
self.nudge = 0.0
def on_rate(self, addr, *a):
if a:
v = max(0.0, min(1.0, float(a[0])))
self.rate_mult = self.rate_min + (self.rate_max - self.rate_min) * v
def on_nudge(self, addr, *a):
if a:
self.nudge = (max(0.0, min(1.0, float(a[0]))) - 0.5) * 2.0
class ReplayBuffer:
def __init__(self, mode: str = "continuous", into_seconds: float = 300.0):
self.mode = mode
self.into = max(1.0, into_seconds) # real seconds for one full loop
self.phase = 0.0
# continuous
self._phases: list[float] = []
self._vals: list[float] = []
# event: parallel sorted lists
self._ev_phase: list[float] = []
self._ev_payload: list[dict] = []
self.loaded = False
# ---- loading -------------------------------------------------------
def load_continuous(self, times: list[float], values: list[float]):
pairs = sorted((t, v) for t, v in zip(times, values) if v is not None)
if len(pairs) < 2:
return
t0, t1 = pairs[0][0], pairs[-1][0]
span = (t1 - t0) or 1.0
self._phases = [(t - t0) / span for t, _ in pairs]
self._vals = [float(v) for _, v in pairs]
self.loaded = True
def load_events(self, events: list[dict], t_key: str = "t"):
"""events: dicts each with a time under t_key; extra fields = payload."""
evs = sorted(events, key=lambda e: e[t_key])
if len(evs) < 1:
return
t0, t1 = evs[0][t_key], evs[-1][t_key]
span = (t1 - t0) or 1.0
self._ev_phase = [(e[t_key] - t0) / span for e in evs]
self._ev_payload = evs
self.loaded = True
# ---- playback ------------------------------------------------------
def tick(self, dt: float, rate_mult: float = 1.0, nudge: float = 0.0):
"""Advance the playhead. rate_mult scales base speed; nudge is bipolar
(-1..1), a momentary platter shove. Returns crossed event payloads
(event mode) or None (continuous)."""
if not self.loaded:
return [] if self.mode == "event" else None
eff = rate_mult + nudge * NUDGE_GAIN # effective multiplier
dphase = (eff / self.into) * dt # one loop = self.into sec
# clamp a runaway single-step to at most one full loop
if dphase > 1.0:
dphase = 1.0
elif dphase < -1.0:
dphase = -1.0
p0 = self.phase
crossed = self._crossed(p0, dphase) if self.mode == "event" else None
self.phase = (p0 + dphase) % 1.0
return crossed
def current(self) -> float:
"""Interpolated value at the playhead (continuous mode)."""
if not self.loaded or not self._phases:
return 0.0
ph = self.phase
ps = self._phases
i = bisect.bisect_right(ps, ph)
if i == 0 or i >= len(ps):
# wrap segment between last and first sample
a, b = ps[-1], ps[0] + 1.0
va, vb = self._vals[-1], self._vals[0]
x = ph if ph >= ps[-1] else ph + 1.0
else:
a, b = ps[i - 1], ps[i]
va, vb = self._vals[i - 1], self._vals[i]
x = ph
f = (x - a) / (b - a) if b != a else 0.0
return va + (vb - va) * f
def _crossed(self, p0: float, dphase: float) -> list[dict]:
if dphase == 0.0:
return []
out = []
end = p0 + dphase
for ep, payload in zip(self._ev_phase, self._ev_payload):
for cand in (ep - 1.0, ep, ep + 1.0):
if dphase > 0 and p0 < cand <= end:
out.append(payload)
elif dphase < 0 and end <= cand < p0:
out.append(payload)
return out