Skip to content

Commit b1048ca

Browse files
committed
Add locks to sound mixer.
Add volume and Pygame style loop parameters to sound play functions.
1 parent 1efd263 commit b1048ca

File tree

1 file changed

+64
-32
lines changed

1 file changed

+64
-32
lines changed

tcod/sdl/audio.py

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,27 @@ def __default_callback(device: AudioDevice, stream: NDArray[Any]) -> None:
179179
stream[...] = device.silence
180180

181181

182+
class _LoopSoundFunc:
183+
def __init__(self, sound: NDArray[Any], loops: int, on_end: Optional[Callable[[Channel], None]]):
184+
self.sound = sound
185+
self.loops = loops
186+
self.on_end = on_end
187+
188+
def __call__(self, channel: Channel) -> None:
189+
if not self.loops:
190+
if self.on_end is not None:
191+
self.on_end(channel)
192+
return
193+
channel.play(self.sound, volume=channel.volume, on_end=self)
194+
if self.loops > 0:
195+
self.loops -= 1
196+
197+
182198
class Channel:
183199
mixer: Mixer
184200

185201
def __init__(self) -> None:
202+
self._lock = threading.RLock()
186203
self.volume: Union[float, Tuple[float, ...]] = 1.0
187204
self.sound_queue: List[NDArray[Any]] = []
188205
self.on_end_callback: Optional[Callable[[Channel], None]] = None
@@ -195,10 +212,17 @@ def play(
195212
self,
196213
sound: ArrayLike,
197214
*,
215+
volume: Union[float, Tuple[float, ...]] = 1.0,
216+
loops: int = 0,
198217
on_end: Optional[Callable[[Channel], None]] = None,
199218
) -> None:
200-
self.sound_queue[:] = [self._verify_audio_sample(sound)]
201-
self.on_end_callback = on_end
219+
sound = self._verify_audio_sample(sound)
220+
with self._lock:
221+
self.volume = volume
222+
self.sound_queue[:] = [sound]
223+
self.on_end_callback = on_end
224+
if loops:
225+
self.on_end_callback = _LoopSoundFunc(sound, loops, on_end)
202226

203227
def _verify_audio_sample(self, sample: ArrayLike) -> NDArray[Any]:
204228
"""Verify an audio sample is valid and return it as a Numpy array."""
@@ -209,27 +233,29 @@ def _verify_audio_sample(self, sample: ArrayLike) -> NDArray[Any]:
209233
return array
210234

211235
def _on_mix(self, stream: NDArray[Any]) -> None:
212-
while self.sound_queue and stream.size:
213-
buffer = self.sound_queue[0]
214-
if buffer.shape[0] > stream.shape[0]:
215-
# Mix part of the buffer into the stream.
216-
stream[:] += buffer[: stream.shape[0]] * self.volume
217-
self.sound_queue[0] = buffer[stream.shape[0] :]
218-
break # Stream was filled.
219-
# Remaining buffer fits the stream array.
220-
stream[: buffer.shape[0]] += buffer * self.volume
221-
stream = stream[buffer.shape[0] :]
222-
self.sound_queue.pop(0)
223-
if not self.sound_queue and self.on_end_callback is not None:
224-
self.on_end_callback(self)
236+
with self._lock:
237+
while self.sound_queue and stream.size:
238+
buffer = self.sound_queue[0]
239+
if buffer.shape[0] > stream.shape[0]:
240+
# Mix part of the buffer into the stream.
241+
stream[:] += buffer[: stream.shape[0]] * self.volume
242+
self.sound_queue[0] = buffer[stream.shape[0] :]
243+
break # Stream was filled.
244+
# Remaining buffer fits the stream array.
245+
stream[: buffer.shape[0]] += buffer * self.volume
246+
stream = stream[buffer.shape[0] :]
247+
self.sound_queue.pop(0)
248+
if not self.sound_queue and self.on_end_callback is not None:
249+
self.on_end_callback(self)
225250

226251
def fadeout(self, time: float) -> None:
227252
assert time >= 0
228-
time_samples = round(time * self.mixer.device.frequency) + 1
229-
buffer: NDArray[np.float32] = np.zeros((time_samples, self.mixer.device.channels), np.float32)
230-
self._on_mix(buffer)
231-
buffer *= np.linspace(1.0, 0.0, time_samples + 1, endpoint=False)[1:]
232-
self.sound_queue[:] = [buffer]
253+
with self._lock:
254+
time_samples = round(time * self.mixer.device.frequency) + 1
255+
buffer: NDArray[np.float32] = np.zeros((time_samples, self.mixer.device.channels), np.float32)
256+
self._on_mix(buffer)
257+
buffer *= np.linspace(1.0, 0.0, time_samples + 1, endpoint=False)[1:]
258+
self.sound_queue[:] = [buffer]
233259

234260
def stop(self) -> None:
235261
self.fadeout(0.0005)
@@ -240,6 +266,7 @@ def __init__(self, device: AudioDevice):
240266
assert device.format == np.float32
241267
super().__init__(daemon=True)
242268
self.device = device
269+
self._lock = threading.RLock()
243270

244271
def run(self) -> None:
245272
buffer = np.full(
@@ -263,32 +290,37 @@ def __init__(self, device: AudioDevice):
263290
self.channels: Dict[Hashable, Channel] = {}
264291

265292
def get_channel(self, key: Hashable) -> Channel:
266-
if key not in self.channels:
267-
self.channels[key] = Channel()
268-
self.channels[key].mixer = self
269-
return self.channels[key]
293+
with self._lock:
294+
if key not in self.channels:
295+
self.channels[key] = Channel()
296+
self.channels[key].mixer = self
297+
return self.channels[key]
270298

271299
def get_free_channel(self) -> Channel:
272-
i = 0
273-
while True:
274-
if not self.get_channel(i).busy:
275-
return self.channels[i]
276-
i += 1
300+
with self._lock:
301+
i = 0
302+
while True:
303+
if not self.get_channel(i).busy:
304+
return self.channels[i]
305+
i += 1
277306

278307
def play(
279308
self,
280309
sound: ArrayLike,
281310
*,
311+
volume: Union[float, Tuple[float, ...]] = 1.0,
312+
loops: int = 0,
282313
on_end: Optional[Callable[[Channel], None]] = None,
283314
) -> Channel:
284315
channel = self.get_free_channel()
285-
channel.play(sound, on_end=on_end)
316+
channel.play(sound, volume=volume, loops=loops, on_end=on_end)
286317
return channel
287318

288319
def on_stream(self, stream: NDArray[Any]) -> None:
289320
super().on_stream(stream)
290-
for channel in list(self.channels.values()):
291-
channel._on_mix(stream)
321+
with self._lock:
322+
for channel in list(self.channels.values()):
323+
channel._on_mix(stream)
292324

293325

294326
class _AudioCallbackUserdata:

0 commit comments

Comments
 (0)