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>
121 lines
4.7 KiB
Python
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()
|