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:
parent
68818758a2
commit
eca505c387
10 changed files with 689 additions and 438 deletions
230
src/main.rs
230
src/main.rs
|
@ -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();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue