-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmake-sounds.py
More file actions
91 lines (69 loc) · 2.91 KB
/
Copy pathmake-sounds.py
File metadata and controls
91 lines (69 loc) · 2.91 KB
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
#!/usr/bin/env python3
"""Generate the sound set: 8-bit mono square waves, the way a cabinet did it.
Run from the repo root to regenerate sounds/: python3 tools/make-sounds.py
Everything here is deliberately crude. 22050Hz, 8-bit unsigned, hard square
waves with a 2ms fade at each end so a blip stops without a click of its own.
The files are committed, so nobody has to run this to play the game.
"""
import math
import os
import random
import struct
import wave
RATE = 22050
FADE = int(RATE * 0.002)
def square(freq_from, freq_to, ms, volume=0.5, duty=0.5):
"""A square wave sweeping between two frequencies."""
n = int(RATE * ms / 1000)
out = []
phase = 0.0
for i in range(n):
f = freq_from + (freq_to - freq_from) * (i / max(1, n - 1))
phase += f / RATE
out.append(volume if (phase % 1.0) < duty else -volume)
return out
def noise(ms, volume=0.5, decay=True):
n = int(RATE * ms / 1000)
rng = random.Random(1983) # fixed, so a rebuild produces the same file
return [rng.uniform(-volume, volume) * ((1 - i / n) if decay else 1) for i in range(n)]
def silence(ms):
return [0.0] * int(RATE * ms / 1000)
def envelope(samples):
n = len(samples)
for i in range(min(FADE, n)):
samples[i] *= i / FADE
samples[n - 1 - i] *= i / FADE
return samples
def write(name, samples):
samples = envelope(list(samples))
frames = b"".join(
struct.pack("B", max(0, min(255, int(round(s * 127)) + 128))) for s in samples
)
path = os.path.join(os.path.dirname(__file__), "..", "sounds", name + ".wav")
with wave.open(os.path.abspath(path), "wb") as f:
f.setnchannels(1)
f.setsampwidth(1)
f.setframerate(RATE)
f.writeframes(frames)
print(name, len(samples) / RATE, "s")
def mix(a, b):
n = max(len(a), len(b))
a = a + [0.0] * (n - len(a))
b = b + [0.0] * (n - len(b))
return [x + y for x, y in zip(a, b)]
# Opening a cell: the sound you hear most, so it is the shortest and quietest.
write("open", square(760, 1080, 32, 0.28))
# Flagging and taking a flag back: the same two notes, either way up.
write("flag", square(1400, 1400, 22, 0.30) + silence(14) + square(2100, 2100, 26, 0.30))
write("unflag", square(2100, 2100, 22, 0.28) + silence(14) + square(1400, 1400, 26, 0.28))
# Clearing around a number: three cells opening at once, so three blips.
write("chord", square(900, 900, 20, 0.26) + silence(8)
+ square(1200, 1200, 20, 0.26) + silence(8)
+ square(1500, 1500, 24, 0.26))
# A new board: a rising sweep, the sound of dealing.
write("deal", square(520, 1040, 70, 0.24))
# The mine: a low sweep under a burst of noise.
write("boom", mix(square(240, 55, 460, 0.42, duty=0.32), noise(300, 0.34)))
# Cleared: the arpeggio, with the octave held at the end.
write("win", square(523, 523, 80, 0.30) + square(659, 659, 80, 0.30)
+ square(784, 784, 80, 0.30) + square(1046, 1046, 240, 0.32))