Skip to content

Commit 10ea7d9

Browse files
authored
Merge 4a2e343 into 421fe4e
2 parents 421fe4e + 4a2e343 commit 10ea7d9

5 files changed

Lines changed: 74 additions & 15 deletions

File tree

source/globalCommands.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# Copyright (C) 2006-2024 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Rui Batista, Joseph Lee,
66
# Leonard de Ruijter, Derek Riemer, Babbage B.V., Davy Kager, Ethan Holliger, Łukasz Golonka, Accessolutions,
77
# Julien Cochuyt, Jakub Lukowicz, Bill Dengler, Cyrille Bougot, Rob Meredith, Luke Davis,
8-
# Burman's Computer and Education Ltd.
8+
# Burman's Computer and Education Ltd, Beka Gozalishvili.
99

1010
import itertools
1111
from typing import (
@@ -58,6 +58,7 @@
5858
import braille
5959
import brailleInput
6060
import inputCore
61+
import nvwave
6162
import characterProcessing
6263
from baseObject import ScriptableObject
6364
import core
@@ -4443,6 +4444,43 @@ def script_cycleParagraphStyle(self, gesture: "inputCore.InputGesture") -> None:
44434444
config.conf["documentNavigation"]["paragraphStyle"] = newFlag.name
44444445
ui.message(newFlag.displayString)
44454446

4447+
@staticmethod
4448+
def _cycleOutputAudioDevices(forward: bool = True):
4449+
"""Cycle through available output devices.
4450+
@param forward: boolean flag if false cycles in backward direction.
4451+
"""
4452+
deviceNames = nvwave.getFriendlyOutputDeviceNames()
4453+
currentIndex = 0
4454+
currentOutputDevice = config.conf["speech"]["outputDevice"]
4455+
if currentOutputDevice in deviceNames:
4456+
currentIndex = deviceNames.index(currentOutputDevice)
4457+
direction = 1 if forward else -1
4458+
newIndex = (currentIndex + direction) % len(deviceNames)
4459+
device = deviceNames[newIndex]
4460+
if not nvwave.setOutputDevice(device):
4461+
return ui.message(_("Failed to set {device} as an output audio device").format(device=device))
4462+
ui.message(device)
4463+
4464+
@script(
4465+
description=_(
4466+
# Translators: Describes the switch to next output device command.
4467+
"switches to the next output device"
4468+
),
4469+
category=SCRCAT_SPEECH,
4470+
)
4471+
def script_switchToNextOutputDevice(self, gesture: inputCore.InputGesture) -> None:
4472+
self._cycleOutputAudioDevices()
4473+
4474+
@script(
4475+
description=_(
4476+
# Translators: Describes the switch to previous output device command.
4477+
"switches to the previous output device"
4478+
),
4479+
category=SCRCAT_SPEECH,
4480+
)
4481+
def script_switchToPreviousOutputDevice(self, gesture: inputCore.InputGesture) -> None:
4482+
self._cycleOutputAudioDevices(-False)
4483+
44464484

44474485
#: The single global commands instance.
44484486
#: @type: L{GlobalCommands}

source/gui/settingsDialogs.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2661,11 +2661,7 @@ def makeSettings(self, settingsSizer: wx.BoxSizer) -> None:
26612661
# Translators: This is the label for the select output device combo in NVDA audio settings.
26622662
# Examples of an output device are default soundcard, usb headphones, etc.
26632663
deviceListLabelText = _("Audio output &device:")
2664-
deviceNames = nvwave.getOutputDeviceNames()
2665-
# #11349: On Windows 10 20H1 and 20H2, Microsoft Sound Mapper returns an empty string.
2666-
if deviceNames[0] in ("", "Microsoft Sound Mapper"):
2667-
# Translators: name for default (Microsoft Sound Mapper) audio output device.
2668-
deviceNames[0] = _("Microsoft Sound Mapper")
2664+
deviceNames = nvwave.getFriendlyOutputDeviceNames()
26692665
self.deviceList = sHelper.addLabeledControl(deviceListLabelText, wx.Choice, choices=deviceNames)
26702666
self.bindHelpEvent("SelectSynthesizerOutputDevice", self.deviceList)
26712667
try:
@@ -2712,16 +2708,9 @@ def makeSettings(self, settingsSizer: wx.BoxSizer) -> None:
27122708
def onSave(self):
27132709
if config.conf["speech"]["outputDevice"] != self.deviceList.GetStringSelection():
27142710
# Synthesizer must be reload if output device changes
2715-
config.conf["speech"]["outputDevice"] = self.deviceList.GetStringSelection()
2716-
currentSynth = getSynth()
2717-
if not setSynth(currentSynth.name):
2711+
if not nvwave.setOutputDevice(self.deviceList.GetStringSelection()):
27182712
_synthWarningDialog(currentSynth.name)
27192713

2720-
# Reinitialize the tones module to update the audio device
2721-
import tones
2722-
tones.terminate()
2723-
tones.initialize()
2724-
27252714
config.conf["audio"]["soundVolumeFollowsVoice"] = self.soundVolFollowCheckBox.IsChecked()
27262715
config.conf["audio"]["soundVolume"] = self.soundVolSlider.GetValue()
27272716

source/nvwave.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,16 @@
5151
import NVDAHelper
5252
import core
5353
import globalVars
54-
54+
from synthDriverHandler import getSynth, setSynth
55+
import tones
5556

5657
__all__ = (
5758
"WavePlayer",
5859
"getOutputDeviceNames",
60+
"getFriendlyOutputDeviceNames",
5961
"outputDeviceIDToName",
6062
"outputDeviceNameToID",
63+
"setOutputDevice",
6164
"decide_playWaveFile",
6265
)
6366

@@ -632,6 +635,18 @@ def getOutputDeviceNames():
632635
"""
633636
return [name for ID, name in _getOutputDevices()]
634637

638+
def getFriendlyOutputDeviceNames():
639+
"""Obtain the names of all audio output devices (including microsoft sound mapper) on the system.
640+
@return: The names of all output devices (including microsoft sound mapper) on the system.
641+
@rtype: [str, ...]
642+
"""
643+
deviceNames = getOutputDeviceNames()
644+
# #11349: On Windows 10 20H1 and 20H2, Microsoft Sound Mapper returns an empty string.
645+
if deviceNames[0] in ("", "Microsoft Sound Mapper"):
646+
# Translators: name for default (Microsoft Sound Mapper) audio output device.
647+
deviceNames[0] = _("Microsoft Sound Mapper")
648+
return deviceNames
649+
635650
def outputDeviceIDToName(ID):
636651
"""Obtain the name of an output device given its device ID.
637652
@param ID: The device ID.
@@ -668,6 +683,21 @@ def outputDeviceNameToID(name: str, useDefaultIfInvalid=False) -> int:
668683
raise LookupError("No such device name")
669684

670685

686+
def setOutputDevice(deviceName: str):
687+
"""Set an output audio device.
688+
@param deviceName: The device name.
689+
"""
690+
config.conf["speech"]["outputDevice"] = deviceName
691+
currentSynth = getSynth()
692+
if not setSynth(currentSynth.name):
693+
return False
694+
695+
# Reinitialize the tones module to update the audio device
696+
tones.terminate()
697+
tones.initialize()
698+
return True
699+
700+
671701
fileWavePlayer: Optional[WavePlayer] = None
672702
fileWavePlayerThread = None
673703

user_docs/en/changes.t2t

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ What's New in NVDA
2222
-
2323
- Added support for the BrailleEdgeS2 braille device. (#16033)
2424
- NVDA will play silence after every speech sequence in order to prevent the start of the next speech being clipped with some audio devices such as Bluetooth headphones. (#14386, @jcsteh, @mltony)
25+
- Added unassigned gestures to quickly cycle through output devices forward and backward. (#8801)
2526
-
2627

2728

user_docs/en/userGuide.t2t

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,6 +1912,7 @@ The Audio category in the NVDA Settings dialog contains options that let you cha
19121912

19131913
==== Output device ====[SelectSynthesizerOutputDevice]
19141914
This option allows you to choose the audio device that NVDA should instruct the selected synthesizer to speak through.
1915+
To switch to the next or previous output device, please assign custom shortcuts from [Input Gestures dialog #InputGestures].
19151916

19161917
%kc:setting
19171918
==== Audio Ducking Mode ====[SelectSynthesizerDuckingMode]

0 commit comments

Comments
 (0)