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>
117 lines
4.0 KiB
Python
117 lines
4.0 KiB
Python
"""
|
|
replay_weather.py — days of sky, sweeping past in minutes.
|
|
|
|
Fetches hourly weather history for the venue from Open-Meteo's archive, then
|
|
replays it compressed onto a loop (default: 7 days -> 4 minutes) so slow weather
|
|
finally moves fast enough to shape a track. Jog-scrubbable like the quake replay.
|
|
|
|
Drop-in replacement for world_weather.py — emits the same /gs/weather/* addresses
|
|
(temp, wind, precip, pressure, humidity) as smoothly interpolated sweeps.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
import argparse
|
|
import datetime
|
|
import json
|
|
import threading
|
|
import time
|
|
import urllib.request
|
|
|
|
from pythonosc.udp_client import SimpleUDPClient
|
|
from pythonosc.dispatcher import Dispatcher
|
|
from pythonosc.osc_server import ThreadingOSCUDPServer
|
|
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
from timewarp import ReplayBuffer
|
|
|
|
UA = "Godstrument/1.0 (live instrument)"
|
|
|
|
FIELDS = {
|
|
"temperature_2m": "temp",
|
|
"wind_speed_10m": "wind",
|
|
"precipitation": "precip",
|
|
"surface_pressure": "pressure",
|
|
"relative_humidity_2m": "humidity",
|
|
}
|
|
|
|
|
|
class Jog:
|
|
def __init__(self, rate_min=0.25, rate_max=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
|
|
|
|
|
|
def fetch(lat: float, lon: float, days: int):
|
|
end = datetime.datetime.now(datetime.timezone.utc).date()
|
|
start = end - datetime.timedelta(days=days)
|
|
hourly = ",".join(FIELDS.keys())
|
|
url = ("https://archive-api.open-meteo.com/v1/archive"
|
|
f"?latitude={lat}&longitude={lon}"
|
|
f"&start_date={start}&end_date={end}&hourly={hourly}")
|
|
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
data = json.load(r)
|
|
h = data["hourly"]
|
|
n = len(h["time"])
|
|
times = list(range(n)) # uniform hourly index is fine for phase mapping
|
|
return times, {out: h.get(api, []) for api, out in FIELDS.items()}
|
|
|
|
|
|
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=9102)
|
|
ap.add_argument("--lat", type=float, default=35.68)
|
|
ap.add_argument("--lon", type=float, default=139.69)
|
|
ap.add_argument("--days", type=int, default=7)
|
|
ap.add_argument("--into", type=float, default=240.0, help="loop length (s)")
|
|
args = ap.parse_args()
|
|
|
|
client = SimpleUDPClient(args.host, args.port)
|
|
jog = Jog()
|
|
bufs: dict[str, ReplayBuffer] = {}
|
|
|
|
while not bufs:
|
|
try:
|
|
times, series = fetch(args.lat, args.lon, args.days)
|
|
for name, vals in series.items():
|
|
b = ReplayBuffer(mode="continuous", into_seconds=args.into)
|
|
b.load_continuous(times, vals)
|
|
if b.loaded:
|
|
bufs[name] = b
|
|
print(f"[replay_weather] {len(times)}h of weather ({args.lat},{args.lon}) "
|
|
f"-> looping every {args.into:g}s (jog udp/{args.jog_port})")
|
|
except Exception as e: # noqa
|
|
print(f"[replay_weather] fetch failed ({e}); retrying in 10s")
|
|
time.sleep(10)
|
|
|
|
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 / 30.0
|
|
while True:
|
|
for name, b in bufs.items():
|
|
b.tick(period, jog.rate_mult, jog.nudge)
|
|
client.send_message(f"/gs/weather/{name}", float(b.current()))
|
|
time.sleep(period)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|