1- using NAudio . CoreAudioApi ;
1+ using NAudio . CoreAudioApi ;
22using System . Data ;
33using System . Management ;
44using 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