1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-04 02:09:26 +02:00

Improve volume controls

This is a squashed commit featuring the following:

Connect:
- Synchronize player volume with mixer volume on playback
- Fix step size on volume up/down events
- Remove no-op mixer started/stopped logic

Playback:
- Move from `connect` to `playback` crate
- Make cubic volume control available to all mixers with `--volume-ctrl cubic`
- Normalize volumes to `[0.0..1.0]` instead of `[0..65535]` for greater precision and performance (breaking)
- Add `--volume-range` option to set dB range and control `log` and `cubic` volume control curves
- Fix `log` and `cubic` volume controls to be mute at zero volume

Alsa mixer:
- Complete rewrite (breaking)
- Query card dB range for the `log` volume control unless specified otherwise
- Query dB range from Alsa softvol (previously only from hardware)
- Use `--device` name for `--mixer-card` unless specified otherwise
- Fix consistency for `cubic` between cards that report minimum volume as mute, and cards that report some dB value
- Fix `--volume-ctrl {linear|log}` to work as expected
- Removed `--mixer-linear-volume` option; use `--volume-ctrl linear` instead
This commit is contained in:
Roderick van Domburg 2021-05-24 15:53:32 +02:00
parent 68818758a2
commit eca505c387
No known key found for this signature in database
GPG key ID: FE2585E713F9F30A
10 changed files with 689 additions and 438 deletions

View file

@ -9,15 +9,16 @@ use url::Url;
use librespot::connect::spirc::Spirc;
use librespot::core::authentication::Credentials;
use librespot::core::cache::Cache;
use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig, VolumeCtrl};
use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig};
use librespot::core::session::Session;
use librespot::core::version;
use librespot::playback::audio_backend::{self, Sink, BACKENDS};
use librespot::playback::audio_backend::{self, SinkBuilder, BACKENDS};
use librespot::playback::config::{
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig,
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl,
};
use librespot::playback::mixer::{self, Mixer, MixerConfig};
use librespot::playback::player::{NormalisationData, Player};
use librespot::playback::mixer::mappings::MappedCtrl;
use librespot::playback::mixer::{self, MixerConfig, MixerFn};
use librespot::playback::player::{db_to_ratio, Player};
mod player_event_handler;
use player_event_handler::{emit_sink_event, run_program_on_events};
@ -66,7 +67,7 @@ fn setup_logging(verbose: bool) {
}
fn list_backends() {
println!("Available Backends : ");
println!("Available backends : ");
for (&(name, _), idx) in BACKENDS.iter().zip(0..) {
if idx == 0 {
println!("- {} (default)", name);
@ -172,11 +173,9 @@ fn print_version() {
#[derive(Clone)]
struct Setup {
format: AudioFormat,
backend: fn(Option<String>, AudioFormat) -> Box<dyn Sink + 'static>,
backend: SinkBuilder,
device: Option<String>,
mixer: fn(Option<MixerConfig>) -> Box<dyn Mixer>,
mixer: MixerFn,
cache: Option<Cache>,
player_config: PlayerConfig,
session_config: SessionConfig,
@ -266,11 +265,6 @@ fn get_setup(args: &[String]) -> Setup {
"Alsa mixer index, Index of the cards mixer. Defaults to 0",
"MIXER_INDEX",
)
.optflag(
"",
"mixer-linear-volume",
"Disable alsa's mapped volume scale (cubic). Default false",
)
.optopt(
"",
"initial-volume",
@ -333,8 +327,14 @@ fn get_setup(args: &[String]) -> Setup {
.optopt(
"",
"volume-ctrl",
"Volume control type - [linear, log, fixed]. Default is logarithmic",
"Volume control type - [cubic, fixed, linear, log]. Default is log.",
"VOLUME_CTRL"
)
.optopt(
"",
"volume-range",
"Range of the volume control (dB). Defaults to 60 for softvol and for alsa what the mixer supports.",
"RANGE",
)
.optflag(
"",
@ -399,18 +399,55 @@ fn get_setup(args: &[String]) -> Setup {
let mixer_name = matches.opt_str("mixer");
let mixer = mixer::find(mixer_name.as_ref()).expect("Invalid mixer");
let mixer_config = MixerConfig {
card: matches
.opt_str("mixer-card")
.unwrap_or_else(|| String::from("default")),
mixer: matches
.opt_str("mixer-name")
.unwrap_or_else(|| String::from("PCM")),
index: matches
let mixer_config = {
let card = matches.opt_str("mixer-card").unwrap_or_else(|| {
if let Some(ref device_name) = device {
device_name.to_string()
} else {
String::from("default")
}
});
let index = matches
.opt_str("mixer-index")
.map(|index| index.parse::<u32>().unwrap())
.unwrap_or(0),
mapped_volume: !matches.opt_present("mixer-linear-volume"),
.unwrap_or(0);
let control = matches
.opt_str("mixer-name")
.unwrap_or_else(|| String::from("PCM"));
let mut volume_range = matches
.opt_str("volume-range")
.map(|range| range.parse::<f32>().unwrap())
.unwrap_or_else(|| match mixer_name.as_ref().map(AsRef::as_ref) {
Some("alsa") => 0.0, // let Alsa query the control
_ => VolumeCtrl::DEFAULT_DB_RANGE,
});
if volume_range < 0.0 {
// User might have specified range as minimum dB volume.
volume_range *= -1.0;
warn!(
"Please enter positive volume ranges only, assuming {:.2} dB",
volume_range
);
}
let volume_ctrl = matches
.opt_str("volume-ctrl")
.as_ref()
.map(|volume_ctrl| {
VolumeCtrl::from_str_with_range(volume_ctrl, volume_range)
.expect("Invalid volume control type")
})
.unwrap_or_else(|| {
let mut volume_ctrl = VolumeCtrl::default();
volume_ctrl.set_db_range(volume_range);
volume_ctrl
});
MixerConfig {
card,
control,
index,
volume_ctrl,
}
};
let cache = {
@ -459,15 +496,18 @@ fn get_setup(args: &[String]) -> Setup {
let initial_volume = matches
.opt_str("initial-volume")
.map(|volume| {
let volume = volume.parse::<u16>().unwrap();
.map(|initial_volume| {
let volume = initial_volume.parse::<u16>().unwrap();
if volume > 100 {
panic!("Initial volume must be in the range 0-100");
error!("Initial volume must be in the range 0-100.");
// the cast will saturate, not necessary to take further action
}
(volume as i32 * 0xFFFF / 100) as u16
(volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16
})
.or_else(|| cache.as_ref().and_then(Cache::volume))
.unwrap_or(0x8000);
.or_else(|| match mixer_name.as_ref().map(AsRef::as_ref) {
Some("alsa") => None,
_ => cache.as_ref().and_then(Cache::volume),
});
let zeroconf_port = matches
.opt_str("zeroconf-port")
@ -506,15 +546,15 @@ fn get_setup(args: &[String]) -> Setup {
match Url::parse(&s) {
Ok(url) => {
if url.host().is_none() || url.port_or_known_default().is_none() {
panic!("Invalid proxy url, only urls on the format \"http://host:port\" are allowed");
panic!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed");
}
if url.scheme() != "http" {
panic!("Only unsecure http:// proxies are supported");
panic!("Only insecure http:// proxies are supported");
}
url
},
Err(err) => panic!("Invalid proxy url: {}, only urls on the format \"http://host:port\" are allowed", err)
Err(err) => panic!("Invalid proxy URL: {}, only URLs in the format \"http://host:port\" are allowed", err)
}
},
),
@ -524,21 +564,14 @@ fn get_setup(args: &[String]) -> Setup {
}
};
let passthrough = matches.opt_present("passthrough");
let player_config = {
let bitrate = matches
.opt_str("b")
.as_ref()
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
.unwrap_or_default();
let gain_type = matches
.opt_str("normalisation-gain-type")
.as_ref()
.map(|gain_type| {
NormalisationType::from_str(gain_type).expect("Invalid normalisation type")
})
.unwrap_or_default();
let gapless = !matches.opt_present("disable-gapless");
let normalisation = matches.opt_present("enable-volume-normalisation");
let normalisation_method = matches
.opt_str("normalisation-method")
.as_ref()
@ -546,41 +579,52 @@ fn get_setup(args: &[String]) -> Setup {
NormalisationMethod::from_str(gain_type).expect("Invalid normalisation method")
})
.unwrap_or_default();
let normalisation_type = matches
.opt_str("normalisation-gain-type")
.as_ref()
.map(|gain_type| {
NormalisationType::from_str(gain_type).expect("Invalid normalisation type")
})
.unwrap_or_default();
let normalisation_pregain = matches
.opt_str("normalisation-pregain")
.map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value"))
.unwrap_or(PlayerConfig::default().normalisation_pregain);
let normalisation_threshold = matches
.opt_str("normalisation-threshold")
.map(|threshold| {
db_to_ratio(
threshold
.parse::<f32>()
.expect("Invalid threshold float value"),
)
})
.unwrap_or(PlayerConfig::default().normalisation_threshold);
let normalisation_attack = matches
.opt_str("normalisation-attack")
.map(|attack| attack.parse::<f32>().expect("Invalid attack float value") / MILLIS)
.unwrap_or(PlayerConfig::default().normalisation_attack);
let normalisation_release = matches
.opt_str("normalisation-release")
.map(|release| release.parse::<f32>().expect("Invalid release float value") / MILLIS)
.unwrap_or(PlayerConfig::default().normalisation_release);
let normalisation_knee = matches
.opt_str("normalisation-knee")
.map(|knee| knee.parse::<f32>().expect("Invalid knee float value"))
.unwrap_or(PlayerConfig::default().normalisation_knee);
let passthrough = matches.opt_present("passthrough");
PlayerConfig {
bitrate,
gapless: !matches.opt_present("disable-gapless"),
normalisation: matches.opt_present("enable-volume-normalisation"),
gapless,
normalisation,
normalisation_type,
normalisation_method,
normalisation_type: gain_type,
normalisation_pregain: matches
.opt_str("normalisation-pregain")
.map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value"))
.unwrap_or(PlayerConfig::default().normalisation_pregain),
normalisation_threshold: matches
.opt_str("normalisation-threshold")
.map(|threshold| {
NormalisationData::db_to_ratio(
threshold
.parse::<f32>()
.expect("Invalid threshold float value"),
)
})
.unwrap_or(PlayerConfig::default().normalisation_threshold),
normalisation_attack: matches
.opt_str("normalisation-attack")
.map(|attack| attack.parse::<f32>().expect("Invalid attack float value") / MILLIS)
.unwrap_or(PlayerConfig::default().normalisation_attack),
normalisation_release: matches
.opt_str("normalisation-release")
.map(|release| {
release.parse::<f32>().expect("Invalid release float value") / MILLIS
})
.unwrap_or(PlayerConfig::default().normalisation_release),
normalisation_knee: matches
.opt_str("normalisation-knee")
.map(|knee| knee.parse::<f32>().expect("Invalid knee float value"))
.unwrap_or(PlayerConfig::default().normalisation_knee),
normalisation_pregain,
normalisation_threshold,
normalisation_attack,
normalisation_release,
normalisation_knee,
passthrough,
}
};
@ -591,39 +635,37 @@ fn get_setup(args: &[String]) -> Setup {
.as_ref()
.map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type"))
.unwrap_or_default();
let volume_ctrl = matches
.opt_str("volume-ctrl")
.as_ref()
.map(|volume_ctrl| VolumeCtrl::from_str(volume_ctrl).expect("Invalid volume ctrl type"))
.unwrap_or_default();
let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed);
let autoplay = matches.opt_present("autoplay");
ConnectConfig {
name,
device_type,
volume: initial_volume,
volume_ctrl,
autoplay: matches.opt_present("autoplay"),
initial_volume,
has_volume_ctrl,
autoplay,
}
};
let enable_discovery = !matches.opt_present("disable-discovery");
let player_event_program = matches.opt_str("onevent");
let emit_sink_events = matches.opt_present("emit-sink-events");
Setup {
format,
backend,
cache,
session_config,
player_config,
connect_config,
credentials,
device,
mixer,
cache,
player_config,
session_config,
connect_config,
mixer_config,
credentials,
enable_discovery,
zeroconf_port,
mixer,
mixer_config,
player_event_program: matches.opt_str("onevent"),
emit_sink_events: matches.opt_present("emit-sink-events"),
player_event_program,
emit_sink_events,
}
}
@ -697,7 +739,7 @@ async fn main() {
session = &mut connecting, if !connecting.is_terminated() => match session {
Ok(session) => {
let mixer_config = setup.mixer_config.clone();
let mixer = (setup.mixer)(Some(mixer_config));
let mixer = (setup.mixer)(mixer_config);
let player_config = setup.player_config.clone();
let connect_config = setup.connect_config.clone();