#!/usr/bin/env python3 """Synthesize insert-tech UI sounds for Sprint 7 #440. Each sound uses a DIFFERENT synthesis technique to ensure distinct character: - cursor_hover: impulse → resonant bandpass (digital click) - weapon_aim: filtered noise + sub thump (mechanical) - monologue_chime: FM synthesis (crystalline bell) - monologue_chime_urgent: FM synthesis + beating/dissonance (tense bell) """ import numpy as np import wave import os SR = 44100 OUTPUT_DIR = os.path.join(os.path.dirname(__file__), "..", "client", "assets", "audio") def write_wav(filename, samples, channels=1): """Write float samples [-1, 1] to 16-bit WAV.""" path = os.path.join(OUTPUT_DIR, filename) # Normalize to peak if it exceeds 1.0 peak = np.max(np.abs(samples)) if peak > 1.0: samples = samples / peak int_samples = np.clip(samples * 32767, -32767, 32767).astype(np.int16) with wave.open(path, "w") as f: f.setnchannels(channels) f.setsampwidth(2) f.setframerate(SR) f.writeframes(int_samples.tobytes()) dur_ms = len(int_samples) / SR * 1000 print(f" wrote {path} ({dur_ms:.0f}ms, {channels}ch)") return path def simple_lowpass(signal, cutoff_hz, sr=SR): """Single-pole IIR low-pass filter.""" rc = 1.0 / (2 * np.pi * cutoff_hz) dt = 1.0 / sr alpha = dt / (rc + dt) out = np.zeros_like(signal) out[0] = alpha * signal[0] for i in range(1, len(signal)): out[i] = out[i - 1] + alpha * (signal[i] - out[i - 1]) return out def simple_highpass(signal, cutoff_hz, sr=SR): """Single-pole IIR high-pass filter.""" rc = 1.0 / (2 * np.pi * cutoff_hz) dt = 1.0 / sr alpha = rc / (rc + dt) out = np.zeros_like(signal) out[0] = signal[0] for i in range(1, len(signal)): out[i] = alpha * (out[i - 1] + signal[i] - signal[i - 1]) return out def cursor_hover(): """UI-001: Digital click/tick on entity hover. Technique: Short noise impulse bandpass-filtered to ~4kHz. Sounds like a tiny digital snap — no sustained tone at all. The "click" of a selection appearing on a HUD. """ duration = 0.035 # 35ms — shorter than before n = int(SR * duration) t = np.linspace(0, duration, n, endpoint=False) rng = np.random.default_rng(77) # White noise impulse impulse = rng.uniform(-1, 1, n) # Bandpass around 4kHz: high-pass at 3kHz, low-pass at 5kHz filtered = simple_highpass(impulse, 3000) filtered = simple_lowpass(filtered, 5500) # Very fast decay — done in 25ms envelope = np.exp(-t * 140) return write_wav("cursor_hover.wav", filtered * envelope * 0.3) def weapon_aim(): """UI-004: Mechanical latch for weapon aim. Technique: Low-pass filtered noise burst (the clack) + sub-bass thump (the weight). No musical pitch — this is a MECHANICAL sound. Think: safety clicking off, bolt sliding home. """ duration = 0.15 # 150ms n = int(SR * duration) t = np.linspace(0, duration, n, endpoint=False) rng = np.random.default_rng(42) # Component 1: Low-pass noise burst — the metallic clack noise = rng.uniform(-1, 1, n) # Low-pass at 1.5kHz — dull, heavy impact, not bright clack = simple_lowpass(noise, 1500) clack_env = np.exp(-t * 60) # fast decay clack = clack * clack_env # Component 2: Sub-bass thump — weight of the mechanism # 80Hz sine, very fast decay thump = np.sin(2 * np.pi * 80 * t) thump_env = np.exp(-t * 40) thump = thump * thump_env # Component 3: High metallic click at the very start (2ms) click = rng.uniform(-1, 1, n) click = simple_highpass(click, 4000) click_env = np.zeros(n) click_mask = t < 0.003 click_env[click_mask] = np.exp(-t[click_mask] * 800) click = click * click_env signal = clack * 0.4 + thump * 0.35 + click * 0.25 return write_wav("weapon_aim.wav", signal * 0.45) def monologue_chime(): """UI-005: Crystalline thought-chime. PLACEHOLDER. Technique: FM synthesis — carrier modulated by a lower frequency creates rich, evolving harmonics that sound like struck glass or crystal. Fundamentally different timbre from additive sine waves. """ duration = 0.75 # 750ms n = int(SR * duration) t = np.linspace(0, duration, n, endpoint=False) # FM synthesis: carrier at 1200Hz, modulator at 420Hz (ratio ~2.86:1) # Inharmonic ratio = bell-like quality f_carrier = 1200 f_mod = 420 # Mod index decays over time — bright attack, mellow sustain mod_index = 3.0 * np.exp(-t * 6) # Modulator signal modulator = mod_index * np.sin(2 * np.pi * f_mod * t) # Carrier with FM signal = np.sin(2 * np.pi * f_carrier * t + modulator) # Gentle attack (20ms), slow decay attack = np.minimum(t / 0.02, 1.0) decay = np.exp(-t * 3.0) envelope = attack * decay return write_wav("sfx_monologue_chime.wav", signal * envelope * 0.22) def monologue_chime_urgent(): """UI-006: Urgent thought-chime. PLACEHOLDER. Technique: FM synthesis with higher mod index (brighter/harsher) + a second detuned carrier that creates beating/tension. The beating is what makes it feel "urgent" — not louder, but unsettled. """ duration = 0.5 # 500ms — shorter n = int(SR * duration) t = np.linspace(0, duration, n, endpoint=False) f_carrier = 1200 f_mod = 420 # Higher mod index = more sidebands = brighter, more aggressive mod_index = 5.0 * np.exp(-t * 7) modulator = mod_index * np.sin(2 * np.pi * f_mod * t) # Primary carrier carrier1 = np.sin(2 * np.pi * f_carrier * t + modulator) # Second carrier, detuned +8Hz — creates beating at 8Hz (nervous flicker) carrier2 = np.sin(2 * np.pi * (f_carrier + 8) * t + modulator * 0.8) signal = carrier1 * 0.6 + carrier2 * 0.4 # Sharp attack (6ms), faster decay attack = np.minimum(t / 0.006, 1.0) decay = np.exp(-t * 4.0) envelope = attack * decay # 15% louder than normal return write_wav("sfx_monologue_chime_urgent.wav", signal * envelope * 0.25) if __name__ == "__main__": os.makedirs(OUTPUT_DIR, exist_ok=True) print("Synthesizing UI sounds for Sprint 7 #440...") print() print("[UI-001] cursor_hover — bandpass noise impulse (digital click)") cursor_hover() print("[UI-004] weapon_aim — filtered noise + sub thump (mechanical latch)") weapon_aim() print("[UI-005] sfx_monologue_chime — FM synthesis bell (PLACEHOLDER)") monologue_chime() print("[UI-006] sfx_monologue_chime_urgent — FM + beating (PLACEHOLDER)") monologue_chime_urgent() print() print("Done. Convert with: db/connectors/audio-post convert ")