From 0c86e42d068136001cb7adb934b1fe687e73384e Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Fri, 21 Jul 2023 13:40:07 +1000 Subject: [PATCH 1/3] Disable WASAPI by default --- source/config/configSpec.py | 4 +-- source/config/profileUpgradeSteps.py | 8 +++--- source/gui/nvdaControls.py | 10 +++++++- source/gui/settingsDialogs.py | 37 ++++++++++++++-------------- user_docs/en/changes.t2t | 17 +++++++------ 5 files changed, 43 insertions(+), 33 deletions(-) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 658b5ce78d8..757c43e470c 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -13,7 +13,7 @@ #: provide an upgrade step (@see profileUpgradeSteps.py). An upgrade step does not need to be added when #: just adding a new element to (or removing from) the schema, only when old versions of the config #: (conforming to old schema versions) will not work correctly with the new schema. -latestSchemaVersion = 10 +latestSchemaVersion = 11 #: The configuration specification string #: @type: String @@ -57,7 +57,7 @@ # Audio settings [audio] audioDuckingMode = integer(default=0) - wasapi = boolean(default=true) + wasapi = featureFlag(optionsEnum="BoolFlag", behaviorOfDefault="disabled") soundVolumeFollowsVoice = boolean(default=false) soundVolume = integer(default=100, min=0, max=100) diff --git a/source/config/profileUpgradeSteps.py b/source/config/profileUpgradeSteps.py index 541ae93e14b..9bfcbe9f66b 100644 --- a/source/config/profileUpgradeSteps.py +++ b/source/config/profileUpgradeSteps.py @@ -23,9 +23,6 @@ ReportTableHeaders, ReportCellBorders, ) -from typing import ( - Dict, -) import configobj.validate from configobj import ConfigObj @@ -354,3 +351,8 @@ def upgradeConfigFrom_9_to_10(profile: ConfigObj) -> None: profile['keyboard']['NVDAModifierKeys'] = val else: log.debug("use*AsNVDAModifierKey values not present, no action taken.") + + +def upgradeConfigFrom_10_to_11(profile: ConfigObj) -> None: + """Change WASAPI to boolean feature flag in alpha snapshots""" + profile["audio"]["wasapi"] = "default" diff --git a/source/gui/nvdaControls.py b/source/gui/nvdaControls.py index f128f4dafca..93f1f10d8c6 100644 --- a/source/gui/nvdaControls.py +++ b/source/gui/nvdaControls.py @@ -499,10 +499,18 @@ def resetToConfigSpecDefault(self) -> None: """ self.SetSelection(self._getChoiceIndex(self.defaultValue)) + def _getControlCurrentValue(self) -> enum.Enum: + return list(self._translatedOptions.keys())[self.GetSelection()] + + def _getControlCurrentFlag(self) -> FeatureFlag: + flagValue = self._getControlCurrentValue() + currentFlag = self._getConfigValue() + return FeatureFlag(flagValue, currentFlag.behaviorOfDefault) + def saveCurrentValueToConf(self) -> None: """ Set the config value to the current value of the control. """ - flagValue: enum.Enum = list(self._translatedOptions.keys())[self.GetSelection()] + flagValue = self._getControlCurrentValue() keyPath = self._confPath if not keyPath or len(keyPath) < 1: raise ValueError("Key path not provided") diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 9906a420bdb..88011c09e74 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -55,6 +55,7 @@ List, Optional, Set, + cast, ) import core import keyboardHandler @@ -3074,19 +3075,17 @@ def __init__(self, parent): audioGroup = guiHelper.BoxSizerHelper(self, sizer=audio) sHelper.addItem(audioGroup) - # Translators: This is the label for a checkbox control in the - # Advanced settings panel. + # Translators: This is the label for a checkbox control in the Advanced settings panel. label = _("Use WASAPI for audio output (requires restart)") - self.wasapiCheckBox: wx.CheckBox = audioGroup.addItem( - wx.CheckBox(audioBox, label=label) - ) - self.bindHelpEvent("WASAPI", self.wasapiCheckBox) - self.wasapiCheckBox.Bind(wx.EVT_CHECKBOX, self.onAudioCheckBoxChange) - self.wasapiCheckBox.SetValue( - config.conf["audio"]["wasapi"] - ) - self.wasapiCheckBox.defaultValue = self._getDefaultValue( - ["audio", "wasapi"]) + self.wasapiComboBox = cast(nvdaControls.FeatureFlagCombo, audioGroup.addLabeledControl( + labelText=label, + wxCtrlClass=nvdaControls.FeatureFlagCombo, + keyPath=["audio", "wasapi"], + conf=config.conf, + )) + self.bindHelpEvent("WASAPI", self.wasapiComboBox) + self.wasapiComboBox.Bind(wx.EVT_CHOICE, self._onWASAPIChange) + # Translators: This is the label for a checkbox control in the # Advanced settings panel. label = _("Volume of NVDA sounds follows voice volume (requires WASAPI)") @@ -3094,7 +3093,7 @@ def __init__(self, parent): wx.CheckBox(audioBox, label=label) ) self.bindHelpEvent("SoundVolumeFollowsVoice", self.soundVolFollowCheckBox) - self.soundVolFollowCheckBox.Bind(wx.EVT_CHECKBOX, self.onAudioCheckBoxChange) + self.soundVolFollowCheckBox.Bind(wx.EVT_CHECKBOX, self._onWASAPIChange) self.soundVolFollowCheckBox.SetValue( config.conf["audio"]["soundVolumeFollowsVoice"] ) @@ -3115,7 +3114,7 @@ def __init__(self, parent): ) self.soundVolSlider.defaultValue = self._getDefaultValue( ["audio", "soundVolume"]) - self.onAudioCheckBoxChange() + self._onWASAPIChange() # Translators: This is the label for a group of advanced options in the # Advanced settings panel @@ -3178,8 +3177,8 @@ def onOpenScratchpadDir(self,evt): path=config.getScratchpadDir(ensureExists=True) os.startfile(path) - def onAudioCheckBoxChange(self, evt: Optional[wx.CommandEvent] = None): - wasapi = self.wasapiCheckBox.IsChecked() + def _onWASAPIChange(self, evt: Optional[wx.CommandEvent] = None): + wasapi = bool(self.wasapiComboBox._getControlCurrentFlag()) self.soundVolFollowCheckBox.Enable(wasapi) self.soundVolSlider.Enable( wasapi @@ -3213,7 +3212,7 @@ def haveConfigDefaultsBeenRestored(self): and self.loadChromeVBufWhenBusyCombo.isValueConfigSpecDefault() and self.caretMoveTimeoutSpinControl.GetValue() == self.caretMoveTimeoutSpinControl.defaultValue and self.reportTransparentColorCheckBox.GetValue() == self.reportTransparentColorCheckBox.defaultValue - and self.wasapiCheckBox.GetValue() == self.wasapiCheckBox.defaultValue + and self.wasapiComboBox.isValueConfigSpecDefault() and self.soundVolFollowCheckBox.GetValue() == self.soundVolFollowCheckBox.defaultValue and self.soundVolSlider.GetValue() == self.soundVolSlider.defaultValue and set(self.logCategoriesList.CheckedItems) == set(self.logCategoriesList.defaultCheckedItems) @@ -3242,7 +3241,7 @@ def restoreToDefaults(self): self.loadChromeVBufWhenBusyCombo.resetToConfigSpecDefault() self.caretMoveTimeoutSpinControl.SetValue(self.caretMoveTimeoutSpinControl.defaultValue) self.reportTransparentColorCheckBox.SetValue(self.reportTransparentColorCheckBox.defaultValue) - self.wasapiCheckBox.SetValue(self.wasapiCheckBox.defaultValue) + self.wasapiComboBox.resetToConfigSpecDefault() self.soundVolFollowCheckBox.SetValue(self.soundVolFollowCheckBox.defaultValue) self.soundVolSlider.SetValue(self.soundVolSlider.defaultValue) self.logCategoriesList.CheckedItems = self.logCategoriesList.defaultCheckedItems @@ -3275,7 +3274,7 @@ def onSave(self): config.conf["documentFormatting"]["reportTransparentColor"] = ( self.reportTransparentColorCheckBox.IsChecked() ) - config.conf["audio"]["wasapi"] = self.wasapiCheckBox.IsChecked() + self.wasapiComboBox.saveCurrentValueToConf() config.conf["audio"]["soundVolumeFollowsVoice"] = self.soundVolFollowCheckBox.IsChecked() config.conf["audio"]["soundVolume"] = self.soundVolSlider.GetValue() config.conf["annotations"]["reportDetails"] = self.annotationsDetailsCheckBox.IsChecked() diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index d3e79a040aa..c5a008118f5 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -13,14 +13,6 @@ What's New in NVDA - The Add-ons Manager has been removed and replaced by the Add-on Store. - For more information please read the updated user guide. - -- Enhanced sound management: - - 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) - - 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 configured in Advanced settings. (#1409) - - - New input gestures: - An unbound gesture to cycle through the available languages for Windows OCR. (#13036) - An unbound gesture to cycle through the Braille show messages modes. (#14864) @@ -57,6 +49,15 @@ What's New in NVDA - When colors are enabled Document Formatting, background colours are now reported in Microsoft Word. (#5866) - 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) - +- Experimental enhanced sound management: + - NVDA can now output audio via the Windows Audio Session API (WASAPI), which may improve the responsiveness, performance and stability of NVDA speech and sounds. (#14697) + - WASAPI usage can be enabled in Advanced settings. + Additionally, if WASAPI is enabled, the following Advanced settings can also be configured. + - An option to have the volume of NVDA sounds and beeps follow the volume setting of the voice you are using. (#1409) + - An option to separately configure the volume of NVDA sounds. (#1409, #15038) + - + - There is a known issue with intermittent crashing when WASAPI is enabled. (#15150) + - - In Mozilla Firefox and Google Chrome, NVDA now reports when a control opens a dialog, grid, list or tree if the author has specified this using aria-haspopup. (#14709) - It is now possible to use system variables (such as ``%temp%`` or ``%homepath%``) in the path specification while creating portable copies of NVDA. (#14680) - In Windows 10 May 2019 Update and later, NVDA can announce virtual desktop names when opening, changing, and closing them. (#5641) From 44f805f9e2b9c19e376c1677a0ac5839aa0ff007 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 24 Jul 2023 09:42:55 +1000 Subject: [PATCH 2/3] address review comments --- source/config/configSpec.py | 4 ++-- source/config/profileUpgradeSteps.py | 5 ----- source/gui/settingsDialogs.py | 2 +- source/nvwave.py | 2 +- user_docs/en/userGuide.t2t | 2 +- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 757c43e470c..91375da0bcc 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -13,7 +13,7 @@ #: provide an upgrade step (@see profileUpgradeSteps.py). An upgrade step does not need to be added when #: just adding a new element to (or removing from) the schema, only when old versions of the config #: (conforming to old schema versions) will not work correctly with the new schema. -latestSchemaVersion = 11 +latestSchemaVersion = 10 #: The configuration specification string #: @type: String @@ -57,7 +57,7 @@ # Audio settings [audio] audioDuckingMode = integer(default=0) - wasapi = featureFlag(optionsEnum="BoolFlag", behaviorOfDefault="disabled") + WASAPI = featureFlag(optionsEnum="BoolFlag", behaviorOfDefault="disabled") soundVolumeFollowsVoice = boolean(default=false) soundVolume = integer(default=100, min=0, max=100) diff --git a/source/config/profileUpgradeSteps.py b/source/config/profileUpgradeSteps.py index 9bfcbe9f66b..cc4ed3f9e71 100644 --- a/source/config/profileUpgradeSteps.py +++ b/source/config/profileUpgradeSteps.py @@ -351,8 +351,3 @@ def upgradeConfigFrom_9_to_10(profile: ConfigObj) -> None: profile['keyboard']['NVDAModifierKeys'] = val else: log.debug("use*AsNVDAModifierKey values not present, no action taken.") - - -def upgradeConfigFrom_10_to_11(profile: ConfigObj) -> None: - """Change WASAPI to boolean feature flag in alpha snapshots""" - profile["audio"]["wasapi"] = "default" diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 88011c09e74..b02aebf277c 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3080,7 +3080,7 @@ def __init__(self, parent): self.wasapiComboBox = cast(nvdaControls.FeatureFlagCombo, audioGroup.addLabeledControl( labelText=label, wxCtrlClass=nvdaControls.FeatureFlagCombo, - keyPath=["audio", "wasapi"], + keyPath=["audio", "WASAPI"], conf=config.conf, )) self.bindHelpEvent("WASAPI", self.wasapiComboBox) diff --git a/source/nvwave.py b/source/nvwave.py index d5e7494b71f..c0dca34094b 100644 --- a/source/nvwave.py +++ b/source/nvwave.py @@ -1044,7 +1044,7 @@ def _deviceNameToId(name, fallbackToDefault=True): def initialize(): global WavePlayer - if not config.conf["audio"]["wasapi"]: + if not config.conf["audio"]["WASAPI"]: return WavePlayer = WasapiWavePlayer NVDAHelper.localLib.wasPlay_create.restype = c_void_p diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index bd0a1f64715..46bab26c876 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -2360,7 +2360,7 @@ With several historically popular GUI APIs, the text may be rendered with a tran ==== Use WASAPI for audio output ====[WASAPI] This option enables audio output via the Windows Audio Session API (WASAPI). WASAPI is a more modern audio framework which may improve the responsiveness, performance and stability of NVDA audio output, including both speech and sounds. -This option is enabled by default. +This option is disabled 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] From 720b1875029ab98db5767fb28618c5cb968668b2 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 24 Jul 2023 10:06:03 +1000 Subject: [PATCH 3/3] Prompt NVDA to restart on WASAPI change --- source/gui/nvdaControls.py | 2 +- source/gui/settingsDialogs.py | 46 ++++++++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/source/gui/nvdaControls.py b/source/gui/nvdaControls.py index 93f1f10d8c6..7002a00b483 100644 --- a/source/gui/nvdaControls.py +++ b/source/gui/nvdaControls.py @@ -459,7 +459,7 @@ def __init__( name=name, ) - self.SetSelection(self._getChoiceIndex(self._getConfigValue().value)) + self.SetSelection(self._getChoiceIndex(configValue.value)) self.defaultValue = self._getConfSpecDefaultValue() """The default value of the config spec. Not the "behavior of default". This is provided to maintain compatibility with other controls in the diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index b02aebf277c..ac4d31ab916 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -977,28 +977,26 @@ def postSave(self): LanguageRestartDialog(self).ShowModal() -class LanguageRestartDialog( +class _RestartDialog( gui.contextHelp.ContextHelpMixin, wx.Dialog, # wxPython does not seem to call base class initializer, put last in MRO ): + _title: str + _message: str - helpId = "GeneralSettingsLanguage" - - def __init__(self, parent): - # Translators: The title of the dialog which appears when the user changed NVDA's interface language. - super(LanguageRestartDialog, self).__init__(parent, title=_("Language Configuration Change")) + def __init__(self, parent: wx.Window): + super().__init__(parent, title=self._title) mainSizer = wx.BoxSizer(wx.VERTICAL) sHelper = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) - # Translators: The message displayed after NVDA interface language has been changed. - sHelper.addItem(wx.StaticText(self, label=_("NVDA must be restarted for the new language to take effect."))) + sHelper.addItem(wx.StaticText(self, label=self._message)) bHelper = sHelper.addDialogDismissButtons(guiHelper.ButtonHelper(wx.HORIZONTAL)) - # Translators: The label for a button in the dialog which appears when the user changed NVDA's interface language. + # Translators: The label for a button in a dialog restartNowButton = bHelper.addButton(self, label=_("Restart &now")) restartNowButton.Bind(wx.EVT_BUTTON, self.onRestartNowButton) restartNowButton.SetFocus() - # Translators: The label for a button in the dialog which appears when the user changed NVDA's interface language. + # Translators: The label for a button in a dialog restartLaterButton = bHelper.addButton(self, wx.ID_CLOSE, label=_("Restart &later")) restartLaterButton.Bind(wx.EVT_BUTTON, lambda evt: self.Close()) self.Bind(wx.EVT_CLOSE, lambda evt: self.Destroy()) @@ -1012,7 +1010,28 @@ def __init__(self, parent): def onRestartNowButton(self, evt): self.Destroy() config.conf.save() - queueHandler.queueFunction(queueHandler.eventQueue,core.restart) + queueHandler.queueFunction(queueHandler.eventQueue, core.restart) + + +class _WASAPIRestartDialog(_RestartDialog): + helpId = "WASAPI" + + # Translators: The title of the dialog which appears when the user updated a setting for audio output + _title = _("Audio output change") + + # Translators: The message displayed when the user updated a setting for audio output + _message = _("NVDA must be restarted for the new audio settings to take effect.") + + +class LanguageRestartDialog(_RestartDialog): + helpId = "GeneralSettingsLanguage" + + # Translators: The title of the dialog which appears when the user changed NVDA's interface language. + _title = _("Language Configuration Change") + + # Translators: The message displayed after NVDA interface language has been changed. + _message = _("NVDA must be restarted for the new language to take effect.") + class SpeechSettingsPanel(SettingsPanel): # Translators: This is the label for the speech panel @@ -3085,6 +3104,7 @@ def __init__(self, parent): )) self.bindHelpEvent("WASAPI", self.wasapiComboBox) self.wasapiComboBox.Bind(wx.EVT_CHOICE, self._onWASAPIChange) + self._oldWASAPIValue: bool = self.wasapiComboBox._getConfigValue().calculated() # Translators: This is the label for a checkbox control in the # Advanced settings panel. @@ -3287,6 +3307,7 @@ def onSave(self): config.conf['debugLog'][key]=self.logCategoriesList.IsChecked(index) config.conf["featureFlag"]["playErrorSound"] = self.playErrorSoundCombo.GetSelection() + class AdvancedPanel(SettingsPanel): enableControlsCheckBox = None # type: wx.CheckBox # Translators: This is the label for the Advanced settings panel. @@ -3358,6 +3379,9 @@ def onSave(self): ): self.advancedControls.onSave() + def postSave(self): + if self.advancedControls._oldWASAPIValue != config.conf["audio"]["WASAPI"].calculated(): + _WASAPIRestartDialog(self).ShowModal() def onEnableControlsCheckBox(self, evt): # due to some not very well understood mis ordering of event processing, we force NVDA to