#!/usr/bin/env python3 """Live global seismic activity from the USGS earthquake feed. Every real earthquake on Earth in the last hour fires a percussive "event" whose magnitude, depth, and location become musical: the planet playing itself as a drum. """ import argparse import json import sys import time import urllib.request import urllib.error from pythonosc.udp_client import SimpleUDPClient NAME = "world_quakes" FEED_URL = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson" POLL_SECONDS = 60 USER_AGENT = "Godstrument/1.0 (live instrument; contact monsterrobotparty@gmail.com)" def fetch_feed(): """Fetch and parse the USGS GeoJSON feed. Returns the parsed dict or None on failure.""" req = urllib.request.Request(FEED_URL, headers={"User-Agent": USER_AGENT}) with urllib.request.urlopen(req, timeout=30) as resp: raw = resp.read() return json.loads(raw) def main(): parser = argparse.ArgumentParser(description="Emit live USGS earthquakes as OSC.") parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=9000) args = parser.parse_args() client = SimpleUDPClient(args.host, args.port) print(f"[{NAME}] emitting /gs/quake/... every {POLL_SECONDS}s") seen = set() first_poll = True backoff = 5 while True: try: data = fetch_feed() backoff = 5 # reset backoff after a good fetch except (urllib.error.URLError, urllib.error.HTTPError, OSError, ValueError) as e: print(f"[{NAME}] warning: fetch failed ({e}); retrying in {backoff}s") time.sleep(backoff) backoff = min(backoff * 2, POLL_SECONDS) continue features = data.get("features") or [] if first_poll: # Record existing quakes but do NOT fire them (no startup burst). for feat in features: fid = feat.get("id") if fid is not None: seen.add(fid) first_poll = False else: for feat in features: fid = feat.get("id") if fid is None or fid in seen: continue seen.add(fid) props = feat.get("properties") or {} mag = props.get("mag") if mag is None: continue # skip quakes with no magnitude place = props.get("place") or "unknown location" geom = feat.get("geometry") or {} coords = geom.get("coordinates") or [None, None, None] lon = coords[0] if len(coords) > 0 else None lat = coords[1] if len(coords) > 1 else None depth = coords[2] if len(coords) > 2 else None try: client.send_message("/gs/quake/event", float(mag)) if depth is not None: client.send_message("/gs/quake/depth", float(depth)) if lat is not None: client.send_message("/gs/quake/lat", float(lat)) if lon is not None: client.send_message("/gs/quake/lon", float(lon)) except (OSError, ValueError, TypeError) as e: print(f"[{NAME}] warning: OSC send failed ({e})") continue depth_str = f"{depth:.0f}km" if depth is not None else "?km" print(f" quake M{float(mag):.1f} {place} (depth {depth_str})") time.sleep(POLL_SECONDS) if __name__ == "__main__": main()