Files
pawplay/tools/generate_sounds.py

117 lines
3.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate the four tiny, gentle WAV effects used by Paw Play.
The sounds are intentionally soft, short and free of sharp transients so they
do not startle a cat. 16-bit mono / 44.1 kHz keeps the APK small.
"""
from __future__ import annotations
import math
import random
import struct
import wave
from pathlib import Path
RATE = 44100
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "app" / "res" / "raw"
def write_wav(name: str, samples: list[float], peak: float = 0.45) -> None:
OUT.mkdir(parents=True, exist_ok=True)
high = max(1e-9, max(abs(s) for s in samples))
scale = peak / high
data = b"".join(
struct.pack("<h", int(max(-1.0, min(1.0, s * scale)) * 32767))
for s in samples
)
with wave.open(str(OUT / name), "wb") as fh:
fh.setnchannels(1)
fh.setsampwidth(2)
fh.setframerate(RATE)
fh.writeframes(data)
def envelope(i: int, n: int, attack: float = 0.01, release: float = 0.08) -> float:
t = i / RATE
total = n / RATE
a = min(1.0, t / attack) if attack > 0 else 1.0
r = min(1.0, max(0.0, (total - t) / release)) if release > 0 else 1.0
return a * r
def plop() -> list[float]:
n = int(0.30 * RATE)
out: list[float] = []
phase = 0.0
for i in range(n):
t = i / RATE
# Falling "bloop" with a tiny watery second partial.
freq = 250 + 620 * math.exp(-t * 18)
phase += 2 * math.pi * freq / RATE
body = math.sin(phase) * math.exp(-t * 9)
water = math.sin(phase * 2.01) * 0.16 * math.exp(-t * 13)
out.append((body + water) * envelope(i, n, 0.006, 0.07))
return out
def chime() -> list[float]:
n = int(0.95 * RATE)
notes = [(1046.50, 0.00), (1318.51, 0.075), (1567.98, 0.15)]
out = [0.0] * n
for freq, delay in notes:
start = int(delay * RATE)
for i in range(start, n):
t = (i - start) / RATE
env = math.exp(-t * 5.4) * min(1.0, t / 0.006)
tone = (
math.sin(2 * math.pi * freq * t)
+ 0.28 * math.sin(2 * math.pi * freq * 2.0 * t)
+ 0.08 * math.sin(2 * math.pi * freq * 3.01 * t)
)
out[i] += tone * env * 0.33
return [s * envelope(i, n, 0.003, 0.18) for i, s in enumerate(out)]
def whoosh() -> list[float]:
n = int(0.40 * RATE)
rng = random.Random(7)
out: list[float] = []
low = 0.0
band = 0.0
for i in range(n):
t = i / RATE
white = rng.uniform(-1.0, 1.0)
low += (white - low) * 0.055
band += (white - band) * 0.025
# Airy noise that opens and closes quickly.
env = math.sin(math.pi * min(1.0, t / 0.36)) ** 1.6
out.append((low - band * 0.55) * env * 0.9)
return out
def ping() -> list[float]:
n = int(0.24 * RATE)
out: list[float] = []
for i in range(n):
t = i / RATE
env = math.exp(-t * 22) * min(1.0, t / 0.003)
tone = (
math.sin(2 * math.pi * 1700 * t)
+ 0.22 * math.sin(2 * math.pi * 2380 * t)
)
out.append(tone * env * 0.5)
return out
def main() -> None:
write_wav("plop.wav", plop(), peak=0.42)
write_wav("chime.wav", chime(), peak=0.34)
write_wav("whoosh.wav", whoosh(), peak=0.25)
write_wav("ping.wav", ping(), peak=0.30)
print("wrote 4 sounds to", OUT)
if __name__ == "__main__":
main()