forked from Random832/ttyemu
-
Notifications
You must be signed in to change notification settings - Fork 2
/
sounds.py
306 lines (275 loc) · 11.2 KB
/
sounds.py
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
#!/usr/bin/env python3
"""
Teletype sounds
(requires pygame)
"""
import os
import random
import logging
import pygame
from pygame.mixer import Sound
logger = logging.getLogger(__name__)
class PygameSounds:
"""Teletype sounds, using pygame mixer"""
# Some events
EVENT_HUM = pygame.USEREVENT+2
EVENT_KEY = pygame.USEREVENT+3
EVENT_CHR = pygame.USEREVENT+4
EVENT_SYNC = pygame.USEREVENT+5
EVENTS = [EVENT_HUM, EVENT_KEY, EVENT_CHR, EVENT_SYNC]
def __init__(self):
pygame.mixer.pre_init(frequency=48000, size=-16, channels=2, buffer=512)
# Load the sounds into a dict for easy access
self.sounds = {}
# Start with the lid up (close the lid with F7 if you want peace and quiet)
self.lid_state = "up"
# Channels for playback
self.ch0 = None
self.ch1 = None
self.ch2 = None
# Channels for effects
self._chfx = [None, None, None]
# The current channel for effects
self._fx = 0
# Sounds that we keep using
self.hum_sound = None
self.spaces_sound = None
self.chars_sound = None
# How many keypresses are queued (including current)
self.active_key_count = 0
# What characters are queued to print (including current)
self.active_printout = ""
def get(self, sound_name):
# Get a sound by name.
# All sounds depend on whether the lid is open, that's part of the name.
actual_name = self.lid_state + "-" + sound_name
if sound_name in self.sounds:
return self.sounds[actual_name]
# There are some 'repeated sample' sounds (e.g. keys) where we choose one at random from the set
sounds = [sound for name, sound in self.sounds.items() if name.startswith(actual_name)]
return random.sample(sounds, 1)[0]
def start(self):
# Load the sound library
try:
with os.scandir(path=os.path.join(os.path.dirname(__file__), "sounds")) as scan:
for entry in scan:
if entry.is_file:
filename, ext = os.path.splitext(entry.name)
if ext == ".wav":
self.sounds[filename] = Sound(entry.path)
except pygame.error:
logging.exception("Could not initialize sounds.")
return
if not self.sounds:
logging.error("Could not load sounds.")
return
pygame.mixer.set_reserved(6)
self.ch0 = pygame.mixer.Channel(0) # used for on/off, background hum, lid
self.ch1 = pygame.mixer.Channel(1) # printing spaces (loop)
self.ch2 = pygame.mixer.Channel(3) # printing characters (loop)
self._chfx = [ # fx: input (keypresses), platen, bells, etc
pygame.mixer.Channel(3),
pygame.mixer.Channel(4),
pygame.mixer.Channel(5)
]
self.hum_sound = self.get("hum")
self.spaces_sound = self.get("print-spaces")
self.chars_sound = self.get("print-chars")
# Play the power-on sound, then to background after 1.5
self.ch0.play(self.get("motor-on"))
pygame.time.set_timer(self.EVENT_SYNC, 1500)
pygame.time.wait(1000)
self._start_paused()
@property
def chfx(self):
# Get a channel for effects
for i in range(len(self._chfx)):
channel = self._chfx[i]
if not channel.get_busy():
self._fx = i
return channel
self._fx = (self._fx + 1) % len(self._chfx)
return self._chfx[self._fx]
def stop(self):
if not self.sounds:
return
# Play the power-off sound
self.ch0.play(self.get("motor-off"))
# Wait until it plays out a bit
pygame.time.wait(500)
# Fade out over 1 second
self.ch0.fadeout(1000)
pygame.time.wait(1000)
def lid(self):
"""Open or close the lid."""
if not self.sounds:
return
logger.debug("lid")
self._fade_to_hum()
self.chfx.play(self.get("lid"))
# Flip the lid state
if self.lid_state == "down":
self.lid_state = "up"
else:
self.lid_state = "down"
# The main sounds will change with the new lid position
pygame.time.set_timer(self.EVENT_SYNC, 250)
def platen(self):
"""Hand-scrolled platen for page up & down"""
if not self.sounds:
return
logger.debug("platen")
self.chfx.play(self.get("platen"))
def _start_loops(self):
self.hum_sound = self.get("hum")
self.spaces_sound = self.get("print-spaces")
self.chars_sound = self.get("print-chars")
self.ch0.play(self.hum_sound, loops=-1)
self.ch1.play(self.spaces_sound, loops=-1)
self.ch2.play(self.chars_sound, loops=-1)
self.hum_sound.set_volume(0.0)
self.spaces_sound.set_volume(0.0)
self.chars_sound.set_volume(0.0)
def _start_paused(self):
self._start_loops()
self.ch1.pause()
self.ch2.pause()
def keypress(self, key):
"""Key pressed at the keyboard (may or may not echo)"""
if not self.sounds:
return
logger.debug("keypress")
self.active_key_count = self.active_key_count + 1
if self.active_key_count > 1:
# Just queue it and keep going
return
# In a while we can press another key
pygame.time.set_timer(self.EVENT_KEY, 100)
self._sound_for_keypress()
def _sound_for_keypress(self):
if self.active_key_count <= 0:
# No next keypress. Cancel the timer.
pygame.time.set_timer(self.EVENT_KEY, 0)
else:
# Press any key (they all sound similar)
self.chfx.play(self.get("key"))
def print_chars(self, chars):
if not self.sounds:
return
logger.debug("print: %s", chars)
# Add to the string that we're printing
self.active_printout = self.active_printout + chars
# Set the print timer for 100ms (repeats)
pygame.time.set_timer(self.EVENT_CHR, 100)
self._sound_for_char()
def _sound_for_char(self):
next_char = self.active_printout[:1]
if next_char == "":
# No next character. Go back to hum.
pygame.time.set_timer(self.EVENT_CHR, 0)
pygame.time.set_timer(self.EVENT_HUM, 100)
elif next_char == '\r':
# Carriage return (not newline, that just sounds as a space)
self.hum_sound.set_volume(0.0)
self.spaces_sound.set_volume(1.0)
self.chars_sound.set_volume(0.0)
self.chfx.play(self.get("cr"))
# Reset the loop timing
pygame.time.set_timer(self.EVENT_SYNC, 10)
elif next_char == '\007':
# Mute the hum/print while we do this
self.hum_sound.set_volume(0.0)
self.spaces_sound.set_volume(0.0)
self.chars_sound.set_volume(0.0)
self.chfx.play(self.get("bell"))
elif ord(next_char) <= 32 or next_char.isspace():
# Control characters and spaces
self._fade_to_spaces()
else:
# Treat anything else as printable
self._fade_to_chars()
def _fade_to_hum(self):
if self.hum_sound.get_volume() <= 0.99:
self.hum_sound.set_volume(0.3)
self.spaces_sound.set_volume(self.spaces_sound.get_volume() * 0.7)
self.chars_sound.set_volume(self.chars_sound.get_volume() * 0.7)
pygame.time.wait(3)
self.hum_sound.set_volume(0.5)
self.spaces_sound.set_volume(self.spaces_sound.get_volume() * 0.7)
self.chars_sound.set_volume(self.chars_sound.get_volume() * 0.7)
pygame.time.wait(3)
self.hum_sound.set_volume(0.7)
self.spaces_sound.set_volume(self.spaces_sound.get_volume() * 0.7)
self.chars_sound.set_volume(self.chars_sound.get_volume() * 0.7)
pygame.time.wait(3)
self.hum_sound.set_volume(1.0)
self.spaces_sound.set_volume(0.0)
self.chars_sound.set_volume(0.0)
self.ch1.pause()
self.ch2.pause()
def _fade_to_spaces(self):
if self.spaces_sound.get_volume() <= 0.99:
self.hum_sound.set_volume(self.hum_sound.get_volume() * 0.7)
self.spaces_sound.set_volume(0.3)
self.chars_sound.set_volume(self.chars_sound.get_volume() * 0.7)
pygame.time.wait(3)
self.hum_sound.set_volume(self.hum_sound.get_volume() * 0.7)
self.spaces_sound.set_volume(0.5)
self.chars_sound.set_volume(self.chars_sound.get_volume() * 0.7)
pygame.time.wait(3)
self.hum_sound.set_volume(self.hum_sound.get_volume() * 0.7)
self.spaces_sound.set_volume(0.7)
self.chars_sound.set_volume(self.chars_sound.get_volume() * 0.7)
pygame.time.wait(3)
self.hum_sound.set_volume(0.0)
self.spaces_sound.set_volume(1.0)
self.chars_sound.set_volume(0.0)
self.ch1.unpause()
self.ch2.unpause()
def _fade_to_chars(self):
if self.chars_sound.get_volume() <= 0.99:
self.hum_sound.set_volume(self.hum_sound.get_volume() * 0.7)
self.spaces_sound.set_volume(self.spaces_sound.get_volume() * 0.7)
self.chars_sound.set_volume(0.3)
pygame.time.wait(3)
self.hum_sound.set_volume(self.hum_sound.get_volume() * 0.7)
self.spaces_sound.set_volume(self.spaces_sound.get_volume() * 0.7)
self.chars_sound.set_volume(0.5)
pygame.time.wait(3)
self.hum_sound.set_volume(self.hum_sound.get_volume() * 0.7)
self.spaces_sound.set_volume(self.spaces_sound.get_volume() * 0.7)
self.chars_sound.set_volume(0.7)
pygame.time.wait(3)
self.hum_sound.set_volume(0.0)
self.spaces_sound.set_volume(0.0)
self.chars_sound.set_volume(1.0)
self.ch1.unpause()
self.ch2.unpause()
def event(self, evt):
# A pygame event happened
if not self.sounds:
return
if evt == self.EVENT_HUM:
logger.debug("EVENT_HUM")
# Cancel the hum timer
pygame.time.set_timer(self.EVENT_HUM, 0)
# Background hum (unless there's print pending)
if self.active_printout:
# No hum yet, we're printing
return
# Go back to playing the hum on loop, and pause the spaces/chars loops
self._fade_to_hum()
elif evt == self.EVENT_KEY:
logger.debug("EVENT_KEY")
self.active_key_count = self.active_key_count - 1
self._sound_for_keypress()
elif evt == self.EVENT_CHR:
self.active_printout = self.active_printout[1:]
self._sound_for_char()
elif evt == self.EVENT_SYNC:
# Sync after startup and CR: reset the spaces/chars loops.
pygame.time.set_timer(self.EVENT_SYNC, 0)
pygame.time.set_timer(self.EVENT_HUM, 100)
self._start_loops()
else:
logger.debug("Event: %s", evt)