Skip to content

Commit 304eedb

Browse files
committed
Clean up and refactor SDL audio module.
1 parent 8a678fd commit 304eedb

File tree

1 file changed

+81
-54
lines changed

1 file changed

+81
-54
lines changed

tcod/sdl/audio.py

Lines changed: 81 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import sys
44
import threading
55
import time
6-
import weakref
76
from typing import Any, Iterator, List, Optional
87

98
import numpy as np
@@ -27,7 +26,7 @@ def _get_format(format: DTypeLike) -> int:
2726
if byteorder == "=":
2827
byteorder = "<" if sys.byteorder == "little" else ">"
2928

30-
return ( # type: ignore
29+
return int(
3130
bitsize
3231
| (lib.SDL_AUDIO_MASK_DATATYPE * is_float)
3332
| (lib.SDL_AUDIO_MASK_ENDIAN * (byteorder == ">"))
@@ -51,57 +50,45 @@ def _dtype_from_format(format: int) -> np.dtype[Any]:
5150

5251

5352
class AudioDevice:
53+
"""An SDL audio device."""
54+
5455
def __init__(
5556
self,
56-
device: Optional[str] = None,
57-
capture: bool = False,
58-
*,
59-
frequency: int = 44100,
60-
format: DTypeLike = np.float32,
61-
channels: int = 2,
62-
samples: int = 0,
63-
allowed_changes: int = 0,
57+
device_id: int,
58+
capture: bool,
59+
spec: Any, # SDL_AudioSpec*
6460
):
65-
self.__sdl_subsystems = tcod.sdl.sys._ScopeInit(tcod.sdl.sys.Subsystem.AUDIO)
66-
self.__handle = ffi.new_handle(weakref.ref(self))
67-
desired = ffi.new(
68-
"SDL_AudioSpec*",
69-
{
70-
"freq": frequency,
71-
"format": _get_format(format),
72-
"channels": channels,
73-
"samples": samples,
74-
"callback": ffi.NULL,
75-
"userdata": self.__handle,
76-
},
77-
)
78-
obtained = ffi.new("SDL_AudioSpec*")
79-
self.device_id = lib.SDL_OpenAudioDevice(
80-
ffi.NULL if device is None else device.encode("utf-8"),
81-
capture,
82-
desired,
83-
obtained,
84-
allowed_changes,
85-
)
86-
assert self.device_id != 0, _get_error()
87-
self.frequency = obtained.freq
61+
assert device_id >= 0
62+
assert ffi.typeof(spec) is ffi.typeof("SDL_AudioSpec*")
63+
assert spec
64+
self.device_id = device_id
65+
self.spec = spec
66+
self.frequency = spec.freq
8867
self.is_capture = capture
89-
self.format = _dtype_from_format(obtained.format)
90-
self.channels = int(obtained.channels)
91-
self.silence = int(obtained.silence)
92-
self.samples = int(obtained.samples)
93-
self.buffer_size = int(obtained.size)
94-
self.unpause()
68+
self.format = _dtype_from_format(spec.format)
69+
self.channels = int(spec.channels)
70+
self.silence = int(spec.silence)
71+
self.samples = int(spec.samples)
72+
self.buffer_size = int(spec.size)
73+
self._callback = self.__default_callback
9574

9675
@property
9776
def _sample_size(self) -> int:
9877
return self.format.itemsize * self.channels
9978

100-
def pause(self) -> None:
101-
lib.SDL_PauseAudioDevice(self.device_id, True)
79+
@property
80+
def stopped(self) -> bool:
81+
"""Is True if the device has failed or was closed."""
82+
return bool(lib.SDL_GetAudioDeviceStatus(self.device_id) != lib.SDL_AUDIO_STOPPED)
83+
84+
@property
85+
def paused(self) -> bool:
86+
"""Get or set the device paused state."""
87+
return bool(lib.SDL_GetAudioDeviceStatus(self.device_id) != lib.SDL_AUDIO_PLAYING)
10288

103-
def unpause(self) -> None:
104-
lib.SDL_PauseAudioDevice(self.device_id, False)
89+
@paused.setter
90+
def paused(self, value: bool) -> None:
91+
lib.SDL_PauseAudioDevice(self.device_id, value)
10592

10693
def _verify_array_format(self, samples: NDArray[Any]) -> NDArray[Any]:
10794
if samples.dtype != self.format:
@@ -121,12 +108,14 @@ def queued_audio_bytes(self) -> int:
121108
return int(lib.SDL_GetQueuedAudioSize(self.device_id))
122109

123110
def queue_audio(self, samples: ArrayLike) -> None:
111+
"""Append audio samples to the audio data queue."""
124112
assert not self.is_capture
125113
samples = self._convert_array(samples)
126114
buffer = ffi.from_buffer(samples)
127115
lib.SDL_QueueAudio(self.device_id, buffer, len(buffer))
128116

129117
def dequeue_audio(self) -> NDArray[Any]:
118+
"""Return the audio buffer from a capture stream."""
130119
assert self.is_capture
131120
out_samples = self.queued_audio_bytes // self._sample_size
132121
out = np.empty((out_samples, self.channels), self.format)
@@ -140,31 +129,30 @@ def __del__(self) -> None:
140129
self.close()
141130

142131
def close(self) -> None:
132+
"""Close this audio device."""
143133
if not self.device_id:
144134
return
145135
lib.SDL_CloseAudioDevice(self.device_id)
146136
self.device_id = 0
147137

148-
@staticmethod
149-
def __default_callback(stream: NDArray[Any], silence: int) -> None:
150-
stream[...] = silence
138+
def __default_callback(self, stream: NDArray[Any]) -> None:
139+
stream[...] = self.silence
151140

152141

153142
class Mixer(threading.Thread):
154143
def __init__(self, device: AudioDevice):
155144
super().__init__(daemon=True)
156145
self.device = device
157-
self.device.unpause()
158-
self.start()
159146

160147
def run(self) -> None:
161148
buffer = np.full((self.device.samples, self.device.channels), self.device.silence, dtype=self.device.format)
162149
while True:
163-
time.sleep(0.001)
164-
if self.device.queued_audio_bytes == 0:
165-
self.on_stream(buffer)
166-
self.device.queue_audio(buffer)
167-
buffer[:] = self.device.silence
150+
if self.device.queued_audio_bytes > 0:
151+
time.sleep(0.001)
152+
continue
153+
self.on_stream(buffer)
154+
self.device.queue_audio(buffer)
155+
buffer[:] = self.device.silence
168156

169157
def on_stream(self, stream: NDArray[Any]) -> None:
170158
pass
@@ -197,7 +185,8 @@ def _sdl_audio_callback(userdata: Any, stream: Any, length: int) -> None:
197185
"""Handle audio device callbacks."""
198186
device: Optional[AudioDevice] = ffi.from_handle(userdata)()
199187
assert device is not None
200-
_ = np.frombuffer(ffi.buffer(stream, length), dtype=device.format).reshape(-1, device.channels)
188+
buffer = np.frombuffer(ffi.buffer(stream, length), dtype=device.format).reshape(-1, device.channels)
189+
device._callback(buffer)
201190

202191

203192
def _get_devices(capture: bool) -> Iterator[str]:
@@ -216,3 +205,41 @@ def get_devices() -> Iterator[str]:
216205
def get_capture_devices() -> Iterator[str]:
217206
"""Iterate over the available audio capture devices."""
218207
yield from _get_devices(capture=True)
208+
209+
210+
def open(
211+
name: Optional[str] = None,
212+
capture: bool = False,
213+
*,
214+
frequency: int = 44100,
215+
format: DTypeLike = np.float32,
216+
channels: int = 2,
217+
samples: int = 0,
218+
allowed_changes: int = 0,
219+
paused: bool = False,
220+
) -> AudioDevice:
221+
"""Open an audio device for playback or capture."""
222+
tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.AUDIO)
223+
desired = ffi.new(
224+
"SDL_AudioSpec*",
225+
{
226+
"freq": frequency,
227+
"format": _get_format(format),
228+
"channels": channels,
229+
"samples": samples,
230+
"callback": ffi.NULL,
231+
"userdata": ffi.NULL,
232+
},
233+
)
234+
obtained = ffi.new("SDL_AudioSpec*")
235+
device_id: int = lib.SDL_OpenAudioDevice(
236+
ffi.NULL if name is None else name.encode("utf-8"),
237+
capture,
238+
desired,
239+
obtained,
240+
allowed_changes,
241+
)
242+
assert device_id >= 0, _get_error()
243+
device = AudioDevice(device_id, capture, obtained)
244+
device.paused = paused
245+
return device

0 commit comments

Comments
 (0)