Skip to content

Commit 45c0c2b

Browse files
qiancclaude
andcommitted
playback: fall back to raw-range mapping for Alsa mixers without dB info
Some Alsa mixer controls expose no dB information at all, most notably the Alsa pulse plugin's `Master`, which maps onto the PulseAudio sink volume. AlsaMixer::open() failed with "Could not get Alsa softvol dB range" for such controls, making them unusable as `--mixer alsa` targets. Instead of failing, map the volume linearly onto the control's raw range and force `VolumeCtrl::Linear`, since without dB information other taper curves cannot be applied faithfully. The softvol antilog compensation is also skipped for these controls: it counteracts Alsa's internal linear-to-dB conversion, which does not happen when the raw volume is applied directly. For PulseAudio a linear raw mapping is the desired behavior anyway, as its raw volume scale is already perceptually (cubically) tapered. This allows `--mixer alsa --alsa-mixer-device pulse --alsa-mixer-control Master` to control the PulseAudio sink (master) volume from Spotify Connect while playing back through the pulseaudio backend. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 9c7d756 commit 45c0c2b

2 files changed

Lines changed: 32 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818

1919
### Fixed
2020

21+
- [playback] Alsa mixer: fall back to a linear mapping on the raw volume range for controls without dB information (e.g. the Alsa pulse plugin's `Master`) instead of failing to open the mixer
2122
- [audio] Fixed integer overflow in throughput calculation
2223
- [main] Fixed `--volume-ctrl fixed` not disabling volume control
2324
- [core] Fix default permissions on credentials file and warn user if file is world readable

playback/src/mixer/alsamixer.rs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub struct AlsaMixer {
2525
has_switch: bool,
2626
is_softvol: bool,
2727
use_linear_in_db: bool,
28+
has_db: bool,
2829
}
2930

3031
// min_db cannot be depended on to be mute. Also note that contrary to
@@ -42,8 +43,6 @@ enum AlsaMixerError {
4243
CouldNotOpenWithDevice(AlsaError),
4344
#[error("Could not open Alsa softvol with that name. {0}")]
4445
CouldNotOpenWithName(NulError),
45-
#[error("Could not get Alsa softvol dB range. {0}")]
46-
NoDbRange(AlsaError),
4746
#[error("Could not convert Alsa raw volume to dB volume. {0}")]
4847
CouldNotConvertRaw(AlsaError),
4948
}
@@ -81,6 +80,7 @@ impl Mixer for AlsaMixer {
8180

8281
// Query dB volume range -- note that Alsa exposes a different
8382
// API for hardware and software mixers
83+
let mut has_db = true;
8484
let (min_millibel, max_millibel) = if is_softvol {
8585
let control =
8686
Ctl::new(&config.device, false).map_err(AlsaMixerError::CouldNotOpenWithDevice)?;
@@ -90,9 +90,23 @@ impl Mixer for AlsaMixer {
9090
.map_err(AlsaMixerError::CouldNotOpenWithName)?,
9191
);
9292
element_id.set_index(config.index);
93-
let (min_millibel, mut max_millibel) = control
94-
.get_db_range(&element_id)
95-
.map_err(AlsaMixerError::NoDbRange)?;
93+
let (min_millibel, mut max_millibel) = match control.get_db_range(&element_id) {
94+
Ok(range) => range,
95+
Err(e) => {
96+
// Some controls (e.g. the Alsa pulse plugin's `Master`,
97+
// which maps onto the PulseAudio sink volume) expose no
98+
// dB information at all. Fall back to mapping the volume
99+
// linearly onto the raw range; for PulseAudio this is
100+
// the right thing since its raw scale is already
101+
// perceptually (cubically) tapered.
102+
info!(
103+
"Alsa mixer control has no dB information ({e}), \
104+
falling back to linear mapping on the raw volume range"
105+
);
106+
has_db = false;
107+
(ZERO_DB, ZERO_DB)
108+
}
109+
};
96110

97111
// Alsa can report incorrect maximum volumes due to rounding
98112
// errors. e.g. Alsa rounds [-60.0..0.0] in range [0..255] to
@@ -170,6 +184,12 @@ impl Mixer for AlsaMixer {
170184
config.volume_ctrl = VolumeCtrl::Linear;
171185
}
172186

187+
// Without dB information the volume can only be mapped onto the raw
188+
// range, so other taper curves cannot be applied faithfully.
189+
if !has_db {
190+
config.volume_ctrl = VolumeCtrl::Linear;
191+
}
192+
173193
debug!("Alsa mixer control is softvol: {}", is_softvol);
174194
debug!("Alsa support for playback (mute) switch: {}", has_switch);
175195
debug!("Alsa raw volume range: [{}..{}] ({})", min, max, range);
@@ -190,6 +210,7 @@ impl Mixer for AlsaMixer {
190210
has_switch,
191211
is_softvol,
192212
use_linear_in_db,
213+
has_db,
193214
})
194215
}
195216

@@ -312,6 +333,10 @@ impl AlsaMixer {
312333
}
313334

314335
fn is_some_linear(&self) -> bool {
315-
self.is_softvol || self.use_linear_in_db
336+
// The antilog compensation assumes Alsa maps the raw range onto a
337+
// dB scale internally (as the softvol plugin does). Controls without
338+
// dB information (e.g. the pulse plugin) apply the raw volume
339+
// directly, so no compensation must be applied there.
340+
(self.is_softvol && self.has_db) || self.use_linear_in_db
316341
}
317342
}

0 commit comments

Comments
 (0)