#!/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()