You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
80 lines
2.3 KiB
80 lines
2.3 KiB
#!/usr/bin/env python3 |
|
from __future__ import annotations |
|
|
|
import math |
|
import wave |
|
from pathlib import Path |
|
|
|
SAMPLE_RATE = 44100 |
|
|
|
PROFILES = { |
|
"confirm": { |
|
"amplitude": 0.28, |
|
"segments": [ |
|
(80, [660, 990]), |
|
(35, []), |
|
(95, [880, 1320]), |
|
(35, []), |
|
(130, [1174, 1568]), |
|
], |
|
}, |
|
"block": { |
|
"amplitude": 0.42, |
|
"segments": [ |
|
(120, [392, 587]), |
|
(36, []), |
|
(130, [294, 440]), |
|
(36, []), |
|
(190, [196, 294]), |
|
], |
|
}, |
|
"refine": { |
|
"amplitude": 0.30, |
|
"segments": [ |
|
(95, [587, 784]), |
|
(32, []), |
|
(95, [659, 880]), |
|
(32, []), |
|
(95, [587, 784]), |
|
], |
|
}, |
|
} |
|
|
|
|
|
def synth_segment(duration_ms: int, freqs: list[int], amplitude: float) -> list[int]: |
|
samples = round((duration_ms / 1000) * SAMPLE_RATE) |
|
data: list[int] = [] |
|
for i in range(samples): |
|
sample = 0.0 |
|
if freqs: |
|
t = i / SAMPLE_RATE |
|
attack = min(1.0, i / max(1, int(samples * 0.1))) |
|
release = min(1.0, (samples - i) / max(1, int(samples * 0.12))) |
|
envelope = min(attack, release) |
|
sample = sum(math.sin(2 * math.pi * freq * t) for freq in freqs) / len(freqs) |
|
sample *= amplitude * envelope |
|
data.append(int(max(-1.0, min(1.0, sample)) * 32767)) |
|
return data |
|
|
|
|
|
def write_wave(path: Path, profile: dict[str, object]) -> None: |
|
amplitude = float(profile["amplitude"]) |
|
segments = profile["segments"] |
|
pcm: list[int] = [] |
|
for duration_ms, freqs in segments: # type: ignore[misc] |
|
pcm.extend(synth_segment(duration_ms, list(freqs), amplitude)) |
|
|
|
path.parent.mkdir(parents=True, exist_ok=True) |
|
with wave.open(str(path), "wb") as wav: |
|
wav.setnchannels(1) |
|
wav.setsampwidth(2) |
|
wav.setframerate(SAMPLE_RATE) |
|
wav.writeframes(b"".join(sample.to_bytes(2, "little", signed=True) for sample in pcm)) |
|
|
|
|
|
if __name__ == "__main__": |
|
root = Path(__file__).resolve().parent.parent |
|
assets = root / "assets" |
|
for name, profile in PROFILES.items(): |
|
write_wave(assets / f"{name}.wav", profile) |
|
print(f"wrote {assets / f'{name}.wav'}")
|
|
|