Skip to content

Commit 2d0a2e5

Browse files
committed
Add more audio callback support and document audio device opening.
1 parent 3654e2f commit 2d0a2e5

File tree

2 files changed

+92
-8
lines changed

2 files changed

+92
-8
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@
299299
"undoc",
300300
"Unifont",
301301
"unraisablehook",
302+
"unraiseable",
302303
"upscaling",
303304
"VAFUNC",
304305
"vcoef",

tcod/sdl/audio.py

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from __future__ import annotations
22

3+
import enum
34
import sys
45
import threading
56
import time
67
from typing import Any, Callable, Dict, Hashable, Iterator, List, Optional, Tuple, Union
78

89
import numpy as np
910
from numpy.typing import ArrayLike, DTypeLike, NDArray
11+
from typing_extensions import Literal
1012

1113
import tcod.sdl.sys
1214
from tcod.loader import ffi, lib
@@ -70,7 +72,20 @@ def __init__(
7072
self.silence = int(spec.silence)
7173
self.samples = int(spec.samples)
7274
self.buffer_size = int(spec.size)
73-
self._callback = self.__default_callback
75+
self._handle: Optional[Any] = None
76+
self._callback: Callable[[AudioDevice, NDArray[Any]], None] = self.__default_callback
77+
78+
@property
79+
def callback(self) -> Callable[[AudioDevice, NDArray[Any]], None]:
80+
if self._handle is None:
81+
raise TypeError("This AudioDevice was opened without a callback.")
82+
return self._callback
83+
84+
@callback.setter
85+
def callback(self, new_callback: Callable[[AudioDevice, NDArray[Any]], None]) -> None:
86+
if self._handle is None:
87+
raise TypeError("This AudioDevice was opened without a callback.")
88+
self._callback = new_callback
7489

7590
@property
7691
def _sample_size(self) -> int:
@@ -135,8 +150,9 @@ def close(self) -> None:
135150
lib.SDL_CloseAudioDevice(self.device_id)
136151
self.device_id = 0
137152

138-
def __default_callback(self, stream: NDArray[Any]) -> None:
139-
stream[...] = self.silence
153+
@staticmethod
154+
def __default_callback(device: AudioDevice, stream: NDArray[Any]) -> None:
155+
stream[...] = device.silence
140156

141157

142158
class Channel:
@@ -249,13 +265,17 @@ def on_stream(self, stream: NDArray[Any]) -> None:
249265
channel._on_mix(stream)
250266

251267

268+
class _AudioCallbackUserdata:
269+
device: AudioDevice
270+
271+
252272
@ffi.def_extern() # type: ignore
253273
def _sdl_audio_callback(userdata: Any, stream: Any, length: int) -> None:
254274
"""Handle audio device callbacks."""
255-
device: Optional[AudioDevice] = ffi.from_handle(userdata)()
256-
assert device is not None
275+
data: _AudioCallbackUserdata = ffi.from_handle(userdata)()
276+
device = data.device
257277
buffer = np.frombuffer(ffi.buffer(stream, length), dtype=device.format).reshape(-1, device.channels)
258-
device._callback(buffer)
278+
device._callback(device, buffer)
259279

260280

261281
def _get_devices(capture: bool) -> Iterator[str]:
@@ -276,6 +296,23 @@ def get_capture_devices() -> Iterator[str]:
276296
yield from _get_devices(capture=True)
277297

278298

299+
class AllowedChanges(enum.IntFlag):
300+
"""Which parameters are allowed to be changed when the values given are not supported."""
301+
302+
NONE = 0
303+
""""""
304+
FREQUENCY = 0x01
305+
""""""
306+
FORMAT = 0x02
307+
""""""
308+
CHANNELS = 0x04
309+
""""""
310+
SAMPLES = 0x08
311+
""""""
312+
ANY = FREQUENCY | FORMAT | CHANNELS | SAMPLES
313+
""""""
314+
315+
279316
def open(
280317
name: Optional[str] = None,
281318
capture: bool = False,
@@ -284,10 +321,43 @@ def open(
284321
format: DTypeLike = np.float32,
285322
channels: int = 2,
286323
samples: int = 0,
287-
allowed_changes: int = 0,
324+
allowed_changes: AllowedChanges = AllowedChanges.NONE,
288325
paused: bool = False,
326+
callback: Union[None, Literal[True], Callable[[AudioDevice, NDArray[Any]], None]] = None,
289327
) -> AudioDevice:
290-
"""Open an audio device for playback or capture."""
328+
"""Open an audio device for playback or capture and return it.
329+
330+
Args:
331+
name: The name of the device to open, or None for the most reasonable default.
332+
capture: True if this is a recording device, or False if this is an output device.
333+
frequency: The desired sample rate to open the device with.
334+
format: The data format to use for samples as a NumPy dtype.
335+
channels: The number of speakers for the device. 1, 2, 4, or 6 are typical options.
336+
samples: The desired size of the audio buffer, must be a power of two.
337+
allowed_changes:
338+
By default if the hardware does not support the desired format than SDL will transparently convert between
339+
formats for you.
340+
Otherwise you can specify which parameters are allowed to be changed to fit the hardware better.
341+
paused:
342+
If True then the device will begin in a paused state.
343+
It can then be unpaused by assigning False to :any:`AudioDevice.paused`.
344+
callback:
345+
If None then this device will be opened in push mode and you'll have to use :any:`AudioDevice.queue_audio`
346+
to send audio data or :any:`AudioDevice.dequeue_audio` to receive it.
347+
If a callback is given then you can change it later, but you can not enable or disable the callback on an
348+
opened device.
349+
If True then a default callback which plays silence will be used, this is useful if you need the audio
350+
device before your callback is ready.
351+
352+
If a callback is given then it will be called with the `AudioDevice` and a Numpy buffer of the data stream.
353+
This callback will be run on a separate thread.
354+
Exceptions not handled by the callback become unraiseable and will be handled by :any:`sys.unraisablehook`.
355+
356+
.. seealso::
357+
https://wiki.libsdl.org/SDL_AudioSpec
358+
https://wiki.libsdl.org/SDL_OpenAudioDevice
359+
360+
"""
291361
tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.AUDIO)
292362
desired = ffi.new(
293363
"SDL_AudioSpec*",
@@ -300,6 +370,14 @@ def open(
300370
"userdata": ffi.NULL,
301371
},
302372
)
373+
callback_data = _AudioCallbackUserdata()
374+
if callback is not None:
375+
handle = ffi.new_handle(callback_data)
376+
desired.callback = lib._sdl_audio_callback
377+
desired.userdata = handle
378+
else:
379+
handle = None
380+
303381
obtained = ffi.new("SDL_AudioSpec*")
304382
device_id: int = lib.SDL_OpenAudioDevice(
305383
ffi.NULL if name is None else name.encode("utf-8"),
@@ -310,5 +388,10 @@ def open(
310388
)
311389
assert device_id >= 0, _get_error()
312390
device = AudioDevice(device_id, capture, obtained)
391+
if callback is not None:
392+
callback_data.device = device
393+
device._handle = handle
394+
if callback is not True:
395+
device._callback = callback
313396
device.paused = paused
314397
return device

0 commit comments

Comments
 (0)