1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . Linq ;
4+ using System . Linq . Expressions ;
5+ using System . Threading ;
6+ using System . Threading . Tasks ;
7+ using AudioSwitcher . AudioApi . CoreAudio . Interfaces ;
8+ using AudioSwitcher . AudioApi . CoreAudio . Threading ;
9+
10+ namespace AudioSwitcher . AudioApi . CoreAudio
11+ {
12+ /// <summary>
13+ /// Enumerates Windows System Devices.
14+ /// Stores the current devices in memory to avoid calling the COM library when not required
15+ /// </summary>
16+ public sealed class CoreAudioController : AudioController < CoreAudioDevice > , ISystemAudioEventClient
17+ {
18+ private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim ( ) ;
19+
20+ private IMMDeviceEnumerator _innerEnumerator ;
21+ private HashSet < CoreAudioDevice > _deviceCache = new HashSet < CoreAudioDevice > ( ) ;
22+
23+ public CoreAudioController ( )
24+ {
25+ ComThread . Invoke ( ( ) =>
26+ {
27+ // ReSharper disable once SuspiciousTypeConversion.Global
28+ _innerEnumerator = new MMDeviceEnumeratorComObject ( ) as IMMDeviceEnumerator ;
29+
30+ if ( _innerEnumerator == null )
31+ return ;
32+
33+ _notificationClient = new MMNotificationClient ( this ) ;
34+ _innerEnumerator . RegisterEndpointNotificationCallback ( _notificationClient ) ;
35+ } ) ;
36+
37+ RefreshSystemDevices ( ) ;
38+ }
39+
40+ ~ CoreAudioController ( )
41+ {
42+ Dispose ( false ) ;
43+ }
44+
45+ protected override void Dispose ( bool disposing )
46+ {
47+ if ( _innerEnumerator != null )
48+ {
49+ ComThread . BeginInvoke ( ( ) =>
50+ {
51+ _innerEnumerator . UnregisterEndpointNotificationCallback ( _notificationClient ) ;
52+ _notificationClient = null ;
53+ _innerEnumerator = null ;
54+ } ) ;
55+ }
56+
57+ if ( _deviceCache != null )
58+ _deviceCache . Clear ( ) ;
59+
60+ if ( _lock != null )
61+ _lock . Dispose ( ) ;
62+
63+ GC . SuppressFinalize ( this ) ;
64+ }
65+
66+ public override CoreAudioDevice GetDevice ( Guid id , DeviceState state )
67+ {
68+ var acquiredLock = _lock . AcquireReadLockNonReEntrant ( ) ;
69+
70+ try
71+ {
72+ return _deviceCache . FirstOrDefault ( x => x . Id == id && state . HasFlag ( x . State ) ) ;
73+ }
74+ finally
75+ {
76+ if ( acquiredLock )
77+ _lock . ExitReadLock ( ) ;
78+ }
79+ }
80+
81+ private CoreAudioDevice GetDevice ( string realId )
82+ {
83+ var acquiredLock = _lock . AcquireReadLockNonReEntrant ( ) ;
84+
85+ try
86+ {
87+ return _deviceCache . FirstOrDefault ( x => String . Equals ( x . RealId , realId , StringComparison . InvariantCultureIgnoreCase ) ) ;
88+ }
89+ finally
90+ {
91+ if ( acquiredLock )
92+ _lock . ExitReadLock ( ) ;
93+ }
94+ }
95+
96+ private void RefreshSystemDevices ( )
97+ {
98+ ComThread . Invoke ( ( ) =>
99+ {
100+ _deviceCache = new HashSet < CoreAudioDevice > ( ) ;
101+ IMMDeviceCollection collection ;
102+ _innerEnumerator . EnumAudioEndpoints ( EDataFlow . All , EDeviceState . All , out collection ) ;
103+
104+ using ( var coll = new MMDeviceCollection ( collection ) )
105+ {
106+ foreach ( var mDev in coll )
107+ CacheDevice ( mDev ) ;
108+ }
109+ } ) ;
110+ }
111+
112+ private CoreAudioDevice GetOrAddDeviceFromRealId ( string deviceId )
113+ {
114+ //This pre-check here may prevent more com objects from being created
115+ var device = GetDevice ( deviceId ) ;
116+ if ( device != null )
117+ return device ;
118+
119+ return ComThread . Invoke ( ( ) =>
120+ {
121+ IMMDevice mDevice ;
122+ _innerEnumerator . GetDevice ( deviceId , out mDevice ) ;
123+
124+ if ( mDevice == null )
125+ return null ;
126+
127+ return CacheDevice ( mDevice ) ;
128+ } ) ;
129+ }
130+
131+ private IEnumerable < CoreAudioDevice > RemoveFromRealId ( string deviceId )
132+ {
133+ var lockAcquired = _lock . AcquireWriteLockNonReEntrant ( ) ;
134+ try
135+ {
136+ var devicesToRemove =
137+ _deviceCache . Where ( x => String . Equals ( x . RealId , deviceId , StringComparison . InvariantCultureIgnoreCase ) ) . ToList ( ) ;
138+
139+ _deviceCache . RemoveWhere ( x => String . Equals ( x . RealId , deviceId , StringComparison . InvariantCultureIgnoreCase ) ) ;
140+
141+ return devicesToRemove ;
142+ }
143+ finally
144+ {
145+ if ( lockAcquired )
146+ _lock . ExitWriteLock ( ) ;
147+ }
148+ }
149+
150+ private CoreAudioDevice CacheDevice ( IMMDevice mDevice )
151+ {
152+ if ( ! DeviceIsValid ( mDevice ) )
153+ return null ;
154+
155+ string id ;
156+ mDevice . GetId ( out id ) ;
157+ var device = GetDevice ( id ) ;
158+
159+ if ( device != null )
160+ return device ;
161+
162+ var lockAcquired = _lock . AcquireWriteLockNonReEntrant ( ) ;
163+
164+ try
165+ {
166+ device = new CoreAudioDevice ( mDevice , this ) ;
167+ _deviceCache . Add ( device ) ;
168+
169+ return device ;
170+ }
171+ finally
172+ {
173+ if ( lockAcquired )
174+ _lock . ExitWriteLock ( ) ;
175+ }
176+ }
177+
178+ private static bool DeviceIsValid ( IMMDevice device )
179+ {
180+ try
181+ {
182+ string id ;
183+ EDeviceState state ;
184+ device . GetId ( out id ) ;
185+ device . GetState ( out state ) ;
186+
187+ return true ;
188+ }
189+ catch
190+ {
191+ return false ;
192+ }
193+ }
194+
195+ private void RaiseAudioDeviceChanged ( DeviceChangedEventArgs e )
196+ {
197+ OnAudioDeviceChanged ( this , e ) ;
198+ }
199+
200+ public override bool SetDefaultDevice ( CoreAudioDevice dev )
201+ {
202+ if ( dev == null )
203+ return false ;
204+
205+ var oldDefault = dev . IsPlaybackDevice ? DefaultPlaybackDevice : DefaultCaptureDevice ;
206+
207+ try
208+ {
209+ PolicyConfig . SetDefaultEndpoint ( dev . RealId , ERole . Console | ERole . Multimedia ) ;
210+ return dev . IsDefaultDevice ;
211+ }
212+ catch
213+ {
214+ return false ;
215+ }
216+ finally
217+ {
218+ //Raise the default changed event on the old device
219+ if ( oldDefault != null && ! oldDefault . IsDefaultDevice )
220+ RaiseAudioDeviceChanged ( new DefaultDeviceChangedEventArgs ( oldDefault ) ) ;
221+ }
222+ }
223+
224+ public override bool SetDefaultCommunicationsDevice ( CoreAudioDevice dev )
225+ {
226+ if ( dev == null )
227+ return false ;
228+
229+ var oldDefault = dev . IsPlaybackDevice ? DefaultPlaybackCommunicationsDevice : DefaultCaptureCommunicationsDevice ;
230+
231+ try
232+ {
233+ PolicyConfig . SetDefaultEndpoint ( dev . RealId , ERole . Communications ) ;
234+
235+ return dev . IsDefaultCommunicationsDevice ;
236+ }
237+ catch
238+ {
239+ return false ;
240+ }
241+ finally
242+ {
243+ //Raise the default changed event on the old device
244+ if ( oldDefault != null && ! oldDefault . IsDefaultCommunicationsDevice )
245+ RaiseAudioDeviceChanged ( new DefaultDeviceChangedEventArgs ( oldDefault , true ) ) ;
246+ }
247+ }
248+
249+ public override CoreAudioDevice GetDefaultDevice ( DeviceType deviceType , Role role )
250+ {
251+ var acquiredLock = _lock . AcquireReadLockNonReEntrant ( ) ;
252+
253+ try
254+ {
255+ IMMDevice dev ;
256+ _innerEnumerator . GetDefaultAudioEndpoint ( deviceType . AsEDataFlow ( ) , role . AsERole ( ) , out dev ) ;
257+ if ( dev == null )
258+ return null ;
259+
260+ string devId ;
261+ dev . GetId ( out devId ) ;
262+ if ( String . IsNullOrEmpty ( devId ) )
263+ return null ;
264+
265+ return _deviceCache . FirstOrDefault ( x => x . RealId == devId ) ;
266+ }
267+ finally
268+ {
269+ if ( acquiredLock )
270+ _lock . ExitReadLock ( ) ;
271+ }
272+ }
273+
274+ public override IEnumerable < CoreAudioDevice > GetDevices ( DeviceType deviceType , DeviceState state )
275+ {
276+ var acquiredLock = _lock . AcquireReadLockNonReEntrant ( ) ;
277+
278+ try
279+ {
280+ return _deviceCache . Where ( x =>
281+ ( x . DeviceType == deviceType || deviceType == DeviceType . All )
282+ && state . HasFlag ( x . State ) ) . ToList ( ) ;
283+ }
284+ finally
285+ {
286+ if ( acquiredLock )
287+ _lock . ExitReadLock ( ) ;
288+ }
289+ }
290+
291+ private MMNotificationClient _notificationClient ;
292+
293+ void ISystemAudioEventClient . OnDeviceStateChanged ( string deviceId , EDeviceState newState )
294+ {
295+ var dev = GetOrAddDeviceFromRealId ( deviceId ) ;
296+
297+ if ( dev != null )
298+ RaiseAudioDeviceChanged ( new DeviceStateChangedEventArgs ( dev , newState . AsDeviceState ( ) ) ) ;
299+ }
300+
301+ void ISystemAudioEventClient . OnDeviceAdded ( string deviceId )
302+ {
303+ var dev = GetOrAddDeviceFromRealId ( deviceId ) ;
304+
305+ if ( dev != null )
306+ RaiseAudioDeviceChanged ( new DeviceAddedEventArgs ( dev ) ) ;
307+ }
308+
309+ void ISystemAudioEventClient . OnDeviceRemoved ( string deviceId )
310+ {
311+ var devicesRemoved = RemoveFromRealId ( deviceId ) ;
312+
313+ foreach ( var dev in devicesRemoved )
314+ RaiseAudioDeviceChanged ( new DeviceRemovedEventArgs ( dev ) ) ;
315+ }
316+
317+ void ISystemAudioEventClient . OnDefaultDeviceChanged ( EDataFlow flow , ERole role , string deviceId )
318+ {
319+ //Ignore multimedia, it seems to fire a console event anyway
320+ if ( role == ERole . Multimedia )
321+ return ;
322+
323+ var dev = GetOrAddDeviceFromRealId ( deviceId ) ;
324+
325+ if ( dev == null )
326+ return ;
327+
328+ Task . Factory . StartNew ( ( ) =>
329+ {
330+ RaiseAudioDeviceChanged ( new DefaultDeviceChangedEventArgs ( dev , role == ERole . Communications ) ) ;
331+ } ) ;
332+ }
333+
334+ private static readonly Dictionary < PropertyKey , Expression < Func < IDevice , object > > > PropertykeyToLambdaMap = new Dictionary < PropertyKey , Expression < Func < IDevice , object > > >
335+ {
336+ { PropertyKeys . PKEY_DEVICE_INTERFACE_FRIENDLY_NAME , x => x . InterfaceName } ,
337+ { PropertyKeys . PKEY_DEVICE_DESCRIPTION , x => x . Name } ,
338+ { PropertyKeys . PKEY_DEVICE_FRIENDLY_NAME , x => x . FullName } ,
339+ { PropertyKeys . PKEY_DEVICE_ICON , x => x . Icon } ,
340+ } ;
341+
342+ void ISystemAudioEventClient . OnPropertyValueChanged ( string deviceId , PropertyKey key )
343+ {
344+ var dev = GetOrAddDeviceFromRealId ( deviceId ) ;
345+
346+ if ( dev == null )
347+ return ;
348+
349+ if ( PropertykeyToLambdaMap . ContainsKey ( key ) )
350+ {
351+ RaiseAudioDeviceChanged ( DevicePropertyChangedEventArgs . FromExpression ( dev , PropertykeyToLambdaMap [ key ] ) ) ;
352+ return ;
353+ }
354+
355+ //Unknown property changed
356+ RaiseAudioDeviceChanged ( new DevicePropertyChangedEventArgs ( dev ) ) ;
357+ }
358+
359+ }
360+ }
0 commit comments