diff --git a/source/config/configSpec.py b/source/config/configSpec.py index c8a7f11f98f..74547146cd1 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -57,6 +57,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 af69a226a1c..bda0fd09f4b 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3032,6 +3032,18 @@ def __init__(self, parent): ) 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"]) # Translators: This is the label for a group of advanced options in the # Advanced settings panel @@ -3121,6 +3133,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. @@ -3147,6 +3160,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 @@ -3177,7 +3191,7 @@ def onSave(self): config.conf["documentFormatting"]["reportTransparentColor"] = ( 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..58ac1914db5 100644 --- a/source/nvwave.py +++ b/source/nvwave.py @@ -814,6 +814,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 +858,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 +915,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 +938,17 @@ def setSessionVolume(self, level: float): """ NVDAHelper.localLib.wasPlay_setSessionVolume(self._player, c_float(level)) + def _sessionVolumeFollow(self): + if ( + self._session is not soundsSession + or not config.conf["audio"]["soundVolumeFollowsVoice"] + ): + 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/userGuide.t2t b/user_docs/en/userGuide.t2t index d3ac48ed23f..23a747b953c 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -2299,6 +2299,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.