#!/usr/bin/env python3 # /// script # requires-python = ">=3.10" # dependencies = ["numpy"] # /// """Synthesize the UI sound kit with numpy (no licensing questions). python3 sfx.py writes tap.wav, key.wav, toggle.wav, whoosh.wav, success.wav, pop.wav Each sound is short and quiet by design; mux.py lines up each file's measured peak with the time the page declared in window.SFX. """ import sys import wave from pathlib import Path import numpy as np SR = 48000 def env(n: int, attack: float, decay: float) -> np.ndarray: t = np.arange(n) / SR return np.minimum(1, t / max(attack, 1e-4)) * np.exp(-t / decay) def tone(freq: float, dur: float, attack=0.002, decay=0.05) -> np.ndarray: n = int(dur * SR) t = np.arange(n) / SR return np.sin(2 * np.pi * freq * t) * env(n, attack, decay) def noise(dur: float, lo: float, hi: float, seed: int = 1) -> np.ndarray: rng = np.random.default_rng(seed) n = int(dur * SR) spec = np.fft.rfft(rng.standard_normal(n)) f = np.fft.rfftfreq(n, 1 / SR) spec[(f < lo) | (f > hi)] = 0 x = np.fft.irfft(spec, n) return x / (np.abs(x).max() + 1e-9) def mixdown(*parts: np.ndarray) -> np.ndarray: """Sum layers of different lengths, padding the shorter ones with silence.""" out = np.zeros(max(len(p) for p in parts)) for p in parts: out[: len(p)] += p return out def kit() -> dict[str, np.ndarray]: tap = mixdown(0.55 * noise(0.03, 2500, 7000, 2) * env(int(0.03 * SR), 0.0005, 0.006), 0.5 * tone(1900, 0.06, decay=0.012)) key = mixdown(0.4 * noise(0.025, 1800, 5000, 3) * env(int(0.025 * SR), 0.0005, 0.005), 0.45 * tone(1300, 0.05, decay=0.01)) toggle = mixdown(0.6 * tone(880, 0.08, decay=0.018), 0.4 * tone(1320, 0.08, decay=0.014)) n = int(0.35 * SR) sweep = noise(0.35, 300, 4000, 4) * np.sin(np.linspace(0, np.pi, n)) ** 2 whoosh = 0.35 * sweep succ = np.zeros(int(0.6 * SR)) for k, (f, dt) in enumerate([(1046.5, 0.0), (1568.0, 0.09)]): s = tone(f, 0.5, attack=0.003, decay=0.16) + 0.25 * tone(f * 2, 0.5, attack=0.003, decay=0.08) i = int(dt * SR) succ[i:i + len(s)] += 0.4 * s[: len(succ) - i] pop = 0.6 * tone(620, 0.09, attack=0.001, decay=0.02) * np.linspace(1.25, 0.8, int(0.09 * SR)) return {"tap": tap, "key": key, "toggle": toggle, "whoosh": whoosh, "success": succ, "pop": pop} def write(path: Path, x: np.ndarray) -> None: x = np.clip(x / (np.abs(x).max() + 1e-9) * 0.5, -1, 1) with wave.open(str(path), "wb") as w: w.setnchannels(1) w.setsampwidth(2) w.setframerate(SR) w.writeframes((x * 32767).astype(" 1 and sys.argv[1].startswith("-"): sys.exit(__doc__) # an option such as --help is never an output folder out = Path(sys.argv[1] if len(sys.argv) > 1 else "sfx") out.mkdir(parents=True, exist_ok=True) for name, x in kit().items(): write(out / f"{name}.wav", x) print(f"sfx kit -> {out}")