Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New speech framework including callbacks, beeps, sounds, profile switches and prioritized queuing #7599

Merged
merged 34 commits into from
May 15, 2019
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f1f2692
Enhance nvwave to simplify accurate indexing for speech synthesizers.
jcsteh Sep 13, 2017
0676756
Enhancements to config profile triggers needed for profile switching …
jcsteh Sep 13, 2017
504704d
Add support for callbacks, beeps, sounds, profile switches and uttera…
jcsteh Sep 13, 2017
0fbcda9
Update the espeak synth driver to support the new speech framework.
jcsteh Sep 13, 2017
1ebe910
Update the oneCore synth driver to support the new speech framework.
jcsteh Sep 13, 2017
9479237
Update comtypes to version 1.1.3.
jcsteh Sep 13, 2017
8102f71
Update the sapi5 synth driver to support the new speech framework.
jcsteh Sep 13, 2017
b2c2f74
Fix submodule URL for comtypes. Oops!
jcsteh Sep 14, 2017
4e08767
Merge branch 'master' into i4877SpeechManager
michaelDCurran Oct 26, 2017
ab63b20
Ensure eSpeak emits index callbacks even if the espeak event chunk co…
michaelDCurran Oct 26, 2017
8b87c6b
Remove some debug print statements
michaelDCurran Oct 27, 2017
1167bd1
Merge branch 'master' into i4877SpeechManager
michaelDCurran Dec 5, 2017
c11013f
Ensure eSpeak sets its speech parameters back to user-configured valu…
michaelDCurran Dec 5, 2017
ff2875b
Merge branch 'master' into i4877SpeechManager
michaelDCurran Dec 20, 2017
167f3a3
Merge master, handling conflict from pr 7489 (auto tether to focus/re…
michaelDCurran Feb 2, 2018
3948d72
Merge branch 'master' into i4877SpeechManager
michaelDCurran Jun 4, 2018
6319675
Add a 'priority' keyword argument to all speech.speak* functions all…
michaelDCurran Jun 4, 2018
c615011
Alerts in Chrome and Firefox now are spoken with a higher speech prio…
michaelDCurran Jun 4, 2018
2a2331c
Merge branch 'master' into i4877SpeechManager
michaelDCurran Dec 6, 2018
b2bb189
Unit tests: fake speech functions now must take a 'priority' keyword …
michaelDCurran Dec 6, 2018
ff07ab1
Merge branch 'master' into i4877speechManager
michaelDCurran Mar 29, 2019
8823902
Remove speech compat
michaelDCurran Apr 1, 2019
cd292d9
Merge branch 'master' into i4877SpeechManager
michaelDCurran Apr 26, 2019
670f6fe
synthDriverHandler.handleConfigProfileSwitch was renamed to handlePos…
michaelDCurran Apr 29, 2019
6b2660a
synthDriverHandler.handlePostConfigProfileSwitch: reset speech queues…
michaelDCurran May 3, 2019
db46a20
Remove the audioLogic synthesizer due to its extremely low usage. If…
michaelDCurran May 3, 2019
6adb6d5
Merge branch 'noSpeechCompat' into i4877SpeechManager
michaelDCurran May 6, 2019
66a1120
Speech: correct indentation, which allows profile switching in a spe…
michaelDCurran May 6, 2019
1f8d917
Sapi4 synthDriver: very basic conversion to speechRefactor supporting…
michaelDCurran May 6, 2019
9bccb58
Convert system test synthDriver to speechRefactor so that indexing an…
michaelDCurran May 6, 2019
4e4ff4b
sayAllhandler: move trigger handling into _readText as recommended.
michaelDCurran May 6, 2019
6c7762a
Remove some more speech compat stuff.
michaelDCurran May 7, 2019
8f8bd9b
Speech: BaseCallbackCommand's run method is now abstract. Note that C…
michaelDCurran May 8, 2019
52ab8cb
Speech: fix up some docstrings.
michaelDCurran May 9, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions source/NVDAObjects/IAccessible/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1421,11 +1421,10 @@ def event_alert(self):
api.processPendingEvents()
if self in api.getFocusAncestors():
return
speech.cancelSpeech()
speech.speakObject(self, reason=controlTypes.REASON_FOCUS)
speech.speakObject(self, reason=controlTypes.REASON_FOCUS,priority=speech.SPRI_NOW)
for child in self.recursiveDescendants:
if controlTypes.STATE_FOCUSABLE in child.states:
speech.speakObject(child, reason=controlTypes.REASON_FOCUS)
speech.speakObject(child, reason=controlTypes.REASON_FOCUS,priority=speech.SPRI_NOW)

def event_caret(self):
focus = api.getFocusObject()
Expand Down
24 changes: 20 additions & 4 deletions source/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ def __init__(self):
#: The names of all profiles that have been modified since they were last saved.
self._dirtyProfiles = set()

def _handleProfileSwitch(self):
def _handleProfileSwitch(self, shouldNotify=True):
if not self._shouldHandleProfileSwitch:
self._pendingHandleProfileSwitch = True
return
Expand All @@ -404,7 +404,8 @@ def _handleProfileSwitch(self):
if init:
# We're still initialising, so don't notify anyone about this change.
return
post_configProfileSwitch.notify(prevConf=currentRootSection.dict())
if shouldNotify:
post_configProfileSwitch.notify(prevConf=currentRootSection.dict())

def _initBaseConf(self, factoryDefaults=False):
fn = os.path.join(globalVars.appArgs.configPath, "nvda.ini")
Expand Down Expand Up @@ -686,6 +687,7 @@ def _triggerProfileEnter(self, trigger):
self._suspendedTriggers[trigger] = "enter"
return

log.debug("Activating triggered profile %s" % trigger.profileName)
try:
profile = trigger._profile = self._getProfile(trigger.profileName)
except:
Expand All @@ -698,7 +700,7 @@ def _triggerProfileEnter(self, trigger):
self.profiles.insert(-1, profile)
else:
self.profiles.append(profile)
self._handleProfileSwitch()
self._handleProfileSwitch(trigger._shouldNotifyProfileSwitch)

def _triggerProfileExit(self, trigger):
"""Called by L{ProfileTrigger.exit}}}.
Expand All @@ -717,14 +719,15 @@ def _triggerProfileExit(self, trigger):
profile = trigger._profile
if profile is None:
return
log.debug("Deactivating triggered profile %s" % trigger.profileName)
profile.triggered = False
try:
self.profiles.remove(profile)
except ValueError:
# This is probably due to the user resetting the configuration.
log.debugWarning("Profile not active when exiting trigger")
return
self._handleProfileSwitch()
self._handleProfileSwitch(trigger._shouldNotifyProfileSwitch)

@contextlib.contextmanager
def atomicProfileSwitch(self):
Expand Down Expand Up @@ -1119,6 +1122,12 @@ class ProfileTrigger(object):
Alternatively, you can use this object as a context manager via the with statement;
i.e. this trigger will apply only inside the with block.
"""
#: Whether to notify handlers when activating a triggered profile.
#: This should usually be C{True}, but might be set to C{False} when
#: only specific settings should be applied.
#: For example, when switching profiles during a speech sequence,
#: we only want to apply speech settings, not switch braille displays.
_shouldNotifyProfileSwitch = True

@baseObject.Getter
def spec(self):
Expand All @@ -1128,6 +1137,13 @@ def spec(self):
"""
raise NotImplementedError

@property
def hasProfile(self):
"""Whether this trigger has an associated profile.
@rtype: bool
"""
return self.spec in conf.triggersToProfiles

def enter(self):
"""Signal that this trigger applies.
The associated profile (if any) will be activated.
Expand Down
60 changes: 56 additions & 4 deletions source/nvwave.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,18 @@ class WavePlayer(object):
"""Synchronously play a stream of audio.
To use, construct an instance and feed it waveform audio using L{feed}.
"""
#: Minimum length of buffer (in ms) before audio is played.
MIN_BUFFER_MS = 300
#: Flag used to signal that L{stop} has been called.
STOPPING = "stopping"
#: A lock to prevent WaveOut* functions from being called simultaneously, as this can cause problems even if they are for different HWAVEOUTs.
_global_waveout_lock = threading.RLock()
_global_waveout_lock = threading.RLock()
_audioDucker=None

def __init__(self, channels, samplesPerSec, bitsPerSample, outputDevice=WAVE_MAPPER, closeWhenIdle=True,wantDucking=True):
def __init__(self, channels, samplesPerSec, bitsPerSample,
outputDevice=WAVE_MAPPER, closeWhenIdle=True, wantDucking=True,
buffered=False
):
"""Constructor.
@param channels: The number of channels of audio; e.g. 2 for stereo, 1 for mono.
@type channels: int
Expand All @@ -115,6 +122,8 @@ def __init__(self, channels, samplesPerSec, bitsPerSample, outputDevice=WAVE_MAP
@type closeWhenIdle: bool
@param wantDucking: if true then background audio will be ducked on Windows 8 and higher
@type wantDucking: bool
@param buffered: Whether to buffer small chunks of audio to prevent audio glitches.
@type buffered: bool
@note: If C{outputDevice} is a name and no such device exists, the default device will be used.
@raise WindowsError: If there was an error opening the audio output device.
"""
Expand All @@ -131,6 +140,15 @@ def __init__(self, channels, samplesPerSec, bitsPerSample, outputDevice=WAVE_MAP
#: If C{True}, close the output device when no audio is being played.
#: @type: bool
self.closeWhenIdle = closeWhenIdle
if buffered:
#: Minimum size of the buffer before audio is played.
#: However, this is ignored if an C{onDone} callback is provided to L{feed}.
self._minBufferSize = samplesPerSec * channels * (bitsPerSample / 8) / 1000 * self.MIN_BUFFER_MS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace 8 with BITS_PER_BYTE I assume and MS_PER_SEC for 1000

self._buffer = ""
else:
self._minBufferSize = None
#: Function to call when the previous chunk of audio has finished playing.
self._prevOnDone = None
self._waveout = None
self._waveout_event = winKernel.kernel32.CreateEventW(None, False, False, None)
self._waveout_lock = threading.RLock()
Expand Down Expand Up @@ -158,15 +176,27 @@ def open(self):
self._waveout = waveout.value
self._prev_whdr = None

def feed(self, data):
def feed(self, data, onDone=None):
"""Feed a chunk of audio data to be played.
This is normally synchronous.
However, synchronisation occurs on the previous chunk, rather than the current chunk; i.e. calling this while no audio is playing will begin playing the chunk but return immediately.
This allows for uninterrupted playback as long as a new chunk is fed before the previous chunk has finished playing.
@param data: Waveform audio in the format specified when this instance was constructed.
@type data: str
@param onDone: Function to call when this chunk has finished playing.
@type onDone: callable
@raise WindowsError: If there was an error playing the audio.
"""
if not self._minBufferSize:
return self._feedUnbuffered(data, onDone=onDone)
self._buffer += data
# If onDone was specified, we must play audio regardless of the minimum buffer size
# so we can accurately call onDone at the end of this chunk.
if onDone or len(self._buffer) > self._minBufferSize:
self._feedUnbuffered(self._buffer, onDone=onDone)
self._buffer = ""

def _feedUnbuffered(self, data, onDone=None):
if self._audioDucker and not self._audioDucker.enable():
return
whdr = WAVEHDR()
Expand All @@ -185,6 +215,10 @@ def feed(self, data):
raise e
self.sync()
self._prev_whdr = whdr
# Don't call onDone if stop was called,
# as this chunk has been truncated in that case.
if self._prevOnDone is not self.STOPPING:
self._prevOnDone = onDone

def sync(self):
"""Synchronise with playback.
Expand All @@ -202,6 +236,12 @@ def sync(self):
with self._global_waveout_lock:
winmm.waveOutUnprepareHeader(self._waveout, LPWAVEHDR(self._prev_whdr), sizeof(WAVEHDR))
self._prev_whdr = None
if self._prevOnDone is not None and self._prevOnDone is not self.STOPPING:
try:
self._prevOnDone()
except:
log.exception("Error calling onDone")
self._prevOnDone = None

def pause(self, switch):
"""Pause or unpause playback.
Expand Down Expand Up @@ -229,6 +269,14 @@ def idle(self):
If L{closeWhenIdle} is C{True}, the output device will be closed.
A subsequent call to L{feed} will reopen it.
"""
if not self._minBufferSize:
return self._idleUnbuffered()
if self._buffer:
self._feedUnbuffered(self._buffer)
self._buffer = ""
return self._idleUnbuffered()

def _idleUnbuffered(self):
with self._lock:
self.sync()
with self._waveout_lock:
Expand All @@ -242,9 +290,12 @@ def stop(self):
"""Stop playback.
"""
if self._audioDucker: self._audioDucker.disable()
if self._minBufferSize:
self._buffer = ""
with self._waveout_lock:
if not self._waveout:
return
self._prevOnDone = self.STOPPING
try:
with self._global_waveout_lock:
# Pausing first seems to make waveOutReset respond faster on some systems.
Expand All @@ -254,7 +305,8 @@ def stop(self):
# waveOutReset seems to fail randomly on some systems.
pass
# Unprepare the previous buffer and close the output device if appropriate.
self.idle()
self._idleUnbuffered()
self._prevOnDone = None

def close(self):
"""Close the output device.
Expand Down
Loading