Skip to content

Commit 8760019

Browse files
authored
Merge d005acc into 690103a
2 parents 690103a + d005acc commit 8760019

6 files changed

Lines changed: 45 additions & 77 deletions

File tree

source/config/profileUpgradeSteps.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,14 +459,14 @@ def _friendlyNameToEndpointId(friendlyName: str) -> str | None:
459459
:param friendlyName: Friendly name of the device to search for.
460460
:return: Endpoint ID string of the best match device, or `None` if no device with a matching friendly name is available.
461461
"""
462-
from utils.mmdevice import _getOutputDevices
462+
from utils.mmdevice import getOutputDevices
463463
from pycaw.constants import DEVICE_STATE
464464

465465
states = (DEVICE_STATE.ACTIVE, DEVICE_STATE.UNPLUGGED, DEVICE_STATE.DISABLED, DEVICE_STATE.NOTPRESENT)
466466
for state in states:
467467
try:
468468
return next(
469-
device for device in _getOutputDevices(stateMask=state) if device.friendlyName == friendlyName
469+
device for device in getOutputDevices(stateMask=state) if device.friendlyName == friendlyName
470470
).id
471471
except StopIteration:
472472
# Proceed to the next device state.

source/gui/settingsDialogs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3035,7 +3035,7 @@ def makeSettings(self, settingsSizer: wx.BoxSizer) -> None:
30353035
# Translators: This is the label for the select output device combo in NVDA audio settings.
30363036
# Examples of an output device are default soundcard, usb headphones, etc.
30373037
deviceListLabelText = _("Audio output &device:")
3038-
self._deviceIds, deviceNames = zip(*mmdevice._getOutputDevices(includeDefault=True))
3038+
self._deviceIds, deviceNames = zip(*mmdevice.getOutputDevices(includeDefault=True))
30393039
self.deviceList = sHelper.addLabeledControl(deviceListLabelText, wx.Choice, choices=deviceNames)
30403040
self.bindHelpEvent("SelectSynthesizerOutputDevice", self.deviceList)
30413041
selectedOutputDevice = config.conf["audio"]["outputDevice"]

source/nvwave.py

Lines changed: 7 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,13 @@
3838
import NVDAHelper
3939
import core
4040
import globalVars
41-
from pycaw.utils import AudioUtilities
4241
from speech import SpeechSequence
4342
from speech.commands import BreakCommand
4443
from synthDriverHandler import pre_synthSpeak
4544

46-
from utils.mmdevice import _getOutputDevices
47-
4845

4946
__all__ = (
5047
"WavePlayer",
51-
"getOutputDeviceNames",
52-
"outputDeviceIDToName",
53-
"outputDeviceNameToID",
5448
"decide_playWaveFile",
5549
)
5650

@@ -92,42 +86,6 @@ class AudioPurpose(Enum):
9286
SOUNDS = auto()
9387

9488

95-
def getOutputDeviceNames() -> list[str]:
96-
"""Obtain the names of all audio output devices on the system.
97-
:return: The names of all output devices on the system.
98-
..note: Depending on number of devices being fetched, this may take some time (~3ms)
99-
"""
100-
return [name for ID, name in _getOutputDevices()]
101-
102-
103-
def outputDeviceIDToName(ID: str) -> str:
104-
"""Obtain the name of an output device given its device ID.
105-
:param ID: The device ID.
106-
:return: The device name.
107-
"""
108-
device = AudioUtilities.GetDeviceEnumerator().GetDevice(id)
109-
return AudioUtilities.CreateDevice(device).FriendlyName
110-
111-
112-
def outputDeviceNameToID(name: str, useDefaultIfInvalid: bool = False) -> str:
113-
"""Obtain the device ID of an output device given its name.
114-
:param name: The device name.
115-
:param useDefaultIfInvalid: `True` to use the default device if there is no such device, `False` to raise an exception.
116-
:return: The device ID.
117-
:raise LookupError: If there is no such device and `useDefaultIfInvalid` is `False`.
118-
..note: Depending on number of devices, and the position of the device in the list, this may take some time (~3ms)
119-
"""
120-
for curID, curName in _getOutputDevices():
121-
if curName == name:
122-
return curID
123-
124-
# No such ID.
125-
if useDefaultIfInvalid:
126-
return AudioUtilities.GetSpeakers().GetId()
127-
else:
128-
raise LookupError("No such device name")
129-
130-
13189
def playWaveFile(
13290
fileName: str,
13391
asynchronous: bool = True,
@@ -211,7 +169,7 @@ def isInError() -> bool:
211169
wasPlay_callback = CFUNCTYPE(None, c_void_p, c_uint)
212170

213171

214-
class WasapiWavePlayer(garbageHandler.TrackedObject):
172+
class WavePlayer(garbageHandler.TrackedObject):
215173
"""Synchronously play a stream of audio using WASAPI.
216174
To use, construct an instance and feed it waveform audio using L{feed}.
217175
Keeps device open until it is either not available, or WavePlayer is explicitly closed / deleted.
@@ -278,20 +236,20 @@ def __init__(
278236
self._player = NVDAHelper.localLib.wasPlay_create(
279237
outputDevice,
280238
format,
281-
WasapiWavePlayer._callback,
239+
WavePlayer._callback,
282240
)
283241
self._doneCallbacks = {}
284242
self._instances[self._player] = self
285243
self.open()
286244
self._lastActiveTime: typing.Optional[float] = None
287245
self._isPaused: bool = False
288-
if config.conf["audio"]["audioAwakeTime"] > 0 and WasapiWavePlayer._silenceDevice != outputDevice:
246+
if config.conf["audio"]["audioAwakeTime"] > 0 and WavePlayer._silenceDevice != outputDevice:
289247
# The output device has changed. (Re)initialize silence.
290248
if self._silenceDevice is not None:
291249
NVDAHelper.localLib.wasSilence_terminate()
292250
if config.conf["audio"]["audioAwakeTime"] > 0:
293251
NVDAHelper.localLib.wasSilence_init(outputDevice)
294-
WasapiWavePlayer._silenceDevice = outputDevice
252+
WavePlayer._silenceDevice = outputDevice
295253
# Enable trimming by default for speech only
296254
self.enableTrimmingLeadingSilence(
297255
purpose is AudioPurpose.SPEECH and config.conf["speech"]["trimLeadingSilence"],
@@ -303,7 +261,7 @@ def __init__(
303261

304262
@wasPlay_callback
305263
def _callback(cppPlayer, feedId):
306-
pyPlayer = WasapiWavePlayer._instances[cppPlayer]
264+
pyPlayer = WavePlayer._instances[cppPlayer]
307265
onDone = pyPlayer._doneCallbacks.pop(feedId, None)
308266
if onDone:
309267
onDone()
@@ -339,7 +297,7 @@ def open(self):
339297
)
340298
WavePlayer.audioDeviceError_static = True
341299
raise
342-
WasapiWavePlayer.audioDeviceError_static = False
300+
WavePlayer.audioDeviceError_static = False
343301
self._setVolumeFromConfig()
344302

345303
def close(self):
@@ -567,7 +525,6 @@ def _onPreSpeak(self, speechSequence: SpeechSequence):
567525
break
568526

569527

570-
WavePlayer = WasapiWavePlayer
571528
fileWavePlayer: Optional[WavePlayer] = None
572529
fileWavePlayerThread: threading.Thread | None = None
573530

@@ -592,7 +549,7 @@ def initialize():
592549

593550

594551
def terminate() -> None:
595-
if WasapiWavePlayer._silenceDevice is not None:
552+
if WavePlayer._silenceDevice is not None:
596553
NVDAHelper.localLib.wasSilence_terminate()
597554
getOnErrorSoundRequested().unregister(playErrorSound)
598555

source/utils/mmdevice.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# This file is covered by the GNU General Public License.
44
# See the file COPYING for more details.
55

6+
"""Tools for interacting with Windows' MMDevice API."""
7+
68
from collections.abc import Generator
79
from typing import NamedTuple, cast
810

@@ -11,16 +13,21 @@
1113
from pycaw.utils import AudioUtilities
1214

1315

14-
class _AudioOutputDevice(NamedTuple):
16+
class AudioOutputDevice(NamedTuple):
17+
"""An MMDevice audio render endpoint."""
18+
1519
id: str
20+
"""The unique identifier of the audio endpoint."""
21+
1622
friendlyName: str
23+
"""The user-friendly name of the audio endpoint."""
1724

1825

19-
def _getOutputDevices(
26+
def getOutputDevices(
2027
*,
2128
includeDefault: bool = False,
2229
stateMask: DEVICE_STATE = DEVICE_STATE.ACTIVE,
23-
) -> Generator[_AudioOutputDevice]:
30+
) -> Generator[AudioOutputDevice]:
2431
"""Generator, yielding device ID and device Name.
2532
.. note:: Depending on number of devices being fetched, this may take some time (~3ms)
2633
@@ -31,7 +38,7 @@ def _getOutputDevices(
3138
:return: Generator of :class:`_AudioOutputDevices` containing all enabled and present audio output devices on the system.
3239
"""
3340
if includeDefault:
34-
yield _AudioOutputDevice(
41+
yield AudioOutputDevice(
3542
id=cast(str, config.conf.getConfigValidation(("audio", "outputDevice")).default),
3643
# Translators: Value to show when choosing to use the default audio output device.
3744
friendlyName=_("Default output device"),
@@ -44,6 +51,6 @@ def _getOutputDevices(
4451
device = AudioUtilities.CreateDevice(endpointCollection.Item(i))
4552
# This should never be None, but just to be sure
4653
if device is not None:
47-
yield _AudioOutputDevice(device.id, device.FriendlyName)
54+
yield AudioOutputDevice(device.id, device.FriendlyName)
4855
else:
4956
continue

tests/unit/test_config.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
from utils.displayString import (
4848
DisplayStringEnum,
4949
)
50-
from utils.mmdevice import _AudioOutputDevice
50+
from utils.mmdevice import AudioOutputDevice
5151

5252

5353
class Config_FeatureFlagEnums_getAvailableEnums(unittest.TestCase):
@@ -918,26 +918,26 @@ def test_updateToDifferentValue(self):
918918
self.assertEqual(self.profile, {"someBool": False})
919919

920920

921-
_DevicesT: typing.TypeAlias = dict[DEVICE_STATE, list[_AudioOutputDevice]]
921+
_DevicesT: typing.TypeAlias = dict[DEVICE_STATE, list[AudioOutputDevice]]
922922

923923

924924
def getOutputDevicesFactory(
925925
devices: _DevicesT,
926-
) -> Callable[[DEVICE_STATE], Generator[_AudioOutputDevice]]:
927-
"""Create a callable that can be used to patch utils.mmdevice._getOutputDevices."""
926+
) -> Callable[[DEVICE_STATE], Generator[AudioOutputDevice]]:
927+
"""Create a callable that can be used to patch utils.mmdevice.getOutputDevices."""
928928

929-
def getOutputDevices(stateMask: DEVICE_STATE, **kw) -> Generator[_AudioOutputDevice]:
929+
def getOutputDevices(stateMask: DEVICE_STATE, **kw) -> Generator[AudioOutputDevice]:
930930
yield from devices.get(stateMask, [])
931931

932932
return getOutputDevices
933933

934934

935935
class Config_ProfileUpgradeSteps_FriendlyNameToEndpointId(unittest.TestCase):
936936
DEFAULT_DEVICES: _DevicesT = {
937-
DEVICE_STATE.ACTIVE: [_AudioOutputDevice("id1", "Device 1")],
938-
DEVICE_STATE.UNPLUGGED: [_AudioOutputDevice("id2", "Device 2")],
939-
DEVICE_STATE.DISABLED: [_AudioOutputDevice("id3", "Device 3")],
940-
DEVICE_STATE.NOTPRESENT: [_AudioOutputDevice("id4", "Device 4")],
937+
DEVICE_STATE.ACTIVE: [AudioOutputDevice("id1", "Device 1")],
938+
DEVICE_STATE.UNPLUGGED: [AudioOutputDevice("id2", "Device 2")],
939+
DEVICE_STATE.DISABLED: [AudioOutputDevice("id3", "Device 3")],
940+
DEVICE_STATE.NOTPRESENT: [AudioOutputDevice("id4", "Device 4")],
941941
}
942942

943943
def test_noDuplicates(self):
@@ -952,10 +952,10 @@ def test_orderOfPrecedence(self):
952952
"""Test that, when there are devices with duplicate names in different states, the one with the preferred state is returned."""
953953
FRIENDLY_NAME = "Device friendly name"
954954
devices: _DevicesT = {
955-
DEVICE_STATE.ACTIVE: [_AudioOutputDevice("idA", FRIENDLY_NAME)],
956-
DEVICE_STATE.DISABLED: [_AudioOutputDevice("idD", FRIENDLY_NAME)],
957-
DEVICE_STATE.NOTPRESENT: [_AudioOutputDevice("idN", FRIENDLY_NAME)],
958-
DEVICE_STATE.UNPLUGGED: [_AudioOutputDevice("idU", FRIENDLY_NAME)],
955+
DEVICE_STATE.ACTIVE: [AudioOutputDevice("idA", FRIENDLY_NAME)],
956+
DEVICE_STATE.DISABLED: [AudioOutputDevice("idD", FRIENDLY_NAME)],
957+
DEVICE_STATE.NOTPRESENT: [AudioOutputDevice("idN", FRIENDLY_NAME)],
958+
DEVICE_STATE.UNPLUGGED: [AudioOutputDevice("idU", FRIENDLY_NAME)],
959959
}
960960
with self.subTest("Friendly name is active"):
961961
self.performTest(*devices[DEVICE_STATE.ACTIVE][0], devices)
@@ -981,11 +981,11 @@ def test_noDevices(self):
981981
self.performTest(friendlyName="Anything", expectedId=None, devices=devices)
982982

983983
def performTest(self, expectedId: str | None, friendlyName: str, devices: _DevicesT):
984-
"""Patch utils.mmdevice._getOutputDevices to return what we tell it, then test that friendlyNameToEndpointId returns the correct ID given a friendly name.
984+
"""Patch utils.mmdevice.getOutputDevices to return what we tell it, then test that friendlyNameToEndpointId returns the correct ID given a friendly name.
985985
The odd order of arguments is so you can directly unpack an AudioOutputDevice.
986986
"""
987987
with patch(
988-
"utils.mmdevice._getOutputDevices",
988+
"utils.mmdevice.getOutputDevices",
989989
autospec=True,
990990
side_effect=getOutputDevicesFactory(devices),
991991
):
@@ -995,10 +995,10 @@ def performTest(self, expectedId: str | None, friendlyName: str, devices: _Devic
995995
class Config_upgradeProfileSteps_upgradeProfileFrom_13_to_14(unittest.TestCase):
996996
def setUp(self):
997997
devices: _DevicesT = {
998-
DEVICE_STATE.ACTIVE: [_AudioOutputDevice("id", "Friendly name")],
998+
DEVICE_STATE.ACTIVE: [AudioOutputDevice("id", "Friendly name")],
999999
}
10001000
self._getOutputDevicesPatcher = patch(
1001-
"utils.mmdevice._getOutputDevices",
1001+
"utils.mmdevice.getOutputDevices",
10021002
autospec=True,
10031003
side_effect=getOutputDevicesFactory(devices),
10041004
)

user_docs/en/changes.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ Add-ons will need to be re-tested and have their manifest updated.
165165
* Added the `matchFunc` parameter to `addUsbDevices` which is also available on `addUsbDevice`.
166166
* This way device detection can be constrained further in cases where a VID/PID-combination is shared by multiple devices across multiple drivers, or when a HID device offers multiple endpoints, for example.
167167
* See the method documentation as well as examples in the albatross and brailliantB drivers for more information.
168+
* Added a new function, `utils.mmdevice.getOutputDevices`, to enumerate audio output devices. (#17678)
168169
* Added a new extension point `pre_synthSpeak` in `synthDriverHandler`, which will be called before the speech manager calls `speak` of the current synthesizer.
169170

170171
#### API Breaking Changes
@@ -183,8 +184,11 @@ As the NVDA update check URL is now configurable directly within NVDA, no replac
183184
* `SymphonyDocument.script_toggleTextAttribute` to `SymphonyDocument.script_changeTextFormatting`
184185
* The `space` keyword argument for `brailleDisplayDrivers.seikantk.InputGesture` now expects an `int` rather than a `bool`. (#17047, @school510587)
185186
* The `[upgrade]` configuration section including `[upgrade][newLaptopKeyboardLayout]` has been removed. (#17191)
186-
* Due to the retirement of NVDA's winmm support (#17496, #17532):
187-
* The following symbols have been removed from `nvwave`: `CALLBACK_EVENT`, `CALLBACK_FUNCTION`, `CALLBACK_NULL`, `HWAVEOUT`, `LPHWAVEOUT`, `LPWAVEFORMATEX`, `LPWAVEHDR`, `MAXPNAMELEN`, `MMSYSERR_NOERROR`, `usingWasapiWavePlayer`, `WAVEHDR`, `WAVEOUTCAPS`, `waveOutProc`, `WAVE_MAPPER`, `WHDR_DONE`, `WinmmWavePlayer`, and `winmm`.
187+
* Due to the retirement of NVDA's winmm support (#17496, #17532, #17678):
188+
* The following symbols have been removed from `nvwave` without replacements: `CALLBACK_EVENT`, `CALLBACK_FUNCTION`, `CALLBACK_NULL`, `HWAVEOUT`, `LPHWAVEOUT`, `LPWAVEFORMATEX`, `LPWAVEHDR`, `MAXPNAMELEN`, `MMSYSERR_NOERROR`, `usingWasapiWavePlayer`, `WAVEHDR`, `WAVEOUTCAPS`, `waveOutProc`, `WAVE_MAPPER`, `WHDR_DONE`, `WinmmWavePlayer`, and `winmm`.
189+
* The following symbols have been removed from `nvwave`: `getOutputDeviceNames`, `outputDeviceIDToName`, `outputDeviceNameToID`.
190+
Use `utils.mmdevice.getOutputDevices` instead.
191+
* `nvwave.WasapiWavePlayer` has been renamed to `WavePlayer`.
188192
* `gui.settingsDialogs.AdvancedPanelControls.wasapiComboBox` has been removed.
189193
* The `WASAPI` key has been removed from the `audio` section of the config spec.
190194
* The output from `nvwave.outputDeviceNameToID`, and input to `nvwave.outputDeviceIDToName` are now string identifiers.

0 commit comments

Comments
 (0)