Skip to content

Commit 6b4c666

Browse files
authored
Merge 781d93a into 738ead5
2 parents 738ead5 + 781d93a commit 6b4c666

27 files changed

Lines changed: 2353 additions & 1 deletion

File tree

source/audio.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# A part of NonVisual Desktop Access (NVDA)
2+
# Copyright (C) 2015-2021 NV Access Limited
3+
# This file is covered by the GNU General Public License.
4+
# See the file COPYING for more details.
5+
6+
import atexit
7+
from comInterfaces.coreAudio.constants import (
8+
CLSID_MMDeviceEnumerator,
9+
EDataFlow,
10+
ERole,
11+
)
12+
import comInterfaces.coreAudio.audioclient as audioclient
13+
import comInterfaces.coreAudio.audiopolicy as audiopolicy
14+
import comInterfaces.coreAudio.mmdeviceapi as mmdeviceapi
15+
import comtypes
16+
import config
17+
from enum import IntEnum, unique
18+
import globalVars
19+
from logHandler import log
20+
import nvwave
21+
from typing import Tuple, Optional, Dict, List, Callable, NoReturn
22+
import ui
23+
from utils.displayString import DisplayStringIntEnum
24+
25+
VolumeTupleT = Tuple[float, float]
26+
27+
28+
@unique
29+
class SoundSplitState(DisplayStringIntEnum):
30+
OFF = 0
31+
NVDA_LEFT = 1
32+
NVDA_RIGHT = 2
33+
34+
@property
35+
def _displayStringLabels(self) -> Dict[IntEnum, str]:
36+
return {
37+
# Translators: Sound split state
38+
SoundSplitState.OFF: _("Disabled sound split"),
39+
# Translators: Sound split state
40+
SoundSplitState.NVDA_LEFT: _("NVDA on the left and applications on the right"),
41+
# Translators: Sound split state
42+
SoundSplitState.NVDA_RIGHT: _("NVDA on the right and applications on the left"),
43+
}
44+
45+
def getAppVolume(self) -> VolumeTupleT:
46+
return {
47+
SoundSplitState.OFF: (1.0, 1.0),
48+
SoundSplitState.NVDA_LEFT: (0.0, 1.0),
49+
SoundSplitState.NVDA_RIGHT: (1.0, 0.0),
50+
}[self]
51+
52+
def getNVDAVolume(self) -> VolumeTupleT:
53+
return {
54+
SoundSplitState.OFF: (1.0, 1.0),
55+
SoundSplitState.NVDA_LEFT: (1.0, 0.0),
56+
SoundSplitState.NVDA_RIGHT: (0.0, 1.0),
57+
}[self]
58+
59+
60+
@unique
61+
class SoundSplitToggleMode(DisplayStringIntEnum):
62+
OFF_LEFT_RIGHT = 0
63+
OFF_AND_NVDA_LEFT = 1
64+
OFF_AND_NVDA_RIGHT = 2
65+
66+
@property
67+
def _displayStringLabels(self) -> Dict[IntEnum, str]:
68+
return {
69+
# Translators: Sound split toggle mode
70+
SoundSplitToggleMode.OFF_AND_NVDA_LEFT: _("Cycles through off and NVDA on the left"),
71+
# Translators: Sound split toggle mode
72+
SoundSplitToggleMode.OFF_AND_NVDA_RIGHT: _("Cycles through off and NVDA on the right"),
73+
# Translators: Sound split toggle mode
74+
SoundSplitToggleMode.OFF_LEFT_RIGHT: _("Cycles through off, NVDA on the left and NVDA on the right"),
75+
}
76+
77+
def getPossibleStates(self) -> List[SoundSplitState]:
78+
result = [SoundSplitState.OFF]
79+
if 'LEFT' in self.name:
80+
result.append(SoundSplitState.NVDA_LEFT)
81+
if 'RIGHT' in self.name:
82+
result.append(SoundSplitState.NVDA_RIGHT)
83+
return result
84+
85+
def getClosestState(self, state: SoundSplitState) -> SoundSplitState:
86+
states = self.getPossibleStates()
87+
if state in states:
88+
return state
89+
return states[-1]
90+
91+
92+
sessionManager: audiopolicy.IAudioSessionManager2 = None
93+
activeCallback: Optional[comtypes.COMObject] = None
94+
95+
96+
def initialize() -> None:
97+
global sessionManager
98+
sessionManager = getSessionManager()
99+
if sessionManager is None:
100+
log.error("Could not initialize audio session manager! ")
101+
return
102+
if nvwave.usingWasapiWavePlayer():
103+
state = SoundSplitState(config.conf['audio']['soundSplitState'])
104+
global activeCallback
105+
activeCallback = setSoundSplitState(state)
106+
107+
108+
@atexit.register
109+
def terminate():
110+
if nvwave.usingWasapiWavePlayer():
111+
setSoundSplitState(SoundSplitState.OFF)
112+
if activeCallback is not None:
113+
unregisterCallback(activeCallback)
114+
115+
116+
def getDefaultAudioDevice(kind: EDataFlow = EDataFlow.eRender) -> Optional[mmdeviceapi.IMMDevice]:
117+
deviceEnumerator = comtypes.CoCreateInstance(
118+
CLSID_MMDeviceEnumerator,
119+
mmdeviceapi.IMMDeviceEnumerator,
120+
comtypes.CLSCTX_INPROC_SERVER,
121+
)
122+
device = deviceEnumerator.GetDefaultAudioEndpoint(
123+
kind.value,
124+
ERole.eMultimedia.value,
125+
)
126+
return device
127+
128+
129+
def getSessionManager() -> audiopolicy.IAudioSessionManager2:
130+
audioDevice = getDefaultAudioDevice()
131+
if audioDevice is None:
132+
raise RuntimeError("No default output audio device found!")
133+
tmp = audioDevice.Activate(audiopolicy.IAudioSessionManager2._iid_, comtypes.CLSCTX_ALL, None)
134+
sessionManager: audiopolicy.IAudioSessionManager2 = tmp.QueryInterface(audiopolicy.IAudioSessionManager2)
135+
return sessionManager
136+
137+
138+
def applyToAllAudioSessions(
139+
func: Callable[[audiopolicy.IAudioSessionControl2], NoReturn],
140+
applyToFuture: bool = True,
141+
) -> Optional[comtypes.COMObject]:
142+
sessionEnumerator: audiopolicy.IAudioSessionEnumerator = sessionManager.GetSessionEnumerator()
143+
for i in range(sessionEnumerator.GetCount()):
144+
session: audiopolicy.IAudioSessionControl = sessionEnumerator.GetSession(i)
145+
session2: audiopolicy.IAudioSessionControl2 = session.QueryInterface(audiopolicy.IAudioSessionControl2)
146+
func(session2)
147+
if applyToFuture:
148+
class AudioSessionNotification(comtypes.COMObject):
149+
_com_interfaces_ = (audiopolicy.IAudioSessionNotification,)
150+
151+
def OnSessionCreated(self, session: audiopolicy.IAudioSessionControl):
152+
session2 = session.QueryInterface(audiopolicy.IAudioSessionControl2)
153+
func(session2)
154+
callback = AudioSessionNotification()
155+
sessionManager.RegisterSessionNotification(callback)
156+
return callback
157+
else:
158+
return None
159+
160+
161+
def unregisterCallback(callback: comtypes.COMObject) -> None:
162+
sessionManager .UnregisterSessionNotification(callback)
163+
164+
165+
def setSoundSplitState(state: SoundSplitState) -> None:
166+
global activeCallback
167+
if activeCallback is not None:
168+
unregisterCallback(activeCallback)
169+
activeCallback = None
170+
leftVolume, rightVolume = state.getAppVolume()
171+
172+
def volumeSetter(session2: audiopolicy.IAudioSessionControl2) -> None:
173+
channelVolume: audioclient.IChannelAudioVolume = session2.QueryInterface(audioclient.IChannelAudioVolume)
174+
channelCount = channelVolume.GetChannelCount()
175+
if channelCount != 2:
176+
pid = session2.GetProcessId()
177+
log.warning(f"Audio session for pid {pid} has {channelCount} channels instead of 2 - cannot set volume!")
178+
return
179+
pid: int = session2.GetProcessId()
180+
if pid != globalVars.appPid:
181+
channelVolume.SetChannelVolume(0, leftVolume, None)
182+
channelVolume.SetChannelVolume(1, rightVolume, None)
183+
else:
184+
channelVolume.SetChannelVolume(1, leftVolume, None)
185+
channelVolume.SetChannelVolume(0, rightVolume, None)
186+
187+
activeCallback = applyToAllAudioSessions(volumeSetter)
188+
189+
190+
def toggleSoundSplitState() -> None:
191+
if not nvwave.usingWasapiWavePlayer():
192+
# Translators: error message when wasapi is turned off.
193+
message = _(
194+
"Sound split is only available in wasapi mode. "
195+
+ "Please enable wasapi on the Advanced panel in NVDA Settings."
196+
)
197+
ui.message(message)
198+
return
199+
toggleMode = SoundSplitToggleMode(config.conf['audio']['soundSplitToggleMode'])
200+
state = SoundSplitState(config.conf['audio']['soundSplitState'])
201+
allowedStates = toggleMode.getPossibleStates()
202+
try:
203+
i = allowedStates.index(state)
204+
except ValueError:
205+
i = -1
206+
i = (i + 1) % len(allowedStates)
207+
newState = allowedStates[i]
208+
setSoundSplitState(newState)
209+
config.conf['audio']['soundSplitState'] = newState.value
210+
ui.message(newState.displayString)

source/comInterfaces/coreAudio.bak/__init__.py

Whitespace-only changes.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# This file is a part of pycaw library (https://github.com/AndreMiras/pycaw).
2+
# Please note that it is distributed under MIT license:
3+
# https://github.com/AndreMiras/pycaw#MIT-1-ov-file
4+
5+
from ctypes import HRESULT, POINTER, c_float
6+
from ctypes import c_longlong as REFERENCE_TIME
7+
from ctypes import c_uint32 as UINT32
8+
from ctypes.wintypes import BOOL, DWORD, HANDLE
9+
10+
from comtypes import COMMETHOD, GUID, IUnknown
11+
12+
from .depend import WAVEFORMATEX
13+
14+
15+
class ISimpleAudioVolume(IUnknown):
16+
_iid_ = GUID("{87CE5498-68D6-44E5-9215-6DA47EF883D8}")
17+
_methods_ = (
18+
# HRESULT SetMasterVolume(
19+
# [in] float fLevel,
20+
# [in] LPCGUID EventContext);
21+
COMMETHOD(
22+
[],
23+
HRESULT,
24+
"SetMasterVolume",
25+
(["in"], c_float, "fLevel"),
26+
(["in"], POINTER(GUID), "EventContext"),
27+
),
28+
# HRESULT GetMasterVolume([out] float *pfLevel);
29+
COMMETHOD(
30+
[], HRESULT, "GetMasterVolume", (["out"], POINTER(c_float), "pfLevel")
31+
),
32+
# HRESULT SetMute(
33+
# [in] BOOL bMute,
34+
# [in] LPCGUID EventContext);
35+
COMMETHOD(
36+
[],
37+
HRESULT,
38+
"SetMute",
39+
(["in"], BOOL, "bMute"),
40+
(["in"], POINTER(GUID), "EventContext"),
41+
),
42+
# HRESULT GetMute([out] BOOL *pbMute);
43+
COMMETHOD([], HRESULT, "GetMute", (["out"], POINTER(BOOL), "pbMute")),
44+
)
45+
46+
47+
class IAudioClient(IUnknown):
48+
_iid_ = GUID("{1cb9ad4c-dbfa-4c32-b178-c2f568a703b2}")
49+
_methods_ = (
50+
# HRESULT Initialize(
51+
# [in] AUDCLNT_SHAREMODE ShareMode,
52+
# [in] DWORD StreamFlags,
53+
# [in] REFERENCE_TIME hnsBufferDuration,
54+
# [in] REFERENCE_TIME hnsPeriodicity,
55+
# [in] const WAVEFORMATEX *pFormat,
56+
# [in] LPCGUID AudioSessionGuid);
57+
COMMETHOD(
58+
[],
59+
HRESULT,
60+
"Initialize",
61+
(["in"], DWORD, "ShareMode"),
62+
(["in"], DWORD, "StreamFlags"),
63+
(["in"], REFERENCE_TIME, "hnsBufferDuration"),
64+
(["in"], REFERENCE_TIME, "hnsPeriodicity"),
65+
(["in"], POINTER(WAVEFORMATEX), "pFormat"),
66+
(["in"], POINTER(GUID), "AudioSessionGuid"),
67+
),
68+
# HRESULT GetBufferSize(
69+
# [out] UINT32 *pNumBufferFrames);
70+
COMMETHOD(
71+
[], HRESULT, "GetBufferSize", (["out"], POINTER(UINT32), "pNumBufferFrames")
72+
),
73+
# HRESULT GetStreamLatency(
74+
# [out] REFERENCE_TIME *phnsLatency);
75+
COMMETHOD(
76+
[],
77+
HRESULT,
78+
"GetStreamLatency",
79+
(["out"], POINTER(REFERENCE_TIME), "phnsLatency"),
80+
),
81+
# HRESULT GetCurrentPadding(
82+
# [out] UINT32 *pNumPaddingFrames);
83+
COMMETHOD(
84+
[],
85+
HRESULT,
86+
"GetCurrentPadding",
87+
(["out"], POINTER(UINT32), "pNumPaddingFrames"),
88+
),
89+
# HRESULT IsFormatSupported(
90+
# [in] AUDCLNT_SHAREMODE ShareMode,
91+
# [in] const WAVEFORMATEX *pFormat,
92+
# [out,unique] WAVEFORMATEX **ppClosestMatch);
93+
COMMETHOD(
94+
[],
95+
HRESULT,
96+
"IsFormatSupported",
97+
(["in"], DWORD, "ShareMode"),
98+
(["in"], POINTER(WAVEFORMATEX), "pFormat"),
99+
(["out"], POINTER(POINTER(WAVEFORMATEX)), "ppClosestMatch"),
100+
),
101+
# HRESULT GetMixFormat(
102+
# [out] WAVEFORMATEX **ppDeviceFormat
103+
# );
104+
COMMETHOD(
105+
[],
106+
HRESULT,
107+
"GetMixFormat",
108+
(["out"], POINTER(POINTER(WAVEFORMATEX)), "ppDeviceFormat"),
109+
),
110+
# HRESULT GetDevicePeriod(
111+
# [out] REFERENCE_TIME *phnsDefaultDevicePeriod,
112+
# [out] REFERENCE_TIME *phnsMinimumDevicePeriod);
113+
COMMETHOD(
114+
[],
115+
HRESULT,
116+
"GetDevicePeriod",
117+
(["out"], POINTER(REFERENCE_TIME), "phnsDefaultDevicePeriod"),
118+
(["out"], POINTER(REFERENCE_TIME), "phnsMinimumDevicePeriod"),
119+
),
120+
# HRESULT Start(void);
121+
COMMETHOD([], HRESULT, "Start"),
122+
# HRESULT Stop(void);
123+
COMMETHOD([], HRESULT, "Stop"),
124+
# HRESULT Reset(void);
125+
COMMETHOD([], HRESULT, "Reset"),
126+
# HRESULT SetEventHandle([in] HANDLE eventHandle);
127+
COMMETHOD(
128+
[],
129+
HRESULT,
130+
"SetEventHandle",
131+
(["in"], HANDLE, "eventHandle"),
132+
),
133+
# HRESULT GetService(
134+
# [in] REFIID riid,
135+
# [out] void **ppv);
136+
COMMETHOD(
137+
[],
138+
HRESULT,
139+
"GetService",
140+
(["in"], POINTER(GUID), "iid"),
141+
(["out"], POINTER(POINTER(IUnknown)), "ppv"),
142+
),
143+
)
144+
145+
146+
class IChannelAudioVolume (IUnknown):
147+
_iid_ = GUID('{1c158861-b533-4b30-b1cf-e853e51c59b8}')
148+
_methods_ = (
149+
COMMETHOD([], HRESULT, 'GetChannelCount',
150+
(['out'], POINTER(UINT), 'pnChannelCount')),
151+
COMMETHOD([], HRESULT, 'SetChannelVolume',
152+
(['in'], UINT, 'dwIndex'),
153+
(['in'], c_float, 'fLevel'),
154+
(['in'], POINTER(GUID), 'EventContext')),
155+
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# This file is a part of pycaw library (https://github.com/AndreMiras/pycaw).
2+
# Please note that it is distributed under MIT license:
3+
# https://github.com/AndreMiras/pycaw#MIT-1-ov-file
4+
5+
from ctypes import Structure
6+
from ctypes.wintypes import WORD
7+
8+
9+
class WAVEFORMATEX(Structure):
10+
_fields_ = [
11+
("wFormatTag", WORD),
12+
("nChannels", WORD),
13+
("nSamplesPerSec", WORD),
14+
("nAvgBytesPerSec", WORD),
15+
("nBlockAlign", WORD),
16+
("wBitsPerSample", WORD),
17+
("cbSize", WORD),
18+
]

0 commit comments

Comments
 (0)