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..4c379014 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,14 @@ enum PlayerCommand { Seek(u32), } +#[derive(Debug)] +struct NormalisationConfig { + track_gain_db: f32, + track_peak: f32, + album_gain_db: f32, + album_peak: f32, +} + impl Player { pub fn new(config: PlayerConfig, session: Session, audio_filter: Option>, @@ -123,10 +132,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 +179,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 +193,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 +241,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 +275,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 +313,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 +323,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 +333,7 @@ impl PlayerInternal { self.state = PlayerState::Paused { decoder: decoder, end_of_track: end_of_track, + normalisation_factor: normalisation_factor, }; } } @@ -402,7 +426,37 @@ impl PlayerInternal { } } - fn load_track(&self, track_id: SpotifyId, position: i64) -> Option { + fn parse_normalisation(&self, file: &mut AudioDecrypt) -> NormalisationConfig { + let track_gain_db: f32; + let track_peak: f32; + let album_gain_db: f32; + let album_peak: f32; + + file.seek(SeekFrom::Start(144)).unwrap(); + track_gain_db = file.read_f32::().unwrap(); + debug!("Track gain: {}db", track_gain_db); + + file.seek(SeekFrom::Start(148)).unwrap(); + track_peak = file.read_f32::().unwrap(); + debug!("Track peak: {}", track_peak); + + file.seek(SeekFrom::Start(152)).unwrap(); + album_gain_db = file.read_f32::().unwrap(); + debug!("Album gain: {}db", album_gain_db); + + file.seek(SeekFrom::Start(156)).unwrap(); + album_peak = file.read_f32::().unwrap(); + debug!("Album peak: {}", album_peak); + + NormalisationConfig { + track_gain_db: track_gain_db, + track_peak: track_peak, + album_gain_db: album_gain_db, + album_peak: album_peak, + } + } + + 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 +486,24 @@ 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 mut normalisation_factor: f32 = 1.0; + + if self.config.normalisation { + let normalisation_config = self.parse_normalisation(&mut decrypted_file); + normalisation_factor = f32::powf(10.0, (normalisation_config.track_gain_db + self.config.normalisation_pregain) / 20.0); + + if normalisation_factor * normalisation_config.track_peak > 1.0 { + debug!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid."); + normalisation_factor = 1.0 / normalisation_config.track_peak; + } + + debug!("Applied normalization factor: {}", normalisation_factor); + } + + let audio_file = Subfile::new(decrypted_file, 0xa7); let mut decoder = VorbisDecoder::new(audio_file).unwrap(); @@ -443,7 +514,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), } };