forked from robhagemans/pcbasic
-
Notifications
You must be signed in to change notification settings - Fork 0
/
audio_pygame.py
245 lines (214 loc) · 8.61 KB
/
audio_pygame.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
"""
PC-BASIC 3.23 - audio_pygame.py
Sound interface based on PyGame
(c) 2013, 2014 Rob Hagemans
This file is released under the GNU GPL version 3.
"""
from math import ceil
try:
import pygame
except ImportError:
pygame = None
try:
import numpy
except ImportError:
numpy = None
import plat
if plat.system == 'Android':
android = True
# don't do sound for now on Android
mixer = None
numpy = None
else:
android = False
if pygame:
import pygame.mixer as mixer
else:
mixer = None
import backend
import logging
def prepare():
""" Initialise sound module. """
if pygame:
# must be called before pygame.init()
if mixer:
mixer.pre_init(sample_rate, -mixer_bits, channels=1, buffer=1024) #4096
def init_sound():
""" Initialise sound system. """
if not numpy:
logging.warning('NumPy module not found. Failed to initialise audio.')
return False
if not mixer:
return False
# initialise mixer as silent
# this takes 0.7s but is necessary to be able to set channels to mono
mixer.quit()
return True
def stop_all_sound():
""" Clear all sound queues and turn off all sounds. """
global sound_queue, loop_sound
for voice in range(4):
stop_channel(voice)
loop_sound = [ None, None, None, None ]
sound_queue = [ [], [], [], [] ]
def check_sound():
""" Update the sound queue and play sounds. """
global loop_sound
current_chunk = [ None, None, None, None ]
if sound_queue == [ [], [], [], [] ] and loop_sound == [ None, None, None, None ]:
return
check_init_mixer()
for voice in range(4):
# if there is a sound queue, stop looping sound
if sound_queue[voice] and loop_sound[voice]:
stop_channel(voice)
loop_sound[voice] = None
if mixer.Channel(voice).get_queue() == None:
if loop_sound[voice]:
# loop the current playing sound; ok to interrupt it with play cos it's the same sound as is playing
current_chunk[voice] = loop_sound[voice].build_chunk()
elif sound_queue[voice]:
current_chunk[voice] = sound_queue[voice][0].build_chunk()
if not current_chunk[voice]:
sound_queue[voice].pop(0)
try:
current_chunk[voice] = sound_queue[voice][0].build_chunk()
except IndexError:
# sound_queue is empty
continue
if sound_queue[voice][0].loop:
loop_sound[voice] = sound_queue[voice].pop(0)
# any next sound in the sound queue will stop this looping sound
else:
loop_sound[voice] = None
for voice in range(4):
if current_chunk[voice]:
mixer.Channel(voice).queue(current_chunk[voice])
for voice in range(4):
# remove the notes that have been sent to mixer
backend.sound_done(voice, len(sound_queue[voice]))
def busy():
""" Is the mixer busy? """
return (not loop_sound[0] and not loop_sound[1] and not loop_sound[2] and not loop_sound[3]) and mixer.get_busy()
def play_sound(frequency, total_duration, fill, loop, voice=0, volume=15):
""" Queue a sound for playing; ignore and work off backend queue. """
sound_queue[voice].append(SoundGenerator(signal_sources[voice], frequency, total_duration, fill, loop, volume))
def set_noise(is_white):
""" Set the character of the noise channel. """
signal_sources[3].feedback = feedback_noise if is_white else feedback_periodic
def quit_sound():
""" Shut down the mixer. """
if mixer.get_init() != None:
mixer.quit()
# implementation
# sound generators for sounds not played yet
sound_queue = [ [], [], [], [] ]
# currently looping sound
loop_sound = [ None, None, None, None ]
# mixer settings
mixer_bits = 16
sample_rate = 44100
# initial condition - see dosbox source
init_noise = 0x0f35
# white noise feedback
feedback_noise = 0x4400
# 'periodic' feedback mask (15-bit rotation)
feedback_periodic = 0x4000
# square wave feedback mask
feedback_tone = 0x2
class SignalSource(object):
""" Linear Feedback Shift Register to generate noise or tone. """
def __init__(self, feedback, init=0x01):
""" Initialise the signal source. """
self.lfsr = init
self.feedback = feedback
def next(self):
""" Get a sample bit. """
bit = self.lfsr & 1
self.lfsr >>= 1
if bit:
self.lfsr ^= self.feedback
return bit
# three tone voices plus a noise source
signal_sources = [ SignalSource(feedback_tone), SignalSource(feedback_tone), SignalSource(feedback_tone),
SignalSource(feedback_noise, init_noise) ]
# The SN76489 attenuates the volume by 2dB for each step in the volume register.
# see http://www.smspower.org/Development/SN76489
max_amplitude = (1<<(mixer_bits-1)) - 1
# 2 dB steps correspond to a voltage factor of 10**(-2./20.) as power ~ voltage**2
step_factor = 10**(-2./20.)
# geometric list of amplitudes for volume values
amplitude = [0]*16 if not numpy else numpy.int16(max_amplitude*(step_factor**numpy.arange(15,-1,-1)))
# zero volume means silent
amplitude[0] = 0
class SoundGenerator(object):
""" Sound sample chunk generator. """
def __init__(self, signal_source, frequency, total_duration, fill, loop, volume):
""" Initialise the generator. """
# noise generator
self.signal_source = signal_source
# one wavelength at 37 Hz is 1192 samples at 44100 Hz
self.chunk_length = 1192 * 4
# actual duration and gap length
self.duration = fill * total_duration
self.gap = (1-fill) * total_duration
self.amplitude = amplitude[volume]
self.frequency = frequency
self.loop = loop
self.bit = 0
self.count_samples = 0
self.num_samples = int(self.duration * sample_rate)
def build_chunk(self):
""" Build a sound chunk. """
if self.count_samples >= self.num_samples:
# done already
return None
# work on last element of sound queue
check_init_mixer()
if self.frequency == 0 or self.frequency == 32767:
chunk = numpy.zeros(self.chunk_length, numpy.int16)
else:
half_wavelength = sample_rate / (2.*self.frequency)
num_half_waves = int(ceil(self.chunk_length / half_wavelength))
# generate bits
bits = []
for _ in range(num_half_waves):
bits.append(-self.amplitude if self.signal_source.next() else self.amplitude)
# do sampling by averaging the signal over bins of given resolution
# this allows to use numpy all the way which is *much* faster than looping over an array
# stretch array by half_wavelength * resolution
resolution = 20
matrix = numpy.repeat(numpy.array(bits, numpy.int16), int(half_wavelength*resolution))
# cut off on round number of resolution blocks
matrix = matrix[:len(matrix)-(len(matrix)%resolution)]
# average over blocks
matrix = matrix.reshape((len(matrix)/resolution, resolution))
chunk = numpy.int16(numpy.average(matrix, axis=1))
if not self.loop:
# last chunk is shorter
if self.count_samples + len(chunk) < self.num_samples:
self.count_samples += len(chunk)
else:
# append final chunk
rest_length = self.num_samples - self.count_samples
chunk = chunk[:rest_length]
# append quiet gap if requested
if self.gap:
gap_chunk = numpy.zeros(int(self.gap * sample_rate), numpy.int16)
chunk = numpy.concatenate((chunk, gap_chunk))
# done
self.count_samples = self.num_samples
# if loop, attach one chunk to loop, do not increment count
return pygame.sndarray.make_sound(chunk)
def stop_channel(channel):
""" Stop sound on a channel. """
if mixer.get_init():
mixer.Channel(channel).stop()
# play short silence to avoid blocking the channel - it won't play on queue()
silence = pygame.sndarray.make_sound(numpy.zeros(1, numpy.int16))
mixer.Channel(channel).play(silence)
def check_init_mixer():
""" Initialise the mixer if necessary. """
if mixer.get_init() == None:
mixer.init()
prepare()