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>
128 lines
4.4 KiB
Python
128 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""The room itself as a modulator: the crowd, sub-bass, and ambience of the live space.
|
|
Loudness, dominant pitch, brightness, and onset energy of the room become control
|
|
signals — so the audience breathing, cheering, and the PA's own bass feed the machine."""
|
|
|
|
import argparse
|
|
import sys
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Godstrument audio (room) worker")
|
|
parser.add_argument("--host", default="127.0.0.1")
|
|
parser.add_argument("--port", type=int, default=9000)
|
|
parser.add_argument("--device", default=None,
|
|
help="optional input device index or name substring")
|
|
args = parser.parse_args()
|
|
|
|
# Lazy imports of optional hardware/DSP libs — never traceback if missing.
|
|
try:
|
|
import numpy as np
|
|
import sounddevice as sd
|
|
except Exception:
|
|
print("audio worker: numpy/sounddevice not available — skipping (optional)")
|
|
sys.exit(0)
|
|
|
|
# Resolve requested device (or None -> default). Bail gracefully if no input.
|
|
device = args.device
|
|
if device is not None:
|
|
try:
|
|
device = int(device)
|
|
except (TypeError, ValueError):
|
|
pass # leave as name substring; sounddevice resolves it
|
|
|
|
try:
|
|
dev_info = sd.query_devices(device, "input")
|
|
if dev_info.get("max_input_channels", 0) < 1:
|
|
raise ValueError("no input channels")
|
|
except Exception:
|
|
print("audio worker: numpy/sounddevice not available — skipping (optional)")
|
|
sys.exit(0)
|
|
|
|
from pythonosc.udp_client import SimpleUDPClient
|
|
|
|
SR = 44100
|
|
BLOCK = 1024
|
|
rate_hz = SR / BLOCK # ~43 Hz block rate
|
|
|
|
client = SimpleUDPClient(args.host, args.port)
|
|
|
|
# Precompute FFT helpers.
|
|
window = np.hanning(BLOCK).astype(np.float32)
|
|
freqs = np.fft.rfftfreq(BLOCK, d=1.0 / SR) # bin -> Hz
|
|
prev_mag = np.zeros(freqs.shape[0], dtype=np.float32)
|
|
|
|
print(f"[audio] emitting /gs/audio/... every {1.0 / rate_hz:.3f}s")
|
|
|
|
def send(addr, value):
|
|
try:
|
|
client.send_message(addr, float(value))
|
|
except Exception as exc:
|
|
# Local UDP send should never really fail; warn and keep going.
|
|
print(f"audio worker: OSC send failed ({exc})")
|
|
|
|
def process(block):
|
|
nonlocal prev_mag
|
|
x = block.astype(np.float32)
|
|
|
|
# RMS loudness of the raw block.
|
|
rms = float(np.sqrt(np.mean(x * x)))
|
|
|
|
# Windowed magnitude spectrum.
|
|
mag = np.abs(np.fft.rfft(x * window))
|
|
|
|
# Dominant frequency: argmax of magnitude spectrum -> Hz.
|
|
peak_bin = int(np.argmax(mag))
|
|
dom_freq = float(freqs[peak_bin])
|
|
|
|
# Spectral centroid (brightness): magnitude-weighted mean frequency.
|
|
mag_sum = float(mag.sum())
|
|
if mag_sum > 1e-9:
|
|
centroid = float(np.sum(freqs * mag) / mag_sum)
|
|
else:
|
|
centroid = 0.0
|
|
|
|
# Spectral flux: sum of positive changes vs previous frame.
|
|
diff = mag - prev_mag
|
|
flux = float(np.sum(diff[diff > 0]))
|
|
prev_mag = mag
|
|
|
|
send("/gs/audio/rms", rms)
|
|
send("/gs/audio/freq", dom_freq)
|
|
send("/gs/audio/centroid", centroid)
|
|
send("/gs/audio/flux", flux)
|
|
|
|
def callback(indata, frames, time_info, status):
|
|
# status may flag overflows; we ignore and keep streaming.
|
|
try:
|
|
# Mix down to mono if needed.
|
|
if indata.ndim > 1 and indata.shape[1] > 1:
|
|
mono = indata.mean(axis=1)
|
|
else:
|
|
mono = indata.reshape(-1)
|
|
process(mono)
|
|
except Exception as exc:
|
|
print(f"audio worker: block error ({exc})")
|
|
|
|
# Reconnect forever: if the stream drops (device unplugged, xrun storm),
|
|
# back off and try to reopen. Never exit on a transient error.
|
|
import time
|
|
backoff = 1.0
|
|
while True:
|
|
try:
|
|
with sd.InputStream(samplerate=SR, blocksize=BLOCK, channels=1,
|
|
dtype="float32", device=device, callback=callback):
|
|
backoff = 1.0 # healthy stream, reset backoff
|
|
while True:
|
|
sd.sleep(1000)
|
|
except KeyboardInterrupt:
|
|
break
|
|
except Exception as exc:
|
|
print(f"audio worker: stream error ({exc}); retrying in {backoff:.0f}s")
|
|
time.sleep(backoff)
|
|
backoff = min(backoff * 2, 30.0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|