Add possibility to cycle through output devices forward and backward#15493
Add possibility to cycle through output devices forward and backward#15493beqabeqa473 wants to merge 3 commits into
Conversation
9060467 to
39bc639
Compare
See test results for failed build of commit af326aebcd |
|
Is there a particular reason why we need two scripts for this? Shoudln't this just be a single cycler? |
|
Yes. if there are lots of audio devices, it will be harder to find
needed devices.
I am sure lots of musicians are using loopback devices, virtual audio
cables and for me for example, it is 15 audio devices available on my
pc right now.
…On 9/21/23, Leonard de Ruijter ***@***.***> wrote:
Is there a particular reason why we need two scripts for this? Shoudln't
this just be a single cycler?
--
Reply to this email directly or view it on GitHub:
#15493 (comment)
You are receiving this because you authored the thread.
Message ID: ***@***.***>
--
with best regards Beqa Gozalishvili
Tell: +995593454005
Email: ***@***.***
Web: https://gozaltech.org
Skype: beqabeqa473
Telegram: https://t.me/gozaltech
facebook: https://facebook.com/gozaltech
twitter: https://twitter.com/beqabeqa473
Instagram: https://instagram.com/beqa.gozalishvili
|
|
I had the same question as @LeonarddeR. I didn't ask it, because I realized that
as long as both of the scripts wrap around, the default assignment (if any) only
has to be to one of them.
Users with only two or three devices will only ever need a single script
assigned. But users with absurd numbers of interfaces, can assign the second
one as well, for increased flexibility.
|
39bc639 to
b9a245f
Compare
See test results for failed build of commit 621b8a74e5 |
|
Is this ready for review? |
|
Not yet.
…On 9/22/23, Sean Budd ***@***.***> wrote:
Is this ready for review?
--
Reply to this email directly or view it on GitHub:
#15493 (comment)
You are receiving this because you authored the thread.
Message ID: ***@***.***>
--
with best regards Beqa Gozalishvili
Tell: +995593454005
Email: ***@***.***
Web: https://gozaltech.org
Skype: beqabeqa473
Telegram: https://t.me/gozaltech
facebook: https://facebook.com/gozaltech
twitter: https://twitter.com/beqabeqa473
Instagram: https://instagram.com/beqa.gozalishvili
|
| from base64 import b16encode | ||
| import vision | ||
| from utils.security import objectBelowLockScreenAndWindowsIsLocked | ||
| from utils.synth import getOutputDeviceNames, setOutputDevice |
There was a problem hiding this comment.
I'm slightly puzzled why this module is called synth while in fact, it contains logic for audio. I'm also not sure why this can't just reside in the nvwave module.
There was a problem hiding this comment.
In fact, it doesn't contain logic for audio. Switching to another
audio device involves reinitializing tts engine and tones module.
I wanted to eliminate code duplication and decoupled this code.
If you have a better alternative i am glad to implement it.
Both of these functions are related to audio, not synthesizers.
I would rename the utils.synth module to audio or move to nvwave like @LeonarddeR suggests
|
Hello.
In fact, it doesn't contain logic for audio. Switching to another
audio device involves reinitializing tts engine and tones module.
I wanted to eliminate code duplication and decoupled this code.
If you have a better alternative i am glad to implement it.
…On 9/22/23, Leonard de Ruijter ***@***.***> wrote:
@LeonarddeR commented on this pull request.
> @@ -66,6 +66,7 @@
from base64 import b16encode
import vision
from utils.security import objectBelowLockScreenAndWindowsIsLocked
+from utils.synth import getOutputDeviceNames, setOutputDevice
I'm slightly puzzled why this module is called synth while in fact, it
contains logic for audio. I'm also not sure why this can't just reside in
the nvwave module.
--
Reply to this email directly or view it on GitHub:
#15493 (review)
You are receiving this because you authored the thread.
Message ID: ***@***.***>
--
with best regards Beqa Gozalishvili
Tell: +995593454005
Email: ***@***.***
Web: https://gozaltech.org
Skype: beqabeqa473
Telegram: https://t.me/gozaltech
facebook: https://facebook.com/gozaltech
twitter: https://twitter.com/beqabeqa473
Instagram: https://instagram.com/beqa.gozalishvili
|
| config.conf["documentNavigation"]["paragraphStyle"] = newFlag.name | ||
| ui.message(newFlag.displayString) | ||
|
|
||
| def cycleOutputAudioDevices(self, direction: int): |
There was a problem hiding this comment.
Can this be made private (underscore prefixed), and converted to a static method, as far as I can tel it does not need access to the instance. Also the type annotation should reflect the fact that direction cannot be an arbitrary int, but only 1, or -1, I'd suggest using typing.Literal here.
There was a problem hiding this comment.
Instead of using an int for direction, why not make it default to forward? Then you could use True/False, with a bool type defaulting to True.
Rename it from direction: int to cycleForward: bool = True or similar.
Then you wouldn't have to worry about int bounds violations, and the type checker could handle all of your call restriction logic.
| ), | ||
| category=SCRCAT_SPEECH, | ||
| ) | ||
| def script_switchToNextOutputDevice(self, gesture: "inputCore.InputGesture") -> None: |
There was a problem hiding this comment.
No point in stringizing type hint here, same for the script below.
| import tones | ||
|
|
||
|
|
||
| def getOutputDeviceNames(): |
There was a problem hiding this comment.
I'd suggest this function should be moved to nvwave, with an appropriate name so that it is clear it returns friendly device names.
|
|
||
| == New Features == | ||
| - Added support for Bluetooth Low Energy HID Braille displays. (#15470) | ||
| - Added unassigned gestures to to quickly cycle through output devices forward and backward. (#8801) |
There was a problem hiding this comment.
Double 'to':
| - Added unassigned gestures to to quickly cycle through output devices forward and backward. (#8801) | |
| - Added unassigned gestures to quickly cycle through output devices forward and backward. (#8801) |
|
|
||
| ==== Output device ====[SelectSynthesizerOutputDevice] | ||
| This option allows you to choose the audio device that NVDA should instruct the selected synthesizer to speak through. | ||
| To switch to the next or previous output device, pleas assign custom shortcuts from [Input Gestures dialog #InputGestures]. |
| def setOutputDevice(device: str): | ||
| config.conf["speech"]["outputDevice"] = device | ||
| currentSynth = getSynth() | ||
| if not setSynth(currentSynth.name): |
There was a problem hiding this comment.
I'm not sure I agree with the logic here. I understand you haven't written this code, but since we're revisiting it anyway let's try to make it nicer. Currently we set the configured device before checking if it can be used i.e. if the current synthesizer can speak through it. Also with the current design there is an import cycle, since you import gui here, yet gui imports from this new module. I'd move this to nvwave, but rewrite the code (in my view basic module for playing sound should not depend on other modules tones, synthesizer drivers, which depend on it), using extensionPoints.Decider in nvwave to which both tones and synthDriverHandler can subscribe (something like onMainAudioDeviceChanged). If both synthesizer and tones can work with the new device, only then it should be set in the config. Also in that case you can check what has been returned from the decider in the settings dialog, and display error in the gui there, and announce the new device, or failure in switching into it in the keyboard scripts. With this approach it would be necessary to modify tones and setSynth to accept the specified audio device, as until now they were always using the default one.
There was a problem hiding this comment.
hi.
Could you please elaborate more on this?
There was a problem hiding this comment.
I could implement decider but could you tell me, how i can understand that synthDriver or tones have succeeded or failed if i won't set config entry first?
There was a problem hiding this comment.
I have looked at the code more closely and while I still believe that setting device in the config before reinitializing synthesizer is not ideal, changing this will require a huge refactoring. Doing this as part of this PR is probably unjustified, so I'm fine with the current approach.
|
Hello.
thanks for suggestions, I will be returning to this PR at these
weekends to finish it.
…On 11/10/23, Łukasz Golonka ***@***.***> wrote:
@lukaszgo1 requested changes on this pull request.
> @@ -4289,6 +4290,37 @@ def script_cycleParagraphStyle(self, gesture:
> "inputCore.InputGesture") -> None:
config.conf["documentNavigation"]["paragraphStyle"] = newFlag.name
ui.message(newFlag.displayString)
+ def cycleOutputAudioDevices(self, direction: int):
Can this be made private (underscore prefixed), and converted to a static
method, as far as I can tel it does not need access to the instance. Also
the type annotation should reflect the fact that `direction` cannot be an
arbitrary int, but only `1`, or `-1`, I'd suggest using `typing.Literal`
here.
> + index = deviceNames.index(config.conf["speech"]["outputDevice"])
+ newIndex = (index + direction) % len(deviceNames)
+ except ValueError:
+ newIndex = 0
+ device = deviceNames[newIndex]
+ setOutputDevice(device)
+ ui.message(device)
+
+ @script(
+ description=_(
+ # Translators: Describes the switch to next output device command.
+ "switches to the next output device"
+ ),
+ category=SCRCAT_SPEECH,
+ )
+ def script_switchToNextOutputDevice(self, gesture:
"inputCore.InputGesture") -> None:
No point in stringizing type hint here, same for the script below.
> @@ -0,0 +1,31 @@
+# A part of NonVisual Desktop Access (NVDA)
+# Copyright (C) 2023 Beka Gozalishvili
+# This file may be used under the terms of the GNU General Public License,
version 2 or later.
+# For more details see: https://www.gnu.org/licenses/gpl-2.0.html
+
+
+import config
+from gui.settingsDialogs import _synthWarningDialog
+import nvwave
+from synthDriverHandler import getSynth, setSynth
+import tones
+
+
+def getOutputDeviceNames():
I'd suggest this function should be moved to `nvwave`, with an appropriate
name so that it is clear it returns friendly device names.
> @@ -8,6 +8,7 @@ What's New in NVDA
== New Features ==
- Added support for Bluetooth Low Energy HID Braille displays. (#15470)
+- Added unassigned gestures to to quickly cycle through output devices
forward and backward. (#8801)
Double 'to':
```suggestion
- Added unassigned gestures to quickly cycle through output devices forward
and backward. (#8801)
```
> @@ -1775,6 +1775,7 @@ The Audio category in the NVDA Settings dialog
> contains options that let you cha
==== Output device ====[SelectSynthesizerOutputDevice]
This option allows you to choose the audio device that NVDA should instruct
the selected synthesizer to speak through.
+To switch to the next or previous output device, pleas assign custom
shortcuts from [Input Gestures dialog #InputGestures].
Pleas -> please
> +import tones
+
+
+def getOutputDeviceNames():
+ deviceNames = nvwave.getOutputDeviceNames()
+ # #11349: On Windows 10 20H1 and 20H2, Microsoft Sound Mapper returns an
empty string.
+ if deviceNames[0] in ("", "Microsoft Sound Mapper"):
+ # Translators: name for default (Microsoft Sound Mapper) audio output
device.
+ deviceNames[0] = _("Microsoft Sound Mapper")
+ return deviceNames
+
+
+def setOutputDevice(device: str):
+ config.conf["speech"]["outputDevice"] = device
+ currentSynth = getSynth()
+ if not setSynth(currentSynth.name):
I'm not sure I agree with the logic here. I understand you haven't written
this code, but since we're revisiting it anyway let's try to make it nicer.
Currently we set the configured device **before** checking if it can be used
i.e. if the current synthesizer can speak through it. Also with the current
design there is an import cycle, since you import `gui` here, yet `gui`
imports from this new module. I'd move this to `nvwave`, but rewrite the
code (in my view basic module for playing sound should not depend on other
modules `tones`, synthesizer drivers, which depend on it), using
`extensionPoints.Decider` in `nvwave` to which both `tones` and
`synthDriverHandler` can subscribe (something like
`onMainAudioDeviceChanged`). If both synthesizer and tones can work with the
new device, only then it should be set in the config. Also in that case you
can check what has been returned from the decider in the settings dialog,
and display error in the gui there, and announce the new device, or failure
in switching into it in the keyboard scripts. With this approach it would be
necessary to modify tones and `setSynth` to accept the specified audio
device, as until now they were always using the default one.
--
Reply to this email directly or view it on GitHub:
#15493 (review)
You are receiving this because you authored the thread.
Message ID: ***@***.***>
--
with best regards Beqa Gozalishvili
Tell: +995593454005
Email: ***@***.***
Web: https://gozaltech.org
Skype: beqabeqa473
Telegram: https://t.me/gozaltech
facebook: https://facebook.com/gozaltech
twitter: https://twitter.com/beqabeqa473
Instagram: https://instagram.com/beqa.gozalishvili
|
|
Hi @beqabeqa473 |
|
@beqabeqa473 What is the status of this? I would love to see it go to beta. I am in need of this feature. I'm going to have to implement it in an add-on if this PR is not proceeding soon. |
|
Hello.
Sorry for delay.
I have working prototype, i will push tomorrow and we will see how to proceed.
…On 2/16/24, Luke Davis ***@***.***> wrote:
@beqabeqa473 What is the status of this? I would love to see it go to beta.
I am in need of this feature. I'm going to have to implement it in an add-on
if this PR is not proceeding soon.
Life gets in the way, I very much understand, but please mention it if you
need help bringing this forward.
--
Reply to this email directly or view it on GitHub:
#15493 (comment)
You are receiving this because you were mentioned.
Message ID: ***@***.***>
--
with best regards Beqa Gozalishvili
Tell: +995593454005
Email: ***@***.***
Web: https://gozaltech.org
Skype: beqabeqa473
Telegram: https://t.me/gozaltech
facebook: https://facebook.com/gozaltech
twitter: https://twitter.com/beqabeqa473
Instagram: https://instagram.com/beqa.gozalishvili
|
b9a245f to
4a2e343
Compare
|
@XLTechie hi, i pushed latest changes, and i think it is ready to review. |
See test results for failed build of commit 10ea7d962c |
4a2e343 to
41e21fd
Compare
See test results for failed build of commit 1bc860a915 |
41e21fd to
827eb2d
Compare
|
@seanbudd I've modified _synthWarningDialog to accept synth instance, while at it, i've made that synthDriverHandler.getSynthList now returns SynthDriver classes rather than names and descriptions and modified synth selection dialog to make it working with new layout. |
9503cf0 to
a71976d
Compare
|
|
||
| def updateSynthesizerList(self): | ||
| driverList=getSynthList() | ||
| self.synthNames=[x[0] for x in driverList] |
There was a problem hiding this comment.
this should not be removed, this is an API breaking change.
I'd encourage reverting these unrelated changes.
|
|
||
|
|
||
| def getSynthList() -> List[Tuple[str, str]]: | ||
| def getSynthList() -> list[SynthDriver]: |
There was a problem hiding this comment.
please don't change the return type of this function, it is an API breaking change
| currentSynth = getSynth() | ||
| if not setSynth(currentSynth.name): | ||
| return False | ||
| onSynthSetFailed(currentSynth) |
There was a problem hiding this comment.
| onSynthSetFailed(currentSynth) | |
| if onSynthSetFailed is not None: | |
| onSynthSetFailed(currentSynth) |
| def setOutputDevice( | ||
| deviceName: str, | ||
| onSynthSetFailed: Callable[[SynthDriver], None] | None = None, | ||
| ): |
There was a problem hiding this comment.
| ): | |
| ) -> bool: |
| currentSynth = getSynth() | ||
| if not setSynth(currentSynth.name): | ||
| onSynthSetFailed(currentSynth) | ||
| return |
There was a problem hiding this comment.
| return | |
| return False |
| @param deviceName: The device name. | ||
| @param onSynthSetFailed: A callable to show an error if setting synthesizer fails. |
There was a problem hiding this comment.
| @param deviceName: The device name. | |
| @param onSynthSetFailed: A callable to show an error if setting synthesizer fails. | |
| :param deviceName: The device name. | |
| :param onSynthSetFailed: A callable to show an error if setting synthesizer fails. | |
| :returns: True if setting the output device succeeds |
| @staticmethod | ||
| def _cycleOutputAudioDevices(forward: bool = True): | ||
| """Cycle through available output devices. | ||
| @param forward: boolean flag if false cycles in backward direction. |
There was a problem hiding this comment.
please update all the docstrings to use sphinx format
https://github.com/nvaccess/nvda/blob/master/projectDocs/dev/codingStandards.md#docstrings
|
@beqabeqa473 are you still working on this? |
|
I will try to revisit this in the near feature. |
|
Closing as abandoned |
Link to issue number:
Fixes #8801
Summary of the issue:
After introducing an additional audio panel and moving output device selection out of synthesizer selection dialog, it became harder to recover from situation where audio device is loosing and speech doesn't output anymore.
It is now possible to quickly switch to the next or previous output devices with shortcuts
Description of user facing changes
Added two unassigned shortcuts to switch to the next and previous output device.
Description of development approach
Added two scripts and a helper method to globalCommands.py
moved out setting output device from gui.settingDialogs to a dedicated utils.synth module to eliminate code duplication
Testing strategy:
Tried to assign shortcuts to scripts listed above and confirmed that they switch output device forward and backward.
Known issues with pull request:
None found
Code Review Checklist: