11from __future__ import annotations
22
3+ import enum
34import sys
45import threading
56import time
67from typing import Any , Callable , Dict , Hashable , Iterator , List , Optional , Tuple , Union
78
89import numpy as np
910from numpy .typing import ArrayLike , DTypeLike , NDArray
11+ from typing_extensions import Literal
1012
1113import tcod .sdl .sys
1214from 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
142158class 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
253273def _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
261281def _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+
279316def 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