Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify application volume control #17335

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions source/audio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2024 NV Access Limited
# Copyright (C) 2024 NV Access Limited, Bill Dengler
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

Expand Down Expand Up @@ -33,8 +33,6 @@ def initialize() -> None:
return
log.debug("Initializing utils")
utils.initialize()
log.debug("Initializing appsVolume")
appsVolume.initialize()
log.debug("Initializing soundSplit")
soundSplit.initialize()
global audioUtilitiesInitialized
Expand Down
95 changes: 21 additions & 74 deletions source/audio/appsVolume.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

import config
import globalVars
from logHandler import log
import nvwave
from pycaw.utils import AudioSession
import ui
from dataclasses import dataclass
from threading import Lock
from config.featureFlagEnums import AppsVolumeAdjusterFlag
from typing import NamedTuple
from .utils import AudioSessionCallback, DummyAudioSessionCallback
from comtypes import COMError
Expand All @@ -25,17 +23,7 @@ class VolumeAndMute(NamedTuple):
_appVolumesCache: dict[int, VolumeAndMute] = {}
_appVolumesCacheLock = Lock()
_activeCallback: DummyAudioSessionCallback | None = None


def initialize() -> None:
state = config.conf["audio"]["applicationsVolumeMode"]
volume = config.conf["audio"]["applicationsSoundVolume"]
muted = config.conf["audio"]["applicationsSoundMuted"]
if muted:
# Muted flag should not be persistent.
config.conf["audio"]["applicationsSoundMuted"] = False
muted = False
_updateAppsVolumeImpl(volume / 100.0, muted, state)
_appVolumeState: VolumeAndMute = VolumeAndMute(100.0, False)


def terminate():
Expand Down Expand Up @@ -85,20 +73,15 @@ def onSessionTerminated(self, session: AudioSession) -> None:
def _updateAppsVolumeImpl(
volume: float,
muted: bool,
state: AppsVolumeAdjusterFlag,
):
global _activeCallback
if state == AppsVolumeAdjusterFlag.DISABLED:
newCallback = DummyAudioSessionCallback()
runTerminators = True
else:
newCallback = VolumeSetter(
volumeAndMute=VolumeAndMute(
volume=volume,
mute=muted,
),
)
runTerminators = False
newCallback = VolumeSetter(
volumeAndMute=VolumeAndMute(
volume=volume,
mute=muted,
),
)
runTerminators = False
if _activeCallback is not None:
_activeCallback.unregister(runTerminators=runTerminators)
_activeCallback = newCallback
Expand All @@ -112,77 +95,41 @@ def _updateAppsVolumeImpl(
)


_VOLUME_ADJUSTMENT_DISABLED_MESSAGE: str = _(
# Translators: error message when applications' volume is disabled
"Application volume control disabled",
)


def _adjustAppsVolume(
volumeAdjustment: int | None = None,
):
global _appVolumeState
volume = _appVolumeState.volume
muted = _appVolumeState.mute
if not nvwave.usingWasapiWavePlayer():
ui.message(_WASAPI_DISABLED_MESSAGE)
return
volume: int = config.conf["audio"]["applicationsSoundVolume"]
muted: bool = config.conf["audio"]["applicationsSoundMuted"]
state = config.conf["audio"]["applicationsVolumeMode"]
if state != AppsVolumeAdjusterFlag.ENABLED:
ui.message(_VOLUME_ADJUSTMENT_DISABLED_MESSAGE)
return
volume += volumeAdjustment
volume = max(0, min(100, volume))
if volumeAdjustment is not None:
volume += volumeAdjustment
volume = max(0, min(100, volume))
log.debug(f"Adjusting applications volume by {volumeAdjustment}% to {volume}%")
config.conf["audio"]["applicationsSoundVolume"] = volume

# We skip running terminators here to avoid application volume spiking to 100% for a split second.
_updateAppsVolumeImpl(volume / 100.0, muted, state)
_updateAppsVolumeImpl(volume / 100.0, muted)
_appVolumeState = VolumeAndMute(volume, muted)
# Translators: Announcing new applications' volume message
msg = _("{} percent application volume").format(volume)
ui.message(msg)


_APPS_VOLUME_STATES_ORDER = [
AppsVolumeAdjusterFlag.DISABLED,
AppsVolumeAdjusterFlag.ENABLED,
]


def _toggleAppsVolumeState():
if not nvwave.usingWasapiWavePlayer():
ui.message(_WASAPI_DISABLED_MESSAGE)
return
state = config.conf["audio"]["applicationsVolumeMode"]
volume: int = config.conf["audio"]["applicationsSoundVolume"]
muted: bool = config.conf["audio"]["applicationsSoundMuted"]
try:
index = _APPS_VOLUME_STATES_ORDER.index(state)
except ValueError:
index = -1
index = (index + 1) % len(_APPS_VOLUME_STATES_ORDER)
state = _APPS_VOLUME_STATES_ORDER[index]
config.conf["audio"]["applicationsVolumeMode"] = state.name
_updateAppsVolumeImpl(volume / 100.0, muted, state)
ui.message(state.displayString)


def _toggleAppsVolumeMute():
global _appVolumeState
if not nvwave.usingWasapiWavePlayer():
ui.message(_WASAPI_DISABLED_MESSAGE)
return
state = config.conf["audio"]["applicationsVolumeMode"]
volume: int = config.conf["audio"]["applicationsSoundVolume"]
muted: bool = config.conf["audio"]["applicationsSoundMuted"]
if state != AppsVolumeAdjusterFlag.ENABLED:
ui.message(_VOLUME_ADJUSTMENT_DISABLED_MESSAGE)
return
muted = not muted
config.conf["audio"]["applicationsSoundMuted"] = muted
_updateAppsVolumeImpl(volume / 100.0, muted, state)
volume = _appVolumeState.volume
muted = not _appVolumeState.mute
_updateAppsVolumeImpl(volume / 100.0, muted)
if muted:
# Translators: Announcing new applications' mute status message
msg = _("Muted other applications")
else:
# Translators: Announcing new applications' mute status message
msg = _("Unmuted other applications")
ui.message(msg)
_appVolumeState = VolumeAndMute(volume, muted)
3 changes: 0 additions & 3 deletions source/config/configSpec.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,6 @@
whiteNoiseVolume = integer(default=0, min=0, max=100)
soundSplitState = integer(default=0)
includedSoundSplitModes = int_list(default=list(0, 2, 3))
applicationsSoundVolume = integer(default=100, min=0, max=100)
applicationsSoundMuted = boolean(default=False)
applicationsVolumeMode = featureFlag(optionsEnum="AppsVolumeAdjusterFlag", behaviorOfDefault="DISABLED")

# Braille settings
[braille]
Expand Down
17 changes: 0 additions & 17 deletions source/config/featureFlagEnums.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,23 +66,6 @@ def __bool__(self):
return self == BoolFlag.ENABLED


class AppsVolumeAdjusterFlag(DisplayStringEnum):
@property
def _displayStringLabels(self):
return {
# Translators: A choice disabling "application volume adjustment"
# in the audio settings panel.
self.DISABLED: _("No"),
# Translators: A choice enabling "application volume adjustment"
# in the audio settings panel.
self.ENABLED: _("Yes"),
}

DEFAULT = enum.auto()
DISABLED = enum.auto()
ENABLED = enum.auto()


class ParagraphNavigationFlag(DisplayStringEnum):
@property
def _displayStringLabels(self):
Expand Down
14 changes: 0 additions & 14 deletions source/globalCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4756,7 +4756,6 @@ def script_cycleSoundSplit(self, gesture: "inputCore.InputGesture") -> None:
"Increases the volume of other applications",
),
category=SCRCAT_AUDIO,
gesture="kb:NVDA+alt+pageUp",
)
def script_increaseApplicationsVolume(self, gesture: "inputCore.InputGesture") -> None:
appsVolume._adjustAppsVolume(5)
Expand All @@ -4767,29 +4766,16 @@ def script_increaseApplicationsVolume(self, gesture: "inputCore.InputGesture") -
"Decreases the volume of other applications",
),
category=SCRCAT_AUDIO,
gesture="kb:NVDA+alt+pageDown",
)
def script_decreaseApplicationsVolume(self, gesture: "inputCore.InputGesture") -> None:
appsVolume._adjustAppsVolume(-5)

@script(
description=_(
# Translators: Describes a command.
"Toggles other applications volume adjuster status",
),
category=SCRCAT_AUDIO,
gesture=None,
)
def script_toggleApplicationsVolumeAdjuster(self, gesture: "inputCore.InputGesture") -> None:
appsVolume._toggleAppsVolumeState()

@script(
description=_(
# Translators: Describes a command.
"Mutes or unmutes other applications",
),
category=SCRCAT_AUDIO,
gesture="kb:NVDA+alt+delete",
)
def script_toggleApplicationsMute(self, gesture: "inputCore.InputGesture") -> None:
appsVolume._toggleAppsVolumeMute()
Expand Down
60 changes: 0 additions & 60 deletions source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3107,48 +3107,6 @@ def makeSettings(self, settingsSizer: wx.BoxSizer) -> None:

self._appendSoundSplitModesList(sHelper)

label = _(
# Translators: This is a label for the
# "allow NVDA to control the volume of other applications"
# combo box in settings.
"&Allow NVDA to control the volume of other applications:",
)
self.appVolAdjusterCombo: nvdaControls.FeatureFlagCombo = sHelper.addLabeledControl(
labelText=label,
wxCtrlClass=nvdaControls.FeatureFlagCombo,
keyPath=["audio", "applicationsVolumeMode"],
conf=config.conf,
)
self.appVolAdjusterCombo.Bind(wx.EVT_CHOICE, self._onSoundVolChange)
self.bindHelpEvent("AppsVolumeAdjusterStatus", self.appVolAdjusterCombo)

# 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)
volume = config.conf["audio"]["applicationsSoundVolume"]
if 0 <= volume <= 100:
self.appSoundVolSlider.SetValue(volume)
else:
log.error("Invalid volume level: {}", volume)
defaultVolume = config.conf.getConfigValidation(["audio", "applicationsSoundVolume"]).default
self.appSoundVolSlider.SetValue(defaultVolume)

self.muteOtherAppsCheckBox: wx.CheckBox = sHelper.addItem(
# Translators: Mute other apps checkbox in settings
wx.CheckBox(self, label=_("Mute other apps")),
)
self.muteOtherAppsCheckBox.SetValue(config.conf["audio"]["applicationsSoundMuted"])
self.bindHelpEvent("OtherAppMute", self.muteOtherAppsCheckBox)

self._onSoundVolChange(None)

audioAwakeTimeLabelText = _(
# Translators: The label for a setting in Audio settings panel
# to change how long the audio device is kept awake after speech
Expand Down Expand Up @@ -3207,14 +3165,6 @@ def onSave(self):
for mIndex in range(len(self._allSoundSplitModes))
if mIndex in self.soundSplitModesList.CheckedItems
]
config.conf["audio"]["applicationsSoundVolume"] = self.appSoundVolSlider.GetValue()
config.conf["audio"]["applicationsSoundMuted"] = self.muteOtherAppsCheckBox.GetValue()
self.appVolAdjusterCombo.saveCurrentValueToConf()
audio.appsVolume._updateAppsVolumeImpl(
volume=self.appSoundVolSlider.GetValue() / 100.0,
muted=self.muteOtherAppsCheckBox.GetValue(),
state=self.appVolAdjusterCombo._getControlCurrentValue(),
)

if audioDucking.isAudioDuckingSupported():
index = self.duckingList.GetSelection()
Expand All @@ -3224,7 +3174,6 @@ def onSave(self):
config.conf["audio"]["audioAwakeTime"] = self.audioAwakeTimeEdit.GetValue()

def onPanelActivated(self):
self._onSoundVolChange(None)
super().onPanelActivated()

def _onSoundVolChange(self, event: wx.Event) -> None:
Expand All @@ -3237,15 +3186,6 @@ def _onSoundVolChange(self, event: wx.Event) -> None:
self.soundSplitComboBox.Enable(wasapi)
self.soundSplitModesList.Enable(wasapi)

avEnabled = config.featureFlagEnums.AppsVolumeAdjusterFlag.ENABLED
self.appSoundVolSlider.Enable(
wasapi and self.appVolAdjusterCombo._getControlCurrentValue() == avEnabled,
)
self.muteOtherAppsCheckBox.Enable(
wasapi and self.appVolAdjusterCombo._getControlCurrentValue() == avEnabled,
)
self.appVolAdjusterCombo.Enable(wasapi)

def isValid(self) -> bool:
enabledSoundSplitModes = self.soundSplitModesList.CheckedItems
if len(enabledSoundSplitModes) < 1:
Expand Down
6 changes: 1 addition & 5 deletions user_docs/en/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@
* Support for math in PDFs has been added.
This works for formulas with associated MathML, such as some files generated by newer versions of TeX/LaTeX.
Currently this is only supported in Foxit Reader & Foxit Editor. (#9288, @NSoiffer)
* Commands to adjust the volume of other applications besides NVDA have been added.
To use this feature, "allow NVDA to control the volume of other applications" must be enabled in the audio settings panel. (#16052, @mltony, @codeofdusk)
* `NVDA+alt+pageUp`: Increase the volume of all other applications.
* `NVDA+alt+pageDown`: Decrease the volume of all other applications.
* `NVDA+alt+delete`: Mute the volume of all other applications.
* Unbound gestures to adjust the volume of other applications besides NVDA have been added. (#16052, #17335, @mltony, @codeofdusk)
* When editing in Microsoft PowerPoint text boxes, you can now move per sentence with `alt+upArrow`/`alt+downArrow`. (#17015, @LeonarddeR)
* In Mozilla Firefox, NVDA will report the highlighted text when a URL containing a text fragment is visited. (#16910, @jcsteh)
* NVDA can now report when a link destination points to the current page. (#141, @LeonarddeR, @nvdaes)
Expand Down
44 changes: 0 additions & 44 deletions user_docs/en/userGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2465,50 +2465,6 @@ 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.

##### Allow NVDA to control the volume of other applications {#AppsVolumeAdjusterStatus}

| . {.hideHeaderRow} |.|
|---|---|
|Options |No, Yes|
|Default |No|

This combo box determines whether NVDA commands can be used to adjust the volume of other applications running on the system.

Possible values are:

* No: NVDA doesn't interfere with the volume levels of other applications.
* Yes: The volume of other applications can be adjusted via [other applications volume slider](#OtherAppVolume) and NVDA commands.
Enabling this option causes NVDA's configuration to override any external changes to running applications' volumes (such as adjustments made by the Windows Volume Mixer) whenever NVDA modifies them.

This option is not available if you have started NVDA with [WASAPI disabled for audio output](#WASAPI) in Advanced Settings.
While [audio ducking](#SelectSynthesizerDuckingMode) does change the volume of other applications when engaged, it operates independently of this option.

##### Volume of other applications {#OtherAppVolume}

This slider allows you to adjust the volume of all currently running applications other than NVDA.
This volume can also be controlled via the following keyboard commands from anywhere:

| Name | Key | Description |
|---|---|---|
| Increase volume of other applications | `NVDA+alt+pageUp` | Increases the volume of all applications except NVDA. |
| Decrease volume of other applications | `NVDA+alt+pageDown` | Decreases the volume of all applications except NVDA. |

This option is not available if you have started NVDA with [WASAPI disabled for audio output](#WASAPI) in Advanced Settings.

##### Mute other applications {#OtherAppMute}

This check box allows you to mute or unmute all applications except NVDA at once.

The following keyboard command can also be used from anywhere:

| Name | Key | Description |
|---|---|---|
| Mute or unmute other applications | `NVDA+alt+delete` | Toggles mute/unmute on other applications |

Please note, that this option is not persistent: other apps will always be unmuted when NVDA restarts.

This option is not available if you have started NVDA with [WASAPI disabled for audio output](#WASAPI) in Advanced Settings.

##### Time to keep audio device awake after speech {#AudioAwakeTime}

This edit box specifies how long NVDA keeps the audio device awake after speech ends.
Expand Down