← Whispering

feat: Show friendly names for speaker monitor devices

088cd140c9806cdffece81aa893d92ee730b644f · Claude · Jan 25, 2026 23:40

Extract user-friendly names from PulseAudio/PipeWire monitor device names.
For example:
- 'alsa_output.usb-MOTU_828...HiFi__Headphones1__sink.monitor'
  -> '828 Headphones (1)'
- 'Monitor of 828 Main Out A (1-2)'
  -> '828 Main Out A (1-2)'

+46 −7 · 1 file changed

  • src/core_parts/audio_devices.py +46 −7
diff --git a/src/core_parts/audio_devices.py b/src/core_parts/audio_devices.py
index 43d0ea8..6823e9d 100644
--- a/src/core_parts/audio_devices.py
+++ b/src/core_parts/audio_devices.py
@@ -59,14 +59,51 @@ def get_mic_names():
     return mics
 
 
+def _extract_friendly_name(device_name):
+    """Extract a friendly display name from a PulseAudio/PipeWire device name.
+
+    Examples:
+        'alsa_output.usb-MOTU_828_828E0238SD-00.HiFi__Headphones1__sink.monitor'
+        -> '828 Headphones (1)'
+
+        'Monitor of 828 Main Out A (1-2)'
+        -> '828 Main Out A (1-2)'
+    """
+    name = device_name
+
+    # If it starts with "Monitor of ", extract the rest
+    if name.lower().startswith('monitor of '):
+        return name[11:]  # Remove "Monitor of "
+
+    # Try to extract from PulseAudio-style names
+    # Pattern: ...HiFi__DeviceName__sink.monitor
+    import re
+
+    # Look for pattern like __SomeName__sink
+    match = re.search(r'__([^_]+\d*)__sink', name)
+    if match:
+        friendly = match.group(1)
+        # Convert camelCase/numbers to spaced format
+        # Headphones1 -> Headphones (1)
+        friendly = re.sub(r'([a-zA-Z])(\d+)$', r'\1 (\2)', friendly)
+        # Add device prefix if we can find it (like "828")
+        prefix_match = re.search(r'usb-([^_]+)_(\d+)', name)
+        if prefix_match:
+            model = prefix_match.group(2)
+            return f"{model} {friendly}"
+        return friendly
+
+    # Fallback: just return the original but truncated if too long
+    if len(name) > 50:
+        return name[:47] + "..."
+    return name
+
+
 def get_monitor_names():
-    """Get list of speaker monitor device names (for capturing system audio output).
+    """Get list of speaker/output monitor device names (for capturing system audio output).
 
     Monitor devices capture what's being played through speakers/headphones.
-    On PulseAudio/PipeWire, these typically have 'Monitor' in the name.
-
-    Unlike regular mics, we accept monitors from any host API except JACK,
-    since PulseAudio/PipeWire monitors may not appear under ALSA.
+    Returns tuples of (device_index, friendly_display_name, original_name).
     """
     devices_list = sd.query_devices()
 
@@ -74,7 +111,8 @@ def get_monitor_names():
 
     for i, d in enumerate(devices_list):
         if d['max_input_channels'] > 0:
-            name_lower = d['name'].lower()
+            name = d['name']
+            name_lower = name.lower()
             api_name = sd.query_hostapis(d['hostapi'])['name'].lower()
 
             # Skip JACK (causes crashes)
@@ -83,7 +121,8 @@ def get_monitor_names():
 
             # Look for monitor devices (PulseAudio/PipeWire convention)
             if 'monitor' in name_lower:
-                monitors.append((i, d['name']))
+                friendly_name = _extract_friendly_name(name)
+                monitors.append((i, friendly_name))
 
     return monitors