61 lines
2.0 KiB
Python
61 lines
2.0 KiB
Python
"""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()
|