mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Add volume normalisation support
This commit is contained in:
parent
685fb4e345
commit
f8db550e5e
6 changed files with 91 additions and 10 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -413,6 +413,7 @@ name = "librespot-playback"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alsa 0.0.1 (git+https://github.com/plietar/rust-alsa)",
|
"alsa 0.0.1 (git+https://github.com/plietar/rust-alsa)",
|
||||||
|
"byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"jack 0.5.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
"jack 0.5.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
|
|
@ -13,6 +13,7 @@ path = "../metadata"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
futures = "0.1.8"
|
futures = "0.1.8"
|
||||||
log = "0.3.5"
|
log = "0.3.5"
|
||||||
|
byteorder = "1.2.1"
|
||||||
|
|
||||||
alsa = { git = "https://github.com/plietar/rust-alsa", optional = true }
|
alsa = { git = "https://github.com/plietar/rust-alsa", optional = true }
|
||||||
portaudio-rs = { version = "0.3.0", optional = true }
|
portaudio-rs = { version = "0.3.0", optional = true }
|
||||||
|
|
|
@ -30,6 +30,8 @@ pub struct PlayerConfig {
|
||||||
pub bitrate: Bitrate,
|
pub bitrate: Bitrate,
|
||||||
pub onstart: Option<String>,
|
pub onstart: Option<String>,
|
||||||
pub onstop: Option<String>,
|
pub onstop: Option<String>,
|
||||||
|
pub normalisation: bool,
|
||||||
|
pub normalisation_pregain: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PlayerConfig {
|
impl Default for PlayerConfig {
|
||||||
|
@ -38,6 +40,8 @@ impl Default for PlayerConfig {
|
||||||
bitrate: Bitrate::default(),
|
bitrate: Bitrate::default(),
|
||||||
onstart: None,
|
onstart: None,
|
||||||
onstop: None,
|
onstop: None,
|
||||||
|
normalisation: false,
|
||||||
|
normalisation_pregain: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#[macro_use] extern crate log;
|
#[macro_use] extern crate log;
|
||||||
|
|
||||||
extern crate futures;
|
extern crate futures;
|
||||||
|
extern crate byteorder;
|
||||||
|
|
||||||
#[cfg(feature = "alsa-backend")]
|
#[cfg(feature = "alsa-backend")]
|
||||||
extern crate alsa;
|
extern crate alsa;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
use futures::sync::oneshot;
|
use futures::sync::oneshot;
|
||||||
use futures::{future, Future};
|
use futures::{future, Future};
|
||||||
use std;
|
use std;
|
||||||
|
@ -43,6 +44,51 @@ enum PlayerCommand {
|
||||||
Seek(u32),
|
Seek(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct NormalisationData {
|
||||||
|
track_gain_db: f32,
|
||||||
|
track_peak: f32,
|
||||||
|
album_gain_db: f32,
|
||||||
|
album_peak: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NormalisationData {
|
||||||
|
fn new<T: Read + Seek>(file: &mut AudioDecrypt<T>) -> NormalisationData {
|
||||||
|
file.seek(SeekFrom::Start(144)).unwrap();
|
||||||
|
|
||||||
|
let track_gain_db: f32 = file.read_f32::<LittleEndian>().unwrap();
|
||||||
|
let track_peak: f32 = file.read_f32::<LittleEndian>().unwrap();
|
||||||
|
let album_gain_db: f32 = file.read_f32::<LittleEndian>().unwrap();
|
||||||
|
let album_peak: f32 = file.read_f32::<LittleEndian>().unwrap();
|
||||||
|
|
||||||
|
NormalisationData {
|
||||||
|
track_gain_db: track_gain_db,
|
||||||
|
track_peak: track_peak,
|
||||||
|
album_gain_db: album_gain_db,
|
||||||
|
album_peak: album_peak,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f32 {
|
||||||
|
let mut normalisation_factor: f32 = 1.0;
|
||||||
|
|
||||||
|
debug!("Normalization Data: {:?}", data);
|
||||||
|
|
||||||
|
if config.normalisation {
|
||||||
|
normalisation_factor = f32::powf(10.0, (data.track_gain_db + config.normalisation_pregain) / 20.0);
|
||||||
|
|
||||||
|
if normalisation_factor * data.track_peak > 1.0 {
|
||||||
|
warn!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid.");
|
||||||
|
normalisation_factor = 1.0 / data.track_peak;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Applied normalization factor: {}", normalisation_factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalisation_factor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
pub fn new<F>(config: PlayerConfig, session: Session,
|
pub fn new<F>(config: PlayerConfig, session: Session,
|
||||||
audio_filter: Option<Box<AudioFilter + Send>>,
|
audio_filter: Option<Box<AudioFilter + Send>>,
|
||||||
|
@ -123,10 +169,12 @@ enum PlayerState {
|
||||||
Paused {
|
Paused {
|
||||||
decoder: Decoder,
|
decoder: Decoder,
|
||||||
end_of_track: oneshot::Sender<()>,
|
end_of_track: oneshot::Sender<()>,
|
||||||
|
normalisation_factor: f32,
|
||||||
},
|
},
|
||||||
Playing {
|
Playing {
|
||||||
decoder: Decoder,
|
decoder: Decoder,
|
||||||
end_of_track: oneshot::Sender<()>,
|
end_of_track: oneshot::Sender<()>,
|
||||||
|
normalisation_factor: f32,
|
||||||
},
|
},
|
||||||
|
|
||||||
Invalid,
|
Invalid,
|
||||||
|
@ -168,10 +216,11 @@ impl PlayerState {
|
||||||
fn paused_to_playing(&mut self) {
|
fn paused_to_playing(&mut self) {
|
||||||
use self::PlayerState::*;
|
use self::PlayerState::*;
|
||||||
match ::std::mem::replace(self, Invalid) {
|
match ::std::mem::replace(self, Invalid) {
|
||||||
Paused { decoder, end_of_track } => {
|
Paused { decoder, end_of_track, normalisation_factor } => {
|
||||||
*self = Playing {
|
*self = Playing {
|
||||||
decoder: decoder,
|
decoder: decoder,
|
||||||
end_of_track: end_of_track,
|
end_of_track: end_of_track,
|
||||||
|
normalisation_factor: normalisation_factor,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_ => panic!("invalid state"),
|
_ => panic!("invalid state"),
|
||||||
|
@ -181,10 +230,11 @@ impl PlayerState {
|
||||||
fn playing_to_paused(&mut self) {
|
fn playing_to_paused(&mut self) {
|
||||||
use self::PlayerState::*;
|
use self::PlayerState::*;
|
||||||
match ::std::mem::replace(self, Invalid) {
|
match ::std::mem::replace(self, Invalid) {
|
||||||
Playing { decoder, end_of_track } => {
|
Playing { decoder, end_of_track, normalisation_factor } => {
|
||||||
*self = Paused {
|
*self = Paused {
|
||||||
decoder: decoder,
|
decoder: decoder,
|
||||||
end_of_track: end_of_track,
|
end_of_track: end_of_track,
|
||||||
|
normalisation_factor: normalisation_factor,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_ => panic!("invalid state"),
|
_ => panic!("invalid state"),
|
||||||
|
@ -228,14 +278,17 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.sink_running {
|
if self.sink_running {
|
||||||
let packet = if let PlayerState::Playing { ref mut decoder, .. } = self.state {
|
let mut current_normalisation_factor: f32 = 1.0;
|
||||||
|
|
||||||
|
let packet = if let PlayerState::Playing { ref mut decoder, normalisation_factor, .. } = self.state {
|
||||||
|
current_normalisation_factor = normalisation_factor;
|
||||||
Some(decoder.next_packet().expect("Vorbis error"))
|
Some(decoder.next_packet().expect("Vorbis error"))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(packet) = packet {
|
if let Some(packet) = packet {
|
||||||
self.handle_packet(packet);
|
self.handle_packet(packet, current_normalisation_factor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,13 +312,19 @@ impl PlayerInternal {
|
||||||
self.sink_running = false;
|
self.sink_running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_packet(&mut self, packet: Option<VorbisPacket>) {
|
fn handle_packet(&mut self, packet: Option<VorbisPacket>, normalisation_factor: f32) {
|
||||||
match packet {
|
match packet {
|
||||||
Some(mut packet) => {
|
Some(mut packet) => {
|
||||||
if let Some(ref editor) = self.audio_filter {
|
if let Some(ref editor) = self.audio_filter {
|
||||||
editor.modify_stream(&mut packet.data_mut())
|
editor.modify_stream(&mut packet.data_mut())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if self.config.normalisation && normalisation_factor != 1.0 {
|
||||||
|
for x in packet.data_mut().iter_mut() {
|
||||||
|
*x = (*x as f32 * normalisation_factor) as i16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(err) = self.sink.write(&packet.data()) {
|
if let Err(err) = self.sink.write(&packet.data()) {
|
||||||
error!("Could not write audio: {}", err);
|
error!("Could not write audio: {}", err);
|
||||||
self.stop_sink();
|
self.stop_sink();
|
||||||
|
@ -291,7 +350,7 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.load_track(track_id, position as i64) {
|
match self.load_track(track_id, position as i64) {
|
||||||
Some(decoder) => {
|
Some((decoder, normalisation_factor)) => {
|
||||||
if play {
|
if play {
|
||||||
if !self.state.is_playing() {
|
if !self.state.is_playing() {
|
||||||
self.run_onstart();
|
self.run_onstart();
|
||||||
|
@ -301,6 +360,7 @@ impl PlayerInternal {
|
||||||
self.state = PlayerState::Playing {
|
self.state = PlayerState::Playing {
|
||||||
decoder: decoder,
|
decoder: decoder,
|
||||||
end_of_track: end_of_track,
|
end_of_track: end_of_track,
|
||||||
|
normalisation_factor: normalisation_factor,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
if self.state.is_playing() {
|
if self.state.is_playing() {
|
||||||
|
@ -310,6 +370,7 @@ impl PlayerInternal {
|
||||||
self.state = PlayerState::Paused {
|
self.state = PlayerState::Paused {
|
||||||
decoder: decoder,
|
decoder: decoder,
|
||||||
end_of_track: end_of_track,
|
end_of_track: end_of_track,
|
||||||
|
normalisation_factor: normalisation_factor,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -402,7 +463,7 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_track(&self, track_id: SpotifyId, position: i64) -> Option<Decoder> {
|
fn load_track(&self, track_id: SpotifyId, position: i64) -> Option<(Decoder, f32)> {
|
||||||
let track = Track::get(&self.session, track_id).wait().unwrap();
|
let track = Track::get(&self.session, track_id).wait().unwrap();
|
||||||
|
|
||||||
info!("Loading track \"{}\"", track.name);
|
info!("Loading track \"{}\"", track.name);
|
||||||
|
@ -432,7 +493,14 @@ impl PlayerInternal {
|
||||||
let key = self.session.audio_key().request(track.id, file_id).wait().unwrap();
|
let key = self.session.audio_key().request(track.id, file_id).wait().unwrap();
|
||||||
|
|
||||||
let encrypted_file = AudioFile::open(&self.session, file_id).wait().unwrap();
|
let encrypted_file = AudioFile::open(&self.session, file_id).wait().unwrap();
|
||||||
let audio_file = Subfile::new(AudioDecrypt::new(key, encrypted_file), 0xa7);
|
|
||||||
|
let mut decrypted_file = AudioDecrypt::new(key, encrypted_file);
|
||||||
|
|
||||||
|
let normalisation_data = NormalisationData::new(&mut decrypted_file);
|
||||||
|
|
||||||
|
let normalisation_factor: f32 = NormalisationData::get_factor(&self.config, normalisation_data);
|
||||||
|
|
||||||
|
let audio_file = Subfile::new(decrypted_file, 0xa7);
|
||||||
|
|
||||||
let mut decoder = VorbisDecoder::new(audio_file).unwrap();
|
let mut decoder = VorbisDecoder::new(audio_file).unwrap();
|
||||||
|
|
||||||
|
@ -443,7 +511,7 @@ impl PlayerInternal {
|
||||||
|
|
||||||
info!("Track \"{}\" loaded", track.name);
|
info!("Track \"{}\" loaded", track.name);
|
||||||
|
|
||||||
Some(decoder)
|
Some((decoder, normalisation_factor))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -102,7 +102,9 @@ fn setup(args: &[String]) -> Setup {
|
||||||
.optopt("", "device", "Audio device to use. Use '?' to list options if using portaudio", "DEVICE")
|
.optopt("", "device", "Audio device to use. Use '?' to list options if using portaudio", "DEVICE")
|
||||||
.optopt("", "mixer", "Mixer to use", "MIXER")
|
.optopt("", "mixer", "Mixer to use", "MIXER")
|
||||||
.optopt("", "initial-volume", "Initial volume in %, once connected (must be from 0 to 100)", "VOLUME")
|
.optopt("", "initial-volume", "Initial volume in %, once connected (must be from 0 to 100)", "VOLUME")
|
||||||
.optopt("", "zeroconf-port", "The port the internal server advertised over zeroconf uses.", "ZEROCONF_PORT");
|
.optopt("", "zeroconf-port", "The port the internal server advertised over zeroconf uses.", "ZEROCONF_PORT")
|
||||||
|
.optflag("", "enable-volume-normalisation", "Play all tracks at the same volume")
|
||||||
|
.optopt("", "normalisation-pregain", "Pregain (dB) applied by volume normalisation", "PREGAIN");
|
||||||
|
|
||||||
let matches = match opts.parse(&args[1..]) {
|
let matches = match opts.parse(&args[1..]) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
|
@ -187,6 +189,10 @@ fn setup(args: &[String]) -> Setup {
|
||||||
bitrate: bitrate,
|
bitrate: bitrate,
|
||||||
onstart: matches.opt_str("onstart"),
|
onstart: matches.opt_str("onstart"),
|
||||||
onstop: matches.opt_str("onstop"),
|
onstop: matches.opt_str("onstop"),
|
||||||
|
normalisation: matches.opt_present("enable-volume-normalisation"),
|
||||||
|
normalisation_pregain: matches.opt_str("normalisation-pregain")
|
||||||
|
.map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value"))
|
||||||
|
.unwrap_or(PlayerConfig::default().normalisation_pregain),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue