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>
79 lines
2.7 KiB
Python
79 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
"""A city's pollution turned into audible grit and glitch.
|
|
Real-time PM2.5/PM10/US-AQI from Open-Meteo's air-quality API for a chosen city.
|
|
Dirtier air -> more grit; spikes glitch the signal chain. Delhi by default.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import time
|
|
import urllib.request
|
|
import urllib.error
|
|
|
|
from pythonosc.udp_client import SimpleUDPClient
|
|
|
|
NAME = "world_air"
|
|
POLL = 300.0 # seconds between API polls (respect the source)
|
|
REEMIT = 5.0 # re-emit last known values this often
|
|
USER_AGENT = "Godstrument/1.0 (+worker world_air)"
|
|
URL = ("https://air-quality-api.open-meteo.com/v1/air-quality"
|
|
"?latitude={lat}&longitude={lon}¤t=pm2_5,pm10,us_aqi")
|
|
|
|
|
|
def fetch(lat, lon):
|
|
"""Return (pm25, pm10, aqi) as floats, or None on any failure."""
|
|
url = URL.format(lat=lat, lon=lon)
|
|
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"))
|
|
cur = data["current"]
|
|
pm25 = float(cur["pm2_5"])
|
|
pm10 = float(cur["pm10"])
|
|
aqi = float(cur["us_aqi"])
|
|
return pm25, pm10, aqi
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="Air-quality -> OSC grit worker.")
|
|
ap.add_argument("--host", default="127.0.0.1")
|
|
ap.add_argument("--port", type=int, default=9000)
|
|
ap.add_argument("--lat", type=float, default=28.61, help="latitude (default Delhi)")
|
|
ap.add_argument("--lon", type=float, default=77.20, help="longitude (default Delhi)")
|
|
args = ap.parse_args()
|
|
|
|
client = SimpleUDPClient(args.host, args.port)
|
|
print(f"[{NAME}] emitting /gs/air/... every {int(REEMIT)}s")
|
|
|
|
last = None # (pm25, pm10, aqi) once we have a reading
|
|
next_poll = 0.0 # poll immediately on startup
|
|
backoff = 5.0
|
|
|
|
while True:
|
|
now = time.monotonic()
|
|
|
|
if now >= next_poll:
|
|
try:
|
|
last = fetch(args.lat, args.lon)
|
|
next_poll = now + POLL
|
|
backoff = 5.0
|
|
except (urllib.error.URLError, urllib.error.HTTPError,
|
|
OSError, ValueError, KeyError, TypeError) as e:
|
|
print(f"[{NAME}] poll failed ({e}); retrying in {backoff:.0f}s")
|
|
next_poll = now + backoff
|
|
backoff = min(backoff * 2, POLL)
|
|
|
|
if last is not None:
|
|
pm25, pm10, aqi = last
|
|
try:
|
|
client.send_message("/gs/air/pm25", float(pm25))
|
|
client.send_message("/gs/air/pm10", float(pm10))
|
|
client.send_message("/gs/air/aqi", float(aqi))
|
|
except OSError as e:
|
|
print(f"[{NAME}] OSC send failed ({e}); continuing")
|
|
|
|
time.sleep(REEMIT)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|