mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
The audible autotest used one beep for eight steps, so steps couldn't be told apart. Each step now has its own self-made sound (synthesize.py): bell (map Baby_Cry_1), chime (F6 post, Buoy_Bell patch 88), wood block (F8), triangle (F10), low horn (F11 landing), click (pop rule), whistle (Digit5), shaker (Digit6), boing (honk rule). HUD: one compact box top-left, every line fits one row, live countdown; camera return after step 3.
219 lines
8.9 KiB
Python
219 lines
8.9 KiB
Python
"""Regenerate the audio content test mod's sounds (48 kHz mono PCM16, self-made: no game audio).
|
|
|
|
python synthesize.py
|
|
|
|
bed.wav: 20 s of soft filtered noise with a slow swell (replaces a zone bed), warn.wav: a short
|
|
falling tone (a speech take), siren.wav: a 3 s rising-falling tone (an added location-set bank),
|
|
fade.wav: 2 s of looping band-passed noise with a slow flutter (a mod crossfade bank's sample, played
|
|
by a declared layout). Every autotest step with something to hear has its own sound, clearly
|
|
different in pitch and character (the listener names the step by its sound):
|
|
bell.wav a deep church bell "bong" at 22.05 kHz (replaces Baby_Cry_1, the map emitter of
|
|
step 1: another rate and length than the retail samples, so the rebuilt sample
|
|
headers are exercised)
|
|
chime.wav a two-tone doorbell chime "ding-dong" (replaces Buoy_Bell, the bank the F6 post plays)
|
|
woodblock.wav a wood block "knock-knock" (the F8 one-shot next to the siren)
|
|
triangle.wav a high triangle "ting" with a long shimmer (replaces clock_bell, the F10 emitter)
|
|
horn.wav a low buzzy horn "honk" (the F11 landing beacon)
|
|
click.wav one short dry click (the pop_click rule on every pop)
|
|
whistle.wav a rising whistle chirp "wheet" (the Digit5 nose rule)
|
|
shaker.wav a shaker / rattle "chk-chk-chk" (replaces Boat_Horns, the Digit6 orbiting emitter)
|
|
boing.wav a cartoon spring "boing" (the honk_beep rule above honking cars)
|
|
dev.csi: a Csis project of the mod's own (doc 16 L4): the class c_dev_mod, the function f_dev_msg
|
|
and the global g_dev_level (default 7); the layout skate-audio's `Project::parse` reads.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import random
|
|
import struct
|
|
import wave
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).resolve().parent / "audio"
|
|
|
|
|
|
def write(name: str, samples: list[float], rate: int = 48000) -> None:
|
|
peak = max(abs(s) for s in samples) or 1.0
|
|
scale = min(0.8 / peak, 1.0)
|
|
pcm = b"".join(struct.pack("<h", int(max(-32767, min(32767, s * scale * 32767.0)))) for s in samples)
|
|
HERE.mkdir(parents=True, exist_ok=True)
|
|
with wave.open(str(HERE / name), "wb") as w:
|
|
w.setnchannels(1)
|
|
w.setsampwidth(2)
|
|
w.setframerate(rate)
|
|
w.writeframes(pcm)
|
|
|
|
|
|
def bed() -> list[float]:
|
|
rng = random.Random(0x5EED)
|
|
out, lp = [], 0.0
|
|
n = 48000 * 20
|
|
for i in range(n):
|
|
lp += 0.02 * (rng.uniform(-1, 1) - lp)
|
|
swell = 0.6 + 0.4 * math.sin(2 * math.pi * i / n)
|
|
out.append(lp * swell)
|
|
return out
|
|
|
|
|
|
def env(i: int, n: int, attack: float, decay: float, rate: int = 48000) -> float:
|
|
"""Linear attack, exponential decay (seconds to -60 dB), a 5 ms fade at the end (no click)."""
|
|
t = i / rate
|
|
a = min(1.0, t / attack) if attack > 0 else 1.0
|
|
return a * math.exp(-6.9 * t / decay) * min(1.0, (n - i) / (rate * 0.005))
|
|
|
|
|
|
def bell(rate: int) -> list[float]:
|
|
# Inharmonic church-bell partials over a 196 Hz strike note (hum, prime, tierce, quint, nominal).
|
|
n = int(rate * 1.6)
|
|
partials = [(0.5, 0.6, 1.6), (1.0, 1.0, 1.4), (1.2, 0.5, 1.0), (1.5, 0.35, 0.8), (2.0, 0.4, 0.7), (2.76, 0.25, 0.5), (5.4, 0.12, 0.25)]
|
|
return [sum(a * math.sin(2 * math.pi * 196 * r * i / rate) * env(i, n, 0.002, d * 1.0, rate) for r, a, d in partials) for i in range(n)]
|
|
|
|
|
|
def chime() -> list[float]:
|
|
# Two pure notes, high then low (E6, then C6), each a sine with a soft octave.
|
|
out = []
|
|
for f in (1318.5, 1046.5):
|
|
n = int(48000 * 0.55)
|
|
out += [(math.sin(2 * math.pi * f * i / 48000) + 0.2 * math.sin(4 * math.pi * f * i / 48000)) * env(i, n, 0.003, 0.9) for i in range(n)]
|
|
return out
|
|
|
|
|
|
def woodblock() -> list[float]:
|
|
# Two hollow knocks: a fast-decaying 900 Hz / 2.3 kHz body with a short noise attack.
|
|
rng = random.Random(0xB10C)
|
|
one = int(48000 * 0.18)
|
|
knock = [(math.sin(2 * math.pi * 900 * i / 48000) + 0.5 * math.sin(2 * math.pi * 2300 * i / 48000)) * env(i, one, 0.0005, 0.07)
|
|
+ rng.uniform(-1, 1) * math.exp(-i / 48.0) * 0.6 for i in range(one)]
|
|
return knock + [0.0] * int(48000 * 0.04) + knock
|
|
|
|
|
|
def triangle() -> list[float]:
|
|
# A struck steel triangle: high inharmonic partials, a slow shimmer, 1.5 s ring.
|
|
n = int(48000 * 1.5)
|
|
partials = [(3100, 1.0), (4650, 0.6), (6900, 0.45), (8400, 0.3)]
|
|
return [sum(a * math.sin(2 * math.pi * f * i / 48000) for f, a in partials) * (0.85 + 0.15 * math.sin(2 * math.pi * 5 * i / 48000)) * env(i, n, 0.001, 1.4) for i in range(n)]
|
|
|
|
|
|
def horn() -> list[float]:
|
|
# A low buzzy horn: a band-limited sawtooth at 98 Hz (G2), flat while held, 0.8 s.
|
|
n, phase, out = int(48000 * 0.8), 0.0, []
|
|
for i in range(n):
|
|
phase += 2 * math.pi * 98 * (1 + 0.004 * math.sin(2 * math.pi * 5 * i / 48000)) / 48000
|
|
a = min(1.0, i / 1440) * min(1.0, (n - i) / 4800)
|
|
out.append(sum(math.sin(k * phase) / k for k in range(1, 16)) * a)
|
|
return out
|
|
|
|
|
|
def click() -> list[float]:
|
|
# One dry click: a 2 ms noise tick through a crude high-pass, 25 ms long.
|
|
rng = random.Random(0xC11C)
|
|
n, prev, out = int(48000 * 0.025), 0.0, []
|
|
for i in range(n):
|
|
x = rng.uniform(-1, 1) * math.exp(-i / 96.0)
|
|
out.append(x - prev)
|
|
prev = x
|
|
return out
|
|
|
|
|
|
def whistle() -> list[float]:
|
|
# A rising whistle chirp, 1.2 -> 2.8 kHz in 0.35 s, a little breath noise.
|
|
rng = random.Random(0x3157)
|
|
n, phase, out = int(48000 * 0.35), 0.0, []
|
|
for i in range(n):
|
|
t = i / n
|
|
phase += 2 * math.pi * (1200 + 1600 * t * t) / 48000
|
|
a = math.sin(math.pi * t) ** 0.5
|
|
out.append((math.sin(phase) + 0.05 * rng.uniform(-1, 1)) * a)
|
|
return out
|
|
|
|
|
|
def shaker() -> list[float]:
|
|
# Three shakes of bright noise (a rattle), 0.15 s apart.
|
|
rng = random.Random(0x5A4E)
|
|
out, prev = [], 0.0
|
|
for shake in range(3):
|
|
n = int(48000 * 0.15)
|
|
for i in range(n):
|
|
x = rng.uniform(-1, 1)
|
|
hp = x - prev
|
|
prev = x
|
|
out.append(hp * min(1.0, i / 480) * math.exp(-i / 2400.0))
|
|
return out
|
|
|
|
|
|
def boing() -> list[float]:
|
|
# A cartoon spring: a wobbling tone falling 420 -> 140 Hz, 0.6 s.
|
|
n, phase, out = int(48000 * 0.6), 0.0, []
|
|
for i in range(n):
|
|
t = i / 48000
|
|
f = (140 + 280 * math.exp(-5 * t)) * (1 + 0.12 * math.sin(2 * math.pi * 14 * t) * math.exp(-3 * t))
|
|
phase += 2 * math.pi * f / 48000
|
|
out.append((math.sin(phase) + 0.3 * math.sin(3 * phase)) * env(i, n, 0.003, 0.9))
|
|
return out
|
|
|
|
|
|
def warn() -> list[float]:
|
|
n = int(48000 * 0.6)
|
|
return [math.sin(2 * math.pi * (500 - 200 * i / n) * i / 48000) * math.exp(-3 * i / n) for i in range(n)]
|
|
|
|
|
|
def siren() -> list[float]:
|
|
n, phase, out = 48000 * 3, 0.0, []
|
|
for i in range(n):
|
|
f = 700 + 400 * math.sin(math.pi * i / n)
|
|
phase += 2 * math.pi * f / 48000
|
|
out.append(math.sin(phase) * math.sin(math.pi * i / n))
|
|
return out
|
|
|
|
|
|
def fade() -> list[float]:
|
|
rng = random.Random(0xFADE)
|
|
n, lp, hp, out = 48000 * 2, 0.0, 0.0, []
|
|
for i in range(n):
|
|
lp += 0.08 * (rng.uniform(-1, 1) - lp)
|
|
hp += 0.01 * (lp - hp)
|
|
# A whole number of flutter cycles, so the loop is seamless.
|
|
out.append((lp - hp) * (0.7 + 0.3 * math.sin(2 * math.pi * 4 * i / n)))
|
|
return out
|
|
|
|
|
|
def csi(path: Path) -> None:
|
|
"""A MOIR project: header 0x28 (counts at 0x0A / 0x0C / 0x0E, id at 0x10, big-endian), the
|
|
function and class records (12 bytes: name offset at +4, name id at +8), the global records (16
|
|
bytes: default at +4, name offset at +8, name id at +12), then the names."""
|
|
tables = [[("f_dev_msg", 1, 0)], [("c_dev_mod", 1, 0)], [("g_dev_level", 1, 7)]]
|
|
records = sum(len(t) * (16 if i == 2 else 12) for i, t in enumerate(tables))
|
|
head = bytearray(0x28)
|
|
head[0:4] = b"MOIR"
|
|
for i, at in enumerate((0x0A, 0x0C, 0x0E)):
|
|
head[at:at + 2] = struct.pack(">H", len(tables[i]))
|
|
head[0x10:0x12] = struct.pack(">H", 0x4445)
|
|
rec, names = bytearray(), bytearray()
|
|
for i, table in enumerate(tables):
|
|
for name, name_id, default in table:
|
|
off = 0x28 + records + len(names)
|
|
names += name.encode() + b"\0"
|
|
if i == 2:
|
|
rec += b"\0" * 4 + struct.pack(">iIH", default, off, name_id) + b"\0" * 2
|
|
else:
|
|
rec += b"\0" * 4 + struct.pack(">IH", off, name_id) + b"\0" * 2
|
|
path.write_bytes(bytes(head + rec + names))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
write("bed.wav", bed())
|
|
write("bell.wav", bell(22050), 22050)
|
|
write("chime.wav", chime())
|
|
write("woodblock.wav", woodblock())
|
|
write("triangle.wav", triangle())
|
|
write("horn.wav", horn())
|
|
write("click.wav", click())
|
|
write("whistle.wav", whistle())
|
|
write("shaker.wav", shaker())
|
|
write("boing.wav", boing())
|
|
write("warn.wav", warn())
|
|
write("siren.wav", siren())
|
|
write("fade.wav", fade())
|
|
csi(HERE / "dev.csi")
|
|
print("wrote", sorted(p.name for p in HERE.glob("*.wav")), "dev.csi")
|