Skip to content

Commit 53a3006

Browse files
authored
fix(device-resolution): Properly resolve pulse: IDs to actual device names on PipeWire systems [hotfix] #10 from Peter-L-SVK/stage
- Added resolve_pulse_device_name() function to map pulse:X IDs to real ALSA device names - Updated extract_device_pattern() to handle PipeWire's PulseAudio compatibility layer - Fixed WirePlumber configuration targeting invalid alsa_output.X patterns - Improved device detection priority (PipeWire > ALSA > PulseAudio) - Enhanced debug output showing both selected ID and resolved device pattern This hotfix ensures that when selecting PulseAudio-compatible device IDs (pulse:65, pulse:66) on PipeWire systems, the configuration correctly targets the actual hardware device names for proper WirePlumber matching.
2 parents ab70008 + 302d93a commit 53a3006

File tree

1 file changed

+159
-25
lines changed

1 file changed

+159
-25
lines changed

src/audio.rs

Lines changed: 159 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,17 @@ pub fn detect_all_audio_devices() -> Result<Vec<AudioDevice>, String> {
5252

5353
println!("=== Scanning for all audio devices ===");
5454

55-
// Method 1: PipeWire devices
55+
// Method 1: PipeWire devices (HIGHEST PRIORITY)
5656
if let Ok(output) = Command::new("pw-cli")
5757
.args(["list-objects", "Node"])
5858
.output() {
5959
devices.extend(parse_pipewire_devices(&output.stdout)?);
6060
}
6161

62-
// Method 2: ALSA devices
62+
// Method 2: ALSA devices (fallback)
6363
devices.extend(detect_alsa_devices()?);
6464

65-
// Method 3: PulseAudio devices (fallback)
65+
// Method 3: PulseAudio devices (lowest priority - compatibility layer)
6666
devices.extend(detect_pulse_devices()?);
6767

6868
println!("Found {} audio devices", devices.len());
@@ -307,13 +307,13 @@ fn parse_pulse_output(output: &str, device_type: DeviceType) -> Vec<AudioDevice>
307307
let parts: Vec<&str> = line.split_whitespace().collect();
308308
if parts.len() >= 2 {
309309
let device = AudioDevice {
310-
name: parts[1].to_string(),
310+
name: parts[1].to_string(), // Use actual device name, not pulse ID
311311
description: if parts.len() >= 3 {
312312
parts[2..].join(" ")
313313
} else {
314314
"PulseAudio Device".to_string()
315315
},
316-
id: format!("pulse:{}", parts[0]),
316+
id: format!("pulse:{}", parts[0]), // Store as pulse:ID for resolution
317317
device_type: device_type.clone(),
318318
available: true,
319319
};
@@ -415,6 +415,9 @@ fn detect_audio_system() -> String {
415415
}
416416

417417
pub fn apply_audio_settings_with_auth_blocking(settings: AudioSettings) -> Result<(), String> {
418+
// DEBUG: See what device ID is being passed
419+
println!("DEBUG: Selected device_id: {}", settings.device_id);
420+
418421
// Get the current username
419422
let username = whoami::username();
420423

@@ -438,6 +441,7 @@ echo "=== Starting Audio Configuration ==="
438441
echo "Running as: $(whoami)"
439442
echo "Target user: {}"
440443
echo "Target device: {}"
444+
echo "Device pattern: {}"
441445
442446
# Get user ID and runtime directory
443447
USER_ID=$(id -u {})
@@ -520,23 +524,26 @@ echo " Sample Rate: {} Hz"
520524
echo " Bit Depth: {} bit"
521525
echo " Buffer Size: {} samples"
522526
echo " Target Device: {}"
527+
echo " Device Pattern: {}"
523528
echo ""
524529
echo "Note: Some settings may require application restart to take effect"
525530
"#,
526531
username, // 1st placeholder: Target user
527532
settings.device_id, // 2nd placeholder: Target device
528-
username, // 3rd placeholder: USER_ID
529-
username, // 4th placeholder: sudo -u
530-
username, // 5th placeholder: CONFIG_DIR
531-
device_pattern, // 6th placeholder: device.name pattern
532-
settings.sample_rate, // 7th placeholder: audio.rate
533-
format, // 8th placeholder: audio.format
534-
settings.buffer_size, // 9th placeholder: api.alsa.period-size
535-
settings.buffer_size, // 10th placeholder: clock.force-quantum
536-
settings.sample_rate, // 11th placeholder: Sample Rate in summary
537-
settings.bit_depth, // 12th placeholder: Bit Depth in summary
538-
settings.buffer_size, // 13th placeholder: Buffer Size in summary
539-
settings.device_id, // 14th placeholder: Target Device in summary
533+
device_pattern, // 3rd placeholder: Device pattern
534+
username, // 4th placeholder: USER_ID
535+
username, // 5th placeholder: sudo -u
536+
username, // 6th placeholder: CONFIG_DIR
537+
device_pattern, // 7th placeholder: device.name pattern
538+
settings.sample_rate, // 8th placeholder: audio.rate
539+
format, // 9th placeholder: audio.format
540+
settings.buffer_size, // 10th placeholder: api.alsa.period-size
541+
settings.buffer_size, // 11th placeholder: clock.force-quantum
542+
settings.sample_rate, // 12th placeholder: Sample Rate in summary
543+
settings.bit_depth, // 13th placeholder: Bit Depth in summary
544+
settings.buffer_size, // 14th placeholder: Buffer Size in summary
545+
settings.device_id, // 15th placeholder: Target Device in summary
546+
device_pattern, // 16th placeholder: Device Pattern in summary
540547
);
541548

542549
// Write temporary script
@@ -603,23 +610,150 @@ echo "Note: Some settings may require application restart to take effect"
603610
// Helper function to extract device pattern for WirePlumber matching
604611
fn extract_device_pattern(device_id: &str) -> String {
605612
match device_id {
606-
"default" => "alsa.*".to_string(), // Default targets all ALSA devices
613+
"default" => "alsa.*".to_string(),
607614
id if id.starts_with("alsa:") => {
608-
// Extract ALSA device name after "alsa:"
609-
id.trim_start_matches("alsa:").to_string()
615+
let alsa_name = id.trim_start_matches("alsa:");
616+
if alsa_name.contains(".") {
617+
alsa_name.to_string()
618+
} else {
619+
format!("alsa_output.{}", alsa_name)
620+
}
610621
}
611622
id if id.starts_with("pipewire:") => {
612-
// For PipeWire devices, use the node ID directly
613623
let node_id = id.trim_start_matches("pipewire:");
614-
format!("alsa_card.{}", node_id) // PipeWire ALSA cards follow this pattern
624+
// Use dynamic resolution to get the actual device name
625+
match resolve_pipewire_device_name(node_id) {
626+
Ok(device_name) => {
627+
println!("✅ Resolved pipewire:{} to device: {}", node_id, device_name);
628+
device_name
629+
},
630+
Err(e) => {
631+
println!("❌ Failed to resolve pipewire:{}: {}", node_id, e);
632+
// Emergency fallback - try to find the device by description
633+
find_device_by_description(node_id).unwrap_or_else(|| {
634+
format!("alsa_output.{}", node_id) // Last resort fallback
635+
})
636+
}
637+
}
615638
}
616639
id if id.starts_with("pulse:") => {
617-
// For PulseAudio devices, convert to ALSA pattern
618640
let pulse_id = id.trim_start_matches("pulse:");
619-
format!("alsa_output.{}", pulse_id)
641+
// NEW: Resolve pulse: IDs to actual device names on PipeWire systems
642+
match resolve_pulse_device_name(pulse_id) {
643+
Ok(device_name) => {
644+
println!("✅ Resolved pulse:{} to device: {}", pulse_id, device_name);
645+
device_name
646+
},
647+
Err(e) => {
648+
println!("❌ Failed to resolve pulse:{}: {}", pulse_id, e);
649+
// Fallback to old behavior
650+
format!("alsa_output.{}", pulse_id)
651+
}
652+
}
653+
}
654+
_ => device_id.to_string(),
655+
}
656+
}
657+
658+
// Helper function to resolve PipeWire node IDs to actual device names
659+
fn resolve_pipewire_device_name(node_id: &str) -> Result<String, String> {
660+
let output = Command::new("pw-cli")
661+
.args(["info", node_id])
662+
.output()
663+
.map_err(|e| format!("Failed to query PipeWire node {}: {}", node_id, e))?;
664+
665+
if !output.status.success() {
666+
return Err(format!("PipeWire query failed for node {}", node_id));
667+
}
668+
669+
let output_str = String::from_utf8_lossy(&output.stdout);
670+
671+
// Look for node.name in the output
672+
for line in output_str.lines() {
673+
if line.contains("node.name") && line.contains('=') {
674+
if let Some(name_part) = line.split('=').nth(1) {
675+
let name = name_part.trim().trim_matches('"').to_string();
676+
if !name.is_empty() {
677+
return Ok(name);
678+
}
679+
}
680+
}
681+
}
682+
683+
Err(format!("Could not find node.name for PipeWire node {}", node_id))
684+
}
685+
686+
// NEW: Helper function to resolve PulseAudio device IDs to actual device names
687+
fn resolve_pulse_device_name(pulse_id: &str) -> Result<String, String> {
688+
// Use pactl to get the actual device name
689+
let output = Command::new("pactl")
690+
.args(["list", "sinks", "short"])
691+
.output()
692+
.map_err(|e| format!("Failed to query PulseAudio devices: {}", e))?;
693+
694+
if !output.status.success() {
695+
return Err("PulseAudio query failed".to_string());
696+
}
697+
698+
let output_str = String::from_utf8_lossy(&output.stdout);
699+
700+
// Parse the output to find the device with the matching pulse ID
701+
for line in output_str.lines() {
702+
let parts: Vec<&str> = line.split_whitespace().collect();
703+
if parts.len() >= 2 && parts[0] == pulse_id {
704+
// Return the actual device name (second column)
705+
return Ok(parts[1].to_string());
706+
}
707+
}
708+
709+
// Also check sources (inputs) if not found in sinks
710+
let output = Command::new("pactl")
711+
.args(["list", "sources", "short"])
712+
.output()
713+
.map_err(|e| format!("Failed to query PulseAudio sources: {}", e))?;
714+
715+
if output.status.success() {
716+
let output_str = String::from_utf8_lossy(&output.stdout);
717+
for line in output_str.lines() {
718+
let parts: Vec<&str> = line.split_whitespace().collect();
719+
if parts.len() >= 2 && parts[0] == pulse_id {
720+
return Ok(parts[1].to_string());
721+
}
722+
}
723+
}
724+
725+
Err(format!("PulseAudio device {} not found", pulse_id))
726+
}
727+
728+
// Emergency fallback - try to find device by scanning all nodes
729+
fn find_device_by_description(target_node_id: &str) -> Option<String> {
730+
if let Ok(output) = Command::new("pw-cli").args(["list-objects", "Node"]).output() {
731+
let output_str = String::from_utf8_lossy(&output.stdout);
732+
let mut current_node_id: Option<String> = None;
733+
let mut current_device_name: Option<String> = None;
734+
735+
for line in output_str.lines() {
736+
if line.contains("id ") && line.contains("type PipeWire:Interface:Node") {
737+
if let Some(id_part) = line.split(',').next() {
738+
if let Some(id) = id_part.split(' ').nth(1) {
739+
current_node_id = Some(id.to_string());
740+
}
741+
}
742+
}
743+
744+
if let Some(ref node_id) = current_node_id {
745+
if node_id == target_node_id && line.contains("node.name") && line.contains('=') {
746+
if let Some(name_part) = line.split('=').nth(1) {
747+
let name = name_part.trim().trim_matches('"').to_string();
748+
if !name.is_empty() {
749+
return Some(name);
750+
}
751+
}
752+
}
753+
}
620754
}
621-
_ => device_id.to_string(), // Fallback to original ID
622755
}
756+
None
623757
}
624758

625759
pub fn detect_current_audio_settings() -> Result<AudioSettings, String> {

0 commit comments

Comments
 (0)