33import sys
44import threading
55import time
6- from typing import Any , Iterator , List , Optional
6+ from typing import Any , Callable , Dict , Hashable , Iterator , List , Optional , Tuple , Union
77
88import numpy as np
99from numpy .typing import ArrayLike , DTypeLike , NDArray
@@ -139,8 +139,65 @@ def __default_callback(self, stream: NDArray[Any]) -> None:
139139 stream [...] = self .silence
140140
141141
142+ class Channel :
143+ mixer : Mixer
144+
145+ def __init__ (self ) -> None :
146+ self .volume : Union [float , Tuple [float , ...]] = 1.0
147+ self .sound_queue : List [NDArray [Any ]] = []
148+ self .on_end_callback : Optional [Callable [[Channel ], None ]] = None
149+
150+ @property
151+ def busy (self ) -> bool :
152+ return bool (self .sound_queue )
153+
154+ def play (
155+ self ,
156+ sound : ArrayLike ,
157+ * ,
158+ on_end : Optional [Callable [[Channel ], None ]] = None ,
159+ ) -> None :
160+ self .sound_queue [:] = [self ._verify_audio_sample (sound )]
161+ self .on_end_callback = on_end
162+
163+ def _verify_audio_sample (self , sample : ArrayLike ) -> NDArray [Any ]:
164+ """Verify an audio sample is valid and return it as a Numpy array."""
165+ array : NDArray [Any ] = np .asarray (sample )
166+ assert array .dtype == self .mixer .device .format
167+ if len (array .shape ) == 1 :
168+ array = array [:, np .newaxis ]
169+ return array
170+
171+ def _on_mix (self , stream : NDArray [Any ]) -> None :
172+ while self .sound_queue and stream .size :
173+ buffer = self .sound_queue [0 ]
174+ if buffer .shape [0 ] > stream .shape [0 ]:
175+ # Mix part of the buffer into the stream.
176+ stream [:] += buffer [: stream .shape [0 ]] * self .volume
177+ self .sound_queue [0 ] = buffer [stream .shape [0 ] :]
178+ break # Stream was filled.
179+ # Remaining buffer fits the stream array.
180+ stream [: buffer .shape [0 ]] += buffer * self .volume
181+ stream = stream [buffer .shape [0 ] :]
182+ self .sound_queue .pop (0 )
183+ if not self .sound_queue and self .on_end_callback is not None :
184+ self .on_end_callback (self )
185+
186+ def fadeout (self , time : float ) -> None :
187+ assert time >= 0
188+ time_samples = round (time * self .mixer .device .frequency ) + 1
189+ buffer : NDArray [np .float32 ] = np .zeros ((time_samples , self .mixer .device .channels ), np .float32 )
190+ self ._on_mix (buffer )
191+ buffer *= np .linspace (1.0 , 0.0 , time_samples + 1 , endpoint = False )[1 :]
192+ self .sound_queue [:] = [buffer ]
193+
194+ def stop (self ) -> None :
195+ self .fadeout (0.0005 )
196+
197+
142198class Mixer (threading .Thread ):
143199 def __init__ (self , device : AudioDevice ):
200+ assert device .format == np .float32
144201 super ().__init__ (daemon = True )
145202 self .device = device
146203
@@ -161,23 +218,35 @@ def on_stream(self, stream: NDArray[Any]) -> None:
161218class BasicMixer (Mixer ):
162219 def __init__ (self , device : AudioDevice ):
163220 super ().__init__ (device )
164- self .play_buffers : List [ List [ NDArray [ Any ]]] = []
221+ self .channels : Dict [ Hashable , Channel ] = {}
165222
166- def play (self , sound : ArrayLike ) -> None :
167- array = np .asarray (sound , dtype = self .device .format )
168- assert array .size
169- if len (array .shape ) == 1 :
170- array = array [:, np .newaxis ]
171- chunks : List [NDArray [Any ]] = np .split (array , range (0 , len (array ), self .device .samples )[1 :])[::- 1 ]
172- self .play_buffers .append (chunks )
223+ def get_channel (self , key : Hashable ) -> Channel :
224+ if key not in self .channels :
225+ self .channels [key ] = Channel ()
226+ self .channels [key ].mixer = self
227+ return self .channels [key ]
228+
229+ def get_free_channel (self ) -> Channel :
230+ i = 0
231+ while True :
232+ if not self .get_channel (i ).busy :
233+ return self .channels [i ]
234+ i += 1
235+
236+ def play (
237+ self ,
238+ sound : ArrayLike ,
239+ * ,
240+ on_end : Optional [Callable [[Channel ], None ]] = None ,
241+ ) -> Channel :
242+ channel = self .get_free_channel ()
243+ channel .play (sound , on_end = on_end )
244+ return channel
173245
174246 def on_stream (self , stream : NDArray [Any ]) -> None :
175247 super ().on_stream (stream )
176- for chunks in self .play_buffers :
177- chunk = chunks .pop ()
178- stream [: len (chunk )] += chunk
179-
180- self .play_buffers = [chunks for chunks in self .play_buffers if chunks ]
248+ for channel in list (self .channels .values ()):
249+ channel ._on_mix (stream )
181250
182251
183252@ffi .def_extern () # type: ignore
0 commit comments