Godstrument/workers/replay_db.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

121 lines
4.7 KiB
Python

"""
replay_db.py — replay your own recorded history, warped and scratchable.
Reads any window of any recorded signals from godstrument.db (written by
recorder.py) and plays them back compressed on a loop, jog-scrubbable like the
other replay workers. Unlike replay_quakes/replay_weather, this isn't limited to
feeds with a public history API — it can replay YOUR hand, the room mic, the ToF
sensor, anything the recorder captured. The instrument replaying itself.
# replay last 24h of weather + solar wind, compressed into 5 min
python workers/replay_db.py --signals weather.temp,weather.wind,sun.speed --window last:24h
# replay a specific past set (your recorded performance) compressed into 3 min
python workers/replay_db.py --signals tof.cx,tof.cy,audio.rms \\
--start 2026-07-04T21:00 --end 2026-07-04T23:30 --into 180
"""
from __future__ import annotations
import argparse
import datetime
import os
import sqlite3
import sys
import threading
import time
from pythonosc.udp_client import SimpleUDPClient
from pythonosc.dispatcher import Dispatcher
from pythonosc.osc_server import ThreadingOSCUDPServer
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from timewarp import ReplayBuffer, Jog
HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def parse_window(window, start, end):
if window and window.startswith("last:"):
spec = window[5:]
mult = {"h": 3600, "d": 86400, "m": 60}.get(spec[-1], 3600)
secs = float(spec[:-1]) * mult
now = time.time()
return now - secs, now
to_ts = lambda s: datetime.datetime.fromisoformat(s).timestamp()
return (to_ts(start) if start else time.time() - 86400,
to_ts(end) if end else time.time())
def osc_addr(signal: str) -> str:
return "/gs/" + signal.replace(".", "/")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=9000)
ap.add_argument("--jog-port", type=int, default=9103)
ap.add_argument("--db", default=os.path.join(HERE, "godstrument.db"))
ap.add_argument("--signals", default="weather.temp,weather.wind,sun.speed",
help="comma-separated signal names to replay")
ap.add_argument("--window", default="last:24h", help="e.g. last:24h / last:3d")
ap.add_argument("--start", help="ISO time (overrides --window)")
ap.add_argument("--end", help="ISO time")
ap.add_argument("--into", type=float, default=300.0, help="loop length (s)")
args = ap.parse_args()
t0, t1 = parse_window(args.window, args.start, args.end)
con = sqlite3.connect(args.db)
signals = [s.strip() for s in args.signals.split(",") if s.strip()]
cont: dict[str, ReplayBuffer] = {}
evt: dict[str, ReplayBuffer] = {}
for sig in signals:
if sig.endswith(".event"):
rows = con.execute("SELECT t, mag FROM events WHERE signal=? "
"AND t BETWEEN ? AND ? ORDER BY t",
(sig.rsplit(".", 1)[0], t0, t1)).fetchall()
if rows:
b = ReplayBuffer("event", args.into)
b.load_events([{"t": r[0], "mag": r[1]} for r in rows])
evt[sig] = b
else:
rows = con.execute("SELECT t, v FROM samples WHERE signal=? "
"AND t BETWEEN ? AND ? ORDER BY t",
(sig, t0, t1)).fetchall()
if len(rows) >= 2:
b = ReplayBuffer("continuous", args.into)
b.load_continuous([r[0] for r in rows], [r[1] for r in rows])
cont[sig] = b
span_h = (t1 - t0) / 3600
have = list(cont) + list(evt)
if not have:
print(f"[replay_db] no recorded data for {signals} in that window. "
f"Record some first with: run.py --record")
return
print(f"[replay_db] replaying {have}{span_h:.1f}h -> loop {args.into:g}s "
f"(jog udp/{args.jog_port})")
client = SimpleUDPClient(args.host, args.port)
jog = Jog()
disp = Dispatcher()
disp.map("/jog/rate", jog.on_rate)
disp.map("/jog/nudge", jog.on_nudge)
srv = ThreadingOSCUDPServer(("127.0.0.1", args.jog_port), disp)
threading.Thread(target=srv.serve_forever, daemon=True).start()
period = 1.0 / 40.0
while True:
for sig, b in cont.items():
b.tick(period, jog.rate_mult, jog.nudge)
client.send_message(osc_addr(sig), float(b.current()))
for sig, b in evt.items():
for _ in b.tick(period, jog.rate_mult, jog.nudge) or []:
client.send_message(osc_addr(sig), 1.0)
time.sleep(period)
if __name__ == "__main__":
main()