88import threading
99from typing import Optional , Callable , Union , TYPE_CHECKING
1010import logging
11+ from datetime import datetime
1112
1213import websockets
1314
1415from ...utils import verboselogs
15- from .constants import LOGGING , CHANNELS , RATE , CHUNK , TIMEOUT
16+ from .constants import LOGGING , CHANNELS , RATE , CHUNK , TIMEOUT , PLAYBACK_DELTA
1617
1718if TYPE_CHECKING :
1819 import pyaudio
1920
21+ HALF_SECOND = 0.5
22+
2023
2124class Speaker : # pylint: disable=too-many-instance-attributes
2225 """
@@ -33,6 +36,11 @@ class Speaker: # pylint: disable=too-many-instance-attributes
3336 _channels : int
3437 _output_device_index : Optional [int ] = None
3538
39+ # last time we received audio
40+ _last_datagram : datetime = datetime .now ()
41+ _last_play_delta_in_ms : int
42+ _lock_wait : threading .Lock
43+
3644 _queue : queue .Queue
3745 _exit : threading .Event
3846
@@ -56,6 +64,7 @@ def __init__(
5664 rate : int = RATE ,
5765 chunk : int = CHUNK ,
5866 channels : int = CHANNELS ,
67+ last_play_delta_in_ms : int = PLAYBACK_DELTA ,
5968 output_device_index : Optional [int ] = None ,
6069 ): # pylint: disable=too-many-positional-arguments
6170 # dynamic import of pyaudio as not to force the requirements on the SDK (and users)
@@ -68,11 +77,15 @@ def __init__(
6877 self ._exit = threading .Event ()
6978 self ._queue = queue .Queue ()
7079
80+ self ._last_datagram = datetime .now ()
81+ self ._lock_wait = threading .Lock ()
82+
7183 self ._audio = pyaudio .PyAudio ()
7284 self ._chunk = chunk
7385 self ._rate = rate
7486 self ._format = pyaudio .paInt16
7587 self ._channels = channels
88+ self ._last_play_delta_in_ms = last_play_delta_in_ms
7689 self ._output_device_index = output_device_index
7790
7891 self ._push_callback_org = push_callback
@@ -192,6 +205,42 @@ def start(self, active_loop: Optional[asyncio.AbstractEventLoop] = None) -> bool
192205
193206 return True
194207
208+ def wait_for_complete (self ):
209+ """
210+ This method will block until the speak is done playing sound.
211+ """
212+ self ._logger .debug ("Speaker.wait_for_complete ENTER" )
213+
214+ delta_in_ms = float (self ._last_play_delta_in_ms )
215+ self ._logger .debug ("Last Play delta: %f" , delta_in_ms )
216+
217+ # set to now
218+ with self ._lock_wait :
219+ self ._last_datagram = datetime .now ()
220+
221+ while True :
222+ # sleep for a bit
223+ self ._exit .wait (HALF_SECOND )
224+
225+ # check if we should exit
226+ if self ._exit .is_set ():
227+ self ._logger .debug ("Exiting wait_for_complete _exit is set" )
228+ break
229+
230+ # check the time
231+ with self ._lock_wait :
232+ delta = datetime .now () - self ._last_datagram
233+ diff_in_ms = delta .total_seconds () * 1000
234+ if diff_in_ms < delta_in_ms :
235+ self ._logger .debug ("LastPlay delta is less than threshold" )
236+ continue
237+
238+ # if we get here, we are done playing audio
239+ self ._logger .debug ("LastPlay delta is greater than threshold. Exit wait!" )
240+ break
241+
242+ self ._logger .debug ("Speaker.wait_for_complete LEAVE" )
243+
195244 def _start_receiver (self ):
196245 # Check if the socket is an asyncio WebSocket
197246 if inspect .iscoroutinefunction (self ._pull_callback_org ):
@@ -315,6 +364,8 @@ def _play(self, audio_out, stream, stop):
315364 while not stop .is_set ():
316365 try :
317366 data = audio_out .get (True , TIMEOUT )
367+ with self ._lock_wait :
368+ self ._last_datagram = datetime .now ()
318369 stream .write (data )
319370 except queue .Empty :
320371 pass
0 commit comments