4747import sys
4848import threading
4949import time
50+ from dataclasses import dataclass
5051from types import TracebackType
5152from typing import Any , Callable , Hashable , Iterator
5253
@@ -65,7 +66,9 @@ def _get_format(format: DTypeLike) -> int:
6566 assert dt .fields is None
6667 bitsize = dt .itemsize * 8
6768 assert 0 < bitsize <= lib .SDL_AUDIO_MASK_BITSIZE
68- assert dt .str [1 ] in "uif"
69+ if not dt .str [1 ] in "uif" :
70+ msg = f"Unexpected dtype: { dt } "
71+ raise TypeError (msg )
6972 is_signed = dt .str [1 ] != "u"
7073 is_float = dt .str [1 ] == "f"
7174 byteorder = dt .byteorder
@@ -81,7 +84,21 @@ def _get_format(format: DTypeLike) -> int:
8184
8285
8386def _dtype_from_format (format : int ) -> np .dtype [Any ]:
84- """Return a dtype from a SDL_AudioFormat."""
87+ """Return a dtype from a SDL_AudioFormat.
88+
89+ >>> _dtype_from_format(tcod.lib.AUDIO_F32LSB)
90+ dtype('float32')
91+ >>> _dtype_from_format(tcod.lib.AUDIO_F32MSB)
92+ dtype('>f4')
93+ >>> _dtype_from_format(tcod.lib.AUDIO_S16LSB)
94+ dtype('int16')
95+ >>> _dtype_from_format(tcod.lib.AUDIO_S16MSB)
96+ dtype('>i2')
97+ >>> _dtype_from_format(tcod.lib.AUDIO_U16LSB)
98+ dtype('uint16')
99+ >>> _dtype_from_format(tcod.lib.AUDIO_U16MSB)
100+ dtype('>u2')
101+ """
85102 bitsize = format & lib .SDL_AUDIO_MASK_BITSIZE
86103 assert bitsize % 8 == 0
87104 byte_size = bitsize // 8
@@ -203,6 +220,8 @@ def __init__(
203220
204221 def __repr__ (self ) -> str :
205222 """Return a representation of this device."""
223+ if self .stopped :
224+ return f"<{ self .__class__ .__name__ } () stopped=True>"
206225 items = [
207226 f"{ self .__class__ .__name__ } (device_id={ self .device_id } )" ,
208227 f"frequency={ self .frequency } " ,
@@ -211,7 +230,9 @@ def __repr__(self) -> str:
211230 f"channels={ self .channels } " ,
212231 f"buffer_samples={ self .buffer_samples } " ,
213232 f"buffer_bytes={ self .buffer_bytes } " ,
233+ f"paused={ self .paused } " ,
214234 ]
235+
215236 if self .silence :
216237 items .append (f"silence={ self .silence } " )
217238 if self ._handle is not None :
@@ -241,7 +262,9 @@ def _sample_size(self) -> int:
241262 @property
242263 def stopped (self ) -> bool :
243264 """Is True if the device has failed or was closed."""
244- return bool (lib .SDL_GetAudioDeviceStatus (self .device_id ) != lib .SDL_AUDIO_STOPPED )
265+ if not hasattr (self , "device_id" ):
266+ return True
267+ return bool (lib .SDL_GetAudioDeviceStatus (self .device_id ) == lib .SDL_AUDIO_STOPPED )
245268
246269 @property
247270 def paused (self ) -> bool :
@@ -404,7 +427,9 @@ def play(
404427 def _verify_audio_sample (self , sample : ArrayLike ) -> NDArray [Any ]:
405428 """Verify an audio sample is valid and return it as a Numpy array."""
406429 array : NDArray [Any ] = np .asarray (sample )
407- assert array .dtype == self .mixer .device .format
430+ if array .dtype != self .mixer .device .format :
431+ msg = f"Audio sample must be dtype={ self .mixer .device .format } , input was dtype={ array .dtype } "
432+ raise TypeError (msg )
408433 if len (array .shape ) == 1 :
409434 array = array [:, np .newaxis ]
410435 return array
@@ -434,7 +459,7 @@ def fadeout(self, time: float) -> None:
434459 time_samples = round (time * self .mixer .device .frequency ) + 1
435460 buffer : NDArray [np .float32 ] = np .zeros ((time_samples , self .mixer .device .channels ), np .float32 )
436461 self ._on_mix (buffer )
437- buffer *= np .linspace (1.0 , 0.0 , time_samples + 1 , endpoint = False )[1 :]
462+ buffer *= np .linspace (1.0 , 0.0 , time_samples + 1 , endpoint = False )[1 :, np . newaxis ]
438463 self .sound_queue [:] = [buffer ]
439464
440465 def stop (self ) -> None :
@@ -536,13 +561,41 @@ class _AudioCallbackUserdata:
536561 device : AudioDevice
537562
538563
564+ @dataclass
565+ class _UnraisableHookArgs :
566+ exc_type : type [BaseException ]
567+ exc_value : BaseException | None
568+ exc_traceback : TracebackType | None
569+ err_msg : str | None
570+ object : object
571+
572+
573+ class _ProtectedContext :
574+ def __init__ (self , obj : object = None ) -> None :
575+ self .obj = obj
576+
577+ def __enter__ (self ) -> None :
578+ pass
579+
580+ def __exit__ (
581+ self , exc_type : type [BaseException ] | None , value : BaseException | None , traceback : TracebackType | None
582+ ) -> bool :
583+ if exc_type is None :
584+ return False
585+ if sys .version_info < (3 , 8 ):
586+ return False
587+ sys .unraisablehook (_UnraisableHookArgs (exc_type , value , traceback , None , self .obj )) # type: ignore[arg-type]
588+ return True
589+
590+
539591@ffi .def_extern () # type: ignore
540- def _sdl_audio_callback (userdata : Any , stream : Any , length : int ) -> None :
592+ def _sdl_audio_callback (userdata : Any , stream : Any , length : int ) -> None : # noqa: ANN401
541593 """Handle audio device callbacks."""
542594 data : _AudioCallbackUserdata = ffi .from_handle (userdata )
543595 device = data .device
544596 buffer = np .frombuffer (ffi .buffer (stream , length ), dtype = device .format ).reshape (- 1 , device .channels )
545- device ._callback (device , buffer )
597+ with _ProtectedContext (device ):
598+ device ._callback (device , buffer )
546599
547600
548601def _get_devices (capture : bool ) -> Iterator [str ]:
0 commit comments