From 275ab4d83d49ea1fba818a866c45a7c4a7593563 Mon Sep 17 00:00:00 2001 From: mltony <34804124+mltony@users.noreply.github.com> Date: Mon, 15 Apr 2024 18:56:37 -0700 Subject: [PATCH] Keystrokes to adjust applications volume and mute (#16273) Closes #16052. Summary of the issue: Needd commands to adjust volume of all applications other than NVDA. Description of user facing changes Adding commands to adjust volume of all applications other than NVDA bound to nvda+alt+pageUp/pageDown. Also adding a command to mute all applications other than NVDA bound to nvda+alt+delete. Also adding corresponding settings to audio panel. Description of development approach Making use of sound split code - we already have code to set volume of all other apps, but it's only used to mute left or right channel. Reusing same code to adjust volume as well. --- source/audio/__init__.py | 2 ++ source/audio/soundSplit.py | 63 ++++++++++++++++++++++++++--------- source/config/configSpec.py | 2 ++ source/globalCommands.py | 57 +++++++++++++++++++++++++++++++ source/gui/settingsDialogs.py | 23 +++++++++++++ user_docs/en/changes.t2t | 4 ++- user_docs/en/userGuide.t2t | 33 ++++++++++++++++++ 7 files changed, 167 insertions(+), 17 deletions(-) diff --git a/source/audio/__init__.py b/source/audio/__init__.py index 47d07112d94..243a4945a2e 100644 --- a/source/audio/__init__.py +++ b/source/audio/__init__.py @@ -6,11 +6,13 @@ from .soundSplit import ( SoundSplitState, setSoundSplitState, + updateSoundSplitState, toggleSoundSplitState, ) __all__ = [ "SoundSplitState", "setSoundSplitState", + "updateSoundSplitState", "toggleSoundSplitState", ] diff --git a/source/audio/soundSplit.py b/source/audio/soundSplit.py index 7c7818476d2..d5909106d28 100644 --- a/source/audio/soundSplit.py +++ b/source/audio/soundSplit.py @@ -15,6 +15,7 @@ import ui from utils.displayString import DisplayStringIntEnum from dataclasses import dataclass +import os from comtypes import COMError from threading import Lock import core @@ -102,6 +103,7 @@ def initialize() -> None: log.exception("Could not initialize audio session manager") return state = SoundSplitState(config.conf["audio"]["soundSplitState"]) + config.conf["audio"]["applicationsMuted"] = False setSoundSplitState(state, initial=True) else: log.debug("Cannot initialize sound split as WASAPI is disabled") @@ -112,7 +114,7 @@ def terminate(): if nvwave.usingWasapiWavePlayer(): state = SoundSplitState(config.conf["audio"]["soundSplitState"]) if state != SoundSplitState.OFF: - setSoundSplitState(SoundSplitState.OFF) + setSoundSplitState(SoundSplitState.OFF, appsVolume=1.0) unregisterCallback() else: log.debug("Skipping terminating sound split as WASAPI is disabled.") @@ -173,21 +175,34 @@ class VolumeSetter(AudioSessionNotification): def on_session_created(self, new_session: AudioSession): pid = new_session.ProcessId + process = new_session.Process + if process is not None: + exe = os.path.basename(process.exe()) + isNvda = exe.lower() == "nvda.exe" + else: + isNvda = False channelVolume = new_session.channelAudioVolume() channelCount = channelVolume.GetChannelCount() if channelCount != 2: log.warning(f"Audio session for pid {pid} has {channelCount} channels instead of 2 - cannot set volume!") self.foundSessionWithNot2Channels = True return - if pid != globalVars.appPid: - channelVolume.SetChannelVolume(0, self.leftVolume, None) - channelVolume.SetChannelVolume(1, self.rightVolume, None) - else: + if pid == globalVars.appPid: channelVolume.SetChannelVolume(0, self.leftNVDAVolume, None) channelVolume.SetChannelVolume(1, self.rightNVDAVolume, None) + elif isNvda: + # This might be NVDA running on secure screen; don't adjust its volume + pass + else: + channelVolume.SetChannelVolume(0, self.leftVolume, None) + channelVolume.SetChannelVolume(1, self.rightVolume, None) -def setSoundSplitState(state: SoundSplitState, initial: bool = False) -> dict: +def setSoundSplitState( + state: SoundSplitState, + appsVolume: float | None = None, + initial: bool = False +) -> dict: applyToFuture = True if state == SoundSplitState.OFF: if initial: @@ -198,6 +213,14 @@ def setSoundSplitState(state: SoundSplitState, initial: bool = False) -> dict: state = SoundSplitState.NVDA_BOTH_APPS_BOTH applyToFuture = False leftVolume, rightVolume = state.getAppVolume() + if appsVolume is None: + appsVolume = ( + config.conf["audio"]["applicationsSoundVolume"] / 100 + * (1 - int(config.conf["audio"]["applicationsMuted"])) + ) + + leftVolume *= appsVolume + rightVolume *= appsVolume leftNVDAVolume, rightNVDAVolume = state.getNVDAVolume() volumeSetter = VolumeSetter(leftVolume, rightVolume, leftNVDAVolume, rightNVDAVolume) applyToAllAudioSessions(volumeSetter, applyToFuture=applyToFuture) @@ -206,7 +229,7 @@ def setSoundSplitState(state: SoundSplitState, initial: bool = False) -> dict: } -def toggleSoundSplitState() -> None: +def updateSoundSplitState(increment: int | None = None) -> None: if not nvwave.usingWasapiWavePlayer(): message = _( # Translators: error message when wasapi is turned off. @@ -216,17 +239,21 @@ def toggleSoundSplitState() -> None: ui.message(message) return state = SoundSplitState(config.conf["audio"]["soundSplitState"]) - allowedStates: list[int] = config.conf["audio"]["includedSoundSplitModes"] - try: - i = allowedStates.index(state) - except ValueError: - # State not found, resetting to default (OFF) - i = -1 - i = (i + 1) % len(allowedStates) - newState = SoundSplitState(allowedStates[i]) + if increment is None: + newState = state + else: + allowedStates: list[int] = config.conf["audio"]["includedSoundSplitModes"] + try: + i = allowedStates.index(state) + except ValueError: + # State not found, resetting to default (OFF) + i = -1 + i = (i + increment) % len(allowedStates) + newState = SoundSplitState(allowedStates[i]) result = setSoundSplitState(newState) config.conf["audio"]["soundSplitState"] = newState.value - ui.message(newState.displayString) + if increment is not None: + ui.message(newState.displayString) if result["foundSessionWithNot2Channels"]: msg = _( # Translators: warning message when sound split trigger wasn't successful due to one of audio sessions @@ -277,3 +304,7 @@ def unregister(self): applicationExitCallbacksLock = Lock() applicationExitCallbacks: dict[int, VolumeRestorer] = {} + + +def toggleSoundSplitState() -> None: + updateSoundSplitState(increment=1) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 67eeb7e096d..90c16e17d59 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -58,6 +58,8 @@ audioAwakeTime = integer(default=30, min=0, max=3600) whiteNoiseVolume = integer(default=0, min=0, max=100) soundSplitState = integer(default=0) + applicationsSoundVolume = integer(default=100, min=0, max=100) + applicationsMuted = boolean(default=False) includedSoundSplitModes = int_list(default=list(0, 2, 3)) # Braille settings diff --git a/source/globalCommands.py b/source/globalCommands.py index 8927b24ed81..3b0e29ec82b 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -4477,6 +4477,63 @@ def script_cycleParagraphStyle(self, gesture: "inputCore.InputGesture") -> None: def script_cycleSoundSplit(self, gesture: "inputCore.InputGesture") -> None: audio.toggleSoundSplitState() + @script( + description=_( + # Translators: Describes a command. + "Increases the volume of the other applications", + ), + category=SCRCAT_AUDIO, + gesture="kb:NVDA+alt+pageUp", + ) + def script_increaseApplicationsVolume(self, gesture: "inputCore.InputGesture") -> None: + volume = config.conf["audio"]["applicationsSoundVolume"] + volume = min(100, volume + 5) + config.conf["audio"]["applicationsSoundVolume"] = volume + config.conf["audio"]["applicationsMuted"] = False + audio.updateSoundSplitState() + # Translators: a message reporting applications volume + msg = _("Applications volume %d") % volume + ui.message(msg) + + @script( + description=_( + # Translators: Describes a command. + "Decreases the volume of the other applications", + ), + category=SCRCAT_AUDIO, + gesture="kb:NVDA+alt+pageDown", + ) + def script_decreaseApplicationsVolume(self, gesture: "inputCore.InputGesture") -> None: + volume = config.conf["audio"]["applicationsSoundVolume"] + volume = max(0, volume - 5) + config.conf["audio"]["applicationsSoundVolume"] = volume + config.conf["audio"]["applicationsMuted"] = False + audio.updateSoundSplitState() + # Translators: a message reporting applications volume + msg = _("Applications volume %d") % volume + ui.message(msg) + + @script( + description=_( + # Translators: Describes a command. + "Toggles other applications mute", + ), + category=SCRCAT_AUDIO, + gesture="kb:NVDA+alt+delete", + ) + def script_toggleApplicationsMute(self, gesture: "inputCore.InputGesture") -> None: + muted = config.conf["audio"]["applicationsMuted"] + muted = not muted + config.conf["audio"]["applicationsMuted"] = muted + audio.updateSoundSplitState() + if muted: + # Translators: a message reporting applications volume + msg = _("Applications muted") + else: + # Translators: a message reporting applications volume + msg = _("Applications unmuted") + ui.message(msg) + #: The single global commands instance. #: @type: L{GlobalCommands} diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 9d08b02c95a..4cfb4ad1671 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2718,6 +2718,25 @@ def makeSettings(self, settingsSizer: wx.BoxSizer) -> None: self.bindHelpEvent("SoundVolume", self.soundVolSlider) self.soundVolSlider.SetValue(config.conf["audio"]["soundVolume"]) + # Translators: This is the label for a slider control in the + # Audio settings panel. + label = _("Volume of other applications") + self.appSoundVolSlider: nvdaControls.EnhancedInputSlider = sHelper.addLabeledControl( + label, + nvdaControls.EnhancedInputSlider, + minValue=0, + maxValue=100 + ) + self.bindHelpEvent("OtherAppVolume", self.appSoundVolSlider) + self.appSoundVolSlider.SetValue(config.conf["audio"]["applicationsSoundVolume"]) + + # Translators: This is the label for a checkbox control in the + # Audio settings panel. + label = _("Mute other applications") + self.muteApplicationsCheckBox: wx.CheckBox = sHelper.addItem(wx.CheckBox(self, label=label)) + self.bindHelpEvent("MuteApplications", self.muteApplicationsCheckBox) + self.muteApplicationsCheckBox.SetValue(config.conf["audio"]["applicationsMuted"]) + # Translators: This is a label for the sound split combo box in the Audio Settings dialog. soundSplitLabelText = _("&Sound split mode:") self.soundSplitComboBox = sHelper.addLabeledControl( @@ -2807,6 +2826,8 @@ def onSave(self): config.conf["audio"]["soundVolumeFollowsVoice"] = self.soundVolFollowCheckBox.IsChecked() config.conf["audio"]["soundVolume"] = self.soundVolSlider.GetValue() + config.conf["audio"]["applicationsSoundVolume"] = self.appSoundVolSlider.GetValue() + config.conf["audio"]["applicationsMuted"] = self.muteApplicationsCheckBox.IsChecked() index = self.soundSplitComboBox.GetSelection() config.conf["audio"]["soundSplitState"] = index @@ -2836,6 +2857,8 @@ def _onSoundVolChange(self, event: wx.Event) -> None: wasapi and not self.soundVolFollowCheckBox.IsChecked() ) + self.appSoundVolSlider.Enable(wasapi) + self.muteApplicationsCheckBox.Enable(wasapi) self.soundSplitComboBox.Enable(wasapi) self.soundSplitModesList.Enable(wasapi) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index e760ed89742..6b5ca337a1d 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -33,8 +33,10 @@ What's New in NVDA - Added support for the BrailleEdgeS2, BrailleEdgeS3 braille device. (#16033, #16279, @EdKweon) - NVDA will keep the audio device awake after speech stops, in order to prevent the start of the next speech being clipped with some audio devices such as Bluetooth headphones. (#14386, @jcsteh, @mltony) - Sound split: (#12985, @mltony) - - Allows splitting NVDA sounds in one channel (e.g. left) while sounds from all other applications in the other channel (e.g. right). + - Allows splitting NVDA sounds into one channel (e.g. left) while sounds from all other applications are placed in the other channel (e.g. right). - Toggled by ``NVDA+alt+s``. + - The volume of the other applications can be adjusted by ``NVDA+alt+pageUp`` and ``NVDA+alt+pageDown``. (#16052, @mltony) + - The sound of the other applications can be muted with ``NVDA+alt+delete``. (#16052, @mltony) - diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index ceaebd11970..80eee5ef157 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1984,11 +1984,15 @@ By default this command will cycle between the following modes: - There are more advanced sound split modes available in NVDA setting combo box. +If you wish to adjust volume of all applications except for NVDA, consider using [the dedicated commands #OtherAppVolume]. Please note, that sound split doesn't work as a mixer. For example, if an application is playing a stereo sound track while sound split is set to "NVDA on the left and applications on the right", then you will only hear the right channel of the sound track, while the left channel of the sound track will be muted. This option is not available if you have started NVDA with [WASAPI disabled for audio output #WASAPI] in Advanced Settings. +Please note, that if NVDA crashes, then it won't be able to restore application sounds volume, and those applications might still output sound only in one channel after NVDA crash. +In order to mitigate this, please restart NVDA. + ==== Customizing Sound split modes====[CustomizeSoundSplitModes] This checkable list allows selecting which sound split modes are included when cycling between them using ``NVDA+alt+s``. Modes which are unchecked are excluded. @@ -2001,6 +2005,35 @@ By default only three modes are included. Note that it is necessary to check at least one mode. This option is not available if you have started NVDA with [WASAPI disabled for audio output #WASAPI] in Advanced Settings. +==== Volume of other applications ====[OtherAppVolume] + +This slider allows you to adjust the volume of all currently running applications other than NVDA. +This volume setting will apply to all other applications sound output, even if they start after this setting is changed. +This volume can also be controlled via the following keyboard commands from anywhere: + +%kc:beginInclude +|| Name | Key | Description | +| Increase applications volume | ``NVDA+alt+pageUp`` | Increases volume of all applications except NVDA. | +| Decrease applications volume | ``NVDA+alt+pageDown`` | Decreases volume of all applications except NVDA. | + +%kc:endInclude + +This option is not available if you have started NVDA with [WASAPI disabled for audio output #WASAPI] in Advanced Settings. + +==== Mute other applications ====[MuteApplications] + +This checkbox allows you to mute all applications other than NVDA. +This mute setting will apply to all other applications outputting sound, even if they start after this setting is changed. +The following keyboard command can also be used from anywhere: + +%kc:beginInclude +|| Name | Key | Description | +| Toggle mute other applications | ``NVDA+alt+delete`` | Mutes or unmutes all applications other than NVDA. | + +%kc:endInclude + +This option is not available if you have started NVDA with [WASAPI disabled for audio output #WASAPI] in Advanced Settings. + +++ Vision +++[VisionSettings] The Vision category in the NVDA Settings dialog allows you to enable, disable and configure [visual aids #Vision].