Repository navigation
Expand file tree
/
Copy pathCoreAudio.Watch.cs
More file actions
376 lines (332 loc) · 12.6 KB
/
Copy pathCoreAudio.Watch.cs
File metadata and controls
376 lines (332 loc) · 12.6 KB
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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
using System;
using System.Runtime.InteropServices;
using System.Threading;
namespace WindowsDeviceControl;
public static partial class CoreAudio
{
/// <summary>What an endpoint watch observed.</summary>
public enum AudioEndpointChange
{
/// <summary>An endpoint appeared.</summary>
Added,
/// <summary>An endpoint was removed.</summary>
Removed,
/// <summary>An endpoint changed state, for example unplugged or disabled.</summary>
StateChanged,
/// <summary>The console default endpoint of a direction changed.</summary>
DefaultChanged
}
/// <summary>One endpoint watch event.</summary>
/// <param name="Change">What changed.</param>
/// <param name="EndpointId">
/// The endpoint's identifier, or null when a direction lost its default endpoint.
/// </param>
/// <param name="Direction">The direction whose default changed; null for the other changes.</param>
public readonly record struct AudioEndpointWatchEvent(
AudioEndpointChange Change,
string? EndpointId,
AudioDirection? Direction);
/// <summary>
/// Starts reporting master volume and mute changes of the default endpoint in one direction.
/// </summary>
/// <param name="direction">Playback or recording endpoint; an undefined value returns E_INVALIDARG.</param>
/// <param name="onChanged">
/// Called on a Core Audio thread, not the caller's, whenever the endpoint's master volume,
/// mute or channel levels change, including changes this process makes. Marshal to your UI
/// thread before touching UI state and keep it short; exceptions it throws are swallowed
/// because a native callback cannot propagate them.
/// </param>
/// <param name="watch">
/// The registration. Dispose it to stop the callbacks; disposing it again does nothing. Each
/// call returns its own registration, and it is null when the return value is an error.
/// </param>
/// <returns>Zero on success, otherwise the HRESULT Core Audio returned.</returns>
/// <exception cref="ArgumentNullException"><paramref name="onChanged" /> is null.</exception>
/// <remarks>
/// The watch is bound to the endpoint that is the default when it starts. When the default
/// changes, which <see cref="StartEndpointWatch" /> reports, dispose this watch and start a
/// new one; the old endpoint keeps reporting its own changes until then. No initial state is
/// delivered. Do not dispose or synchronously wait for disposal inside a callback; native
/// unregistration can wait for callback completion.
/// </remarks>
public static int StartVolumeWatch(AudioDirection direction, Action onChanged, out IDisposable? watch)
{
ArgumentNullException.ThrowIfNull(onChanged);
watch = null;
if (!IsDirection(direction))
{
return InvalidArgument;
}
IMMDevice? device = null;
IAudioEndpointVolume? volume = null;
VolumeWatch? registration = null;
try
{
var result = OpenDefaultVolume(direction, out device, out volume);
if (result < 0 || volume is null || device is null)
{
return result < 0 ? result : Failure;
}
// Transfer both COM references to the registration before attempting registration.
registration = new VolumeWatch(device, volume, onChanged);
device = null;
volume = null;
result = registration.Register();
if (result < 0)
{
registration.Dispose();
return result;
}
watch = registration;
return 0;
}
catch (COMException ex)
{
registration?.Dispose();
return ex.HResult;
}
finally
{
Release(volume);
Release(device);
}
}
/// <summary>
/// Starts reporting endpoint arrivals, removals, state changes and console default changes.
/// </summary>
/// <param name="onEvent">
/// Called on a Core Audio thread, not the caller's. Marshal to your UI thread before touching
/// UI state and keep it short; exceptions it throws are swallowed because a native callback
/// cannot propagate them. Property changes on endpoints are not reported.
/// </param>
/// <param name="watch">
/// The registration. Dispose it to stop the callbacks; disposing it again does nothing. Each
/// call returns its own registration, and it is null when the return value is an error.
/// </param>
/// <returns>Zero on success, otherwise the HRESULT Core Audio returned.</returns>
/// <exception cref="ArgumentNullException"><paramref name="onEvent" /> is null.</exception>
/// <remarks>
/// Default changes are reported for the console role only, which default-volume reads and watches
/// use. SetDefaultEndpoint writes all three roles, but multimedia and communications notifications
/// are filtered here. Consumers needing those roles require their own role-specific observation.
/// No initial enumeration is delivered. Dispose outside callbacks; native unregistration can
/// wait for callback completion.
/// </remarks>
public static int StartEndpointWatch(Action<AudioEndpointWatchEvent> onEvent, out IDisposable? watch)
{
ArgumentNullException.ThrowIfNull(onEvent);
watch = null;
EndpointWatch? registration = null;
try
{
var enumerator = Enumerator();
registration = new EndpointWatch(enumerator, onEvent);
var result = ForgetEnumeratorIfGone(enumerator, registration.Register());
if (result < 0)
{
registration.Dispose();
return result;
}
watch = registration;
return 0;
}
catch (COMException ex)
{
registration?.Dispose();
return ex.HResult;
}
}
private sealed class VolumeWatch : IDisposable
{
private readonly VolumeCallback _callback;
private nint _callbackPointer;
private IMMDevice? _device;
private IAudioEndpointVolume? _volume;
internal VolumeWatch(IMMDevice device, IAudioEndpointVolume volume, Action onChanged)
{
_device = device;
_volume = volume;
_callback = new VolumeCallback(onChanged);
}
internal int Register()
{
_callbackPointer = Marshal.GetComInterfaceForObject(_callback, typeof(IAudioEndpointVolumeCallback));
return _volume!.RegisterControlChangeNotify(_callbackPointer);
}
public void Dispose()
{
var volume = Interlocked.Exchange(ref _volume, null);
if (volume is null)
{
return;
}
var device = _device;
_device = null;
var pointer = _callbackPointer;
_callbackPointer = 0;
// Unregistering waits for an in-flight callback to return, so nothing here holds a lock
// the callback could want.
_callback.Detach();
try
{
UnregisterCallback(pointer, volume.UnregisterControlChangeNotify);
}
finally
{
Release(volume);
Release(device);
}
}
}
private sealed class EndpointWatch : IDisposable
{
private readonly EndpointCallback _callback;
private readonly IMMDeviceEnumerator _enumerator;
private nint _callbackPointer;
private int _disposed;
internal EndpointWatch(IMMDeviceEnumerator enumerator, Action<AudioEndpointWatchEvent> onEvent)
{
_enumerator = enumerator;
_callback = new EndpointCallback(onEvent);
}
internal int Register()
{
_callbackPointer = Marshal.GetComInterfaceForObject(_callback, typeof(IMMNotificationClient));
return _enumerator.RegisterEndpointNotificationCallback(_callbackPointer);
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
{
return;
}
// Unregistering waits for an in-flight callback to return, so nothing here holds a lock
// the callback could want.
_callback.Detach();
var pointer = _callbackPointer;
_callbackPointer = 0;
UnregisterCallback(pointer, _enumerator.UnregisterEndpointNotificationCallback);
}
}
/// <summary>
/// Unregisters one callback pointer and releases it. A COMException from the unregistration
/// means the endpoint or enumerator is gone, which leaves nothing to unregister from.
/// </summary>
private static void UnregisterCallback(nint pointer, Func<nint, int> unregister)
{
if (pointer == 0)
{
return;
}
try
{
_ = unregister(pointer);
}
catch (COMException)
{
// The endpoint or enumerator is gone; there is nothing left to unregister from.
}
finally
{
Marshal.Release(pointer);
}
}
[ComVisible(true)]
private sealed class VolumeCallback(Action onChanged) : IAudioEndpointVolumeCallback
{
private Action? _onChanged = onChanged;
public int OnNotify(nint notificationData)
{
try
{
_onChanged?.Invoke();
}
catch
{
// A native callback cannot propagate an exception.
}
return 0;
}
internal void Detach()
{
_onChanged = null;
}
}
[ComVisible(true)]
private sealed class EndpointCallback(Action<AudioEndpointWatchEvent> onEvent) : IMMNotificationClient
{
private Action<AudioEndpointWatchEvent>? _onEvent = onEvent;
public int OnDeviceStateChanged(string deviceId, uint newState)
{
Raise(new AudioEndpointWatchEvent(AudioEndpointChange.StateChanged, deviceId, null));
return 0;
}
public int OnDeviceAdded(string deviceId)
{
Raise(new AudioEndpointWatchEvent(AudioEndpointChange.Added, deviceId, null));
return 0;
}
public int OnDeviceRemoved(string deviceId)
{
Raise(new AudioEndpointWatchEvent(AudioEndpointChange.Removed, deviceId, null));
return 0;
}
public int OnDefaultDeviceChanged(DataFlow flow, AudioRole role, string? defaultDeviceId)
{
if (role == AudioRole.Console && flow is DataFlow.Render or DataFlow.Capture)
{
Raise(new AudioEndpointWatchEvent(
AudioEndpointChange.DefaultChanged,
defaultDeviceId,
(AudioDirection)flow));
}
return 0;
}
public int OnPropertyValueChanged(nint deviceId, PropertyKey key)
{
return 0;
}
internal void Detach()
{
_onEvent = null;
}
private void Raise(AudioEndpointWatchEvent change)
{
try
{
_onEvent?.Invoke(change);
}
catch
{
// A native callback cannot propagate an exception.
}
}
}
[ComImport]
[Guid("657804FA-D6AD-4496-8A60-352752AF4F89")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IAudioEndpointVolumeCallback
{
[PreserveSig]
int OnNotify(nint notificationData);
}
[ComImport]
[Guid("7991EEC9-7E89-4D85-8390-6C703CEC60C0")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IMMNotificationClient
{
[PreserveSig]
int OnDeviceStateChanged([MarshalAs(UnmanagedType.LPWStr)] string deviceId, uint newState);
[PreserveSig]
int OnDeviceAdded([MarshalAs(UnmanagedType.LPWStr)] string deviceId);
[PreserveSig]
int OnDeviceRemoved([MarshalAs(UnmanagedType.LPWStr)] string deviceId);
[PreserveSig]
int OnDefaultDeviceChanged(
DataFlow flow,
AudioRole role,
[MarshalAs(UnmanagedType.LPWStr)] string? defaultDeviceId);
[PreserveSig]
int OnPropertyValueChanged(nint deviceId, PropertyKey key);
}
}