-
-
Notifications
You must be signed in to change notification settings - Fork 649
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
1,407 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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,204 @@ | ||
# A part of NonVisual Desktop Access (NVDA) | ||
# Copyright (C) 2015-2021 NV Access Limited | ||
# This file is covered by the GNU General Public License. | ||
# See the file COPYING for more details. | ||
|
||
import atexit | ||
from comInterfaces.coreAudio.constants import ( | ||
CLSID_MMDeviceEnumerator, | ||
EDataFlow, | ||
ERole, | ||
) | ||
import comInterfaces.coreAudio.audioclient as audioclient | ||
import comInterfaces.coreAudio.audiopolicy as audiopolicy | ||
import comInterfaces.coreAudio.mmdeviceapi as mmdeviceapi | ||
import comtypes | ||
import config | ||
from enum import IntEnum, unique | ||
import globalVars | ||
import json | ||
from logHandler import log | ||
import nvwave | ||
from typing import Callable | ||
import ui | ||
from utils.displayString import DisplayStringIntEnum | ||
import _ctypes | ||
|
||
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: _("Disabled sound split"), | ||
# 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: | ||
if self == SoundSplitState.OFF or 'APPS_BOTH' in self.name: | ||
return (1.0, 1.0) | ||
elif 'APPS_LEFT' in self.name: | ||
return (1.0, 0.0) | ||
elif 'APPS_RIGHT' in self.name: | ||
return (0.0, 1.0) | ||
else: | ||
raise RuntimeError | ||
|
||
def getNVDAVolume(self) -> VolumeTupleT: | ||
if self == SoundSplitState.OFF or 'NVDA_BOTH' in self.name: | ||
return (1.0, 1.0) | ||
elif 'NVDA_LEFT' in self.name: | ||
return (1.0, 0.0) | ||
elif 'NVDA_RIGHT' in self.name: | ||
return (0.0, 1.0) | ||
else: | ||
raise RuntimeError | ||
|
||
|
||
sessionManager: audiopolicy.IAudioSessionManager2 = None | ||
activeCallback: comtypes.COMObject | None = None | ||
|
||
|
||
def initialize() -> None: | ||
global sessionManager | ||
try: | ||
sessionManager = getSessionManager() | ||
except _ctypes.COMError as e: | ||
log.error("Could not initialize audio session manager! ", e) | ||
return | ||
if sessionManager is None: | ||
log.error("Could not initialize audio session manager! ") | ||
return | ||
if nvwave.usingWasapiWavePlayer(): | ||
state = SoundSplitState(config.conf['audio']['soundSplitState']) | ||
global activeCallback | ||
activeCallback = setSoundSplitState(state) | ||
|
||
|
||
@atexit.register | ||
def terminate(): | ||
global activeCallback | ||
if nvwave.usingWasapiWavePlayer(): | ||
setSoundSplitState(SoundSplitState.OFF) | ||
if activeCallback is not None: | ||
unregisterCallback(activeCallback) | ||
activeCallback = None | ||
|
||
|
||
def getDefaultAudioDevice(kind: EDataFlow = EDataFlow.eRender) -> mmdeviceapi.IMMDevice | None: | ||
deviceEnumerator = comtypes.CoCreateInstance( | ||
CLSID_MMDeviceEnumerator, | ||
mmdeviceapi.IMMDeviceEnumerator, | ||
comtypes.CLSCTX_INPROC_SERVER, | ||
) | ||
device = deviceEnumerator.GetDefaultAudioEndpoint( | ||
kind.value, | ||
ERole.eMultimedia.value, | ||
) | ||
return device | ||
|
||
|
||
def getSessionManager() -> audiopolicy.IAudioSessionManager2: | ||
audioDevice = getDefaultAudioDevice() | ||
if audioDevice is None: | ||
raise RuntimeError("No default output audio device found!") | ||
tmp = audioDevice.Activate(audiopolicy.IAudioSessionManager2._iid_, comtypes.CLSCTX_ALL, None) | ||
sessionManager: audiopolicy.IAudioSessionManager2 = tmp.QueryInterface(audiopolicy.IAudioSessionManager2) | ||
return sessionManager | ||
|
||
|
||
def applyToAllAudioSessions( | ||
func: Callable[[audiopolicy.IAudioSessionControl2], None], | ||
applyToFuture: bool = True, | ||
) -> comtypes.COMObject | None: | ||
sessionEnumerator: audiopolicy.IAudioSessionEnumerator = sessionManager.GetSessionEnumerator() | ||
for i in range(sessionEnumerator.GetCount()): | ||
session: audiopolicy.IAudioSessionControl = sessionEnumerator.GetSession(i) | ||
session2: audiopolicy.IAudioSessionControl2 = session.QueryInterface(audiopolicy.IAudioSessionControl2) | ||
func(session2) | ||
if applyToFuture: | ||
class AudioSessionNotification(comtypes.COMObject): | ||
_com_interfaces_ = (audiopolicy.IAudioSessionNotification,) | ||
|
||
def OnSessionCreated(self, session: audiopolicy.IAudioSessionControl): | ||
session2 = session.QueryInterface(audiopolicy.IAudioSessionControl2) | ||
func(session2) | ||
callback = AudioSessionNotification() | ||
sessionManager.RegisterSessionNotification(callback) | ||
return callback | ||
else: | ||
return None | ||
|
||
|
||
def unregisterCallback(callback: comtypes.COMObject) -> None: | ||
sessionManager .UnregisterSessionNotification(callback) | ||
|
||
|
||
def setSoundSplitState(state: SoundSplitState) -> None: | ||
global activeCallback | ||
if activeCallback is not None: | ||
unregisterCallback(activeCallback) | ||
activeCallback = None | ||
leftVolume, rightVolume = state.getAppVolume() | ||
leftNVDAVolume, rightNVDAVolume = state.getNVDAVolume() | ||
|
||
def volumeSetter(session2: audiopolicy.IAudioSessionControl2) -> None: | ||
channelVolume: audioclient.IChannelAudioVolume = session2.QueryInterface(audioclient.IChannelAudioVolume) | ||
channelCount = channelVolume.GetChannelCount() | ||
if channelCount != 2: | ||
pid = session2.GetProcessId() | ||
log.warning(f"Audio session for pid {pid} has {channelCount} channels instead of 2 - cannot set volume!") | ||
return | ||
pid: int = session2.GetProcessId() | ||
if pid != globalVars.appPid: | ||
channelVolume.SetChannelVolume(0, leftVolume, None) | ||
channelVolume.SetChannelVolume(1, rightVolume, None) | ||
else: | ||
channelVolume.SetChannelVolume(0, leftNVDAVolume, None) | ||
channelVolume.SetChannelVolume(1, rightNVDAVolume, None) | ||
|
||
activeCallback = applyToAllAudioSessions(volumeSetter) | ||
|
||
|
||
def toggleSoundSplitState() -> None: | ||
if not nvwave.usingWasapiWavePlayer(): | ||
message = _( | ||
# Translators: error message when wasapi is turned off. | ||
"Sound split is only available in wasapi mode. " | ||
"Please enable wasapi on the Advanced panel in NVDA Settings." | ||
) | ||
ui.message(message) | ||
return | ||
state = SoundSplitState(config.conf['audio']['soundSplitState']) | ||
allowedStates: list[int] = json.loads(config.conf["audio"]["includedSoundSplitModes"]) | ||
try: | ||
i = allowedStates.index(state) | ||
except ValueError: | ||
i = -1 | ||
i = (i + 1) % len(allowedStates) | ||
newState = SoundSplitState(allowedStates[i]) | ||
setSoundSplitState(newState) | ||
config.conf['audio']['soundSplitState'] = newState.value | ||
ui.message(newState.displayString) |
Empty file.
This file contains 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,155 @@ | ||
# This file is a part of pycaw library (https://github.com/AndreMiras/pycaw). | ||
# Please note that it is distributed under MIT license: | ||
# https://github.com/AndreMiras/pycaw#MIT-1-ov-file | ||
|
||
from ctypes import HRESULT, POINTER, c_float | ||
from ctypes import c_longlong as REFERENCE_TIME | ||
from ctypes import c_uint32 as UINT32 | ||
from ctypes.wintypes import BOOL, DWORD, HANDLE, UINT | ||
|
||
from comtypes import COMMETHOD, GUID, IUnknown | ||
|
||
from .depend import WAVEFORMATEX | ||
|
||
|
||
class ISimpleAudioVolume(IUnknown): | ||
_iid_ = GUID("{87CE5498-68D6-44E5-9215-6DA47EF883D8}") | ||
_methods_ = ( | ||
# HRESULT SetMasterVolume( | ||
# [in] float fLevel, | ||
# [in] LPCGUID EventContext); | ||
COMMETHOD( | ||
[], | ||
HRESULT, | ||
"SetMasterVolume", | ||
(["in"], c_float, "fLevel"), | ||
(["in"], POINTER(GUID), "EventContext"), | ||
), | ||
# HRESULT GetMasterVolume([out] float *pfLevel); | ||
COMMETHOD( | ||
[], HRESULT, "GetMasterVolume", (["out"], POINTER(c_float), "pfLevel") | ||
), | ||
# HRESULT SetMute( | ||
# [in] BOOL bMute, | ||
# [in] LPCGUID EventContext); | ||
COMMETHOD( | ||
[], | ||
HRESULT, | ||
"SetMute", | ||
(["in"], BOOL, "bMute"), | ||
(["in"], POINTER(GUID), "EventContext"), | ||
), | ||
# HRESULT GetMute([out] BOOL *pbMute); | ||
COMMETHOD([], HRESULT, "GetMute", (["out"], POINTER(BOOL), "pbMute")), | ||
) | ||
|
||
|
||
class IAudioClient(IUnknown): | ||
_iid_ = GUID("{1cb9ad4c-dbfa-4c32-b178-c2f568a703b2}") | ||
_methods_ = ( | ||
# HRESULT Initialize( | ||
# [in] AUDCLNT_SHAREMODE ShareMode, | ||
# [in] DWORD StreamFlags, | ||
# [in] REFERENCE_TIME hnsBufferDuration, | ||
# [in] REFERENCE_TIME hnsPeriodicity, | ||
# [in] const WAVEFORMATEX *pFormat, | ||
# [in] LPCGUID AudioSessionGuid); | ||
COMMETHOD( | ||
[], | ||
HRESULT, | ||
"Initialize", | ||
(["in"], DWORD, "ShareMode"), | ||
(["in"], DWORD, "StreamFlags"), | ||
(["in"], REFERENCE_TIME, "hnsBufferDuration"), | ||
(["in"], REFERENCE_TIME, "hnsPeriodicity"), | ||
(["in"], POINTER(WAVEFORMATEX), "pFormat"), | ||
(["in"], POINTER(GUID), "AudioSessionGuid"), | ||
), | ||
# HRESULT GetBufferSize( | ||
# [out] UINT32 *pNumBufferFrames); | ||
COMMETHOD( | ||
[], HRESULT, "GetBufferSize", (["out"], POINTER(UINT32), "pNumBufferFrames") | ||
), | ||
# HRESULT GetStreamLatency( | ||
# [out] REFERENCE_TIME *phnsLatency); | ||
COMMETHOD( | ||
[], | ||
HRESULT, | ||
"GetStreamLatency", | ||
(["out"], POINTER(REFERENCE_TIME), "phnsLatency"), | ||
), | ||
# HRESULT GetCurrentPadding( | ||
# [out] UINT32 *pNumPaddingFrames); | ||
COMMETHOD( | ||
[], | ||
HRESULT, | ||
"GetCurrentPadding", | ||
(["out"], POINTER(UINT32), "pNumPaddingFrames"), | ||
), | ||
# HRESULT IsFormatSupported( | ||
# [in] AUDCLNT_SHAREMODE ShareMode, | ||
# [in] const WAVEFORMATEX *pFormat, | ||
# [out,unique] WAVEFORMATEX **ppClosestMatch); | ||
COMMETHOD( | ||
[], | ||
HRESULT, | ||
"IsFormatSupported", | ||
(["in"], DWORD, "ShareMode"), | ||
(["in"], POINTER(WAVEFORMATEX), "pFormat"), | ||
(["out"], POINTER(POINTER(WAVEFORMATEX)), "ppClosestMatch"), | ||
), | ||
# HRESULT GetMixFormat( | ||
# [out] WAVEFORMATEX **ppDeviceFormat | ||
# ); | ||
COMMETHOD( | ||
[], | ||
HRESULT, | ||
"GetMixFormat", | ||
(["out"], POINTER(POINTER(WAVEFORMATEX)), "ppDeviceFormat"), | ||
), | ||
# HRESULT GetDevicePeriod( | ||
# [out] REFERENCE_TIME *phnsDefaultDevicePeriod, | ||
# [out] REFERENCE_TIME *phnsMinimumDevicePeriod); | ||
COMMETHOD( | ||
[], | ||
HRESULT, | ||
"GetDevicePeriod", | ||
(["out"], POINTER(REFERENCE_TIME), "phnsDefaultDevicePeriod"), | ||
(["out"], POINTER(REFERENCE_TIME), "phnsMinimumDevicePeriod"), | ||
), | ||
# HRESULT Start(void); | ||
COMMETHOD([], HRESULT, "Start"), | ||
# HRESULT Stop(void); | ||
COMMETHOD([], HRESULT, "Stop"), | ||
# HRESULT Reset(void); | ||
COMMETHOD([], HRESULT, "Reset"), | ||
# HRESULT SetEventHandle([in] HANDLE eventHandle); | ||
COMMETHOD( | ||
[], | ||
HRESULT, | ||
"SetEventHandle", | ||
(["in"], HANDLE, "eventHandle"), | ||
), | ||
# HRESULT GetService( | ||
# [in] REFIID riid, | ||
# [out] void **ppv); | ||
COMMETHOD( | ||
[], | ||
HRESULT, | ||
"GetService", | ||
(["in"], POINTER(GUID), "iid"), | ||
(["out"], POINTER(POINTER(IUnknown)), "ppv"), | ||
), | ||
) | ||
|
||
|
||
class IChannelAudioVolume (IUnknown): | ||
_iid_ = GUID('{1c158861-b533-4b30-b1cf-e853e51c59b8}') | ||
_methods_ = ( | ||
COMMETHOD([], HRESULT, 'GetChannelCount', | ||
(['out'], POINTER(UINT), 'pnChannelCount')), | ||
COMMETHOD([], HRESULT, 'SetChannelVolume', | ||
(['in'], UINT, 'dwIndex'), | ||
(['in'], c_float, 'fLevel'), | ||
(['in'], POINTER(GUID), 'EventContext')), | ||
) |
This file contains 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,22 @@ | ||
# This file is a part of pycaw library (https://github.com/AndreMiras/pycaw). | ||
# Please note that it is distributed under MIT license: | ||
# https://github.com/AndreMiras/pycaw#MIT-1-ov-file | ||
|
||
from ctypes import Structure | ||
from ctypes.wintypes import WORD | ||
|
||
|
||
class WAVEFORMATEX(Structure): | ||
_fields_ = [ | ||
("wFormatTag", WORD), | ||
("nChannels", WORD), | ||
("nSamplesPerSec", WORD), | ||
("nAvgBytesPerSec", WORD), | ||
("nBlockAlign", WORD), | ||
("wBitsPerSample", WORD), | ||
("cbSize", WORD), | ||
] | ||
|
||
|
||
|
||
|
Oops, something went wrong.