From f8db550e5ee98b67f54c406b7800d82e1dc17fcb Mon Sep 17 00:00:00 2001 From: Sasha Hilton Date: Fri, 23 Feb 2018 20:08:20 +0100 Subject: [PATCH 1/4] Add volume normalisation support --- Cargo.lock | 1 + playback/Cargo.toml | 1 + playback/src/config.rs | 4 ++ playback/src/lib.rs | 1 + playback/src/player.rs | 86 +++++++++++++++++++++++++++++++++++++----- src/main.rs | 8 +++- 6 files changed, 91 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d5f96a7b..a325f759 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,6 +413,7 @@ name = "librespot-playback" version = "0.1.0" dependencies = [ "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)", "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)", diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 96c44bec..653cbe6c 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -13,6 +13,7 @@ path = "../metadata" [dependencies] futures = "0.1.8" log = "0.3.5" +byteorder = "1.2.1" alsa = { git = "https://github.com/plietar/rust-alsa", optional = true } portaudio-rs = { version = "0.3.0", optional = true } diff --git a/playback/src/config.rs b/playback/src/config.rs index d44e937a..bfd97392 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -30,6 +30,8 @@ pub struct PlayerConfig { pub bitrate: Bitrate, pub onstart: Option, pub onstop: Option, + pub normalisation: bool, + pub normalisation_pregain: f32, } impl Default for PlayerConfig { @@ -38,6 +40,8 @@ impl Default for PlayerConfig { bitrate: Bitrate::default(), onstart: None, onstop: None, + normalisation: false, + normalisation_pregain: 0.0, } } } diff --git a/playback/src/lib.rs b/playback/src/lib.rs index 014c7814..3effd43a 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -1,6 +1,7 @@ #[macro_use] extern crate log; extern crate futures; +extern crate byteorder; #[cfg(feature = "alsa-backend")] extern crate alsa; diff --git a/playback/src/player.rs b/playback/src/player.rs index e5497365..855ec93a 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1,3 +1,4 @@ +use byteorder::{LittleEndian, ReadBytesExt}; use futures::sync::oneshot; use futures::{future, Future}; use std; @@ -43,6 +44,51 @@ enum PlayerCommand { 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(file: &mut AudioDecrypt) -> NormalisationData { + file.seek(SeekFrom::Start(144)).unwrap(); + + let track_gain_db: f32 = file.read_f32::().unwrap(); + let track_peak: f32 = file.read_f32::().unwrap(); + let album_gain_db: f32 = file.read_f32::().unwrap(); + let album_peak: f32 = file.read_f32::().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 { pub fn new(config: PlayerConfig, session: Session, audio_filter: Option>, @@ -123,10 +169,12 @@ enum PlayerState { Paused { decoder: Decoder, end_of_track: oneshot::Sender<()>, + normalisation_factor: f32, }, Playing { decoder: Decoder, end_of_track: oneshot::Sender<()>, + normalisation_factor: f32, }, Invalid, @@ -168,10 +216,11 @@ impl PlayerState { fn paused_to_playing(&mut self) { use self::PlayerState::*; match ::std::mem::replace(self, Invalid) { - Paused { decoder, end_of_track } => { + Paused { decoder, end_of_track, normalisation_factor } => { *self = Playing { decoder: decoder, end_of_track: end_of_track, + normalisation_factor: normalisation_factor, }; } _ => panic!("invalid state"), @@ -181,10 +230,11 @@ impl PlayerState { fn playing_to_paused(&mut self) { use self::PlayerState::*; match ::std::mem::replace(self, Invalid) { - Playing { decoder, end_of_track } => { + Playing { decoder, end_of_track, normalisation_factor } => { *self = Paused { decoder: decoder, end_of_track: end_of_track, + normalisation_factor: normalisation_factor, }; } _ => panic!("invalid state"), @@ -228,14 +278,17 @@ impl PlayerInternal { } 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")) } else { None }; 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; } - fn handle_packet(&mut self, packet: Option) { + fn handle_packet(&mut self, packet: Option, normalisation_factor: f32) { match packet { Some(mut packet) => { if let Some(ref editor) = self.audio_filter { 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()) { error!("Could not write audio: {}", err); self.stop_sink(); @@ -291,7 +350,7 @@ impl PlayerInternal { } match self.load_track(track_id, position as i64) { - Some(decoder) => { + Some((decoder, normalisation_factor)) => { if play { if !self.state.is_playing() { self.run_onstart(); @@ -301,6 +360,7 @@ impl PlayerInternal { self.state = PlayerState::Playing { decoder: decoder, end_of_track: end_of_track, + normalisation_factor: normalisation_factor, }; } else { if self.state.is_playing() { @@ -310,6 +370,7 @@ impl PlayerInternal { self.state = PlayerState::Paused { decoder: decoder, 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 { + fn load_track(&self, track_id: SpotifyId, position: i64) -> Option<(Decoder, f32)> { let track = Track::get(&self.session, track_id).wait().unwrap(); 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 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(); @@ -443,7 +511,7 @@ impl PlayerInternal { info!("Track \"{}\" loaded", track.name); - Some(decoder) + Some((decoder, normalisation_factor)) } } diff --git a/src/main.rs b/src/main.rs index 01c34421..fa40bd04 100644 --- a/src/main.rs +++ b/src/main.rs @@ -102,7 +102,9 @@ fn setup(args: &[String]) -> Setup { .optopt("", "device", "Audio device to use. Use '?' to list options if using portaudio", "DEVICE") .optopt("", "mixer", "Mixer to use", "MIXER") .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..]) { Ok(m) => m, @@ -187,6 +189,10 @@ fn setup(args: &[String]) -> Setup { bitrate: bitrate, onstart: matches.opt_str("onstart"), onstop: matches.opt_str("onstop"), + normalisation: matches.opt_present("enable-volume-normalisation"), + normalisation_pregain: matches.opt_str("normalisation-pregain") + .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) + .unwrap_or(PlayerConfig::default().normalisation_pregain), } }; From fc6c414e71940692d7d0483add230ee80dd2cd55 Mon Sep 17 00:00:00 2001 From: Sasha Hilton Date: Fri, 23 Feb 2018 20:33:58 +0100 Subject: [PATCH 2/4] [ci skip] Correct spelling --- playback/src/player.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index 855ec93a..ac985d0b 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -72,7 +72,7 @@ impl NormalisationData { fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f32 { let mut normalisation_factor: f32 = 1.0; - debug!("Normalization Data: {:?}", data); + debug!("Normalisation Data: {:?}", data); if config.normalisation { normalisation_factor = f32::powf(10.0, (data.track_gain_db + config.normalisation_pregain) / 20.0); @@ -82,7 +82,7 @@ impl NormalisationData { normalisation_factor = 1.0 / data.track_peak; } - debug!("Applied normalization factor: {}", normalisation_factor); + debug!("Applied normalisation factor: {}", normalisation_factor); } normalisation_factor From 127f8b7bab39e089223cd112a02460eb213a9364 Mon Sep 17 00:00:00 2001 From: Sasha Hilton Date: Fri, 23 Feb 2018 20:52:28 +0100 Subject: [PATCH 3/4] Add constant for readability --- playback/src/player.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index ac985d0b..6e510373 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -54,7 +54,8 @@ struct NormalisationData { impl NormalisationData { fn new(file: &mut AudioDecrypt) -> NormalisationData { - file.seek(SeekFrom::Start(144)).unwrap(); + static SPOTIFY_HEADER_START_OFFSET: u64 = 144; + file.seek(SeekFrom::Start(SPOTIFY_HEADER_START_OFFSET)).unwrap(); let track_gain_db: f32 = file.read_f32::().unwrap(); let track_peak: f32 = file.read_f32::().unwrap(); From 542ec9d3b5e84e00ff71421897508fd29979ea68 Mon Sep 17 00:00:00 2001 From: Sasha Hilton Date: Sat, 24 Feb 2018 16:30:24 +0100 Subject: [PATCH 4/4] Minor style changes to normalisation code --- playback/src/player.rs | 49 +++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index 6e510373..62af77ec 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -53,38 +53,35 @@ struct NormalisationData { } impl NormalisationData { - fn new(file: &mut AudioDecrypt) -> NormalisationData { - static SPOTIFY_HEADER_START_OFFSET: u64 = 144; - file.seek(SeekFrom::Start(SPOTIFY_HEADER_START_OFFSET)).unwrap(); + fn parse_from_file(mut file: T) -> Result { + const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; + file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET)).unwrap(); - let track_gain_db: f32 = file.read_f32::().unwrap(); - let track_peak: f32 = file.read_f32::().unwrap(); - let album_gain_db: f32 = file.read_f32::().unwrap(); - let album_peak: f32 = file.read_f32::().unwrap(); + let track_gain_db = file.read_f32::().unwrap(); + let track_peak = file.read_f32::().unwrap(); + let album_gain_db = file.read_f32::().unwrap(); + let album_peak = file.read_f32::().unwrap(); - NormalisationData { + let r = NormalisationData { track_gain_db: track_gain_db, track_peak: track_peak, album_gain_db: album_gain_db, album_peak: album_peak, - } + }; + + Ok(r) } fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f32 { - let mut normalisation_factor: f32 = 1.0; + let mut 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!("Normalisation 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 normalisation factor: {}", normalisation_factor); - } + debug!("Applied normalisation factor: {}", normalisation_factor); normalisation_factor } @@ -497,9 +494,13 @@ impl PlayerInternal { 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 normalisation_factor = match NormalisationData::parse_from_file(&mut decrypted_file) { + Ok(normalisation_data) => NormalisationData::get_factor(&self.config, normalisation_data), + Err(_) => { + warn!("Unable to extract normalisation data, using default value."); + 1.0 as f32 + }, + }; let audio_file = Subfile::new(decrypted_file, 0xa7);