Skip to content

Commit 77cb061

Browse files
authored
Merge 565fb45 into 5d1b0e8
2 parents 5d1b0e8 + 565fb45 commit 77cb061

3 files changed

Lines changed: 76 additions & 1 deletion

File tree

nvdaHelper/local/nvdaHelperLocal.def

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ EXPORTS
7070
wasPlay_create
7171
wasPlay_destroy
7272
wasPlay_open
73+
wasPlay_close
7374
wasPlay_feed
7475
wasPlay_stop
7576
wasPlay_sync

nvdaHelper/local/wasapi.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ class WasapiPlayer {
161161
*/
162162
HRESULT open(bool force=false);
163163

164+
HRESULT close();
165+
164166
/**
165167
* Feed a chunk of audio.
166168
* If not null, id will be set to a number used to identify the audio
@@ -277,6 +279,13 @@ HRESULT WasapiPlayer::open(bool force) {
277279
return S_OK;
278280
}
279281

282+
HRESULT WasapiPlayer::close() {
283+
client = nullptr;
284+
render = nullptr;
285+
clock = nullptr;
286+
return S_OK;
287+
}
288+
280289
HRESULT WasapiPlayer::feed(unsigned char* data, unsigned int size,
281290
unsigned int* id
282291
) {
@@ -530,6 +539,10 @@ HRESULT wasPlay_open(WasapiPlayer* player) {
530539
return player->open();
531540
}
532541

542+
HRESULT wasPlay_close(WasapiPlayer* player) {
543+
return player->close();
544+
}
545+
533546
HRESULT wasPlay_feed(WasapiPlayer* player, unsigned char* data,
534547
unsigned int size, unsigned int* id
535548
) {

source/nvwave.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from comtypes.hresult import S_OK
4141
import atexit
4242
import weakref
43+
import time
4344
import garbageHandler
4445
import winKernel
4546
import wave
@@ -48,6 +49,7 @@
4849
import os.path
4950
import extensionPoints
5051
import 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

Comments
 (0)