""" 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()