fix(volume): average all channels instead of returning the first

Previously returned the first channel percentage found, which is
incorrect when channels are unbalanced. Average across all channels
matches what OS volume sliders display and avoids a misleading reading
in edge cases such as hard-panned mono (e.g. left=100%, right=0%
would have incorrectly reported 100% rather than 50%).

Add a test covering the unbalanced case.
This commit is contained in:
2026-04-10 14:52:58 -04:00
parent aadb0a4a57
commit e83bc02563
+27 -11
View File
@@ -65,18 +65,27 @@ fn query_volume() -> Option<u8> {
parse_volume(&String::from_utf8_lossy(&out.stdout))
}
/// Parse the first percentage value from pactl output such as:
/// Parse the average percentage across all channels from pactl output such as:
/// `"Volume: front-left: 65536 / 100% / 0.00 dB, front-right: 65536 / 100% ..."`
///
/// Averaging across channels matches what OS volume sliders display and avoids
/// a misleading reading when channels are unbalanced (e.g. hard-panned mono).
fn parse_volume(s: &str) -> Option<u8> {
for part in s.split('/') {
let trimmed = part.trim();
if let Some(stripped) = trimmed.strip_suffix('%') {
if let Ok(n) = stripped.trim().parse::<u8>() {
return Some(n);
}
}
let levels: Vec<u32> = s
.split('/')
.filter_map(|part| {
part.trim()
.strip_suffix('%')
.and_then(|n| n.trim().parse::<u32>().ok())
})
.collect();
if levels.is_empty() {
return None;
}
None
let avg = levels.iter().sum::<u32>() / levels.len() as u32;
Some(avg.min(100) as u8)
}
fn broadcast(pkt: Packet, keyboards: &[Keyboard]) {
@@ -99,17 +108,24 @@ mod tests {
use super::parse_volume;
#[test]
fn parses_stereo_output() {
fn parses_stereo_equal() {
let s = "Volume: front-left: 65536 / 100% / 0.00 dB, front-right: 65536 / 100% / 0.00 dB";
assert_eq!(parse_volume(s), Some(100));
}
#[test]
fn parses_partial_volume() {
fn parses_stereo_equal_partial() {
let s = "Volume: front-left: 45875 / 70% / -9.03 dB, front-right: 45875 / 70% / -9.03 dB";
assert_eq!(parse_volume(s), Some(70));
}
#[test]
fn averages_unbalanced_channels() {
// left=80%, right=20% → average=50%
let s = "Volume: front-left: 52429 / 80% / -5.08 dB, front-right: 13107 / 20% / -13.98 dB";
assert_eq!(parse_volume(s), Some(50));
}
#[test]
fn returns_none_for_garbage() {
assert_eq!(parse_volume("no percentage here"), None);