@@ -179,10 +179,27 @@ def __default_callback(device: AudioDevice, stream: NDArray[Any]) -> None:
179179 stream [...] = device .silence
180180
181181
182+ class _LoopSoundFunc :
183+ def __init__ (self , sound : NDArray [Any ], loops : int , on_end : Optional [Callable [[Channel ], None ]]):
184+ self .sound = sound
185+ self .loops = loops
186+ self .on_end = on_end
187+
188+ def __call__ (self , channel : Channel ) -> None :
189+ if not self .loops :
190+ if self .on_end is not None :
191+ self .on_end (channel )
192+ return
193+ channel .play (self .sound , volume = channel .volume , on_end = self )
194+ if self .loops > 0 :
195+ self .loops -= 1
196+
197+
182198class Channel :
183199 mixer : Mixer
184200
185201 def __init__ (self ) -> None :
202+ self ._lock = threading .RLock ()
186203 self .volume : Union [float , Tuple [float , ...]] = 1.0
187204 self .sound_queue : List [NDArray [Any ]] = []
188205 self .on_end_callback : Optional [Callable [[Channel ], None ]] = None
@@ -195,10 +212,17 @@ def play(
195212 self ,
196213 sound : ArrayLike ,
197214 * ,
215+ volume : Union [float , Tuple [float , ...]] = 1.0 ,
216+ loops : int = 0 ,
198217 on_end : Optional [Callable [[Channel ], None ]] = None ,
199218 ) -> None :
200- self .sound_queue [:] = [self ._verify_audio_sample (sound )]
201- self .on_end_callback = on_end
219+ sound = self ._verify_audio_sample (sound )
220+ with self ._lock :
221+ self .volume = volume
222+ self .sound_queue [:] = [sound ]
223+ self .on_end_callback = on_end
224+ if loops :
225+ self .on_end_callback = _LoopSoundFunc (sound , loops , on_end )
202226
203227 def _verify_audio_sample (self , sample : ArrayLike ) -> NDArray [Any ]:
204228 """Verify an audio sample is valid and return it as a Numpy array."""
@@ -209,27 +233,29 @@ def _verify_audio_sample(self, sample: ArrayLike) -> NDArray[Any]:
209233 return array
210234
211235 def _on_mix (self , stream : NDArray [Any ]) -> None :
212- while self .sound_queue and stream .size :
213- buffer = self .sound_queue [0 ]
214- if buffer .shape [0 ] > stream .shape [0 ]:
215- # Mix part of the buffer into the stream.
216- stream [:] += buffer [: stream .shape [0 ]] * self .volume
217- self .sound_queue [0 ] = buffer [stream .shape [0 ] :]
218- break # Stream was filled.
219- # Remaining buffer fits the stream array.
220- stream [: buffer .shape [0 ]] += buffer * self .volume
221- stream = stream [buffer .shape [0 ] :]
222- self .sound_queue .pop (0 )
223- if not self .sound_queue and self .on_end_callback is not None :
224- self .on_end_callback (self )
236+ with self ._lock :
237+ while self .sound_queue and stream .size :
238+ buffer = self .sound_queue [0 ]
239+ if buffer .shape [0 ] > stream .shape [0 ]:
240+ # Mix part of the buffer into the stream.
241+ stream [:] += buffer [: stream .shape [0 ]] * self .volume
242+ self .sound_queue [0 ] = buffer [stream .shape [0 ] :]
243+ break # Stream was filled.
244+ # Remaining buffer fits the stream array.
245+ stream [: buffer .shape [0 ]] += buffer * self .volume
246+ stream = stream [buffer .shape [0 ] :]
247+ self .sound_queue .pop (0 )
248+ if not self .sound_queue and self .on_end_callback is not None :
249+ self .on_end_callback (self )
225250
226251 def fadeout (self , time : float ) -> None :
227252 assert time >= 0
228- time_samples = round (time * self .mixer .device .frequency ) + 1
229- buffer : NDArray [np .float32 ] = np .zeros ((time_samples , self .mixer .device .channels ), np .float32 )
230- self ._on_mix (buffer )
231- buffer *= np .linspace (1.0 , 0.0 , time_samples + 1 , endpoint = False )[1 :]
232- self .sound_queue [:] = [buffer ]
253+ with self ._lock :
254+ time_samples = round (time * self .mixer .device .frequency ) + 1
255+ buffer : NDArray [np .float32 ] = np .zeros ((time_samples , self .mixer .device .channels ), np .float32 )
256+ self ._on_mix (buffer )
257+ buffer *= np .linspace (1.0 , 0.0 , time_samples + 1 , endpoint = False )[1 :]
258+ self .sound_queue [:] = [buffer ]
233259
234260 def stop (self ) -> None :
235261 self .fadeout (0.0005 )
@@ -240,6 +266,7 @@ def __init__(self, device: AudioDevice):
240266 assert device .format == np .float32
241267 super ().__init__ (daemon = True )
242268 self .device = device
269+ self ._lock = threading .RLock ()
243270
244271 def run (self ) -> None :
245272 buffer = np .full (
@@ -263,32 +290,37 @@ def __init__(self, device: AudioDevice):
263290 self .channels : Dict [Hashable , Channel ] = {}
264291
265292 def get_channel (self , key : Hashable ) -> Channel :
266- if key not in self .channels :
267- self .channels [key ] = Channel ()
268- self .channels [key ].mixer = self
269- return self .channels [key ]
293+ with self ._lock :
294+ if key not in self .channels :
295+ self .channels [key ] = Channel ()
296+ self .channels [key ].mixer = self
297+ return self .channels [key ]
270298
271299 def get_free_channel (self ) -> Channel :
272- i = 0
273- while True :
274- if not self .get_channel (i ).busy :
275- return self .channels [i ]
276- i += 1
300+ with self ._lock :
301+ i = 0
302+ while True :
303+ if not self .get_channel (i ).busy :
304+ return self .channels [i ]
305+ i += 1
277306
278307 def play (
279308 self ,
280309 sound : ArrayLike ,
281310 * ,
311+ volume : Union [float , Tuple [float , ...]] = 1.0 ,
312+ loops : int = 0 ,
282313 on_end : Optional [Callable [[Channel ], None ]] = None ,
283314 ) -> Channel :
284315 channel = self .get_free_channel ()
285- channel .play (sound , on_end = on_end )
316+ channel .play (sound , volume = volume , loops = loops , on_end = on_end )
286317 return channel
287318
288319 def on_stream (self , stream : NDArray [Any ]) -> None :
289320 super ().on_stream (stream )
290- for channel in list (self .channels .values ()):
291- channel ._on_mix (stream )
321+ with self ._lock :
322+ for channel in list (self .channels .values ()):
323+ channel ._on_mix (stream )
292324
293325
294326class _AudioCallbackUserdata :
0 commit comments