1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
"""Regenerate Keyboard Foley's original sound samples with the standard library."""
import array
import math
from pathlib import Path
import random
import sys
import wave
RATE = 48000
ROOT = Path(__file__).resolve().parents[1] / "samples"
THEMES = ("mechanical", "typewriter", "soft-pop")
KINDS = ("key-1", "key-2", "key-3", "space", "enter", "backspace")
def synth(theme, kind):
rng = random.Random(f"keyboard-foley-v1/{theme}/{kind}")
pitch = {"key-1": 1, "key-2": 1.06, "key-3": 0.94,
"space": 0.68, "enter": 0.82, "backspace": 1.18}[kind]
duration = {"mechanical": 0.095, "typewriter": 0.14, "soft-pop": 0.10}[theme]
if kind in ("space", "enter"):
duration += 0.025
out = []
low, previous, phase = 0.0, 0.0, 0.0
for i in range(round(duration * RATE)):
t = i / RATE
noise = rng.uniform(-1, 1)
low += 0.16 * (noise - low)
high = noise - previous
previous = noise
if theme == "mechanical":
value = 0.65 * high * math.exp(-t / 0.0018)
value += 0.48 * low * math.exp(-t / 0.008)
value += 0.38 * math.sin(2 * math.pi * 240 * pitch * t) * math.exp(-t / 0.013)
value += 0.14 * math.sin(2 * math.pi * 1450 * pitch * t) * math.exp(-t / 0.004)
if t > 0.021:
value += 0.17 * high * math.exp(-(t - 0.021) / 0.0015)
elif theme == "typewriter":
value = 0.75 * high * math.exp(-t / 0.003)
for freq, amp, decay in ((740, 0.3, 0.018), (1730, 0.21, 0.012),
(3270, 0.12, 0.007)):
value += amp * math.sin(2 * math.pi * freq * pitch * t) * math.exp(-t / decay)
value += 0.4 * low * math.exp(-t / 0.014)
if t > 0.032:
tail = t - 0.032
value += (0.25 * high + 0.18 * math.sin(2 * math.pi * 380 * pitch * tail)) * math.exp(-tail / 0.007)
else:
freq = (155 + 620 * math.exp(-t / 0.009)) * pitch
phase += 2 * math.pi * freq / RATE
value = math.sin(phase) * math.exp(-t / 0.014)
value += 0.10 * low * math.exp(-t / 0.005)
out.append(value)
mean = sum(out) / len(out)
attack = round(RATE * (0.0015 if theme == "soft-pop" else 0.0005))
release = round(RATE * 0.008)
for i, value in enumerate(out):
fade_in = min(1, i / attack)
fade_out = min(1, (len(out) - 1 - i) / release)
out[i] = (value - mean) * fade_in * fade_out
peak = max(abs(value) for value in out)
scale = 0.28 * 32767 / peak
pcm = array.array("h", (round(value * scale) for value in out))
if sys.byteorder != "little":
pcm.byteswap()
return pcm
def main():
if sys.argv[1:] not in ([], ["--check"]):
raise SystemExit("usage: synth.py [--check]")
check = sys.argv[1:] == ["--check"]
signatures = set()
for theme in THEMES:
folder = ROOT / theme
if not check:
folder.mkdir(parents=True, exist_ok=True)
for kind in KINDS:
pcm = synth(theme, kind)
path = folder / f"{kind}.wav"
if check:
with wave.open(str(path), "rb") as source:
assert (source.getnchannels(), source.getsampwidth(), source.getframerate()) == (1, 2, RATE), path
raw = source.readframes(source.getnframes())
assert raw == pcm.tobytes(), f"sample differs from synthesis: {path}"
assert raw[:2] == raw[-2:] == b"\x00\x00", f"nonzero boundary: {path}"
assert raw not in signatures, f"duplicate sample: {path}"
signatures.add(raw)
assert 0.005 < math.sqrt(sum(x * x for x in pcm) / len(pcm)) / 32767 < 0.2, path
else:
with wave.open(str(path), "wb") as output:
output.setnchannels(1)
output.setsampwidth(2)
output.setframerate(RATE)
output.writeframes(pcm.tobytes())
print(f"{theme}/{kind}.wav {len(pcm) / RATE:.3f} s")
if check:
print("PASS: 18 distinct, reproducible PCM samples with tapered boundaries")
if __name__ == "__main__":
main()