33import sys
44import threading
55import time
6- import weakref
76from typing import Any , Iterator , List , Optional
87
98import numpy as np
@@ -27,7 +26,7 @@ def _get_format(format: DTypeLike) -> int:
2726 if byteorder == "=" :
2827 byteorder = "<" if sys .byteorder == "little" else ">"
2928
30- return ( # type: ignore
29+ return int (
3130 bitsize
3231 | (lib .SDL_AUDIO_MASK_DATATYPE * is_float )
3332 | (lib .SDL_AUDIO_MASK_ENDIAN * (byteorder == ">" ))
@@ -51,57 +50,45 @@ def _dtype_from_format(format: int) -> np.dtype[Any]:
5150
5251
5352class AudioDevice :
53+ """An SDL audio device."""
54+
5455 def __init__ (
5556 self ,
56- device : Optional [str ] = None ,
57- capture : bool = False ,
58- * ,
59- frequency : int = 44100 ,
60- format : DTypeLike = np .float32 ,
61- channels : int = 2 ,
62- samples : int = 0 ,
63- allowed_changes : int = 0 ,
57+ device_id : int ,
58+ capture : bool ,
59+ spec : Any , # SDL_AudioSpec*
6460 ):
65- self .__sdl_subsystems = tcod .sdl .sys ._ScopeInit (tcod .sdl .sys .Subsystem .AUDIO )
66- self .__handle = ffi .new_handle (weakref .ref (self ))
67- desired = ffi .new (
68- "SDL_AudioSpec*" ,
69- {
70- "freq" : frequency ,
71- "format" : _get_format (format ),
72- "channels" : channels ,
73- "samples" : samples ,
74- "callback" : ffi .NULL ,
75- "userdata" : self .__handle ,
76- },
77- )
78- obtained = ffi .new ("SDL_AudioSpec*" )
79- self .device_id = lib .SDL_OpenAudioDevice (
80- ffi .NULL if device is None else device .encode ("utf-8" ),
81- capture ,
82- desired ,
83- obtained ,
84- allowed_changes ,
85- )
86- assert self .device_id != 0 , _get_error ()
87- self .frequency = obtained .freq
61+ assert device_id >= 0
62+ assert ffi .typeof (spec ) is ffi .typeof ("SDL_AudioSpec*" )
63+ assert spec
64+ self .device_id = device_id
65+ self .spec = spec
66+ self .frequency = spec .freq
8867 self .is_capture = capture
89- self .format = _dtype_from_format (obtained .format )
90- self .channels = int (obtained .channels )
91- self .silence = int (obtained .silence )
92- self .samples = int (obtained .samples )
93- self .buffer_size = int (obtained .size )
94- self .unpause ()
68+ self .format = _dtype_from_format (spec .format )
69+ self .channels = int (spec .channels )
70+ self .silence = int (spec .silence )
71+ self .samples = int (spec .samples )
72+ self .buffer_size = int (spec .size )
73+ self ._callback = self . __default_callback
9574
9675 @property
9776 def _sample_size (self ) -> int :
9877 return self .format .itemsize * self .channels
9978
100- def pause (self ) -> None :
101- lib .SDL_PauseAudioDevice (self .device_id , True )
79+ @property
80+ def stopped (self ) -> bool :
81+ """Is True if the device has failed or was closed."""
82+ return bool (lib .SDL_GetAudioDeviceStatus (self .device_id ) != lib .SDL_AUDIO_STOPPED )
83+
84+ @property
85+ def paused (self ) -> bool :
86+ """Get or set the device paused state."""
87+ return bool (lib .SDL_GetAudioDeviceStatus (self .device_id ) != lib .SDL_AUDIO_PLAYING )
10288
103- def unpause (self ) -> None :
104- lib .SDL_PauseAudioDevice (self .device_id , False )
89+ @paused .setter
90+ def paused (self , value : bool ) -> None :
91+ lib .SDL_PauseAudioDevice (self .device_id , value )
10592
10693 def _verify_array_format (self , samples : NDArray [Any ]) -> NDArray [Any ]:
10794 if samples .dtype != self .format :
@@ -121,12 +108,14 @@ def queued_audio_bytes(self) -> int:
121108 return int (lib .SDL_GetQueuedAudioSize (self .device_id ))
122109
123110 def queue_audio (self , samples : ArrayLike ) -> None :
111+ """Append audio samples to the audio data queue."""
124112 assert not self .is_capture
125113 samples = self ._convert_array (samples )
126114 buffer = ffi .from_buffer (samples )
127115 lib .SDL_QueueAudio (self .device_id , buffer , len (buffer ))
128116
129117 def dequeue_audio (self ) -> NDArray [Any ]:
118+ """Return the audio buffer from a capture stream."""
130119 assert self .is_capture
131120 out_samples = self .queued_audio_bytes // self ._sample_size
132121 out = np .empty ((out_samples , self .channels ), self .format )
@@ -140,31 +129,30 @@ def __del__(self) -> None:
140129 self .close ()
141130
142131 def close (self ) -> None :
132+ """Close this audio device."""
143133 if not self .device_id :
144134 return
145135 lib .SDL_CloseAudioDevice (self .device_id )
146136 self .device_id = 0
147137
148- @staticmethod
149- def __default_callback (stream : NDArray [Any ], silence : int ) -> None :
150- stream [...] = silence
138+ def __default_callback (self , stream : NDArray [Any ]) -> None :
139+ stream [...] = self .silence
151140
152141
153142class Mixer (threading .Thread ):
154143 def __init__ (self , device : AudioDevice ):
155144 super ().__init__ (daemon = True )
156145 self .device = device
157- self .device .unpause ()
158- self .start ()
159146
160147 def run (self ) -> None :
161148 buffer = np .full ((self .device .samples , self .device .channels ), self .device .silence , dtype = self .device .format )
162149 while True :
163- time .sleep (0.001 )
164- if self .device .queued_audio_bytes == 0 :
165- self .on_stream (buffer )
166- self .device .queue_audio (buffer )
167- buffer [:] = self .device .silence
150+ if self .device .queued_audio_bytes > 0 :
151+ time .sleep (0.001 )
152+ continue
153+ self .on_stream (buffer )
154+ self .device .queue_audio (buffer )
155+ buffer [:] = self .device .silence
168156
169157 def on_stream (self , stream : NDArray [Any ]) -> None :
170158 pass
@@ -197,7 +185,8 @@ def _sdl_audio_callback(userdata: Any, stream: Any, length: int) -> None:
197185 """Handle audio device callbacks."""
198186 device : Optional [AudioDevice ] = ffi .from_handle (userdata )()
199187 assert device is not None
200- _ = np .frombuffer (ffi .buffer (stream , length ), dtype = device .format ).reshape (- 1 , device .channels )
188+ buffer = np .frombuffer (ffi .buffer (stream , length ), dtype = device .format ).reshape (- 1 , device .channels )
189+ device ._callback (buffer )
201190
202191
203192def _get_devices (capture : bool ) -> Iterator [str ]:
@@ -216,3 +205,41 @@ def get_devices() -> Iterator[str]:
216205def get_capture_devices () -> Iterator [str ]:
217206 """Iterate over the available audio capture devices."""
218207 yield from _get_devices (capture = True )
208+
209+
210+ def open (
211+ name : Optional [str ] = None ,
212+ capture : bool = False ,
213+ * ,
214+ frequency : int = 44100 ,
215+ format : DTypeLike = np .float32 ,
216+ channels : int = 2 ,
217+ samples : int = 0 ,
218+ allowed_changes : int = 0 ,
219+ paused : bool = False ,
220+ ) -> AudioDevice :
221+ """Open an audio device for playback or capture."""
222+ tcod .sdl .sys .init (tcod .sdl .sys .Subsystem .AUDIO )
223+ desired = ffi .new (
224+ "SDL_AudioSpec*" ,
225+ {
226+ "freq" : frequency ,
227+ "format" : _get_format (format ),
228+ "channels" : channels ,
229+ "samples" : samples ,
230+ "callback" : ffi .NULL ,
231+ "userdata" : ffi .NULL ,
232+ },
233+ )
234+ obtained = ffi .new ("SDL_AudioSpec*" )
235+ device_id : int = lib .SDL_OpenAudioDevice (
236+ ffi .NULL if name is None else name .encode ("utf-8" ),
237+ capture ,
238+ desired ,
239+ obtained ,
240+ allowed_changes ,
241+ )
242+ assert device_id >= 0 , _get_error ()
243+ device = AudioDevice (device_id , capture , obtained )
244+ device .paused = paused
245+ return device
0 commit comments