"""Synthesizes a seamless 9.5 s fire-crackle loop (SL sound uploads max out at 10 s) for the fire pit. Layers: a low breathy roar (filtered noise with slow swells), soft ticks, and a few sharp pops/snaps with short resonant tails. Built circularly so the loop point is inaudible. Writes crackle.wav (44.1 kHz mono 16-bit). Run with local/hear-venv python (numpy). Claude Opus 5.5 (Tessel), 2026-10-10.""" import wave import numpy as np SR = 44100 DUR = 9.5 N = int(SR * DUR) rng = np.random.default_rng(23) def circ_lowpass(x, cutoff_hz): """Brick-ish lowpass in the frequency domain; circular, so the loop stays seamless.""" X = np.fft.rfft(x) f = np.fft.rfftfreq(len(x), 1 / SR) X *= 1 / (1 + (f / cutoff_hz) ** 4) return np.fft.irfft(X, len(x)) def circ_bandpass(x, lo, hi): X = np.fft.rfft(x) f = np.fft.rfftfreq(len(x), 1 / SR) X *= (1 / (1 + (lo / np.maximum(f, 1)) ** 4)) * (1 / (1 + (f / hi) ** 4)) return np.fft.irfft(X, len(x)) def add_circ(buf, sig, start): idx = (start + np.arange(len(sig))) % len(buf) np.add.at(buf, idx, sig) # roar: low rumble, swelling slowly (periodic envelope = whole cycles over the loop) roar = circ_lowpass(rng.standard_normal(N), 350) t = np.arange(N) / SR swell = 0.75 + 0.15 * np.sin(2 * np.pi * 2 * t / DUR) + 0.1 * np.sin(2 * np.pi * 5 * t / DUR + 1.3) roar *= swell roar /= np.abs(roar).max() hiss = circ_bandpass(rng.standard_normal(N), 2500, 9000) hiss /= np.abs(hiss).max() crack = np.zeros(N) def pop(amp, length_ms, bright): n = int(SR * length_ms / 1000) env = np.exp(-np.linspace(0, 7 + 6 * rng.random(), n)) s = rng.standard_normal(n) * env if bright < 1: s = np.convolve(s, np.ones(int(2 + 8 * (1 - bright))) / (2 + 8 * (1 - bright)), mode="same") # a tiny resonant body: wood splitting has a pitch f0 = rng.uniform(900, 3200) s += 0.35 * np.sin(2 * np.pi * f0 * np.arange(n) / SR) * env ** 2 return amp * s # soft ticks: many, clustered (fires crackle in bursts) pos = 0 while pos < N: pos += int(rng.exponential(0.09) * SR) burst = rng.random() < 0.25 for _ in range(rng.integers(2, 6) if burst else 1): add_circ(crack, pop(rng.uniform(0.05, 0.22), rng.uniform(3, 12), rng.uniform(0.3, 1)), pos % N) pos += int(rng.uniform(0.008, 0.04) * SR) # loud snaps: a handful for _ in range(9): add_circ(crack, pop(rng.uniform(0.45, 0.8), rng.uniform(15, 45), 1.0), int(rng.uniform(0, N))) mix = 0.32 * roar + 0.05 * hiss + crack mix /= np.abs(mix).max() mix *= 0.85 pcm = (mix * 32767).astype(np.int16) with wave.open("crackle.wav", "wb") as w: w.setnchannels(1) w.setsampwidth(2) w.setframerate(SR) w.writeframes(pcm.tobytes()) print(f"crackle.wav: {DUR}s, peak {np.abs(mix).max():.2f}")