Skip to content

Commit

Permalink
Keystrokes to adjust applications volume and mute (#16273)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mltony committed Apr 16, 2024
1 parent 7556a84 commit 275ab4d
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 17 deletions.
2 changes: 2 additions & 0 deletions source/audio/__init__.py
Expand Up @@ -6,11 +6,13 @@
from .soundSplit import (
SoundSplitState,
setSoundSplitState,
updateSoundSplitState,
toggleSoundSplitState,
)

__all__ = [
"SoundSplitState",
"setSoundSplitState",
"updateSoundSplitState",
"toggleSoundSplitState",
]
63 changes: 47 additions & 16 deletions source/audio/soundSplit.py
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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.")
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -277,3 +304,7 @@ def unregister(self):

applicationExitCallbacksLock = Lock()
applicationExitCallbacks: dict[int, VolumeRestorer] = {}


def toggleSoundSplitState() -> None:
updateSoundSplitState(increment=1)
2 changes: 2 additions & 0 deletions source/config/configSpec.py
Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions source/globalCommands.py
Expand Up @@ -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}
Expand Down
23 changes: 23 additions & 0 deletions source/gui/settingsDialogs.py
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
4 changes: 3 additions & 1 deletion user_docs/en/changes.t2t
Expand Up @@ -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)
-


Expand Down
33 changes: 33 additions & 0 deletions user_docs/en/userGuide.t2t
Expand Up @@ -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.
Expand All @@ -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].

Expand Down

0 comments on commit 275ab4d

Please sign in to comment.