Skip to content

Commit 2d97f85

Browse files
authored
Merge pull request #1414 from Belphemur/refactor-cached-devices
feat(device::list): refactor completely how to get the the list of device and keep it up-to-date
2 parents 8ed0f14 + 513f795 commit 2d97f85

22 files changed

+475
-504
lines changed

SoundSwitch.Audio.Manager/AudioSwitcher.cs

+44-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#nullable enable
22
using System;
3+
using System.Collections.Generic;
34
using System.Diagnostics;
45
using System.Linq;
56
using NAudio.CoreAudioApi;
@@ -195,7 +196,7 @@ public void SetVolumeFromDefaultDevice(DeviceInfo device)
195196
if (currentDefault == null)
196197
return;
197198

198-
var audioInfo = InteractWithMmDevice(currentDefault, mmDevice =>
199+
var audioInfo = InteractWithDevice(currentDefault, mmDevice =>
199200
{
200201
var defaultDeviceAudioEndpointVolume = mmDevice.AudioEndpointVolume;
201202
return defaultDeviceAudioEndpointVolume == null ? default : (Volume: defaultDeviceAudioEndpointVolume.MasterVolumeLevelScalar, IsMuted: defaultDeviceAudioEndpointVolume.Mute);
@@ -205,15 +206,15 @@ public void SetVolumeFromDefaultDevice(DeviceInfo device)
205206
return;
206207

207208
var nextDevice = GetDevice(device.Id);
208-
209-
if(nextDevice == null)
209+
210+
if (nextDevice == null)
210211
return;
211-
212-
InteractWithMmDevice(nextDevice, mmDevice =>
212+
213+
InteractWithDevice(nextDevice, mmDevice =>
213214
{
214215
if (mmDevice is not { State: DeviceState.Active })
215216
return nextDevice;
216-
217+
217218
if (mmDevice.AudioEndpointVolume == null)
218219
return nextDevice;
219220

@@ -223,9 +224,10 @@ public void SetVolumeFromDefaultDevice(DeviceInfo device)
223224
mmDevice.AudioEndpointVolume.Channels[1].VolumeLevelScalar = audioInfo.Volume;
224225
}
225226
else
226-
{
227+
{
227228
mmDevice.AudioEndpointVolume.MasterVolumeLevelScalar = audioInfo.Volume;
228229
}
230+
229231
mmDevice.AudioEndpointVolume.Mute = audioInfo.IsMuted;
230232
return mmDevice;
231233
});
@@ -274,7 +276,15 @@ public bool IsDefault(string deviceId, EDataFlow flow, ERole role)
274276
/// <param name="device"></param>
275277
/// <param name="interaction"></param>
276278
/// <typeparam name="T"></typeparam>
277-
public T InteractWithMmDevice<T>(MMDevice device, Func<MMDevice, T> interaction) => ComThread.Invoke(() => interaction(device));
279+
public T InteractWithDevice<T>(MMDevice device, Func<MMDevice, T> interaction) => ComThread.Invoke(() => interaction(device));
280+
281+
/// <summary>
282+
/// Used to interact directly with a <see cref="DeviceFullInfo"/>
283+
/// </summary>
284+
/// <param name="device"></param>
285+
/// <param name="interaction"></param>
286+
/// <typeparam name="T"></typeparam>
287+
public T InteractWithDevice<T>(DeviceFullInfo device, Func<DeviceFullInfo, T> interaction) => ComThread.Invoke(() => interaction(device));
278288

279289
/// <summary>
280290
/// Get the current default endpoint
@@ -302,6 +312,32 @@ public bool IsDefault(string deviceId, EDataFlow flow, ERole role)
302312
return device == null ? null : new DeviceFullInfo(device);
303313
});
304314

315+
/// <summary>
316+
/// Get audio endpoints for the given flow and state
317+
/// </summary>
318+
/// <param name="flow"></param>
319+
/// <param name="state"></param>
320+
/// <returns></returns>
321+
public IEnumerable<DeviceFullInfo> GetAudioEndpoints(EDataFlow flow, EDeviceState state) => ComThread.Invoke(() =>
322+
{
323+
var devices = EnumeratorClient.GetEndpoints(flow, state);
324+
return devices.Select(device =>
325+
{
326+
try
327+
{
328+
return new DeviceFullInfo(device);
329+
}
330+
catch (Exception e)
331+
{
332+
Trace.TraceError("Couldn't get device info [{0}]: {1}", device.ID, e);
333+
return null;
334+
}
335+
})
336+
.Where(device => device != null)
337+
.Where(device => !string.IsNullOrEmpty(device?.Name))
338+
.Cast<DeviceFullInfo>().ToArray();
339+
});
340+
305341
/// <summary>
306342
/// Reset Windows configuration for the process that had their audio device changed
307343
/// </summary>

SoundSwitch.Audio.Manager/Interop/Client/EnumeratorClient.cs

+15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#nullable enable
22
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
35
using System.Runtime.InteropServices;
46
using NAudio.CoreAudioApi;
57
using SoundSwitch.Audio.Manager.Interop.Enum;
@@ -73,6 +75,19 @@ public bool IsDefault(string deviceId, EDataFlow flow, ERole role)
7375
}
7476
}
7577

78+
/// <summary>
79+
/// Get all the endpoints of specific dataflow and state
80+
/// </summary>
81+
/// <param name="dataFlow"></param>
82+
/// <param name="state"></param>
83+
/// <returns></returns>
84+
public IEnumerable<MMDevice> GetEndpoints(EDataFlow dataFlow, EDeviceState state)
85+
{
86+
var deviceCollection = _enumerator.EnumerateAudioEndPoints((DataFlow)dataFlow, (DeviceState)state);
87+
88+
return deviceCollection.ToArray();
89+
}
90+
7691
[ComImport, Guid(ComGuid.AUDIO_IMMDEVICE_ENUMERATOR_OBJECT_IID)]
7792
private class _MMDeviceEnumerator
7893
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
3+
namespace SoundSwitch.Audio.Manager.Interop.Enum;
4+
5+
/// <summary>Device State</summary>
6+
[Flags]
7+
public enum EDeviceState
8+
{
9+
/// <summary>DEVICE_STATE_ACTIVE</summary>
10+
Active = 1,
11+
/// <summary>DEVICE_STATE_DISABLED</summary>
12+
Disabled = 2,
13+
/// <summary>DEVICE_STATE_NOTPRESENT</summary>
14+
NotPresent = 4,
15+
/// <summary>DEVICE_STATE_UNPLUGGED</summary>
16+
Unplugged = 8,
17+
/// <summary>DEVICE_STATEMASK_ALL</summary>
18+
All = Unplugged | NotPresent | Disabled | Active, // 0x0000000F
19+
}

SoundSwitch.Audio.Manager/SoundSwitch.Audio.Manager.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<Configurations>Debug;Release;Nightly</Configurations>
88
<Platforms>AnyCPU</Platforms>
99
<AssemblyTitle>SoundSwitch.Audio.Manager</AssemblyTitle>
10-
<Version>4.0.0</Version>
10+
<Version>4.1.0</Version>
1111
</PropertyGroup>
1212
<ItemGroup>
1313
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />

SoundSwitch/Framework/Audio/Lister/CachedAudioDeviceLister.cs

+126-45
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,31 @@
1616
using System.Collections.Generic;
1717
using System.Diagnostics;
1818
using System.Linq;
19+
using System.Reactive.Linq;
20+
using System.Reactive.Subjects;
1921
using System.Runtime.InteropServices;
2022
using System.Threading;
2123
using System.Threading.Tasks;
2224
using NAudio.CoreAudioApi;
2325
using Serilog;
26+
using SoundSwitch.Audio.Manager;
27+
using SoundSwitch.Audio.Manager.Interop.Enum;
2428
using SoundSwitch.Common.Framework.Audio.Collection;
2529
using SoundSwitch.Common.Framework.Audio.Device;
26-
using SoundSwitch.Common.Framework.Dispose;
27-
using SoundSwitch.Framework.Audio.Lister.Job;
28-
using SoundSwitch.Framework.NotificationManager;
29-
using SoundSwitch.Framework.Threading;
3030
using SoundSwitch.Model;
3131

3232
namespace SoundSwitch.Framework.Audio.Lister
3333
{
3434
public class CachedAudioDeviceLister : IAudioDeviceLister
3535
{
3636
/// <inheritdoc />
37-
private DeviceFullInfo[] PlaybackDevices { get; set; } = Array.Empty<DeviceFullInfo>();
37+
private Dictionary<string, DeviceFullInfo> PlaybackDevices { get; set; } = new();
3838

3939
/// <inheritdoc />
40-
private DeviceFullInfo[] RecordingDevices { get; set; } = Array.Empty<DeviceFullInfo>();
40+
private Dictionary<string, DeviceFullInfo> RecordingDevices { get; set; } = new();
41+
42+
private readonly ISubject<DefaultDevicePayload> _defaultDeviceChanged = new Subject<DefaultDevicePayload>();
43+
public IObservable<DefaultDevicePayload> DefaultDeviceChanged => _defaultDeviceChanged.AsObservable();
4144

4245
/// <summary>
4346
/// Get devices per type and state
@@ -50,8 +53,8 @@ public DeviceReadOnlyCollection<DeviceFullInfo> GetDevices(DataFlow type, Device
5053
{
5154
return type switch
5255
{
53-
DataFlow.Render => new DeviceReadOnlyCollection<DeviceFullInfo>(PlaybackDevices.Where(info => state.HasFlag(info.State)), type),
54-
DataFlow.Capture => new DeviceReadOnlyCollection<DeviceFullInfo>(RecordingDevices.Where(info => state.HasFlag(info.State)), type),
56+
DataFlow.Render => new DeviceReadOnlyCollection<DeviceFullInfo>(PlaybackDevices.Values.Where(info => state.HasFlag(info.State)), type),
57+
DataFlow.Capture => new DeviceReadOnlyCollection<DeviceFullInfo>(RecordingDevices.Values.Where(info => state.HasFlag(info.State)), type),
5558
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
5659
};
5760
}
@@ -80,14 +83,109 @@ private set
8083
public CachedAudioDeviceLister(DeviceState state)
8184
{
8285
_state = state;
83-
MMNotificationClient.Instance.DevicesChanged += DeviceChanged;
8486
_context = Log.ForContext("State", _state);
8587
}
8688

87-
private void DeviceChanged(object sender, DeviceChangedEventBase e)
89+
private void DisposeDevice(DeviceFullInfo deviceFullInfo)
8890
{
89-
_context.Verbose("Device Changed received, triggering job");
90-
JobScheduler.Instance.ScheduleJob(new DebounceRefreshJob(_state, this, _context), e.Token);
91+
_ = AudioSwitcher.Instance.InteractWithDevice(deviceFullInfo, device =>
92+
{
93+
device.Dispose();
94+
return device;
95+
});
96+
}
97+
98+
/// <summary>
99+
/// Process device updates
100+
/// </summary>
101+
/// <param name="deviceChangedEvents"></param>
102+
/// <exception cref="ArgumentOutOfRangeException"></exception>
103+
public void ProcessDeviceUpdates(IEnumerable<DeviceChangedEvent> deviceChangedEvents)
104+
{
105+
bool GetDevice(DeviceChangedEvent deviceChangedEvent, out DeviceFullInfo device)
106+
{
107+
device = AudioSwitcher.Instance.GetAudioEndpoint(deviceChangedEvent.DeviceId);
108+
if (device == null)
109+
{
110+
_context.Warning("Can't get device {deviceId}", deviceChangedEvent.DeviceId);
111+
return true;
112+
}
113+
114+
return false;
115+
}
116+
117+
void UpdateDeviceCache(DeviceChangedEvent deviceChangedEvent)
118+
{
119+
if (GetDevice(deviceChangedEvent, out var device)) return;
120+
121+
switch (device.Type)
122+
{
123+
case DataFlow.Render:
124+
if (PlaybackDevices.TryGetValue(device.Id, out var oldPlaybackDevice))
125+
{
126+
DisposeDevice(oldPlaybackDevice);
127+
}
128+
129+
PlaybackDevices[device.Id] = device;
130+
break;
131+
case DataFlow.Capture:
132+
if (RecordingDevices.TryGetValue(device.Id, out var oldRecordingDevice))
133+
{
134+
DisposeDevice(oldRecordingDevice);
135+
}
136+
137+
RecordingDevices[device.Id] = device;
138+
break;
139+
case DataFlow.All:
140+
break;
141+
default:
142+
throw new ArgumentOutOfRangeException();
143+
}
144+
145+
_context.Information("Updated device {deviceId} in cache", device.Id);
146+
}
147+
148+
foreach (var deviceChangedEvent in deviceChangedEvents)
149+
{
150+
try
151+
{
152+
switch (deviceChangedEvent.Action)
153+
{
154+
case EventType.Removed:
155+
if (PlaybackDevices.Remove(deviceChangedEvent.DeviceId, out var playbackDevice))
156+
{
157+
DisposeDevice(playbackDevice);
158+
}
159+
160+
if (RecordingDevices.Remove(deviceChangedEvent.DeviceId, out var recordingDevice))
161+
{
162+
DisposeDevice(recordingDevice);
163+
}
164+
165+
break;
166+
case EventType.Added:
167+
case EventType.StateChanged:
168+
case EventType.PropertyChanged:
169+
UpdateDeviceCache(deviceChangedEvent);
170+
break;
171+
case EventType.DefaultChanged:
172+
if (!PlaybackDevices.TryGetValue(deviceChangedEvent.DeviceId, out var device) && !RecordingDevices.TryGetValue(deviceChangedEvent.DeviceId, out device))
173+
{
174+
_context.Warning("Can't get device {deviceId}", deviceChangedEvent.DeviceId);
175+
break;
176+
}
177+
178+
_defaultDeviceChanged.OnNext(new DefaultDevicePayload(device, ((DefaultDeviceChangedEvent)deviceChangedEvent).Role));
179+
break;
180+
default:
181+
throw new ArgumentOutOfRangeException();
182+
}
183+
}
184+
catch (Exception e)
185+
{
186+
_context.Warning(e, "Couldn't process event: {event} for device {deviceId}", deviceChangedEvent.Action, deviceChangedEvent.DeviceId);
187+
}
188+
}
91189
}
92190

93191
public void Refresh(CancellationToken cancellationToken = default)
@@ -116,46 +214,31 @@ public void Refresh(CancellationToken cancellationToken = default)
116214
try
117215
{
118216
logContext.Information("Refreshing all devices");
119-
var enumerator = new MMDeviceEnumerator();
120-
using var _ = enumerator.DisposeOnCancellation(cancellationToken);
121-
foreach (var endPoint in enumerator.EnumerateAudioEndPoints(DataFlow.All, _state))
217+
foreach (var deviceInfo in AudioSwitcher.Instance.GetAudioEndpoints((EDataFlow)DataFlow.All, (EDeviceState)_state))
122218
{
123219
cancellationToken.ThrowIfCancellationRequested();
124-
try
220+
switch (deviceInfo.Type)
125221
{
126-
var deviceInfo = new DeviceFullInfo(endPoint);
127-
if (string.IsNullOrEmpty(deviceInfo.Name))
128-
{
129-
continue;
130-
}
131-
132-
switch (deviceInfo.Type)
133-
{
134-
case DataFlow.Render:
135-
playbackDevices.Add(deviceInfo.Id, deviceInfo);
136-
break;
137-
case DataFlow.Capture:
138-
recordingDevices.Add(deviceInfo.Id, deviceInfo);
139-
break;
140-
case DataFlow.All:
141-
break;
142-
default:
143-
throw new ArgumentOutOfRangeException();
144-
}
145-
}
146-
catch (Exception e)
147-
{
148-
logContext.Warning(e, "Can't get name of device {device}", endPoint.ID);
222+
case DataFlow.Render:
223+
playbackDevices.Add(deviceInfo.Id, deviceInfo);
224+
break;
225+
case DataFlow.Capture:
226+
recordingDevices.Add(deviceInfo.Id, deviceInfo);
227+
break;
228+
case DataFlow.All:
229+
break;
230+
default:
231+
throw new ArgumentOutOfRangeException();
149232
}
150233
}
151234

152235
foreach (var device in PlaybackDevices.Union(RecordingDevices))
153236
{
154-
device.Dispose();
237+
DisposeDevice(device.Value);
155238
}
156239

157-
PlaybackDevices = playbackDevices.Values.ToArray();
158-
RecordingDevices = recordingDevices.Values.ToArray();
240+
PlaybackDevices = playbackDevices;
241+
RecordingDevices = recordingDevices;
159242

160243

161244
logContext.Information("Refreshed all devices in {@StopTime}. {@Recording}/rec, {@Playback}/play", stopWatch.Elapsed, recordingDevices.Count, playbackDevices.Count);
@@ -179,11 +262,9 @@ public void Refresh(CancellationToken cancellationToken = default)
179262

180263
public void Dispose()
181264
{
182-
MMNotificationClient.Instance.DevicesChanged -= DeviceChanged;
183-
184265
foreach (var device in PlaybackDevices.Union(RecordingDevices))
185266
{
186-
device.Dispose();
267+
DisposeDevice(device.Value);
187268
}
188269

189270
_refreshCancellationTokenSource.Dispose();

0 commit comments

Comments
 (0)