117 lines
3.4 KiB
Python
Executable File
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()
|