Add volume normalisation support

This commit is contained in:
Sasha Hilton 2018-02-23 20:08:20 +01:00
parent 685fb4e345
commit f8db550e5e
6 changed files with 91 additions and 10 deletions

1
Cargo.lock generated
View file

@ -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)",

View file

@ -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 }

View file

@ -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,
} }
} }
} }

View file

@ -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;

View file

@ -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))
} }
} }

View file

@ -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),
} }
}; };