4040from comtypes .hresult import S_OK
4141import atexit
4242import weakref
43+ import time
4344import garbageHandler
4445import winKernel
4546import wave
4849import os .path
4950import extensionPoints
5051import NVDAHelper
52+ import core
5153
5254
5355__all__ = (
@@ -759,6 +761,13 @@ class WasapiWavePlayer(garbageHandler.TrackedObject):
759761 #: This allows us to have a single callback in the class rather than on
760762 #: each instance, which prevents reference cycles.
761763 _instances = weakref .WeakValueDictionary ()
764+ #: How long (in seconds) to wait before closing an audio stream that hasn't
765+ #: played.
766+ _STREAM_CLOSE_TIMEOUT : int = 60
767+ #: How often (in ms) to check whether streams should be closed.
768+ _STREAM_CLOSE_CHECK_INTERVAL : int = 20000
769+ #: Whether there is a pending stream close check.
770+ _isStreamCloseCheckPending : bool = False
762771
763772 def __init__ (
764773 self ,
@@ -807,7 +816,9 @@ def __init__(
807816 )
808817 self ._doneCallbacks = {}
809818 self ._instances [self ._player ] = self
819+ self ._isOpen : bool
810820 self .open ()
821+ self ._lastActiveTime : float = time .time ()
811822
812823 @wasPlay_callback
813824 def _callback (cppPlayer , feedId ):
@@ -845,11 +856,16 @@ def open(self):
845856 raise
846857 WasapiWavePlayer .audioDeviceError_static = False
847858 self ._setVolumeFromConfig ()
859+ self ._isOpen = True
848860
849861 def close (self ):
850- """For WASAPI, this just stops playback .
862+ """Close the output device .
851863 """
864+ if not self ._isOpen :
865+ return
852866 self .stop ()
867+ NVDAHelper .localLib .wasPlay_close (self ._player )
868+ self ._isOpen = False
853869
854870 def feed (
855871 self ,
@@ -868,6 +884,7 @@ def feed(
868884 @param onDone: Function to call when this chunk has finished playing.
869885 @raise WindowsError: If there was an error playing the audio.
870886 """
887+ self .open ()
871888 if self ._audioDucker :
872889 self ._audioDucker .enable ()
873890 feedId = c_uint () if onDone else None
@@ -879,6 +896,8 @@ def feed(
879896 )
880897 if onDone :
881898 self ._doneCallbacks [feedId .value ] = onDone
899+ self ._lastActiveTime = time .time ()
900+ self ._scheduleStreamCloseCheck ()
882901
883902 def sync (self ):
884903 """Synchronise with playback.
@@ -897,6 +916,8 @@ def idle(self):
897916 def stop (self ):
898917 """Stop playback.
899918 """
919+ if not self ._isOpen :
920+ return
900921 if self ._audioDucker :
901922 self ._audioDucker .disable ()
902923 NVDAHelper .localLib .wasPlay_stop (self ._player )
@@ -950,6 +971,45 @@ def _setVolumeFromConfig(self):
950971 volume = synth .volume
951972 self .setVolume (all = volume / 100 )
952973
974+ @classmethod
975+ def _scheduleStreamCloseCheck (cls ):
976+ if not cls ._isStreamCloseCheckPending :
977+ core .callLater (
978+ cls ._STREAM_CLOSE_CHECK_INTERVAL ,
979+ cls ._streamCloseCheck
980+ )
981+ cls ._isStreamCloseCheckPending = True
982+
983+ @classmethod
984+ def _streamCloseCheck (cls ):
985+ """Check whether there are open audio streams that should be considered
986+ inactive. If there are any, close them. If there are open streams that
987+ aren't ready to be closed yet, schedule another check.
988+ This is necessary because holding streams open can prevent sleep on some
989+ systems.
990+ We do this in a single, class-wide check rather than separately for each
991+ instance to avoid continually resetting a timer for each call to feed().
992+ Resetting timers from another thread involves queuing to the main thread.
993+ Doing that for every chunk of audio would not be very efficient.
994+ Doing this with a class-wide check means that some checks might not take any
995+ action and some streams might be kept open for a little longer than the
996+ timeout, but this isn't problematic for our purposes.
997+ """
998+ cls ._isStreamCloseCheckPending = False
999+ threshold = time .time () - cls ._STREAM_CLOSE_TIMEOUT
1000+ stillOpenStream = False
1001+ for player in cls ._instances .values ():
1002+ if not player ._isOpen :
1003+ continue
1004+ if player ._lastActiveTime <= threshold :
1005+ player .close ()
1006+ else :
1007+ stillOpenStream = True
1008+ if stillOpenStream :
1009+ # There's still at least one open stream that wasn't ready to be closed.
1010+ # Schedule another check here in case feed isn't called for a while.
1011+ cls ._scheduleStreamCloseCheck ()
1012+
9531013 @staticmethod
9541014 def _getDevices ():
9551015 rawDevs = BSTR ()
@@ -988,6 +1048,7 @@ def initialize():
9881048 for func in (
9891049 NVDAHelper .localLib .wasPlay_startup ,
9901050 NVDAHelper .localLib .wasPlay_open ,
1051+ NVDAHelper .localLib .wasPlay_close ,
9911052 NVDAHelper .localLib .wasPlay_feed ,
9921053 NVDAHelper .localLib .wasPlay_stop ,
9931054 NVDAHelper .localLib .wasPlay_sync ,
0 commit comments