Files
ZenXercise/tools/sounds/make_cues.py
T

61 lines
2.0 KiB
Python
Raw Normal View History

"""Synthesises the Player's two cue sounds into assets/sounds/ (standard library only).
- stretching_cue.wav: a soft singing bowl, slow attack, long gentle decay.
- workout_cue.wav: a short, muted wooden "tok".
Both are kept quiet (peak about -12 dBFS) so they sit under the user's music.
"""
import math
import random
import struct
import wave
from pathlib import Path
RATE = 22050
def write(path: Path, samples: list[float]) -> None:
peak = max(abs(s) for s in samples) or 1.0
scale = 0.25 / peak # about -12 dBFS
with wave.open(str(path), "wb") as out:
out.setnchannels(1)
out.setsampwidth(2)
out.setframerate(RATE)
out.writeframes(b"".join(struct.pack("<h", int(s * scale * 32767)) for s in samples))
def singing_bowl(seconds: float = 2.6) -> list[float]:
# Inharmonic partials of a bowl, each fading at its own pace, with a slight beat.
partials = [(392.0, 1.0, 1.4), (393.2, 0.6, 1.5), (1062.0, 0.35, 0.8), (2010.0, 0.12, 0.45)]
samples = []
for n in range(int(seconds * RATE)):
t = n / RATE
attack = min(1.0, t / 0.04)
value = sum(a * math.exp(-t / decay) * math.sin(2 * math.pi * f * t) for f, a, decay in partials)
samples.append(attack * value)
return samples
def wood_tok(seconds: float = 0.12) -> list[float]:
rng = random.Random(7)
samples = []
for n in range(int(seconds * RATE)):
t = n / RATE
body = math.exp(-t / 0.018) * (math.sin(2 * math.pi * 820 * t) + 0.4 * math.sin(2 * math.pi * 1640 * t))
click = math.exp(-t / 0.002) * (rng.random() * 2 - 1) * 0.3
samples.append(body + click)
return samples
def main() -> None:
out = Path(__file__).resolve().parents[2] / "assets" / "sounds"
out.mkdir(parents=True, exist_ok=True)
write(out / "stretching_cue.wav", singing_bowl())
write(out / "workout_cue.wav", wood_tok())
for f in sorted(out.glob("*.wav")):
print(f"{f.name}: {f.stat().st_size / 1000:.0f} kB")
if __name__ == "__main__":
main()