""" world_sky.py — the local sky over the venue, as a slow LFO. Computes the sun's elevation above the horizon at a given latitude/longitude and the current instant, using the NOAA solar-position algorithm. No network, no API key, never rate-limited — just astronomy. This is the most place-specific signal in the whole instrument: playing Brisbane at 2am sounds nothing like London at sunset, because the sun is literally somewhere else in the sky. Emits: /gs/sky/elev solar elevation in degrees (-90 night .. +90 overhead) /gs/sky/day 0..1 smooth day/night (0 deep night, 1 broad daylight) /gs/sky/az solar azimuth in degrees (0=N, 90=E, 180=S, 270=W) """ from __future__ import annotations import argparse import datetime import math import time from pythonosc.udp_client import SimpleUDPClient def solar_position(lat: float, lon: float, when: datetime.datetime): """Return (elevation_deg, azimuth_deg) for lat/lon at UTC time `when`.""" n = when.timetuple().tm_yday hour = when.hour + when.minute / 60 + when.second / 3600 g = 2 * math.pi / 365 * (n - 1 + (hour - 12) / 24) # fractional year eqtime = 229.18 * (0.000075 + 0.001868 * math.cos(g) - 0.032077 * math.sin(g) - 0.014615 * math.cos(2 * g) - 0.040849 * math.sin(2 * g)) # minutes decl = (0.006918 - 0.399912 * math.cos(g) + 0.070257 * math.sin(g) - 0.006758 * math.cos(2 * g) + 0.000907 * math.sin(2 * g) - 0.002697 * math.cos(3 * g) + 0.00148 * math.sin(3 * g)) # radians time_offset = eqtime + 4 * lon # minutes (E +) tst = hour * 60 + time_offset # true solar time ha = math.radians(tst / 4 - 180) # hour angle lat_r = math.radians(lat) cos_zen = (math.sin(lat_r) * math.sin(decl) + math.cos(lat_r) * math.cos(decl) * math.cos(ha)) cos_zen = max(-1.0, min(1.0, cos_zen)) zen = math.acos(cos_zen) elev = 90 - math.degrees(zen) # azimuth sin_az = -math.sin(ha) * math.cos(decl) cos_az = (math.sin(decl) - math.sin(lat_r) * math.cos(zen)) / \ (math.cos(lat_r) * math.sin(zen) + 1e-9) az = (math.degrees(math.atan2(sin_az, cos_az)) + 360) % 360 return elev, az def day_fraction(elev: float) -> float: """Smooth 0..1 across the twilight band (civil dusk .. full day).""" return max(0.0, min(1.0, (elev + 6.0) / 18.0)) 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("--lat", type=float, default=-27.47, help="latitude (default Brisbane)") ap.add_argument("--lon", type=float, default=153.02, help="longitude (default Brisbane)") ap.add_argument("--interval", type=float, default=10.0) args = ap.parse_args() client = SimpleUDPClient(args.host, args.port) print(f"[sky] emitting /gs/sky/elev|day|az for ({args.lat},{args.lon}) " f"every {args.interval:g}s") while True: try: elev, az = solar_position(args.lat, args.lon, datetime.datetime.now(datetime.timezone.utc)) client.send_message("/gs/sky/elev", float(elev)) client.send_message("/gs/sky/day", day_fraction(elev)) client.send_message("/gs/sky/az", float(az)) client.send_message("/gs/sky/lat", float(args.lat)) # the venue's own coordinates, client.send_message("/gs/sky/lon", float(args.lon)) # so the console can place "here" on a map except Exception as e: # noqa print(f"[sky] warn: {e}") time.sleep(args.interval) if __name__ == "__main__": main()