-
-
Notifications
You must be signed in to change notification settings - Fork 784
Sound split #16071
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
Merged
Merged
Sound split #16071
Changes from all commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
f73618e
Sound split
mltony adf2a8e
Addressing comments
mltony a856a87
Addressing comments by @@CyrilleB79
mltony 8f8ce12
Update user_docs/en/userGuide.t2t
mltony 501737f
According to @lukaszgo1 converting json-encoded list into native list…
mltony bfef0fc
Update source/audio.py
mltony eff0c6d
Addressing comment by @LeonarddeR
mltony 7834395
Addressing comments
mltony 7873e96
Addresing comment
mltony 1f6d4a4
lint
mltony 4202ab6
Addressing comments
mltony 11f5edc
Merge branch 'master' into soundSplit
mltony 68e5d8d
Update source/audio/soundSplit.py
mltony 74f525c
Addressing comment
mltony f2d5701
lint
mltony 1d4ffb3
Update source/audio/soundSplit.py
mltony 62f9415
Update source/audio/soundSplit.py
mltony 64aac39
Update doc
mltony 8bddb3b
Update user_docs/en/changes.t2t
mltony 007e108
Update source/audio/__init__.py
mltony 6d60437
Switching to pycaw
mltony 780a402
Merge branch 'master' into soundSplit
mltony 14db885
lint
mltony 66753cb
Revert sconscript
mltony 0fc96c7
Update pycaw version
mltony 5da261d
Update source/config/configSpec.py
mltony 4671781
Update source/audio/soundSplit.py
mltony 549dde3
Update source/audio/soundSplit.py
mltony ce52f0f
Update source/audio/soundSplit.py
mltony a9bfe6a
Update source/audio/soundSplit.py
mltony 01ce970
Update source/audio/soundSplit.py
mltony 04eb90a
Update source/gui/settingsDialogs.py
mltony 466075c
doc
mltony 5832eb7
Merge branch 'master' into soundSplit
mltony cfe3015
Update source/audio/soundSplit.py
mltony b6adad1
Addressing comments
mltony cf1f6ba
Merge branch 'master' into soundSplit
mltony 26d319d
docs
mltony 520b4fc
commit suggestion
seanbudd 633b814
Update source/audio/soundSplit.py
mltony 880544a
Update source/audio/soundSplit.py
mltony 34cc3ab
Update source/gui/settingsDialogs.py
mltony 161bc2a
Update user_docs/en/userGuide.t2t
mltony ef5c81c
Update user_docs/en/userGuide.t2t
mltony fb9c561
Update user_docs/en/userGuide.t2t
mltony 26f75fd
Update source/gui/settingsDialogs.py
seanbudd e3227ca
fixup changes
seanbudd 9c109d3
Update user_docs/en/changes.t2t
seanbudd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # A part of NonVisual Desktop Access (NVDA) | ||
| # Copyright (C) 2024 NV Access Limited | ||
| # This file is covered by the GNU General Public License. | ||
| # See the file COPYING for more details. | ||
|
|
||
| from .soundSplit import ( | ||
| SoundSplitState, | ||
| setSoundSplitState, | ||
| toggleSoundSplitState, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "SoundSplitState", | ||
| "setSoundSplitState", | ||
| "toggleSoundSplitState", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| # A part of NonVisual Desktop Access (NVDA) | ||
| # Copyright (C) 2024 NV Access Limited | ||
| # This file is covered by the GNU General Public License. | ||
| # See the file COPYING for more details. | ||
|
|
||
| import atexit | ||
| import config | ||
| from enum import IntEnum, unique | ||
| import globalVars | ||
| from logHandler import log | ||
| import nvwave | ||
| from pycaw.api.audiopolicy import IAudioSessionManager2 | ||
| from pycaw.callbacks import AudioSessionNotification | ||
| from pycaw.utils import AudioSession, AudioUtilities | ||
| import ui | ||
| from utils.displayString import DisplayStringIntEnum | ||
| from dataclasses import dataclass | ||
|
|
||
| VolumeTupleT = tuple[float, float] | ||
|
|
||
|
|
||
| @unique | ||
| class SoundSplitState(DisplayStringIntEnum): | ||
| OFF = 0 | ||
| NVDA_LEFT_APPS_RIGHT = 1 | ||
| NVDA_LEFT_APPS_BOTH = 2 | ||
| NVDA_RIGHT_APPS_LEFT = 3 | ||
| NVDA_RIGHT_APPS_BOTH = 4 | ||
| NVDA_BOTH_APPS_LEFT = 5 | ||
| NVDA_BOTH_APPS_RIGHT = 6 | ||
|
|
||
| @property | ||
| def _displayStringLabels(self) -> dict[IntEnum, str]: | ||
| return { | ||
| # Translators: Sound split state | ||
| SoundSplitState.OFF: pgettext("SoundSplit", "Disabled"), | ||
| # Translators: Sound split state | ||
| SoundSplitState.NVDA_LEFT_APPS_RIGHT: _("NVDA on the left and applications on the right"), | ||
| # Translators: Sound split state | ||
| SoundSplitState.NVDA_LEFT_APPS_BOTH: _("NVDA on the left and applications in both channels"), | ||
| # Translators: Sound split state | ||
| SoundSplitState.NVDA_RIGHT_APPS_LEFT: _("NVDA on the right and applications on the left"), | ||
| # Translators: Sound split state | ||
| SoundSplitState.NVDA_RIGHT_APPS_BOTH: _("NVDA on the right and applications in both channels"), | ||
| # Translators: Sound split state | ||
| SoundSplitState.NVDA_BOTH_APPS_LEFT: _("NVDA in both channels and applications on the left"), | ||
| # Translators: Sound split state | ||
| SoundSplitState.NVDA_BOTH_APPS_RIGHT: _("NVDA in both channels and applications on the right"), | ||
| } | ||
|
|
||
| def getAppVolume(self) -> VolumeTupleT: | ||
| match self: | ||
| case SoundSplitState.OFF | SoundSplitState.NVDA_LEFT_APPS_BOTH | SoundSplitState.NVDA_RIGHT_APPS_BOTH: | ||
| return (1.0, 1.0) | ||
| case SoundSplitState.NVDA_RIGHT_APPS_LEFT | SoundSplitState.NVDA_BOTH_APPS_LEFT: | ||
| return (1.0, 0.0) | ||
| case SoundSplitState.NVDA_LEFT_APPS_RIGHT | SoundSplitState.NVDA_BOTH_APPS_RIGHT: | ||
| return (0.0, 1.0) | ||
| case _: | ||
| raise RuntimeError(f"Unexpected or unknown state {self=}") | ||
|
|
||
| def getNVDAVolume(self) -> VolumeTupleT: | ||
| match self: | ||
| case SoundSplitState.OFF | SoundSplitState.NVDA_BOTH_APPS_LEFT | SoundSplitState.NVDA_BOTH_APPS_RIGHT: | ||
| return (1.0, 1.0) | ||
| case SoundSplitState.NVDA_LEFT_APPS_RIGHT | SoundSplitState.NVDA_LEFT_APPS_BOTH: | ||
| return (1.0, 0.0) | ||
| case SoundSplitState.NVDA_RIGHT_APPS_LEFT | SoundSplitState.NVDA_RIGHT_APPS_BOTH: | ||
| return (0.0, 1.0) | ||
| case _: | ||
| raise RuntimeError(f"Unexpected or unknown state {self=}") | ||
|
|
||
|
|
||
| audioSessionManager: IAudioSessionManager2 | None = None | ||
| activeCallback: AudioSessionNotification | None = None | ||
|
|
||
|
|
||
| def initialize() -> None: | ||
| if nvwave.usingWasapiWavePlayer(): | ||
| global audioSessionManager | ||
| audioSessionManager = AudioUtilities.GetAudioSessionManager() | ||
| state = SoundSplitState(config.conf["audio"]["soundSplitState"]) | ||
| setSoundSplitState(state) | ||
| else: | ||
| log.debug("Cannot initialize sound split as WASAPI is disabled") | ||
|
|
||
|
|
||
| @atexit.register | ||
| def terminate(): | ||
| if nvwave.usingWasapiWavePlayer(): | ||
|
mltony marked this conversation as resolved.
|
||
| setSoundSplitState(SoundSplitState.OFF) | ||
| unregisterCallback() | ||
| else: | ||
| log.debug("Skipping terminating sound split as WASAPI is disabled.") | ||
|
|
||
|
|
||
| def applyToAllAudioSessions( | ||
|
seanbudd marked this conversation as resolved.
|
||
| callback: AudioSessionNotification, | ||
| applyToFuture: bool = True, | ||
| ) -> None: | ||
| """ | ||
| Executes provided callback function on all active audio sessions. | ||
| Additionally, if applyToFuture is True, then it will register a notification with audio session manager, | ||
| which will execute the same callback for all future sessions as they are created. | ||
| That notification will be active until next invokation of this function, | ||
| or until unregisterCallback() is called. | ||
| """ | ||
| unregisterCallback() | ||
| if applyToFuture: | ||
| audioSessionManager.RegisterSessionNotification(callback) | ||
| # The following call is required to make callback to work: | ||
| audioSessionManager.GetSessionEnumerator() | ||
| global activeCallback | ||
| activeCallback = callback | ||
| sessions: list[AudioSession] = AudioUtilities.GetAllSessions() | ||
| for session in sessions: | ||
| callback.on_session_created(session) | ||
|
|
||
|
|
||
| def unregisterCallback() -> None: | ||
| global activeCallback | ||
| if activeCallback is not None: | ||
| audioSessionManager.UnregisterSessionNotification(activeCallback) | ||
| activeCallback = None | ||
|
|
||
|
|
||
| @dataclass(unsafe_hash=True) | ||
| class VolumeSetter(AudioSessionNotification): | ||
| leftVolume: float | ||
| rightVolume: float | ||
| leftNVDAVolume: float | ||
| rightNVDAVolume: float | ||
| foundSessionWithNot2Channels: bool = False | ||
|
|
||
| def on_session_created(self, new_session: AudioSession): | ||
| pid = new_session.ProcessId | ||
| channelVolume = new_session.channelAudioVolume() | ||
| channelCount = channelVolume.GetChannelCount() | ||
| if channelCount != 2: | ||
| log.warning(f"Audio session for pid {pid} has {channelCount} channels instead of 2 - cannot set volume!") | ||
| self.foundSessionWithNot2Channels = True | ||
| return | ||
| if pid != globalVars.appPid: | ||
| channelVolume.SetChannelVolume(0, self.leftVolume, None) | ||
| channelVolume.SetChannelVolume(1, self.rightVolume, None) | ||
| else: | ||
| channelVolume.SetChannelVolume(0, self.leftNVDAVolume, None) | ||
| channelVolume.SetChannelVolume(1, self.rightNVDAVolume, None) | ||
|
|
||
|
|
||
| def setSoundSplitState(state: SoundSplitState) -> dict: | ||
| leftVolume, rightVolume = state.getAppVolume() | ||
| leftNVDAVolume, rightNVDAVolume = state.getNVDAVolume() | ||
| volumeSetter = VolumeSetter(leftVolume, rightVolume, leftNVDAVolume, rightNVDAVolume) | ||
| applyToAllAudioSessions(volumeSetter) | ||
| return { | ||
| "foundSessionWithNot2Channels": volumeSetter.foundSessionWithNot2Channels, | ||
| } | ||
|
|
||
|
|
||
| def toggleSoundSplitState() -> None: | ||
| if not nvwave.usingWasapiWavePlayer(): | ||
| message = _( | ||
|
seanbudd marked this conversation as resolved.
|
||
| # Translators: error message when wasapi is turned off. | ||
| "Sound split cannot be used. " | ||
| "Please enable WASAPI in the Advanced category in NVDA Settings to use it." | ||
| ) | ||
| ui.message(message) | ||
| return | ||
| state = SoundSplitState(config.conf["audio"]["soundSplitState"]) | ||
| allowedStates: list[int] = config.conf["audio"]["includedSoundSplitModes"] | ||
| try: | ||
| i = allowedStates.index(state) | ||
| except ValueError: | ||
| # State not found, resetting to default (OFF) | ||
| i = -1 | ||
| i = (i + 1) % len(allowedStates) | ||
| newState = SoundSplitState(allowedStates[i]) | ||
| result = setSoundSplitState(newState) | ||
| config.conf["audio"]["soundSplitState"] = newState.value | ||
| ui.message(newState.displayString) | ||
| if result["foundSessionWithNot2Channels"]: | ||
| msg = _( | ||
| # Translators: warning message when sound split trigger wasn't successful due to one of audio sessions | ||
| # had number of channels other than 2 . | ||
| "Warning: couldn't set volumes for sound split: " | ||
| "one of audio sessions is either mono, or has more than 2 audio channels." | ||
| ) | ||
| ui.message(msg) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.