Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(device::list): refactor completely how to get the the list of device and keep it up-to-date #1414

Merged
merged 10 commits into from
Apr 3, 2024
Merged
52 changes: 44 additions & 8 deletions SoundSwitch.Audio.Manager/AudioSwitcher.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NAudio.CoreAudioApi;
Expand Down Expand Up @@ -195,7 +196,7 @@
if (currentDefault == null)
return;

var audioInfo = InteractWithMmDevice(currentDefault, mmDevice =>
var audioInfo = InteractWithDevice(currentDefault, mmDevice =>
{
var defaultDeviceAudioEndpointVolume = mmDevice.AudioEndpointVolume;
return defaultDeviceAudioEndpointVolume == null ? default : (Volume: defaultDeviceAudioEndpointVolume.MasterVolumeLevelScalar, IsMuted: defaultDeviceAudioEndpointVolume.Mute);
Expand All @@ -205,15 +206,15 @@
return;

var nextDevice = GetDevice(device.Id);
if(nextDevice == null)

if (nextDevice == null)
return;
InteractWithMmDevice(nextDevice, mmDevice =>

InteractWithDevice(nextDevice, mmDevice =>
{
if (mmDevice is not { State: DeviceState.Active })
return nextDevice;

if (mmDevice.AudioEndpointVolume == null)
return nextDevice;

Expand All @@ -223,9 +224,10 @@
mmDevice.AudioEndpointVolume.Channels[1].VolumeLevelScalar = audioInfo.Volume;
}
else
{
{
mmDevice.AudioEndpointVolume.MasterVolumeLevelScalar = audioInfo.Volume;
}

mmDevice.AudioEndpointVolume.Mute = audioInfo.IsMuted;
return mmDevice;
});
Expand Down Expand Up @@ -274,7 +276,15 @@
/// <param name="device"></param>
/// <param name="interaction"></param>
/// <typeparam name="T"></typeparam>
public T InteractWithMmDevice<T>(MMDevice device, Func<MMDevice, T> interaction) => ComThread.Invoke(() => interaction(device));
public T InteractWithDevice<T>(MMDevice device, Func<MMDevice, T> interaction) => ComThread.Invoke(() => interaction(device));

/// <summary>
/// Used to interact directly with a <see cref="DeviceFullInfo"/>
/// </summary>
/// <param name="device"></param>
/// <param name="interaction"></param>
/// <typeparam name="T"></typeparam>
public T InteractWithDevice<T>(DeviceFullInfo device, Func<DeviceFullInfo, T> interaction) => ComThread.Invoke(() => interaction(device));

/// <summary>
/// Get the current default endpoint
Expand Down Expand Up @@ -302,6 +312,32 @@
return device == null ? null : new DeviceFullInfo(device);
});

/// <summary>
/// Get audio endpoints for the given flow and state
/// </summary>
/// <param name="flow"></param>
/// <param name="state"></param>
/// <returns></returns>
public IEnumerable<DeviceFullInfo> GetAudioEndpoints(EDataFlow flow, EDeviceState state) => ComThread.Invoke(() =>
{
var devices = EnumeratorClient.GetEndpoints(flow, state);
return devices.Select(device =>
{
try
{
return new DeviceFullInfo(device);
}
catch (Exception e)
{
Trace.TraceError("Couldn't get device info [{0}]: {1}", device.ID, e);
return null;
}
})
.Where(device => device != null)
.Where(device => !string.IsNullOrEmpty(device?.Name))

Check warning on line 337 in SoundSwitch.Audio.Manager/AudioSwitcher.cs

View workflow job for this annotation

GitHub Actions / build

'DeviceInfo.Name' is obsolete: 'Use NameClean'
.Cast<DeviceFullInfo>().ToArray();
});

/// <summary>
/// Reset Windows configuration for the process that had their audio device changed
/// </summary>
Expand Down
15 changes: 15 additions & 0 deletions SoundSwitch.Audio.Manager/Interop/Client/EnumeratorClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using NAudio.CoreAudioApi;
using SoundSwitch.Audio.Manager.Interop.Enum;
Expand Down Expand Up @@ -73,6 +75,19 @@ public bool IsDefault(string deviceId, EDataFlow flow, ERole role)
}
}

/// <summary>
/// Get all the endpoints of specific dataflow and state
/// </summary>
/// <param name="dataFlow"></param>
/// <param name="state"></param>
/// <returns></returns>
public IEnumerable<MMDevice> GetEndpoints(EDataFlow dataFlow, EDeviceState state)
{
var deviceCollection = _enumerator.EnumerateAudioEndPoints((DataFlow)dataFlow, (DeviceState)state);

return deviceCollection.ToArray();
}

[ComImport, Guid(ComGuid.AUDIO_IMMDEVICE_ENUMERATOR_OBJECT_IID)]
private class _MMDeviceEnumerator
{
Expand Down
19 changes: 19 additions & 0 deletions SoundSwitch.Audio.Manager/Interop/Enum/EDeviceState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;

namespace SoundSwitch.Audio.Manager.Interop.Enum;

/// <summary>Device State</summary>
[Flags]
public enum EDeviceState
{
/// <summary>DEVICE_STATE_ACTIVE</summary>
Active = 1,
/// <summary>DEVICE_STATE_DISABLED</summary>
Disabled = 2,
/// <summary>DEVICE_STATE_NOTPRESENT</summary>
NotPresent = 4,
/// <summary>DEVICE_STATE_UNPLUGGED</summary>
Unplugged = 8,
/// <summary>DEVICE_STATEMASK_ALL</summary>
All = Unplugged | NotPresent | Disabled | Active, // 0x0000000F
}
2 changes: 1 addition & 1 deletion SoundSwitch.Audio.Manager/SoundSwitch.Audio.Manager.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<Configurations>Debug;Release;Nightly</Configurations>
<Platforms>AnyCPU</Platforms>
<AssemblyTitle>SoundSwitch.Audio.Manager</AssemblyTitle>
<Version>4.0.0</Version>
<Version>4.1.0</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
Expand Down
171 changes: 126 additions & 45 deletions SoundSwitch/Framework/Audio/Lister/CachedAudioDeviceLister.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,31 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using NAudio.CoreAudioApi;
using Serilog;
using SoundSwitch.Audio.Manager;
using SoundSwitch.Audio.Manager.Interop.Enum;
using SoundSwitch.Common.Framework.Audio.Collection;
using SoundSwitch.Common.Framework.Audio.Device;
using SoundSwitch.Common.Framework.Dispose;
using SoundSwitch.Framework.Audio.Lister.Job;
using SoundSwitch.Framework.NotificationManager;
using SoundSwitch.Framework.Threading;
using SoundSwitch.Model;

namespace SoundSwitch.Framework.Audio.Lister
{
public class CachedAudioDeviceLister : IAudioDeviceLister
{
/// <inheritdoc />
private DeviceFullInfo[] PlaybackDevices { get; set; } = Array.Empty<DeviceFullInfo>();
private Dictionary<string, DeviceFullInfo> PlaybackDevices { get; set; } = new();

/// <inheritdoc />
private DeviceFullInfo[] RecordingDevices { get; set; } = Array.Empty<DeviceFullInfo>();
private Dictionary<string, DeviceFullInfo> RecordingDevices { get; set; } = new();

private readonly ISubject<DefaultDevicePayload> _defaultDeviceChanged = new Subject<DefaultDevicePayload>();
public IObservable<DefaultDevicePayload> DefaultDeviceChanged => _defaultDeviceChanged.AsObservable();

/// <summary>
/// Get devices per type and state
Expand All @@ -50,8 +53,8 @@ public DeviceReadOnlyCollection<DeviceFullInfo> GetDevices(DataFlow type, Device
{
return type switch
{
DataFlow.Render => new DeviceReadOnlyCollection<DeviceFullInfo>(PlaybackDevices.Where(info => state.HasFlag(info.State)), type),
DataFlow.Capture => new DeviceReadOnlyCollection<DeviceFullInfo>(RecordingDevices.Where(info => state.HasFlag(info.State)), type),
DataFlow.Render => new DeviceReadOnlyCollection<DeviceFullInfo>(PlaybackDevices.Values.Where(info => state.HasFlag(info.State)), type),
DataFlow.Capture => new DeviceReadOnlyCollection<DeviceFullInfo>(RecordingDevices.Values.Where(info => state.HasFlag(info.State)), type),
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}
Expand Down Expand Up @@ -80,14 +83,109 @@ private set
public CachedAudioDeviceLister(DeviceState state)
{
_state = state;
MMNotificationClient.Instance.DevicesChanged += DeviceChanged;
_context = Log.ForContext("State", _state);
}

private void DeviceChanged(object sender, DeviceChangedEventBase e)
private void DisposeDevice(DeviceFullInfo deviceFullInfo)
{
_context.Verbose("Device Changed received, triggering job");
JobScheduler.Instance.ScheduleJob(new DebounceRefreshJob(_state, this, _context), e.Token);
_ = AudioSwitcher.Instance.InteractWithDevice(deviceFullInfo, device =>
{
device.Dispose();
return device;
});
}

/// <summary>
/// Process device updates
/// </summary>
/// <param name="deviceChangedEvents"></param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public void ProcessDeviceUpdates(IEnumerable<DeviceChangedEvent> deviceChangedEvents)
{
bool GetDevice(DeviceChangedEvent deviceChangedEvent, out DeviceFullInfo device)
{
device = AudioSwitcher.Instance.GetAudioEndpoint(deviceChangedEvent.DeviceId);
if (device == null)
{
_context.Warning("Can't get device {deviceId}", deviceChangedEvent.DeviceId);
return true;
}

return false;
}

void UpdateDeviceCache(DeviceChangedEvent deviceChangedEvent)
{
if (GetDevice(deviceChangedEvent, out var device)) return;

switch (device.Type)
{
case DataFlow.Render:
if (PlaybackDevices.TryGetValue(device.Id, out var oldPlaybackDevice))
{
DisposeDevice(oldPlaybackDevice);
}

PlaybackDevices[device.Id] = device;
break;
case DataFlow.Capture:
if (RecordingDevices.TryGetValue(device.Id, out var oldRecordingDevice))
{
DisposeDevice(oldRecordingDevice);
}

RecordingDevices[device.Id] = device;
break;
case DataFlow.All:
break;
default:
throw new ArgumentOutOfRangeException();
}

_context.Information("Updated device {deviceId} in cache", device.Id);
}

foreach (var deviceChangedEvent in deviceChangedEvents)
{
try
{
switch (deviceChangedEvent.Action)
{
case EventType.Removed:
if (PlaybackDevices.Remove(deviceChangedEvent.DeviceId, out var playbackDevice))
{
DisposeDevice(playbackDevice);
}

if (RecordingDevices.Remove(deviceChangedEvent.DeviceId, out var recordingDevice))
{
DisposeDevice(recordingDevice);
}

break;
case EventType.Added:
case EventType.StateChanged:
case EventType.PropertyChanged:
UpdateDeviceCache(deviceChangedEvent);
break;
case EventType.DefaultChanged:
if (!PlaybackDevices.TryGetValue(deviceChangedEvent.DeviceId, out var device) && !RecordingDevices.TryGetValue(deviceChangedEvent.DeviceId, out device))
{
_context.Warning("Can't get device {deviceId}", deviceChangedEvent.DeviceId);
break;
}

_defaultDeviceChanged.OnNext(new DefaultDevicePayload(device, ((DefaultDeviceChangedEvent)deviceChangedEvent).Role));
break;
default:
throw new ArgumentOutOfRangeException();
}
}
catch (Exception e)
{
_context.Warning(e, "Couldn't process event: {event} for device {deviceId}", deviceChangedEvent.Action, deviceChangedEvent.DeviceId);
}
}
}

public void Refresh(CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -116,46 +214,31 @@ public void Refresh(CancellationToken cancellationToken = default)
try
{
logContext.Information("Refreshing all devices");
var enumerator = new MMDeviceEnumerator();
using var _ = enumerator.DisposeOnCancellation(cancellationToken);
foreach (var endPoint in enumerator.EnumerateAudioEndPoints(DataFlow.All, _state))
foreach (var deviceInfo in AudioSwitcher.Instance.GetAudioEndpoints((EDataFlow)DataFlow.All, (EDeviceState)_state))
{
cancellationToken.ThrowIfCancellationRequested();
try
switch (deviceInfo.Type)
{
var deviceInfo = new DeviceFullInfo(endPoint);
if (string.IsNullOrEmpty(deviceInfo.Name))
{
continue;
}

switch (deviceInfo.Type)
{
case DataFlow.Render:
playbackDevices.Add(deviceInfo.Id, deviceInfo);
break;
case DataFlow.Capture:
recordingDevices.Add(deviceInfo.Id, deviceInfo);
break;
case DataFlow.All:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
catch (Exception e)
{
logContext.Warning(e, "Can't get name of device {device}", endPoint.ID);
case DataFlow.Render:
playbackDevices.Add(deviceInfo.Id, deviceInfo);
break;
case DataFlow.Capture:
recordingDevices.Add(deviceInfo.Id, deviceInfo);
break;
case DataFlow.All:
break;
default:
throw new ArgumentOutOfRangeException();
}
}

foreach (var device in PlaybackDevices.Union(RecordingDevices))
{
device.Dispose();
DisposeDevice(device.Value);
}

PlaybackDevices = playbackDevices.Values.ToArray();
RecordingDevices = recordingDevices.Values.ToArray();
PlaybackDevices = playbackDevices;
RecordingDevices = recordingDevices;


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

public void Dispose()
{
MMNotificationClient.Instance.DevicesChanged -= DeviceChanged;

foreach (var device in PlaybackDevices.Union(RecordingDevices))
{
device.Dispose();
DisposeDevice(device.Value);
}

_refreshCancellationTokenSource.Dispose();
Expand Down
Loading
Loading