1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-03 17:59:24 +02:00

Expose possible mixer opening errors (#1488)

* playback: handle errors when opening mixer

* chore: update CHANGELOG.md

* fix tests and typo
This commit is contained in:
Felix Prillwitz 2025-07-14 17:39:33 +02:00 committed by GitHub
parent 80c27ec476
commit be37402421
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 71 additions and 39 deletions

View file

@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [connect] Moved all public items to the highest level (breaking) - [connect] Moved all public items to the highest level (breaking)
- [connect] Replaced Mercury usage in `Spirc` with Dealer - [connect] Replaced Mercury usage in `Spirc` with Dealer
- [metadata] Replaced `AudioFileFormat` with own enum. (breaking) - [metadata] Replaced `AudioFileFormat` with own enum. (breaking)
- [playback] Changed trait `Mixer::open` to return `Result<Self, Error>` instead of `Self` (breaking)
- [playback] Changed type alias `MixerFn` to return `Result<Arc<dyn Mixer>, Error>` instead of `Arc<dyn Mixer>` (breaking)
### Added ### Added

View file

@ -55,7 +55,7 @@ async fn create_basic_spirc() -> Result<(), Error> {
session, session,
credentials, credentials,
player, player,
mixer(MixerConfig::default()) mixer(MixerConfig::default())?
).await?; ).await?;
Ok(()) Ok(())

View file

@ -50,7 +50,7 @@ async fn main() -> Result<(), Error> {
})?; })?;
let session = Session::new(session_config, Some(cache)); let session = Session::new(session_config, Some(cache));
let mixer = mixer_builder(mixer_config); let mixer = mixer_builder(mixer_config)?;
let player = Player::new( let player = Player::new(
player_config, player_config,

View file

@ -5,9 +5,12 @@ use super::{Mixer, MixerConfig, VolumeCtrl};
use alsa::ctl::{ElemId, ElemIface}; use alsa::ctl::{ElemId, ElemIface};
use alsa::mixer::{MilliBel, SelemChannelId, SelemId}; use alsa::mixer::{MilliBel, SelemChannelId, SelemId};
use alsa::Error as AlsaError;
use alsa::{Ctl, Round}; use alsa::{Ctl, Round};
use std::ffi::CString; use librespot_core::Error;
use std::ffi::{CString, NulError};
use thiserror::Error;
#[derive(Clone)] #[derive(Clone)]
#[allow(dead_code)] #[allow(dead_code)]
@ -29,8 +32,30 @@ pub struct AlsaMixer {
const SND_CTL_TLV_DB_GAIN_MUTE: MilliBel = MilliBel(-9999999); const SND_CTL_TLV_DB_GAIN_MUTE: MilliBel = MilliBel(-9999999);
const ZERO_DB: MilliBel = MilliBel(0); const ZERO_DB: MilliBel = MilliBel(0);
#[derive(Debug, Error)]
enum AlsaMixerError {
#[error("Could not open Alsa mixer. {0}")]
CouldNotOpen(AlsaError),
#[error("Could not find Alsa mixer control")]
CouldNotFindController,
#[error("Could not open Alsa softvol with that device. {0}")]
CouldNotOpenWithDevice(AlsaError),
#[error("Could not open Alsa softvol with that name. {0}")]
CouldNotOpenWithName(NulError),
#[error("Could not get Alsa softvol dB range. {0}")]
NoDbRange(AlsaError),
#[error("Could not convert Alsa raw volume to dB volume. {0}")]
CouldNotConvertRaw(AlsaError),
}
impl From<AlsaMixerError> for Error {
fn from(value: AlsaMixerError) -> Self {
Error::failed_precondition(value)
}
}
impl Mixer for AlsaMixer { impl Mixer for AlsaMixer {
fn open(config: MixerConfig) -> Self { fn open(config: MixerConfig) -> Result<Self, Error> {
info!( info!(
"Mixing with Alsa and volume control: {:?} for device: {} with mixer control: {},{}", "Mixing with Alsa and volume control: {:?} for device: {} with mixer control: {},{}",
config.volume_ctrl, config.device, config.control, config.index, config.volume_ctrl, config.device, config.control, config.index,
@ -39,10 +64,10 @@ impl Mixer for AlsaMixer {
let mut config = config; // clone let mut config = config; // clone
let mixer = let mixer =
alsa::mixer::Mixer::new(&config.device, false).expect("Could not open Alsa mixer"); alsa::mixer::Mixer::new(&config.device, false).map_err(AlsaMixerError::CouldNotOpen)?;
let simple_element = mixer let simple_element = mixer
.find_selem(&SelemId::new(&config.control, config.index)) .find_selem(&SelemId::new(&config.control, config.index))
.expect("Could not find Alsa mixer control"); .ok_or(AlsaMixerError::CouldNotFindController)?;
// Query capabilities // Query capabilities
let has_switch = simple_element.has_playback_switch(); let has_switch = simple_element.has_playback_switch();
@ -57,17 +82,17 @@ impl Mixer for AlsaMixer {
// Query dB volume range -- note that Alsa exposes a different // Query dB volume range -- note that Alsa exposes a different
// API for hardware and software mixers // API for hardware and software mixers
let (min_millibel, max_millibel) = if is_softvol { let (min_millibel, max_millibel) = if is_softvol {
let control = Ctl::new(&config.device, false) let control =
.expect("Could not open Alsa softvol with that device"); Ctl::new(&config.device, false).map_err(AlsaMixerError::CouldNotOpenWithDevice)?;
let mut element_id = ElemId::new(ElemIface::Mixer); let mut element_id = ElemId::new(ElemIface::Mixer);
element_id.set_name( element_id.set_name(
&CString::new(config.control.as_str()) &CString::new(config.control.as_str())
.expect("Could not open Alsa softvol with that name"), .map_err(AlsaMixerError::CouldNotOpenWithName)?,
); );
element_id.set_index(config.index); element_id.set_index(config.index);
let (min_millibel, mut max_millibel) = control let (min_millibel, mut max_millibel) = control
.get_db_range(&element_id) .get_db_range(&element_id)
.expect("Could not get Alsa softvol dB range"); .map_err(AlsaMixerError::NoDbRange)?;
// Alsa can report incorrect maximum volumes due to rounding // Alsa can report incorrect maximum volumes due to rounding
// errors. e.g. Alsa rounds [-60.0..0.0] in range [0..255] to // errors. e.g. Alsa rounds [-60.0..0.0] in range [0..255] to
@ -97,7 +122,7 @@ impl Mixer for AlsaMixer {
debug!("Alsa mixer reported minimum dB as mute, trying workaround"); debug!("Alsa mixer reported minimum dB as mute, trying workaround");
min_millibel = simple_element min_millibel = simple_element
.ask_playback_vol_db(min + 1) .ask_playback_vol_db(min + 1)
.expect("Could not convert Alsa raw volume to dB volume"); .map_err(AlsaMixerError::CouldNotConvertRaw)?;
} }
(min_millibel, max_millibel) (min_millibel, max_millibel)
}; };
@ -150,7 +175,7 @@ impl Mixer for AlsaMixer {
); );
debug!("Alsa forcing linear dB mapping: {}", use_linear_in_db); debug!("Alsa forcing linear dB mapping: {}", use_linear_in_db);
Self { Ok(Self {
config, config,
min, min,
max, max,
@ -161,7 +186,7 @@ impl Mixer for AlsaMixer {
has_switch, has_switch,
is_softvol, is_softvol,
use_linear_in_db, use_linear_in_db,
} })
} }
fn volume(&self) -> u16 { fn volume(&self) -> u16 {

View file

@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::config::VolumeCtrl; use crate::config::VolumeCtrl;
use librespot_core::Error;
use std::sync::Arc;
pub mod mappings; pub mod mappings;
use self::mappings::MappedCtrl; use self::mappings::MappedCtrl;
@ -8,12 +8,12 @@ use self::mappings::MappedCtrl;
pub struct NoOpVolume; pub struct NoOpVolume;
pub trait Mixer: Send + Sync { pub trait Mixer: Send + Sync {
fn open(config: MixerConfig) -> Self fn open(config: MixerConfig) -> Result<Self, Error>
where where
Self: Sized; Self: Sized;
fn set_volume(&self, volume: u16);
fn volume(&self) -> u16; fn volume(&self) -> u16;
fn set_volume(&self, volume: u16);
fn get_soft_volume(&self) -> Box<dyn VolumeGetter + Send> { fn get_soft_volume(&self) -> Box<dyn VolumeGetter + Send> {
Box::new(NoOpVolume) Box::new(NoOpVolume)
@ -57,10 +57,10 @@ impl Default for MixerConfig {
} }
} }
pub type MixerFn = fn(MixerConfig) -> Arc<dyn Mixer>; pub type MixerFn = fn(MixerConfig) -> Result<Arc<dyn Mixer>, Error>;
fn mk_sink<M: Mixer + 'static>(config: MixerConfig) -> Arc<dyn Mixer> { fn mk_sink<M: Mixer + 'static>(config: MixerConfig) -> Result<Arc<dyn Mixer>, Error> {
Arc::new(M::open(config)) Ok(Arc::new(M::open(config)?))
} }
pub const MIXERS: &[(&str, MixerFn)] = &[ pub const MIXERS: &[(&str, MixerFn)] = &[

View file

@ -1,10 +1,10 @@
use portable_atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use super::VolumeGetter; use super::VolumeGetter;
use super::{MappedCtrl, VolumeCtrl}; use super::{MappedCtrl, VolumeCtrl};
use super::{Mixer, MixerConfig}; use super::{Mixer, MixerConfig};
use librespot_core::Error;
use portable_atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
#[derive(Clone)] #[derive(Clone)]
pub struct SoftMixer { pub struct SoftMixer {
@ -15,14 +15,14 @@ pub struct SoftMixer {
} }
impl Mixer for SoftMixer { impl Mixer for SoftMixer {
fn open(config: MixerConfig) -> Self { fn open(config: MixerConfig) -> Result<Self, Error> {
let volume_ctrl = config.volume_ctrl; let volume_ctrl = config.volume_ctrl;
info!("Mixing with softvol and volume control: {:?}", volume_ctrl); info!("Mixing with softvol and volume control: {:?}", volume_ctrl);
Self { Ok(Self {
volume: Arc::new(AtomicU64::new(f64::to_bits(0.5))), volume: Arc::new(AtomicU64::new(f64::to_bits(0.5))),
volume_ctrl, volume_ctrl,
} })
} }
fn volume(&self) -> u16 { fn volume(&self) -> u16 {

View file

@ -1,14 +1,3 @@
use std::{
env,
fs::create_dir_all,
ops::RangeInclusive,
path::{Path, PathBuf},
pin::Pin,
process::exit,
str::FromStr,
time::{Duration, Instant},
};
use data_encoding::HEXLOWER; use data_encoding::HEXLOWER;
use futures_util::StreamExt; use futures_util::StreamExt;
#[cfg(feature = "alsa-backend")] #[cfg(feature = "alsa-backend")]
@ -33,6 +22,16 @@ use librespot::{
use librespot_oauth::OAuthClientBuilder; use librespot_oauth::OAuthClientBuilder;
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
use std::{
env,
fs::create_dir_all,
ops::RangeInclusive,
path::{Path, PathBuf},
pin::Pin,
process::exit,
str::FromStr,
time::{Duration, Instant},
};
use sysinfo::{ProcessesToUpdate, System}; use sysinfo::{ProcessesToUpdate, System};
use thiserror::Error; use thiserror::Error;
use url::Url; use url::Url;
@ -1943,7 +1942,13 @@ async fn main() {
} }
let mixer_config = setup.mixer_config.clone(); let mixer_config = setup.mixer_config.clone();
let mixer = (setup.mixer)(mixer_config); let mixer = match (setup.mixer)(mixer_config) {
Ok(mixer) => mixer,
Err(why) => {
error!("{why}");
exit(1)
}
};
let player_config = setup.player_config.clone(); let player_config = setup.player_config.clone();
let soft_volume = mixer.get_soft_volume(); let soft_volume = mixer.get_soft_volume();