Skip to content

Commit 45d308b

Browse files
committed
Experiment with mixer channels.
1 parent 6f1b22c commit 45d308b

File tree

1 file changed

+83
-14
lines changed

1 file changed

+83
-14
lines changed

tcod/sdl/audio.py

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sys
44
import threading
55
import time
6-
from typing import Any, Iterator, List, Optional
6+
from typing import Any, Callable, Dict, Hashable, Iterator, List, Optional, Tuple, Union
77

88
import numpy as np
99
from numpy.typing import ArrayLike, DTypeLike, NDArray
@@ -139,8 +139,65 @@ def __default_callback(self, stream: NDArray[Any]) -> None:
139139
stream[...] = self.silence
140140

141141

142+
class Channel:
143+
mixer: Mixer
144+
145+
def __init__(self) -> None:
146+
self.volume: Union[float, Tuple[float, ...]] = 1.0
147+
self.sound_queue: List[NDArray[Any]] = []
148+
self.on_end_callback: Optional[Callable[[Channel], None]] = None
149+
150+
@property
151+
def busy(self) -> bool:
152+
return bool(self.sound_queue)
153+
154+
def play(
155+
self,
156+
sound: ArrayLike,
157+
*,
158+
on_end: Optional[Callable[[Channel], None]] = None,
159+
) -> None:
160+
self.sound_queue[:] = [self._verify_audio_sample(sound)]
161+
self.on_end_callback = on_end
162+
163+
def _verify_audio_sample(self, sample: ArrayLike) -> NDArray[Any]:
164+
"""Verify an audio sample is valid and return it as a Numpy array."""
165+
array: NDArray[Any] = np.asarray(sample)
166+
assert array.dtype == self.mixer.device.format
167+
if len(array.shape) == 1:
168+
array = array[:, np.newaxis]
169+
return array
170+
171+
def _on_mix(self, stream: NDArray[Any]) -> None:
172+
while self.sound_queue and stream.size:
173+
buffer = self.sound_queue[0]
174+
if buffer.shape[0] > stream.shape[0]:
175+
# Mix part of the buffer into the stream.
176+
stream[:] += buffer[: stream.shape[0]] * self.volume
177+
self.sound_queue[0] = buffer[stream.shape[0] :]
178+
break # Stream was filled.
179+
# Remaining buffer fits the stream array.
180+
stream[: buffer.shape[0]] += buffer * self.volume
181+
stream = stream[buffer.shape[0] :]
182+
self.sound_queue.pop(0)
183+
if not self.sound_queue and self.on_end_callback is not None:
184+
self.on_end_callback(self)
185+
186+
def fadeout(self, time: float) -> None:
187+
assert time >= 0
188+
time_samples = round(time * self.mixer.device.frequency) + 1
189+
buffer: NDArray[np.float32] = np.zeros((time_samples, self.mixer.device.channels), np.float32)
190+
self._on_mix(buffer)
191+
buffer *= np.linspace(1.0, 0.0, time_samples + 1, endpoint=False)[1:]
192+
self.sound_queue[:] = [buffer]
193+
194+
def stop(self) -> None:
195+
self.fadeout(0.0005)
196+
197+
142198
class Mixer(threading.Thread):
143199
def __init__(self, device: AudioDevice):
200+
assert device.format == np.float32
144201
super().__init__(daemon=True)
145202
self.device = device
146203

@@ -161,23 +218,35 @@ def on_stream(self, stream: NDArray[Any]) -> None:
161218
class BasicMixer(Mixer):
162219
def __init__(self, device: AudioDevice):
163220
super().__init__(device)
164-
self.play_buffers: List[List[NDArray[Any]]] = []
221+
self.channels: Dict[Hashable, Channel] = {}
165222

166-
def play(self, sound: ArrayLike) -> None:
167-
array = np.asarray(sound, dtype=self.device.format)
168-
assert array.size
169-
if len(array.shape) == 1:
170-
array = array[:, np.newaxis]
171-
chunks: List[NDArray[Any]] = np.split(array, range(0, len(array), self.device.samples)[1:])[::-1]
172-
self.play_buffers.append(chunks)
223+
def get_channel(self, key: Hashable) -> Channel:
224+
if key not in self.channels:
225+
self.channels[key] = Channel()
226+
self.channels[key].mixer = self
227+
return self.channels[key]
228+
229+
def get_free_channel(self) -> Channel:
230+
i = 0
231+
while True:
232+
if not self.get_channel(i).busy:
233+
return self.channels[i]
234+
i += 1
235+
236+
def play(
237+
self,
238+
sound: ArrayLike,
239+
*,
240+
on_end: Optional[Callable[[Channel], None]] = None,
241+
) -> Channel:
242+
channel = self.get_free_channel()
243+
channel.play(sound, on_end=on_end)
244+
return channel
173245

174246
def on_stream(self, stream: NDArray[Any]) -> None:
175247
super().on_stream(stream)
176-
for chunks in self.play_buffers:
177-
chunk = chunks.pop()
178-
stream[: len(chunk)] += chunk
179-
180-
self.play_buffers = [chunks for chunks in self.play_buffers if chunks]
248+
for channel in list(self.channels.values()):
249+
channel._on_mix(stream)
181250

182251

183252
@ffi.def_extern() # type: ignore

0 commit comments

Comments
 (0)