feat: add Paw Play, a cat-friendly Android touch toy
This commit is contained in:
Executable
+116
@@ -0,0 +1,116 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user