diff --git a/nvdaHelper/local/wasapi.cpp b/nvdaHelper/local/wasapi.cpp index 9e5c78f4a8b..db08351dab4 100644 --- a/nvdaHelper/local/wasapi.cpp +++ b/nvdaHelper/local/wasapi.cpp @@ -513,6 +513,16 @@ HRESULT WasapiPlayer::resume() { HRESULT WasapiPlayer::setSessionVolume(float level) { CComPtr volume; HRESULT hr = client->GetService(IID_ISimpleAudioVolume, (void**)&volume); + if (hr == AUDCLNT_E_DEVICE_INVALIDATED) { + // If we're using a specific device, it's just been invalidated. Fall back + // to the default device. + deviceId.clear(); + hr = open(true); + if (FAILED(hr)) { + return hr; + } + hr = client->GetService(IID_ISimpleAudioVolume, (void**)&volume); + } if (FAILED(hr)) { return hr; } diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 2ea4f4d9d86..2a233998e5d 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -58,6 +58,7 @@ [audio] audioDuckingMode = integer(default=0) wasapi = boolean(default=true) + soundVolumeFollowsVoice = boolean(default=false) # Braille settings [braille] diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 7211525db45..48d2c4d0218 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3062,11 +3062,26 @@ def __init__(self, parent): wx.CheckBox(audioBox, label=label) ) self.bindHelpEvent("WASAPI", self.wasapiCheckBox) + self.wasapiCheckBox.Bind(wx.EVT_CHECKBOX, self.onWasapiChange) self.wasapiCheckBox.SetValue( config.conf["audio"]["wasapi"] ) self.wasapiCheckBox.defaultValue = self._getDefaultValue( ["audio", "wasapi"]) + # Translators: This is the label for a checkbox control in the + # Advanced settings panel. + label = _("Volume of NVDA sounds follows voice volume (requires WASAPI)") + self.soundVolFollowCheckBox: wx.CheckBox = audioGroup.addItem( + wx.CheckBox(audioBox, label=label) + ) + self.bindHelpEvent("SoundVolumeFollowsVoice", self.soundVolFollowCheckBox) + self.soundVolFollowCheckBox.SetValue( + config.conf["audio"]["soundVolumeFollowsVoice"] + ) + self.soundVolFollowCheckBox.defaultValue = self._getDefaultValue( + ["audio", "soundVolumeFollowsVoice"]) + if not self.wasapiCheckBox.GetValue(): + self.soundVolFollowCheckBox.Disable() # Translators: This is the label for a group of advanced options in the # Advanced settings panel @@ -3129,6 +3144,9 @@ def onOpenScratchpadDir(self,evt): path=config.getScratchpadDir(ensureExists=True) os.startfile(path) + def onWasapiChange(self, evt: wx.CommandEvent): + self.soundVolFollowCheckBox.Enable(evt.IsChecked()) + def _getDefaultValue(self, configPath): return config.conf.getConfigValidation(configPath).default @@ -3156,6 +3174,7 @@ def haveConfigDefaultsBeenRestored(self): and self.caretMoveTimeoutSpinControl.GetValue() == self.caretMoveTimeoutSpinControl.defaultValue and self.reportTransparentColorCheckBox.GetValue() == self.reportTransparentColorCheckBox.defaultValue and self.wasapiCheckBox.GetValue() == self.wasapiCheckBox.defaultValue + and self.soundVolFollowCheckBox.GetValue() == self.soundVolFollowCheckBox.defaultValue and set(self.logCategoriesList.CheckedItems) == set(self.logCategoriesList.defaultCheckedItems) and self.playErrorSoundCombo.GetSelection() == self.playErrorSoundCombo.defaultValue and True # reduce noise in diff when the list is extended. @@ -3182,6 +3201,7 @@ def restoreToDefaults(self): self.caretMoveTimeoutSpinControl.SetValue(self.caretMoveTimeoutSpinControl.defaultValue) self.reportTransparentColorCheckBox.SetValue(self.reportTransparentColorCheckBox.defaultValue) self.wasapiCheckBox.SetValue(self.wasapiCheckBox.defaultValue) + self.soundVolFollowCheckBox.SetValue(self.soundVolFollowCheckBox.defaultValue) self.logCategoriesList.CheckedItems = self.logCategoriesList.defaultCheckedItems self.playErrorSoundCombo.SetSelection(self.playErrorSoundCombo.defaultValue) self._defaultsRestored = True @@ -3213,6 +3233,7 @@ def onSave(self): self.reportTransparentColorCheckBox.IsChecked() ) config.conf["audio"]["wasapi"] = self.wasapiCheckBox.IsChecked() + config.conf["audio"]["soundVolumeFollowsVoice"] = self.soundVolFollowCheckBox.IsChecked() config.conf["annotations"]["reportDetails"] = self.annotationsDetailsCheckBox.IsChecked() config.conf["annotations"]["reportAriaDescription"] = self.ariaDescCheckBox.IsChecked() config.conf["braille"]["enableHidBrailleSupport"] = self.supportHidBrailleCombo.GetSelection() diff --git a/source/nvwave.py b/source/nvwave.py index 61c3ce8ed73..c4cd4fd2836 100644 --- a/source/nvwave.py +++ b/source/nvwave.py @@ -774,6 +774,10 @@ class WasapiWavePlayer(garbageHandler.TrackedObject): #: This allows us to have a single callback in the class rather than on #: each instance, which prevents reference cycles. _instances = weakref.WeakValueDictionary() + #: The previous value of the soundVolumeFollowsVoice setting. This is used to + #: determine when this setting has been disabled when it was previously + #: enabled. + _prevSoundVolFollow: bool = False def __init__( self, @@ -814,6 +818,7 @@ def __init__( import audioDucking if audioDucking.isAudioDuckingSupported(): self._audioDucker = audioDucking.AudioDucker() + self._session = session self._player = NVDAHelper.localLib.wasPlay_create( self._deviceNameToId(outputDevice), format, @@ -857,6 +862,7 @@ def open(self): WavePlayer.audioDeviceError_static = True raise WasapiWavePlayer.audioDeviceError_static = False + self._sessionVolumeFollow() def close(self): """For WASAPI, this just stops playback. @@ -913,6 +919,7 @@ def stop(self): self._audioDucker.disable() NVDAHelper.localLib.wasPlay_stop(self._player) self._doneCallbacks = {} + self._sessionVolumeFollow() def pause(self, switch: bool): """Pause or unpause playback. @@ -935,6 +942,21 @@ def setSessionVolume(self, level: float): """ NVDAHelper.localLib.wasPlay_setSessionVolume(self._player, c_float(level)) + def _sessionVolumeFollow(self): + if self._session is not soundsSession: + return + follow = config.conf["audio"]["soundVolumeFollowsVoice"] + if not follow and WavePlayer._prevSoundVolFollow: + # Following was disabled. Reset the sound volume to maximum. + self.setSessionVolume(1.0) + WavePlayer._prevSoundVolFollow = follow + if not follow: + return + import synthDriverHandler + synth = synthDriverHandler.getSynth() + if synth and synth.isSupported("volume"): + self.setSessionVolume(synth.volume / 100) + @staticmethod def _getDevices(): rawDevs = BSTR() diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index aac11f75595..377ff02ee94 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -31,10 +31,15 @@ There are now gestures for showing the braille settings dialog, accessing the st - When highlighted text is enabled Document Formatting, highlight colours are now reported in Microsoft Word. (#7396, #12101, #5866) - When colors are enabled Document Formatting, background colours are now reported in Microsoft Word. (#5866) - When pressing ``numpad2`` three times to report the numerical value of the character at the position of the review cursor, the information is now also provided in braille. (#14826) -- NVDA now outputs audio via the Windows Audio Session API (WASAPI), which may improve the responsiveness, performance and stability of NVDA speech and sounds. This can be disabled in Advanced settings if audio problems are encountered. (#14697) +- NVDA now outputs audio via the Windows Audio Session API (WASAPI), which may improve the responsiveness, performance and stability of NVDA speech and sounds. +This can be disabled in Advanced settings if audio problems are encountered. (#14697) - When using Excel shortcuts to toggle format such as bold, italic, underline and strike through of a cell in Excel, the result is now reported. (#14923) - Added support for the Help Tech Activator Braille display. (#14917) -- In Windows 10 May 2019 Update and later, NVDA can announe virtual desktop names when opening, changing, and closing them. (#5641) +- In Windows 10 May 2019 Update and later, NVDA can announce virtual desktop names when opening, changing, and closing them. (#5641) +- It is now possible to have the volume of NVDA sounds and beeps follow the volume setting of the voice you are using. +This option can be enabled in Advanced settings. (#1409) +- You can now separately control the volume of NVDA sounds. +This can be done using the Windows Volume Mixer. (#1409) - diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 583b0d52d21..4d440e2f9ba 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -2324,6 +2324,13 @@ WASAPI is a more modern audio framework which may improve the responsiveness, pe This option is enabled by default. After changing this option, you will need to restart NVDA for the change to take effect. +==== Volume of NVDA sounds follows voice volume ====[SoundVolumeFollowsVoice] +When this option is enabled, the volume of NVDA sounds and beeps will follow the volume setting of the voice you are using. +If you decrease the volume of the voice, the volume of sounds will decrease. +Similarly, if you increase the volume of the voice, the volume of sounds will increase. +This option only takes effect when "Use WASAPI for audio output" is enabled. +This option is disabled by default. + ==== Debug logging categories ====[AdvancedSettingsDebugLoggingCategories] The checkboxes in this list allow you to enable specific categories of debug messages in NVDA's log. Logging these messages can result in decreased performance and large log files.