84 lines
2.4 KiB
Python
84 lines
2.4 KiB
Python
"""Generate synthetic mechanical keyboard switch samples."""
|
|
|
|
import math
|
|
import struct
|
|
import wave
|
|
from pathlib import Path
|
|
|
|
SAMPLE_RATE = 48000
|
|
OUT_DIR = Path(__file__).resolve().parent.parent / "assets" / "samples"
|
|
|
|
|
|
def write_wav(path: Path, samples: list[float]) -> None:
|
|
pcm = b"".join(
|
|
struct.pack("<h", max(-32768, min(32767, int(s * 32767)))) for s in samples
|
|
)
|
|
with wave.open(str(path), "w") as wf:
|
|
wf.setnchannels(1)
|
|
wf.setsampwidth(2)
|
|
wf.setframerate(SAMPLE_RATE)
|
|
wf.writeframes(pcm)
|
|
|
|
|
|
def envelope(length: int, attack: float, decay: float) -> list[float]:
|
|
env = []
|
|
attack_samples = int(attack * SAMPLE_RATE)
|
|
decay_samples = int(decay * SAMPLE_RATE)
|
|
for i in range(length):
|
|
if i < attack_samples:
|
|
env.append(i / max(attack_samples, 1))
|
|
elif i < attack_samples + decay_samples:
|
|
t = (i - attack_samples) / max(decay_samples, 1)
|
|
env.append(1.0 - t)
|
|
else:
|
|
env.append(0.0)
|
|
return env
|
|
|
|
|
|
def generate_click(
|
|
freq: float,
|
|
duration: float,
|
|
attack: float,
|
|
decay: float,
|
|
noise_mix: float = 0.0,
|
|
click_burst: bool = False,
|
|
) -> list[float]:
|
|
length = int(duration * SAMPLE_RATE)
|
|
env = envelope(length, attack, decay)
|
|
out = []
|
|
for i in range(length):
|
|
t = i / SAMPLE_RATE
|
|
tone = math.sin(2 * math.pi * freq * t) * (0.7 if not click_burst else 0.4)
|
|
if click_burst and t < 0.008:
|
|
tone += math.sin(2 * math.pi * (freq * 2.8) * t) * 0.5
|
|
noise = 0.0
|
|
if noise_mix > 0:
|
|
noise = (((i * 1103515245 + 12345) & 0x7FFFFFFF) / 0x7FFFFFFF - 0.5) * noise_mix
|
|
out.append((tone + noise) * env[i])
|
|
peak = max(abs(s) for s in out) or 1.0
|
|
return [s / peak * 0.85 for s in out]
|
|
|
|
|
|
def main() -> None:
|
|
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
profiles = {
|
|
"cherry-mx-blue.wav": generate_click(
|
|
2800, 0.09, 0.001, 0.085, noise_mix=0.15, click_burst=True
|
|
),
|
|
"cherry-mx-red.wav": generate_click(
|
|
1200, 0.05, 0.002, 0.045, noise_mix=0.08
|
|
),
|
|
"cherry-mx-brown.wav": generate_click(
|
|
1800, 0.07, 0.002, 0.065, noise_mix=0.12, click_burst=True
|
|
),
|
|
}
|
|
|
|
for name, samples in profiles.items():
|
|
write_wav(OUT_DIR / name, samples)
|
|
print(f"Wrote {OUT_DIR / name}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|