Skip to content

Commit

Permalink
Fix audio device callback. Closes #128
Browse files Browse the repository at this point in the history
Allow audio conversions of floating types other than float32.

Setup AudioDevice as a context manager and add a `__repr__` method.
  • Loading branch information
HexDecimal committed May 28, 2023
1 parent 37ec7c0 commit eaf4571
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 4 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version
- Added PathLike support to more libtcodpy functions.
- New `tcod.sdl.mouse.show` function for querying or setting mouse visibility.
- New class method `tcod.image.Image.from_file` to load images with. This replaces `tcod.image_load`.
- `tcod.sdl.audio.AudioDevice` is now a context manager.

### Changed
- SDL audio conversion will now pass unconvertible floating types as float32 instead of raising.

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

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

## [15.0.3] - 2023-05-25
### Deprecated
Expand Down
60 changes: 56 additions & 4 deletions tcod/sdl/audio.py
Expand Up @@ -47,11 +47,12 @@
import sys
import threading
import time
from types import TracebackType
from typing import Any, Callable, Hashable, Iterator

import numpy as np
from numpy.typing import ArrayLike, DTypeLike, NDArray
from typing_extensions import Final, Literal
from typing_extensions import Final, Literal, Self

import tcod.sdl.sys
from tcod.loader import ffi, lib
Expand Down Expand Up @@ -110,6 +111,9 @@ def convert_audio(
.. versionadded:: 13.6
.. versionchanged:: Unreleased
Now converts floating types to `np.float32` when SDL doesn't support the specific format.
.. seealso::
:any:`AudioDevice.convert`
"""
Expand All @@ -123,8 +127,26 @@ def convert_audio(
in_channels = in_array.shape[1]
in_format = _get_format(in_array.dtype)
out_sdl_format = _get_format(out_format)
if _check(lib.SDL_BuildAudioCVT(cvt, in_format, in_channels, in_rate, out_sdl_format, out_channels, out_rate)) == 0:
return in_array # No conversion needed.
try:
if (
_check(lib.SDL_BuildAudioCVT(cvt, in_format, in_channels, in_rate, out_sdl_format, out_channels, out_rate))
== 0
):
return in_array # No conversion needed.
except RuntimeError as exc:
if ( # SDL now only supports float32, but later versions may add more support for more formats.
exc.args[0] == "Invalid source format"
and np.issubdtype(in_array.dtype, np.floating)
and in_array.dtype != np.float32
):
return convert_audio( # Try again with float32
in_array.astype(np.float32),
in_rate,
out_rate=out_rate,
out_format=out_format,
out_channels=out_channels,
)
raise
# Upload to the SDL_AudioCVT buffer.
cvt.len = in_array.itemsize * in_array.size
out_buffer = cvt.buf = ffi.new("uint8_t[]", cvt.len * cvt.len_mult)
Expand All @@ -144,6 +166,9 @@ class AudioDevice:
When you use this object directly the audio passed to :any:`queue_audio` is always played synchronously.
For more typical asynchronous audio you should pass an AudioDevice to :any:`BasicMixer`.
.. versionchanged:: Unreleased
Can now be used as a context which will close the device on exit.
"""

def __init__(
Expand Down Expand Up @@ -176,6 +201,23 @@ def __init__(
self._handle: Any | None = None
self._callback: Callable[[AudioDevice, NDArray[Any]], None] = self.__default_callback

def __repr__(self) -> str:
"""Return a representation of this device."""
items = [
f"{self.__class__.__name__}(device_id={self.device_id})",
f"frequency={self.frequency}",
f"is_capture={self.is_capture}",
f"format={self.format}",
f"channels={self.channels}",
f"buffer_samples={self.buffer_samples}",
f"buffer_bytes={self.buffer_bytes}",
]
if self.silence:
items.append(f"silence={self.silence}")
if self._handle is not None:
items.append(f"callback={self._callback}")
return f"""<{" ".join(items)}>"""

@property
def callback(self) -> Callable[[AudioDevice, NDArray[Any]], None]:
"""If the device was opened with a callback enabled, then you may get or set the callback with this attribute."""
Expand Down Expand Up @@ -288,6 +330,16 @@ def close(self) -> None:
lib.SDL_CloseAudioDevice(self.device_id)
del self.device_id

def __enter__(self) -> Self:
"""Return self and enter a managed context."""
return self

def __exit__(
self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
) -> None:
"""Close the device when exiting the context."""
self.close()

@staticmethod
def __default_callback(device: AudioDevice, stream: NDArray[Any]) -> None:
stream[...] = device.silence
Expand Down Expand Up @@ -487,7 +539,7 @@ class _AudioCallbackUserdata:
@ffi.def_extern() # type: ignore
def _sdl_audio_callback(userdata: Any, stream: Any, length: int) -> None:
"""Handle audio device callbacks."""
data: _AudioCallbackUserdata = ffi.from_handle(userdata)()
data: _AudioCallbackUserdata = ffi.from_handle(userdata)
device = data.device
buffer = np.frombuffer(ffi.buffer(stream, length), dtype=device.format).reshape(-1, device.channels)
device._callback(device, buffer)
Expand Down

0 comments on commit eaf4571

Please sign in to comment.