-
Notifications
You must be signed in to change notification settings - Fork 0
/
wca.go
218 lines (166 loc) · 6.45 KB
/
wca.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
package deej
import (
"fmt"
"time"
"unsafe"
"github.com/go-ole/go-ole"
"github.com/moutend/go-wca"
)
func (m *sessionMap) getAllSessions() error {
// we must call this every time we're about to list devices, i think. could be wrong
if err := ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED); err != nil {
m.logger.Warnw("Failed to call CoInitializeEx", "error", err)
return fmt.Errorf("call CoInitializeEx: %w", err)
}
defer ole.CoUninitialize()
// get the active device
defaultAudioEndpoint, err := getDefaultAudioEndpoint()
if err != nil {
m.logger.Warnw("Failed to get default audio endpoint", "error", err)
return fmt.Errorf("get default audio endpoint: %w", err)
}
defer defaultAudioEndpoint.Release()
// get the master session
if err := m.getAndAddMasterSession(defaultAudioEndpoint); err != nil {
m.logger.Warnw("Failed to get master audio session", "error", err)
return fmt.Errorf("get master audio session: %w", err)
}
// get an enumerator for the rest of the sessions
sessionEnumerator, err := getSessionEnumerator(defaultAudioEndpoint)
if err != nil {
m.logger.Warnw("Failed to get audio session enumerator", "error", err)
return fmt.Errorf("get audio session enumerator: %w", err)
}
defer sessionEnumerator.Release()
// enumerate it and add sessions along the way
if err := m.enumerateAndAddSessions(sessionEnumerator); err != nil {
m.logger.Warnw("Failed to enumerate audio sessions", "error", err)
return fmt.Errorf("enumerate audio sessions: %w", err)
}
m.logger.Debugw("Got all audio sessions successfully", "sessionMap", m)
// mark completion
m.lastSessionRefresh = time.Now()
return nil
}
func getDefaultAudioEndpoint() (*wca.IMMDevice, error) {
// get the IMMDeviceEnumerator
var mmDeviceEnumerator *wca.IMMDeviceEnumerator
if err := wca.CoCreateInstance(
wca.CLSID_MMDeviceEnumerator,
0,
wca.CLSCTX_ALL,
wca.IID_IMMDeviceEnumerator,
&mmDeviceEnumerator,
); err != nil {
return nil, err
}
defer mmDeviceEnumerator.Release()
// get the default audio endpoint as an IMMDevice
var mmDevice *wca.IMMDevice
if err := mmDeviceEnumerator.GetDefaultAudioEndpoint(wca.ERender, wca.EConsole, &mmDevice); err != nil {
return nil, err
}
return mmDevice, nil
}
func (m *sessionMap) getAndAddMasterSession(mmDevice *wca.IMMDevice) error {
var audioEndpointVolume *wca.IAudioEndpointVolume
if err := mmDevice.Activate(wca.IID_IAudioEndpointVolume, wca.CLSCTX_ALL, nil, &audioEndpointVolume); err != nil {
m.logger.Warnw("Failed to activate AudioEndpointVolume for master session", "error", err)
return fmt.Errorf("activate master session: %w", err)
}
// create the master session
master, err := newMasterSession(m.logger, audioEndpointVolume, m.eventCtx)
if err != nil {
m.logger.Warnw("Failed to create master session instance", "error", err)
return fmt.Errorf("create master session: %w", err)
}
m.add(master)
return nil
}
func getSessionEnumerator(mmDevice *wca.IMMDevice) (*wca.IAudioSessionEnumerator, error) {
// query the given IMMDevice's IAudioSessionManager2 interface
var audioSessionManager2 *wca.IAudioSessionManager2
if err := mmDevice.Activate(
wca.IID_IAudioSessionManager2,
wca.CLSCTX_ALL,
nil,
&audioSessionManager2,
); err != nil {
return nil, err
}
defer audioSessionManager2.Release()
// get its IAudioSessionEnumerator
var audioSessionEnumerator *wca.IAudioSessionEnumerator
if err := audioSessionManager2.GetSessionEnumerator(&audioSessionEnumerator); err != nil {
return nil, err
}
return audioSessionEnumerator, nil
}
func (m *sessionMap) enumerateAndAddSessions(sessionEnumerator *wca.IAudioSessionEnumerator) error {
// check how many audio sessions there are
var sessionCount int
if err := sessionEnumerator.GetCount(&sessionCount); err != nil {
m.logger.Warnw("Failed to get session count from session enumerator", "error", err)
return fmt.Errorf("get session count: %w", err)
}
m.logger.Debugw("Got session count from session enumerator", "count", sessionCount)
// for each session:
for sessionIdx := 0; sessionIdx < sessionCount; sessionIdx++ {
// get the IAudioSessionControl
var audioSessionControl *wca.IAudioSessionControl
if err := sessionEnumerator.GetSession(sessionIdx, &audioSessionControl); err != nil {
m.logger.Warnw("Failed to get session from session enumerator",
"error", err,
"sessionIdx", sessionIdx)
return fmt.Errorf("get session %d from enumerator: %w", sessionIdx, err)
}
// query its IAudioSessionControl2
dispatch, err := audioSessionControl.QueryInterface(wca.IID_IAudioSessionControl2)
if err != nil {
m.logger.Warnw("Failed to query session's IAudioSessionControl2",
"error", err,
"sessionIdx", sessionIdx)
return fmt.Errorf("query session %d IAudioSessionControl2: %w", sessionIdx, err)
}
// we no longer need the IAudioSessionControl, release it
audioSessionControl.Release()
// receive a useful object instead of our dispatch
audioSessionControl2 := (*wca.IAudioSessionControl2)(unsafe.Pointer(dispatch))
var pid uint32
// get the session's PID
if err := audioSessionControl2.GetProcessId(&pid); err != nil {
// if this is the system sounds session, GetProcessId will error with an undocumented
// AUDCLNT_S_NO_CURRENT_PROCESS (0x889000D) - this is fine, we actually want to treat it a bit differently
if audioSessionControl2.IsSystemSoundsSession() == nil {
// system sounds session
} else {
// of course, if it's not the system sounds session, we got a problem
m.logger.Warnw("Failed to query session's pid",
"error", err,
"sessionIdx", sessionIdx)
return fmt.Errorf("query session %d pid: %w", sessionIdx, err)
}
}
// get its ISimpleAudioVolume
dispatch, err = audioSessionControl2.QueryInterface(wca.IID_ISimpleAudioVolume)
if err != nil {
m.logger.Warnw("Failed to query session's ISimpleAudioVolume",
"error", err,
"sessionIdx", sessionIdx)
return fmt.Errorf("query session %d ISimpleAudioVolume: %w", sessionIdx, err)
}
// make it useful, again
simpleAudioVolume := (*wca.ISimpleAudioVolume)(unsafe.Pointer(dispatch))
// create the deej session object
newSession, err := newWCASession(m.logger, audioSessionControl2, simpleAudioVolume, pid, m.eventCtx)
if err != nil {
m.logger.Warnw("Failed to create new WCA session instance",
"error", err,
"sessionIdx", sessionIdx)
return fmt.Errorf("create wca session for session %d: %w", sessionIdx, err)
}
// add it to our map
m.add(newSession)
}
return nil
}