Godstrument/workers/audio_worker.py
monsterrobotparty 8deb65b4fe Godstrument: a multi-modal sensor-fusion instrument
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>
2026-07-05 02:13:05 +10:00

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()