#!/usr/bin/env python3 """Generate Petal's cute UI sound palette as small WAV assets. These are warm, rounded little sounds — bubble pops, water drops, soft bells — played when suggestions and companion bubbles appear. We synthesize them here (rather than ship someone else's clips) so they match the app's gentle aesthetic exactly and stay tiny. Output lands in web/src/assets/sounds/ where Vite bundles and fingerprints them into dist (and so into the embedded Go binary). Run: python3 scripts/gen_sounds.py """ import math import os import struct import wave SR = 44100 OUT = os.path.join(os.path.dirname(__file__), "..", "web", "src", "assets", "sounds") def env(n, attack, decay, total): """Smooth attack + exponential decay envelope over `total` seconds.""" a = max(1, int(attack * SR)) out = [] for i in range(n): t = i / SR if i < a: amp = i / a else: amp = math.exp(-(t - attack) / decay) out.append(amp) return out def sine(freq_at, n): """Sine with a per-sample frequency function freq_at(t)->Hz (phase-accurate).""" out = [] phase = 0.0 for i in range(n): t = i / SR f = freq_at(t) phase += 2 * math.pi * f / SR out.append(math.sin(phase)) return out def mix(*layers): n = max(len(l) for l in layers) out = [0.0] * n for l in layers: for i, v in enumerate(l): out[i] += v return out def soft_clip(s): return [math.tanh(v * 1.2) for v in s] def normalize(s, peak=0.9): m = max(1e-9, max(abs(v) for v in s)) g = peak / m return [v * g for v in s] def write_wav(name, samples): samples = soft_clip(samples) samples = normalize(samples, 0.85) # 4ms fade-out tail so nothing clicks at the end. tail = int(0.004 * SR) for i in range(tail): samples[-1 - i] *= i / tail os.makedirs(OUT, exist_ok=True) path = os.path.join(OUT, name) with wave.open(path, "w") as w: w.setnchannels(1) w.setsampwidth(2) w.setframerate(SR) frames = b"".join(struct.pack("