Skip to content

Commit

Permalink
Removed config.conf["speech"]["outputDevice"] in favour of config.con…
Browse files Browse the repository at this point in the history
…f["audio"]["outputDevice"]
  • Loading branch information
SaschaCowley committed Dec 18, 2024
1 parent 9c5dc27 commit e48f5a4
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 5 deletions.
2 changes: 1 addition & 1 deletion source/config/configSpec.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
includeCLDR = boolean(default=True)
symbolDictionaries = string_list(default=list("cldr"))
beepSpeechModePitch = integer(default=10000,min=50,max=11025)
outputDevice = string(default=default)
autoLanguageSwitching = boolean(default=true)
autoDialectSwitching = boolean(default=false)
delayedCharacterDescriptions = boolean(default=false)
Expand All @@ -55,6 +54,7 @@
# Audio settings
[audio]
outputDevice = string(default=default)
audioDuckingMode = integer(default=0)
soundVolumeFollowsVoice = boolean(default=false)
soundVolume = integer(default=100, min=0, max=100)
Expand Down
50 changes: 50 additions & 0 deletions source/config/profileUpgradeSteps.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,53 @@ def upgradeConfigFrom_12_to_13(profile: ConfigObj) -> None:
log.debug(
f"Handled cldr value of {setting!r}. List is now: {profile['speech']['symbolDictionaries']}",
)


def upgradeConfigFrom_13_to_14(profile: ConfigObj):
"""Set [audio][outputDevice] to the endpointID of [speech][outputDevice], and delete the latter.
"""
try:
friendlyName = profile["speech"]["outputDevice"]
except KeyError:
log.debug("Output device not present in config. Taking no action.")
return
if friendlyName == "default":
log.debug("Output device is set to default. Not writing a new value to config.")
elif endpointId := _friendlyNameToEndpointId(friendlyName):
log.debug(f"Best match for device with {friendlyName=} has {endpointId=}. Writing new value to config.")
profile["audio"]["outputDevice"] = endpointId
else:
log.debug(f"Could not find an audio output device with {friendlyName=}. Not writing a new value to config.")
log.debug("Deleting old config value.")
del profile["speech"]["outputDevice"]


def _friendlyNameToEndpointId(friendlyName: str) -> str | None:
"""_summary_
Since friendly names are not unique, there may be many devices on one system with the same friendly name.
As the order of devices in an IMMEndpointEnumerator is arbitrary, we cannot assume that the first device with a matching friendly name is the device the user wants.
We also can't guarantee that the device the user has selected is active, so we need to retrieve devices by state, in order from most to least preferable.
It is probably a safe bet that the device the user wants to use is either active or unplugged.
Thus, the preference order for states is:
1. ACTIVE- The audio adapter that connects to the endpoint device is present and enabled.
In addition, if the endpoint device plugs into a jack on the adapter, then the endpoint device is plugged in.
2. UNPLUGGED - The audio adapter that contains the jack for the endpoint device is present and enabled, but the endpoint device is not plugged into the jack.
3. DISABLED - The user has disabled the device in the Windows multimedia control panel.
4. NOTPRESENT - The audio adapter that connects to the endpoint device has been removed from the system, or the user has disabled the adapter device in Device Manager.
Within a state, if there is more than one device with the selected friendly name, we use the first one.
:param friendlyName: Friendly name of the device to search for.
:return: Endpoint ID string of the best match device, or `None` if no device with a matching friendly name is available.
"""
from nvwave import _getOutputDevices
from pycaw.constants import DEVICE_STATE

states = (DEVICE_STATE.ACTIVE, DEVICE_STATE.UNPLUGGED, DEVICE_STATE.DISABLED, DEVICE_STATE.NOTPRESENT)
for state in states:
try:
return next(device for device in _getOutputDevices(stateMask=state) if device.friendlyName == friendlyName).id
except StopIteration:
# Proceed to the next device state.
continue
return None
13 changes: 9 additions & 4 deletions source/nvwave.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,18 @@ class _AudioOutputDevice(typing.NamedTuple):
friendlyName: str


def _getOutputDevices(*, includeDefault: bool = False) -> Generator[_AudioOutputDevice]:
def _getOutputDevices(
*,
includeDefault: bool = False,
stateMask: DEVICE_STATE = DEVICE_STATE.ACTIVE,
) -> Generator[_AudioOutputDevice]:
"""Generator, yielding device ID and device Name.
..note: Depending on number of devices being fetched, this may take some time (~3ms)
:param includeDefault: Whether to include a value representing the system default output device in the generator.
:param includeDefault: Whether to include a value representing the system default output device in the generator, defaults to False.
..note: The ID of this device is **not** a valid mmdevice endpoint ID string, and is for internal use only.
The friendly name is **not** generated by the operating system, and no endpoint devices should have this friendly name.
The friendly name is **not** generated by the operating system, and it is highly unlikely that it will match any real output device.
:param state: What device states to include in the resultant generator, defaults to DEVICE_STATE.ACTIVE.
:return: Generator of :class:`_AudioOutputDevices` containing all enabled and present audio output devices on the system.
"""
if includeDefault:
Expand All @@ -122,7 +127,7 @@ def _getOutputDevices(*, includeDefault: bool = False) -> Generator[_AudioOutput
)
endpointCollection = AudioUtilities.GetDeviceEnumerator().EnumAudioEndpoints(
EDataFlow.eRender.value,
DEVICE_STATE.ACTIVE.value,
stateMask.value,
)
for i in range(endpointCollection.GetCount()):
device = AudioUtilities.CreateDevice(endpointCollection.Item(i))
Expand Down
2 changes: 2 additions & 0 deletions user_docs/en/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ As the NVDA update check URL is now configurable directly within NVDA, no replac
* `gui.settingsDialogs.AdvancedPanelControls.wasapiComboBox` has been removed.
* The `WASAPI` key has been removed from the `audio` section of the config spec.
* The output from `nvwave.outputDeviceNameToID`, and input to `nvwave.outputDeviceIDToName` are now string identifiers.
* The configuration key `config.conf["speech"]["outputDevice"]` has been removed.
It has been replaced by `config.conf["audio"]["outputDevice"]`, which stores a Windows core audio endpoint device ID. (#17547)
* In `NVDAObjects.window.scintilla.ScintillaTextInfo`, if no text is selected, the `collapse` method is overriden to expand to line if the `end` parameter is set to `True` (#17431, @nvdaes)
* The following symbols have been removed with no replacement: `languageHandler.getLanguageCliArgs`, `__main__.quitGroup` and `__main__.installGroup` . (#17486, @CyrilleB79)
* Prefix matching on command line flags, e.g. using `--di` for `--disable-addons` is no longer supported. (#11644, @CyrilleB79)
Expand Down

0 comments on commit e48f5a4

Please sign in to comment.