Skip to content

Commit 0546441

Browse files
Improvements for "Sound Channel Inspector"
- The logic of "Sound Channel Inspector" is improved
1 parent 88ea053 commit 0546441

1 file changed

Lines changed: 126 additions & 41 deletions

File tree

SoundChannelInspector/Program.cs

Lines changed: 126 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using NAudio.CoreAudioApi;
1+
using NAudio.CoreAudioApi;
22
using System.Data;
33
using System.Management;
44
using System.Runtime.Versioning;
@@ -81,7 +81,7 @@ private static void EnumerateAllEndpointChannels()
8181
string flowLabel = device.DataFlow == DataFlow.Render ? "Output" : "Input";
8282

8383
Console.ForegroundColor = device.State == DeviceState.Active ? ConsoleColor.Green : ConsoleColor.DarkGray;
84-
Console.WriteLine($"─────────────────────────────────────────────");
84+
Console.WriteLine($"-------------------------------");
8585
Console.WriteLine($" Device: {device.FriendlyName}");
8686
Console.WriteLine($" Flow: {flowLabel}");
8787
Console.WriteLine($" State: {stateLabel}");
@@ -141,9 +141,14 @@ private static void EnumerateAllEndpointChannels()
141141
}
142142
}
143143

144-
// ──────────────────────────────────────────────────────────
144+
// ----------------------------------------------------------------
145145
// DeviceTopology COM interop to discover slider/part names
146-
// ──────────────────────────────────────────────────────────
146+
// ----------------------------------------------------------------
147+
148+
[DllImport("ole32.dll")]
149+
private static extern int CoCreateInstance(
150+
ref Guid rclsid, IntPtr pUnkOuter, uint dwClsContext,
151+
ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv);
147152

148153
[SupportedOSPlatform("windows")]
149154
private static void EnumerateTopologyParts(MMDevice device, HashSet<string> walkedHardwareDevices)
@@ -192,14 +197,15 @@ private static void EnumerateTopologyParts(MMDevice device, HashSet<string> walk
192197
topo.GetConnectorCount(out uint connectorCount);
193198

194199
var visitedPartIds = new HashSet<string>();
195-
196-
// Collect endpoint-visible part global IDs so we can mark others as hidden
197200
var endpointPartIds = new HashSet<string>();
198201

199202
Console.ForegroundColor = ConsoleColor.Cyan;
200203
Console.WriteLine($" Endpoint Topology Parts:");
201204
Console.ResetColor();
202205

206+
// Collect the hardware filter device ID from the connected-to side of endpoint connectors
207+
string hwDeviceId = null;
208+
203209
for (uint c = 0; c < connectorCount; c++)
204210
{
205211
topo.GetConnector(c, out IConnector connector);
@@ -209,6 +215,12 @@ private static void EnumerateTopologyParts(MMDevice device, HashSet<string> walk
209215
if (!isConnected)
210216
continue;
211217

218+
// Get the hardware filter device ID from the endpoint's connector
219+
if (hwDeviceId == null)
220+
{
221+
try { connector.GetDeviceIdConnectedTo(out hwDeviceId); } catch { }
222+
}
223+
212224
connector.GetConnectedTo(out IConnector connectedTo);
213225

214226
IntPtr connectedToPtr = Marshal.GetIUnknownForObject(connectedTo);
@@ -221,32 +233,23 @@ private static void EnumerateTopologyParts(MMDevice device, HashSet<string> walk
221233
try
222234
{
223235
var part = (IPart)Marshal.GetObjectForIUnknown(partPtr);
224-
225-
// Collect endpoint-visible IDs first
226236
CollectPartIds(part, endpointPartIds, new HashSet<string>());
227-
228-
// Then print them (not hidden)
229237
WalkParts(part, depth: 2, visitedPartIds, endpointPartIds, isHardwareFilter: false);
230238

231-
// Follow into the hardware filter topology via GetTopologyObject
232-
try
239+
// Fallback: get HW device ID from the part's topology object
240+
if (hwDeviceId == null)
233241
{
234-
part.GetTopologyObject(out IDeviceTopology hwTopo);
235-
if (hwTopo != null)
242+
try
236243
{
237-
hwTopo.GetDeviceId(out string hwDeviceId);
238-
if (!string.IsNullOrEmpty(hwDeviceId) &&
239-
walkedHardwareDevices.Add(hwDeviceId))
240-
{
241-
WalkHardwareFilterTopology(hwTopo, visitedPartIds, endpointPartIds);
242-
}
243-
else
244+
part.GetTopologyObject(out IDeviceTopology partTopo);
245+
if (partTopo != null)
244246
{
245-
Marshal.ReleaseComObject(hwTopo);
247+
partTopo.GetDeviceId(out hwDeviceId);
248+
Marshal.ReleaseComObject(partTopo);
246249
}
247250
}
251+
catch { }
248252
}
249-
catch (COMException) { }
250253
}
251254
finally
252255
{
@@ -261,16 +264,67 @@ private static void EnumerateTopologyParts(MMDevice device, HashSet<string> walk
261264

262265
Marshal.ReleaseComObject(connectedTo);
263266
}
264-
catch (COMException)
265-
{
266-
// Not connected - skip
267-
}
267+
catch (COMException) { }
268268
finally
269269
{
270270
Marshal.ReleaseComObject(connector);
271271
}
272272
}
273273

274+
// Walk the ACTUAL hardware filter topology to find hidden mixer lines.
275+
// The hardware filter is a SEPARATE device with its own IDeviceTopology.
276+
// We must activate it via IMMDeviceEnumerator::GetDevice using the device ID
277+
// obtained from the endpoint's connector. This is the only way to reach
278+
// parts like PC Beep, S/PDIF, CD Audio, Aux, etc. that are NOT exposed
279+
// through the endpoint topology.
280+
if (!string.IsNullOrEmpty(hwDeviceId) && walkedHardwareDevices.Add(hwDeviceId))
281+
{
282+
Console.ForegroundColor = ConsoleColor.Magenta;
283+
Console.WriteLine($" Hardware Filter Topology (all mixer lines including hidden):");
284+
Console.ResetColor();
285+
286+
try
287+
{
288+
Guid CLSID_MMDeviceEnumerator = new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E");
289+
Guid IID_IMMDeviceEnumerator = new Guid("A95664D2-9614-4F35-A746-DE8DB63617E6");
290+
291+
int hrCreate = CoCreateInstance(
292+
ref CLSID_MMDeviceEnumerator, IntPtr.Zero, CLSCTX_ALL,
293+
ref IID_IMMDeviceEnumerator, out object enumObj);
294+
295+
if (hrCreate == 0 && enumObj != null)
296+
{
297+
var mmEnum = (IMMDeviceEnumeratorNative)enumObj;
298+
int hrGetDev = mmEnum.GetDevice(hwDeviceId, out IMMDevice hwDevice);
299+
300+
if (hrGetDev == 0 && hwDevice != null)
301+
{
302+
Guid iidTopo = typeof(IDeviceTopology).GUID;
303+
int hrAct = hwDevice.Activate(ref iidTopo, CLSCTX_ALL, IntPtr.Zero, out object hwTopoObj);
304+
305+
if (hrAct == 0 && hwTopoObj != null)
306+
{
307+
var hwTopo = (IDeviceTopology)hwTopoObj;
308+
// Use a SEPARATE visited set for the hardware filter walk
309+
// so parts already seen in the endpoint walk are still printed
310+
// and correctly classified as hidden or not
311+
var hwVisitedPartIds = new HashSet<string>();
312+
WalkHardwareFilterTopologyDirect(hwTopo, hwVisitedPartIds, endpointPartIds);
313+
Marshal.ReleaseComObject(hwTopoObj);
314+
}
315+
}
316+
317+
Marshal.ReleaseComObject(enumObj);
318+
}
319+
}
320+
catch (Exception ex)
321+
{
322+
Console.ForegroundColor = ConsoleColor.DarkYellow;
323+
Console.WriteLine($" (Could not walk hardware filter topology: {ex.Message})");
324+
Console.ResetColor();
325+
}
326+
}
327+
274328
Marshal.ReleaseComObject(topoObj);
275329
}
276330

@@ -324,17 +378,14 @@ private static void CollectPartIds(IPart part, HashSet<string> ids, HashSet<stri
324378
}
325379

326380
/// <summary>
327-
/// Walks the hardware filter device topology obtained via IPart::GetTopologyObject()
328-
/// to discover hidden mixer lines like PC Beep, S/PDIF, CD Audio, Aux, etc.
381+
/// Walks the hardware filter device topology directly to discover ALL mixer lines,
382+
/// including hidden ones like PC Beep, S/PDIF, CD Audio, Aux, etc.
383+
/// This enumerates both subunits and connectors at the device level.
329384
/// </summary>
330385
[SupportedOSPlatform("windows")]
331-
private static void WalkHardwareFilterTopology(IDeviceTopology hwTopo, HashSet<string> visitedPartIds, HashSet<string> endpointPartIds)
386+
private static void WalkHardwareFilterTopologyDirect(IDeviceTopology hwTopo, HashSet<string> visitedPartIds, HashSet<string> endpointPartIds)
332387
{
333-
Console.ForegroundColor = ConsoleColor.Magenta;
334-
Console.WriteLine($" Hardware Filter Topology (includes hidden channels):");
335-
Console.ResetColor();
336-
337-
// Walk all subunits - these contain volume/mute controls for hidden lines
388+
// Walk all subunits - these contain volume/mute controls for mixer lines
338389
hwTopo.GetSubunitCount(out uint subunitCount);
339390
for (uint s = 0; s < subunitCount; s++)
340391
{
@@ -365,13 +416,18 @@ private static void WalkHardwareFilterTopology(IDeviceTopology hwTopo, HashSet<s
365416
}
366417
}
367418

368-
// Walk all connectors - input connectors are where PC Beep, S/PDIF, etc. live
419+
// Walk ALL connectors - this is where PC Beep, S/PDIF, etc. are exposed
420+
// Hidden channels are typically INPUT connectors (DataFlow.Capture/Recording)
369421
hwTopo.GetConnectorCount(out uint hwConnCount);
370422
for (uint c = 0; c < hwConnCount; c++)
371423
{
372424
hwTopo.GetConnector(c, out IConnector hwConn);
373425
try
374426
{
427+
// Check connector data flow to identify input lines
428+
hwConn.GetDataFlow(out uint dataFlow);
429+
string flowType = dataFlow == 0 ? "Render/Output" : "Capture/Input";
430+
375431
IntPtr hwConnPtr = Marshal.GetIUnknownForObject(hwConn);
376432
try
377433
{
@@ -382,6 +438,16 @@ private static void WalkHardwareFilterTopology(IDeviceTopology hwTopo, HashSet<s
382438
try
383439
{
384440
var part = (IPart)Marshal.GetObjectForIUnknown(partPtr);
441+
442+
// Display connector flow direction before walking
443+
part.GetName(out string connName);
444+
if (!string.IsNullOrWhiteSpace(connName))
445+
{
446+
Console.ForegroundColor = ConsoleColor.DarkCyan;
447+
Console.WriteLine($" [{flowType}]");
448+
Console.ResetColor();
449+
}
450+
385451
WalkParts(part, depth: 2, visitedPartIds, endpointPartIds, isHardwareFilter: true);
386452
}
387453
finally
@@ -400,8 +466,6 @@ private static void WalkHardwareFilterTopology(IDeviceTopology hwTopo, HashSet<s
400466
Marshal.ReleaseComObject(hwConn);
401467
}
402468
}
403-
404-
Marshal.ReleaseComObject(hwTopo);
405469
}
406470

407471
[SupportedOSPlatform("windows")]
@@ -467,7 +531,7 @@ private static void WalkParts(IPart part, int depth, HashSet<string> visitedPart
467531
else
468532
Console.ForegroundColor = ConsoleColor.Gray;
469533

470-
Console.WriteLine($"{indent}├─ {name} ({typeLabel}){markers}");
534+
Console.WriteLine($"{indent} - {typeLabel}: {name}{markers}");
471535
Console.ResetColor();
472536
}
473537

@@ -508,9 +572,30 @@ private static void WalkParts(IPart part, int depth, HashSet<string> visitedPart
508572
catch { }
509573
}
510574

511-
// ──────────────────────────────────────────────
575+
// ------------------------------------------------
512576
// COM interface declarations
513-
// ──────────────────────────────────────────────
577+
// ------------------------------------------------
578+
579+
[ComImport]
580+
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
581+
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
582+
interface IMMDeviceEnumeratorNative
583+
{
584+
[PreserveSig]
585+
int EnumAudioEndpoints(int dataFlow, int dwStateMask, out IntPtr ppDevices);
586+
587+
[PreserveSig]
588+
int GetDefaultAudioEndpoint(int dataFlow, int role, out IntPtr ppEndpoint);
589+
590+
[PreserveSig]
591+
int GetDevice([MarshalAs(UnmanagedType.LPWStr)] string pwstrId, out IMMDevice ppDevice);
592+
593+
[PreserveSig]
594+
int RegisterEndpointNotificationCallback(IntPtr pClient);
595+
596+
[PreserveSig]
597+
int UnregisterEndpointNotificationCallback(IntPtr pClient);
598+
}
514599

515600
[ComImport]
516601
[Guid("D666063F-1587-4E43-81F1-B948E807363F")]

0 commit comments

Comments
 (0)