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>
88 lines
3.1 KiB
Python
88 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""The live sky over a chosen city (default Tokyo) breathing into the pads.
|
|
Temperature, wind, rain, barometric pressure and humidity become slow control
|
|
signals -- weather is inherently musical: it drifts, swells, and never repeats.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
from pythonosc.udp_client import SimpleUDPClient
|
|
|
|
NAME = "world_weather"
|
|
POLL_INTERVAL = 300.0 # seconds between real API polls
|
|
REEMIT_INTERVAL = 5.0 # seconds between re-emitting last known values
|
|
USER_AGENT = "Godstrument/1.0 (world_weather worker)"
|
|
|
|
# open-meteo "current" key -> OSC address suffix
|
|
FIELDS = {
|
|
"temperature_2m": "/gs/weather/temp",
|
|
"wind_speed_10m": "/gs/weather/wind",
|
|
"precipitation": "/gs/weather/precip",
|
|
"surface_pressure": "/gs/weather/pressure",
|
|
"relative_humidity_2m": "/gs/weather/humidity",
|
|
}
|
|
|
|
|
|
def fetch_current(lat, lon):
|
|
"""Fetch the 'current' block from open-meteo. Returns a dict or None."""
|
|
params = ",".join(FIELDS.keys())
|
|
url = (
|
|
"https://api.open-meteo.com/v1/forecast"
|
|
f"?latitude={lat}&longitude={lon}¤t={params}"
|
|
)
|
|
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
|
with urllib.request.urlopen(req, timeout=20) as resp:
|
|
data = json.loads(resp.read().decode("utf-8"))
|
|
return data.get("current") or {}
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="Weather -> OSC control signals.")
|
|
ap.add_argument("--host", default="127.0.0.1")
|
|
ap.add_argument("--port", type=int, default=9000)
|
|
ap.add_argument("--lat", type=float, default=35.68, help="latitude (default Tokyo)")
|
|
ap.add_argument("--lon", type=float, default=139.69, help="longitude (default Tokyo)")
|
|
args = ap.parse_args()
|
|
|
|
client = SimpleUDPClient(args.host, args.port)
|
|
print(f"[{NAME}] emitting /gs/weather/... every {int(REEMIT_INTERVAL)}s")
|
|
|
|
last = {} # OSC address -> last known float
|
|
next_poll = 0.0 # poll immediately on first loop
|
|
backoff = 5.0 # network retry backoff, grows on repeated failure
|
|
|
|
while True:
|
|
now = time.monotonic()
|
|
if now >= next_poll:
|
|
try:
|
|
current = fetch_current(args.lat, args.lon)
|
|
for key, addr in FIELDS.items():
|
|
if key in current and current[key] is not None:
|
|
last[addr] = float(current[key])
|
|
next_poll = now + POLL_INTERVAL
|
|
backoff = 5.0
|
|
except (urllib.error.URLError, OSError, ValueError, json.JSONDecodeError) as e:
|
|
print(f"[{NAME}] poll failed: {e}; retry in {backoff:.0f}s")
|
|
next_poll = now + backoff
|
|
backoff = min(backoff * 2, POLL_INTERVAL)
|
|
|
|
# Re-emit last known values so downstream signals stay live.
|
|
for addr, value in last.items():
|
|
try:
|
|
client.send_message(addr, value)
|
|
except OSError as e:
|
|
print(f"[{NAME}] send failed on {addr}: {e}")
|
|
|
|
time.sleep(REEMIT_INTERVAL)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt:
|
|
sys.exit(0)
|