Vanara/UnitTests/PInvoke/CoreAudio/DeviceTests.cs

401 lines
16 KiB
C#

using Microsoft.Win32;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Vanara.Extensions;
using Vanara.InteropServices;
using static Vanara.PInvoke.CoreAudio;
using static Vanara.PInvoke.Ole32;
using static Vanara.PInvoke.PropSys;
using static Vanara.PInvoke.Winmm;
namespace Vanara.PInvoke.Tests
{
public partial class CoreAudioTests
{
private static readonly PROPERTYKEY PKEY_Device_FriendlyName = new PROPERTYKEY(new Guid(0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0), 14);
private static Dictionary<string, string> lookup = new Dictionary<string, string>();
public static IEnumerable<IMMDevice> CreateIMMDeviceCollection(IMMDeviceEnumerator deviceEnumerator, EDataFlow direction = EDataFlow.eAll, DEVICE_STATE stateMasks = DEVICE_STATE.DEVICE_STATEMASK_ALL)
{
using var deviceCollection = ComReleaserFactory.Create(deviceEnumerator.EnumAudioEndpoints(direction, stateMasks));
var deviceList = new List<IMMDevice>();
var cnt = deviceCollection.Item.GetCount();
if (cnt == 0) Assert.Inconclusive("No devices were found.");
for (uint i = 0; i < cnt; i++)
{
deviceCollection.Item.Item(i, out var dev).ThrowIfFailed();
deviceList.Add(dev);
}
return deviceList;
}
/// This test ensures that each device can use any valid COM interface returned from the Activate method. It checks to make sure
/// each received interface is not null and an HRESULT of S_OK is returned. </summary>
[Test]
public void IMMDevice_Activate()
{
using var enumerator = ComReleaserFactory.Create(new IMMDeviceEnumerator());
foreach (var d in CreateIMMDeviceCollection(enumerator.Item, EDataFlow.eAll, DEVICE_STATE.DEVICE_STATE_ACTIVE))
{
TestActivation<IAudioClient>();
TestActivation<IAudioEndpointVolume>();
TestActivation<IAudioMeterInformation>();
TestActivation<IAudioSessionManager>();
TestActivation<IAudioSessionManager2>();
TestActivation<IDeviceTopology>();
void TestActivation<T>() where T : class
{
Assert.That(d.Activate(typeof(T).GUID, CLSCTX.CLSCTX_INPROC_SERVER, default, out var objInterface), ResultIs.Successful);
Assert.IsNotNull(objInterface as T);
Marshal.ReleaseComObject(objInterface);
}
}
}
/// <summary>This test ensures that each device can get its ID. It also checks that the received ID is not null.</summary>
[Test]
public void IMMDevice_GetId()
{
using var enumerator = ComReleaserFactory.Create(new IMMDeviceEnumerator());
foreach (var d in CreateIMMDeviceCollection(enumerator.Item))
{
string strId = null;
Assert.That(() => strId = d.GetId(), Throws.Nothing);
Assert.IsNotNull(strId);
}
}
/// <summary>
/// This test ensures that each device can get its state. It also checks that the received state is a valid device state constant.
/// </summary>
[Test]
public void IMMDevice_GetState()
{
using var enumerator = ComReleaserFactory.Create(new IMMDeviceEnumerator());
foreach (var d in CreateIMMDeviceCollection(enumerator.Item))
{
DEVICE_STATE deviceState = 0;
Assert.That(() => deviceState = d.GetState(), Throws.Nothing);
Assert.That(Enum.IsDefined(typeof(DEVICE_STATE), deviceState), Is.True);
}
}
/// <summary>
/// This test ensures that each device can open a property store in READWRITE mode and that the received property store is non-null.
/// It also checks that the property store object works correctly by making a call to get the property count.
/// </summary>
[Test]
public void IMMDevice_OpenPropertyStore()
{
var tested = false;
using var enumerator = ComReleaserFactory.Create(new IMMDeviceEnumerator());
foreach (var d in CreateIMMDeviceCollection(enumerator.Item))
{
TestContext.WriteLine($"**** {GetDeviceName(d.GetId())} ****");
// Open the property store
IPropertyStore propertyStore = null;
Assert.That(() => propertyStore = d.OpenPropertyStore(STGM.STGM_READ), Throws.Nothing);
// Verify the count can be received.
var propertyCount = uint.MaxValue;
Assert.That(() => propertyCount = propertyStore.GetCount(), Throws.Nothing);
Assert.AreNotEqual(uint.MaxValue, propertyCount, "The property count was not received.");
// Get each property key, then get value.
for (uint i = 0; i < propertyCount; i++)
{
PROPERTYKEY propertyKey = default;
Assert.That(() => propertyKey = propertyStore.GetAt(i), Throws.Nothing);
var value = GetPropertyValue(propertyStore, propertyKey);
if (value != null)
tested = true;
TestContext.WriteLine($"{propertyKey.GetCanonicalName()}={value ?? "null"}");
}
}
if (!tested) Assert.Inconclusive("No property values returned valid, non-null values.");
}
/// <summary>
/// Tests that the individual render and capture device collections have a combined count equal to the total device count.
/// </summary>
[Test]
public void IMMDeviceCollection_GetCount()
{
using var enumerator = ComReleaserFactory.Create(new IMMDeviceEnumerator());
var allCaptureDevices = enumerator.Item.EnumAudioEndpoints(EDataFlow.eCapture, DEVICE_STATE.DEVICE_STATEMASK_ALL);
var allRenderDevices = enumerator.Item.EnumAudioEndpoints(EDataFlow.eRender, DEVICE_STATE.DEVICE_STATEMASK_ALL);
var allDevices = enumerator.Item.EnumAudioEndpoints(EDataFlow.eAll, DEVICE_STATE.DEVICE_STATEMASK_ALL);
Assert.IsNotNull(allCaptureDevices, "The IMMDeviceCollection object is null.");
Assert.IsNotNull(allRenderDevices, "The IMMDeviceCollection object is null.");
Assert.IsNotNull(allDevices, "The IMMDeviceCollection object is null.");
uint captureCount = uint.MaxValue, renderCount = uint.MaxValue, allCount = uint.MaxValue;
Assert.That(() => captureCount = allCaptureDevices.GetCount(), Throws.Nothing);
Assert.AreNotEqual(uint.MaxValue, captureCount, "Device count was not received.");
Assert.That(() => renderCount = allRenderDevices.GetCount(), Throws.Nothing);
Assert.AreNotEqual(uint.MaxValue, renderCount, "Device count was not received.");
Assert.That(() => allCount = allDevices.GetCount(), Throws.Nothing);
Assert.AreNotEqual(uint.MaxValue, allDevices, "Device count was not received.");
Assert.AreEqual(allCount, captureCount + renderCount, "The combined number of capture and render devices is not equal to the total device count.");
}
/// <summary>Tests the all devices from index zero to [count - 1] can be received with S_OK HRESULT and each device is not null.</summary>
[Test]
public void IMMDeviceCollection_Item()
{
using var enumerator = ComReleaserFactory.Create(new IMMDeviceEnumerator());
IMMDeviceCollection allDevices = null;
Assert.That(() => allDevices = enumerator.Item.EnumAudioEndpoints(EDataFlow.eAll, DEVICE_STATE.DEVICE_STATEMASK_ALL), Throws.Nothing);
Assert.IsNotNull(allDevices, "The IMMDeviceCollection object is null");
uint count = 0;
Assert.That(() => count = allDevices.GetCount(), Throws.Nothing);
IMMDevice device;
for (uint i = 0; i < count; i++)
{
Assert.That(allDevices.Item(i, out device), ResultIs.Successful);
}
}
/// <summary>
/// This test method does nothing. Testing of the EnumAudioEndpoints method is implicit by testing other aspects of the IMMDevice API.
/// </summary>
[Test]
public void IMMDeviceEnumerator_EnumAudioEndpoints()
{
// This method is thouroughly tested through various other unit tests. The entry point for most other tests starts with calling EnumAudioEndpoints.
// TODO: Add specific test for this.
}
/// <summary>
/// Tests that the default audio endpoint for all combinations of data flow and roles can be created with S_OK HRESULT and that each
/// device is not null.
/// </summary>
[Test]
public void IMMDeviceEnumerator_GetDefaultAudioEndpoint()
{
IMMDevice device = null;
using var enumerator = ComReleaserFactory.Create(new IMMDeviceEnumerator());
// data flow - eAll (this should always produce HRESULT of E_INVALIDARG, which is 0x80070057)
Assert.That(() => device = enumerator.Item.GetDefaultAudioEndpoint(EDataFlow.eAll, ERole.eCommunications), Throws.Exception);
Assert.That(() => device = enumerator.Item.GetDefaultAudioEndpoint(EDataFlow.eAll, ERole.eConsole), Throws.Exception);
Assert.That(() => device = enumerator.Item.GetDefaultAudioEndpoint(EDataFlow.eAll, ERole.eMultimedia), Throws.Exception);
// data flow - eCapture
Assert.That(() => device = enumerator.Item.GetDefaultAudioEndpoint(EDataFlow.eCapture, ERole.eCommunications), Throws.Nothing);
Assert.IsNotNull(device);
Assert.That(() => device = enumerator.Item.GetDefaultAudioEndpoint(EDataFlow.eCapture, ERole.eConsole), Throws.Nothing);
Assert.IsNotNull(device);
Assert.That(() => device = enumerator.Item.GetDefaultAudioEndpoint(EDataFlow.eCapture, ERole.eMultimedia), Throws.Nothing);
Assert.IsNotNull(device);
// data flow - eRender
Assert.That(() => device = enumerator.Item.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eCommunications), Throws.Nothing);
Assert.IsNotNull(device);
Assert.That(() => device = enumerator.Item.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eConsole), Throws.Nothing);
Assert.IsNotNull(device);
Assert.That(() => device = enumerator.Item.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia), Throws.Nothing);
Assert.IsNotNull(device);
}
/// <summary>Tests that the GetDevice method can get each audio device individually, by ID.</summary>
[Test]
public void IMMDeviceEnumerator_GetDevice()
{
using var enumerator = ComReleaserFactory.Create(new IMMDeviceEnumerator());
foreach (var device in CreateIMMDeviceCollection(enumerator.Item))
{
// Get the device ID.
string deviceId = null;
Assert.That(() => deviceId = device.GetId(), Throws.Nothing);
Assert.IsNotNull(deviceId, "The device string is null.");
// Get the IMMDevice directly from the ID.
IMMDevice deviceFromId = null;
Assert.That(() => deviceFromId = enumerator.Item.GetDevice(deviceId), Throws.Nothing);
Assert.IsNotNull(deviceFromId, "The IMMDevice object is null.");
// Ensure the IDs of each device match.
string deviceId2 = null;
Assert.That(() => deviceId2 = deviceFromId.GetId(), Throws.Nothing);
Assert.IsNotNull(deviceId2, "The device string is null.");
Assert.AreEqual(deviceId, deviceId2, "The device IDs are not equal.");
}
}
/// <summary>Tests that a valid client can be registered and an HRESULT of S_OK is returned.</summary>
[Test]
public void IMMDeviceEnumerator_RegisterEndpointNotificationCallback()
{
var cTok = new CancellationTokenSource();
var task = Task.Run(() =>
{
using var enumerator = ComReleaserFactory.Create(new IMMDeviceEnumerator());
var client = new MMDeviceNotifyClient(TestContext.Out);
Assert.That(() => enumerator.Item.RegisterEndpointNotificationCallback(client), Throws.Nothing);
while (!cTok.Token.IsCancellationRequested)
Thread.Sleep(50);
Assert.That(() => enumerator.Item.UnregisterEndpointNotificationCallback(client), Throws.Nothing);
}, cTok.Token);
try
{
// Make changes
using var enumerator = ComReleaserFactory.Create(new IMMDeviceEnumerator());
var activeEndpoints = CreateIMMDeviceCollection(enumerator.Item, EDataFlow.eAll, DEVICE_STATE.DEVICE_STATE_ACTIVE).ToList();
using var ep = ComReleaserFactory.Create(enumerator.Item.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia));
using var alt = ComReleaserFactory.Create(activeEndpoints.First(d => d.GetId() != ep.Item.GetId()));
using var pc = ComReleaserFactory.Create(new CoreAudio.IPolicyConfig());
Assert.That(pc.Item.SetDefaultEndpoint(alt.Item.GetId(), ERole.eMultimedia), ResultIs.Successful);
Thread.Sleep(250);
Assert.That(pc.Item.SetDefaultEndpoint(ep.Item.GetId(), ERole.eMultimedia), ResultIs.Successful);
Thread.Sleep(250);
// Registry hack to disable
//Registry.SetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Render\{14b8ed7a-b84f-43b1-8de6-dc678cd96836}", "DeviceState", 0x2, RegistryValueKind.DWord);
//Thread.Sleep(100);
//Registry.SetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Render\{14b8ed7a-b84f-43b1-8de6-dc678cd96836}", "DeviceState", 0x1, RegistryValueKind.DWord);
//Thread.Sleep(100);
//using var vol = ComReleaserFactory.Create(ep.Item.Activate<IAudioEndpointVolume>());
//var mute = vol.Item.GetMute();
//vol.Item.SetMute(!mute, Guid.NewGuid());
//Thread.Sleep(100);
//Assert.That(vol.Item.GetMute(), Is.EqualTo(!mute));
//vol.Item.SetMute(mute, Guid.NewGuid());
//Thread.Sleep(100);
//Assert.That(vol.Item.GetMute(), Is.EqualTo(mute));
}
finally
{
cTok.Cancel();
task.Wait(1000);
}
}
[Test]
public void IMMEndpoint_GetDataFlow()
{
using var enumerator = ComReleaserFactory.Create(new IMMDeviceEnumerator());
foreach (var device in CreateIMMDeviceCollection(enumerator.Item))
{
// Cast compiles to QueryInterface call.
var endpoint = (IMMEndpoint)device;
Assert.IsNotNull(endpoint);
EDataFlow dataFlow = EDataFlow.eAll;
Assert.That(() => dataFlow = endpoint.GetDataFlow(), Throws.Nothing);
Assert.AreNotEqual(EDataFlow.eAll, dataFlow);
}
}
private static string GetDeviceName(string devId)
{
if (lookup.TryGetValue(devId, out var val)) return val;
using var pEnum = ComReleaserFactory.Create(new IMMDeviceEnumerator());
using var pDev = ComReleaserFactory.Create(pEnum.Item.GetDevice(devId));
using var pProps = ComReleaserFactory.Create(pDev.Item.OpenPropertyStore(STGM.STGM_READ));
using var pv = new PROPVARIANT();
pProps.Item.GetValue(PKEY_Device_FriendlyName, pv);
lookup.Add(devId, pv.pwszVal);
return pv.pwszVal;
}
private object GetPropertyValue(IPropertyStore propertyStore, PROPERTYKEY propertyKey)
{
try
{
using var pv = new PROPVARIANT();
propertyStore.GetValue(propertyKey, pv);
if (propertyKey == AudioPropertyKeys.PKEY_AudioEngine_DeviceFormat || propertyKey == AudioPropertyKeys.PKEY_AudioEngine_OEMFormat)
{
Assert.That(pv.vt, Is.EqualTo(VARTYPE.VT_BLOB));
var format = pv.blob.pBlobData.ToStructure<WAVEFORMATEX>(pv.blob.cbSize);
if (format.nChannels != 0 && format.nSamplesPerSec != 0 && format.wBitsPerSample != 0)
Assert.AreEqual(format.nChannels * format.nSamplesPerSec * format.wBitsPerSample, format.nAvgBytesPerSec * 8, "The wave format was not valid.");
}
return pv.Value;
}
catch (Exception ex)
{
return "ERROR: " + ex.Message;
}
}
[ComVisible(true), Guid("e5b6a8de-913e-4756-b1c7-7c73a92eeb3f")]
public class MMDeviceNotifyClient : IMMNotificationClient
{
private readonly TextWriter textWriter;
public MMDeviceNotifyClient(TextWriter writer) => textWriter = writer;
HRESULT IMMNotificationClient.OnDefaultDeviceChanged(EDataFlow flow, ERole role, string pwstrDefaultDeviceId)
{
textWriter.WriteLine($"DefDevChg: flow={flow}, role={role}, dev={GetDeviceName(pwstrDefaultDeviceId)}");
return HRESULT.S_OK;
}
HRESULT IMMNotificationClient.OnDeviceAdded(string pwstrDeviceId)
{
textWriter.WriteLine($"DevAdd: dev={GetDeviceName(pwstrDeviceId)}");
return HRESULT.S_OK;
}
HRESULT IMMNotificationClient.OnDeviceRemoved(string pwstrDeviceId)
{
textWriter.WriteLine($"DevRmv: dev={GetDeviceName(pwstrDeviceId)}");
return HRESULT.S_OK;
}
HRESULT IMMNotificationClient.OnDeviceStateChanged(string pwstrDeviceId, DEVICE_STATE dwNewState)
{
textWriter.WriteLine($"DevStateChg: dev={GetDeviceName(pwstrDeviceId)}, state={dwNewState}");
return HRESULT.S_OK;
}
HRESULT IMMNotificationClient.OnPropertyValueChanged(string pwstrDeviceId, PROPERTYKEY key)
{
textWriter.WriteLine($"DefPropChg: dev={GetDeviceName(pwstrDeviceId)}, key={key}");
return HRESULT.S_OK;
}
}
}
}