Skip to content

Commit eaf4571

Browse files
committed
Fix audio device callback. Closes #128
Allow audio conversions of floating types other than float32. Setup AudioDevice as a context manager and add a `__repr__` method.
1 parent 37ec7c0 commit eaf4571

File tree

2 files changed

+62
-4
lines changed

2 files changed

+62
-4
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version
88
- Added PathLike support to more libtcodpy functions.
99
- New `tcod.sdl.mouse.show` function for querying or setting mouse visibility.
1010
- New class method `tcod.image.Image.from_file` to load images with. This replaces `tcod.image_load`.
11+
- `tcod.sdl.audio.AudioDevice` is now a context manager.
12+
13+
### Changed
14+
- SDL audio conversion will now pass unconvertible floating types as float32 instead of raising.
1115

1216
### Deprecated
1317
- Deprecated the libtcodpy functions for images and noise generators.
@@ -17,6 +21,8 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version
1721

1822
### Fixed
1923
- Fix `tcod.sdl.mouse.warp_in_window` function.
24+
- Fix `TypeError: '_AudioCallbackUserdata' object is not callable` when using an SDL audio device callback.
25+
[#128](https://github.com/libtcod/python-tcod/issues/128)
2026

2127
## [15.0.3] - 2023-05-25
2228
### Deprecated

tcod/sdl/audio.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,12 @@
4747
import sys
4848
import threading
4949
import time
50+
from types import TracebackType
5051
from typing import Any, Callable, Hashable, Iterator
5152

5253
import numpy as np
5354
from numpy.typing import ArrayLike, DTypeLike, NDArray
54-
from typing_extensions import Final, Literal
55+
from typing_extensions import Final, Literal, Self
5556

5657
import tcod.sdl.sys
5758
from tcod.loader import ffi, lib
@@ -110,6 +111,9 @@ def convert_audio(
110111
111112
.. versionadded:: 13.6
112113
114+
.. versionchanged:: Unreleased
115+
Now converts floating types to `np.float32` when SDL doesn't support the specific format.
116+
113117
.. seealso::
114118
:any:`AudioDevice.convert`
115119
"""
@@ -123,8 +127,26 @@ def convert_audio(
123127
in_channels = in_array.shape[1]
124128
in_format = _get_format(in_array.dtype)
125129
out_sdl_format = _get_format(out_format)
126-
if _check(lib.SDL_BuildAudioCVT(cvt, in_format, in_channels, in_rate, out_sdl_format, out_channels, out_rate)) == 0:
127-
return in_array # No conversion needed.
130+
try:
131+
if (
132+
_check(lib.SDL_BuildAudioCVT(cvt, in_format, in_channels, in_rate, out_sdl_format, out_channels, out_rate))
133+
== 0
134+
):
135+
return in_array # No conversion needed.
136+
except RuntimeError as exc:
137+
if ( # SDL now only supports float32, but later versions may add more support for more formats.
138+
exc.args[0] == "Invalid source format"
139+
and np.issubdtype(in_array.dtype, np.floating)
140+
and in_array.dtype != np.float32
141+
):
142+
return convert_audio( # Try again with float32
143+
in_array.astype(np.float32),
144+
in_rate,
145+
out_rate=out_rate,
146+
out_format=out_format,
147+
out_channels=out_channels,
148+
)
149+
raise
128150
# Upload to the SDL_AudioCVT buffer.
129151
cvt.len = in_array.itemsize * in_array.size
130152
out_buffer = cvt.buf = ffi.new("uint8_t[]", cvt.len * cvt.len_mult)
@@ -144,6 +166,9 @@ class AudioDevice:
144166
145167
When you use this object directly the audio passed to :any:`queue_audio` is always played synchronously.
146168
For more typical asynchronous audio you should pass an AudioDevice to :any:`BasicMixer`.
169+
170+
.. versionchanged:: Unreleased
171+
Can now be used as a context which will close the device on exit.
147172
"""
148173

149174
def __init__(
@@ -176,6 +201,23 @@ def __init__(
176201
self._handle: Any | None = None
177202
self._callback: Callable[[AudioDevice, NDArray[Any]], None] = self.__default_callback
178203

204+
def __repr__(self) -> str:
205+
"""Return a representation of this device."""
206+
items = [
207+
f"{self.__class__.__name__}(device_id={self.device_id})",
208+
f"frequency={self.frequency}",
209+
f"is_capture={self.is_capture}",
210+
f"format={self.format}",
211+
f"channels={self.channels}",
212+
f"buffer_samples={self.buffer_samples}",
213+
f"buffer_bytes={self.buffer_bytes}",
214+
]
215+
if self.silence:
216+
items.append(f"silence={self.silence}")
217+
if self._handle is not None:
218+
items.append(f"callback={self._callback}")
219+
return f"""<{" ".join(items)}>"""
220+
179221
@property
180222
def callback(self) -> Callable[[AudioDevice, NDArray[Any]], None]:
181223
"""If the device was opened with a callback enabled, then you may get or set the callback with this attribute."""
@@ -288,6 +330,16 @@ def close(self) -> None:
288330
lib.SDL_CloseAudioDevice(self.device_id)
289331
del self.device_id
290332

333+
def __enter__(self) -> Self:
334+
"""Return self and enter a managed context."""
335+
return self
336+
337+
def __exit__(
338+
self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
339+
) -> None:
340+
"""Close the device when exiting the context."""
341+
self.close()
342+
291343
@staticmethod
292344
def __default_callback(device: AudioDevice, stream: NDArray[Any]) -> None:
293345
stream[...] = device.silence
@@ -487,7 +539,7 @@ class _AudioCallbackUserdata:
487539
@ffi.def_extern() # type: ignore
488540
def _sdl_audio_callback(userdata: Any, stream: Any, length: int) -> None:
489541
"""Handle audio device callbacks."""
490-
data: _AudioCallbackUserdata = ffi.from_handle(userdata)()
542+
data: _AudioCallbackUserdata = ffi.from_handle(userdata)
491543
device = data.device
492544
buffer = np.frombuffer(ffi.buffer(stream, length), dtype=device.format).reshape(-1, device.channels)
493545
device._callback(device, buffer)

0 commit comments

Comments
 (0)