Skip to content

Commit

Permalink
core: Add audio normalization
Browse files Browse the repository at this point in the history
Spotify gives us two normalization levels, track and album. We use the
album level when playing whole album, and track level in every other
case.
  • Loading branch information
jpochyla committed Jan 7, 2021
1 parent e0e7b1c commit 5b49b34
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 31 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ $ cargo run
- Rename playlist
- [ ] Playback queue
- [ ] Audio volume control
- [ ] Audio loudness normalization
- [x] Audio loudness normalization
- [ ] React to audio output device events
- Pause after disconnecting headphones
- Transfer playback after connecting headphones
Expand Down
11 changes: 10 additions & 1 deletion psst-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use psst_core::{
audio_normalize::NormalizationLevel,
audio_output::AudioOutput,
audio_player::{PlaybackConfig, PlaybackItem, Player, PlayerCommand, PlayerEvent},
cache::{Cache, CacheHandle},
Expand Down Expand Up @@ -34,7 +35,15 @@ fn start(session: SessionHandle) -> Result<(), Error> {
let cdn = Cdn::connect(session.clone());
let cache = Cache::new(PathBuf::from("cache"))?;
let item_id = ItemId::from_base62("6UCFZ9ZOFRxK8oak7MdPZu", ItemIdType::Track).unwrap();
play_item(session, cdn, cache, PlaybackItem { item_id })
play_item(
session,
cdn,
cache,
PlaybackItem {
item_id,
norm_level: NormalizationLevel::Track,
},
)
}

fn play_item(
Expand Down
23 changes: 9 additions & 14 deletions psst-core/src/audio_decode.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{audio_output::AudioSource, error::Error};
use crate::error::Error;
use std::io;

pub struct VorbisDecoder<R>
Expand Down Expand Up @@ -45,6 +45,14 @@ where
}
}
}

fn channels(&self) -> u8 {
self.vorbis.channels
}

fn sample_rate(&self) -> u32 {
self.vorbis.sample_rate
}
}

impl<R> Iterator for VorbisDecoder<R>
Expand Down Expand Up @@ -81,19 +89,6 @@ where
}
}

impl<R> AudioSource for VorbisDecoder<R>
where
R: io::Read + io::Seek,
{
fn channels(&self) -> u8 {
self.vorbis.channels
}

fn sample_rate(&self) -> u32 {
self.vorbis.sample_rate
}
}

impl From<minivorbis::Error> for Error {
fn from(err: minivorbis::Error) -> Error {
Error::AudioDecodingError(Box::new(err))
Expand Down
12 changes: 9 additions & 3 deletions psst-core/src/audio_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::{
audio_decode::VorbisDecoder,
audio_decrypt::AudioDecrypt,
audio_key::AudioKey,
audio_normalize::NormalizationData,
cache::CacheHandle,
cdn::{CdnHandle, CdnUrl},
error::Error,
Expand Down Expand Up @@ -91,7 +92,11 @@ impl AudioFile {
}
}

pub fn audio_source<T>(&self, key: AudioKey, on_blocking: T) -> Result<FileAudioSource, Error>
pub fn audio_source<T>(
&self,
key: AudioKey,
on_blocking: T,
) -> Result<(FileAudioSource, NormalizationData), Error>
where
T: Fn(u64) + Send + 'static,
{
Expand All @@ -100,10 +105,11 @@ impl AudioFile {
Self::Cached { cached_file, .. } => cached_file.storage.reader(on_blocking)?,
};
let reader = BufReader::new(reader);
let reader = AudioDecrypt::new(key, reader);
let mut reader = AudioDecrypt::new(key, reader);
let normalization = NormalizationData::parse(&mut reader)?;
let reader = OffsetFile::new(reader, self.header_length())?;
let reader = VorbisDecoder::new(reader)?;
Ok(reader)
Ok((reader, normalization))
}

fn header_length(&self) -> u64 {
Expand Down
55 changes: 55 additions & 0 deletions psst-core/src/audio_normalize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use byteorder::{ReadBytesExt, LE};
use std::io;
use std::io::SeekFrom;
use std::io::{Read, Seek};

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum NormalizationLevel {
None,
Track,
Album,
}

#[derive(Clone, Copy)]
pub struct NormalizationData {
track_gain_db: f32,
track_peak: f32,
album_gain_db: f32,
album_peak: f32,
}

impl NormalizationData {
pub fn parse(mut file: impl Read + Seek) -> io::Result<Self> {
const NORMALIZATION_OFFSET: u64 = 144;

file.seek(SeekFrom::Start(NORMALIZATION_OFFSET))?;

let track_gain_db = file.read_f32::<LE>()?;
let track_peak = file.read_f32::<LE>()?;
let album_gain_db = file.read_f32::<LE>()?;
let album_peak = file.read_f32::<LE>()?;

Ok(Self {
track_gain_db,
track_peak,
album_gain_db,
album_peak,
})
}

pub fn factor_for_level(&self, level: NormalizationLevel, pregain: f32) -> f32 {
match level {
NormalizationLevel::None => 1.0,
NormalizationLevel::Track => Self::factor(pregain, self.track_gain_db, self.track_peak),
NormalizationLevel::Album => Self::factor(pregain, self.album_gain_db, self.album_peak),
}
}

fn factor(pregain: f32, gain: f32, peak: f32) -> f32 {
let mut nf = f32::powf(10.0, (pregain + gain) / 20.0);
if nf * peak > 1.0 {
nf = 1.0 / peak;
}
nf
}
}
9 changes: 8 additions & 1 deletion psst-core/src/audio_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub type AudioSample = i16;
pub trait AudioSource: Iterator<Item = AudioSample> {
fn channels(&self) -> u8;
fn sample_rate(&self) -> u32;
fn normalization_factor(&self) -> Option<f32>;
}

pub struct AudioOutputRemote {
Expand Down Expand Up @@ -78,8 +79,14 @@ impl AudioOutput {

// Move the source into the config's data callback. Callback will get cloned
// for each device we create.
config.set_data_callback(move |_device, output, _frames| {
config.set_data_callback(move |device, output, _frames| {
let mut source = source.lock().expect("Failed to acquire audio source lock");
// Apply correct normalization factor before each audio packet.
if let Some(norm_factor) = source.normalization_factor() {
// TODO: Add a global master volume to the calculation.
device.set_master_volume(norm_factor);
}
// Fill the buffer with audio samples from the source.
for sample in output.as_samples_mut() {
*sample = source.next().unwrap_or(0); // Use silence in case the
// source has finished.
Expand Down
41 changes: 31 additions & 10 deletions psst-core/src/audio_player.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
audio_file::{AudioFile, AudioPath, FileAudioSource},
audio_key::AudioKey,
audio_normalize::NormalizationLevel,
audio_output::{AudioOutputRemote, AudioSample, AudioSource},
cache::CacheHandle,
cdn::CdnHandle,
Expand All @@ -24,17 +25,22 @@ const PREVIOUS_TRACK_THRESHOLD: Duration = Duration::from_secs(3);
#[derive(Clone)]
pub struct PlaybackConfig {
pub bitrate: usize,
pub pregain: f32,
}

impl Default for PlaybackConfig {
fn default() -> Self {
Self { bitrate: 320 }
Self {
bitrate: 320,
pregain: 3.0,
}
}
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct PlaybackItem {
pub item_id: ItemId,
pub norm_level: NormalizationLevel,
}

impl PlaybackItem {
Expand All @@ -48,7 +54,12 @@ impl PlaybackItem {
let path = load_audio_path(self.item_id, &session, &cache, &config)?;
let key = load_audio_key(&path, &session, &cache)?;
let file = AudioFile::open(path, cdn, cache)?;
Ok(LoadedPlaybackItem { key, file })
Ok(LoadedPlaybackItem {
key,
file,
norm_level: self.norm_level,
norm_pregain: config.pregain,
})
}
}

Expand Down Expand Up @@ -154,6 +165,8 @@ fn load_audio_key(
pub struct LoadedPlaybackItem {
key: AudioKey,
file: AudioFile,
norm_level: NormalizationLevel,
norm_pregain: f32,
}

pub struct Player {
Expand Down Expand Up @@ -609,6 +622,7 @@ const PROGRESS_PRECISION_SAMPLES: u64 = (OUTPUT_SAMPLE_RATE / 10) as u64;
struct CurrentPlaybackItem {
file: AudioFile,
source: FileAudioSource,
norm_factor: f32,
}

struct PlayerAudioSource {
Expand Down Expand Up @@ -638,15 +652,18 @@ impl PlayerAudioSource {
}

fn play_now(&mut self, item: LoadedPlaybackItem) -> Result<(), Error> {
let (source, normalization) = item.file.audio_source(item.key, {
let event_sender = self.event_sender.clone();
move |_| {
event_sender
.send(PlayerEvent::Blocked)
.expect("Failed to send PlayerEvent::Blocked");
}
})?;
let norm_factor = normalization.factor_for_level(item.norm_level, item.norm_pregain);
self.current.replace(CurrentPlaybackItem {
source: item.file.audio_source(item.key, {
let event_sender = self.event_sender.clone();
move |_| {
event_sender
.send(PlayerEvent::Blocked)
.expect("Failed to send PlayerEvent::Blocked");
}
})?,
source,
norm_factor,
file: item.file,
});
self.samples = 0;
Expand Down Expand Up @@ -694,6 +711,10 @@ impl AudioSource for PlayerAudioSource {
fn sample_rate(&self) -> u32 {
OUTPUT_SAMPLE_RATE
}

fn normalization_factor(&self) -> Option<f32> {
self.current.as_ref().map(|current| current.norm_factor)
}
}

impl Iterator for PlayerAudioSource {
Expand Down
1 change: 1 addition & 0 deletions psst-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod audio_decode;
pub mod audio_decrypt;
pub mod audio_file;
pub mod audio_key;
pub mod audio_normalize;
pub mod audio_output;
pub mod audio_player;
pub mod cache;
Expand Down
1 change: 1 addition & 0 deletions psst-gui/src/data/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ impl Config {
pub fn playback(&self) -> PlaybackConfig {
PlaybackConfig {
bitrate: self.audio_quality.as_bitrate(),
..PlaybackConfig::default()
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion psst-gui/src/delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use druid::{
};
use lru_cache::LruCache;
use psst_core::{
audio_normalize::NormalizationLevel,
audio_output::AudioOutput,
audio_player::{PlaybackConfig, PlaybackItem, Player, PlayerCommand, PlayerEvent},
cache::Cache,
Expand Down Expand Up @@ -175,7 +176,13 @@ impl PlayerDelegate {
let items = self
.player_queue
.iter()
.map(|(_origin, track)| PlaybackItem { item_id: *track.id })
.map(|(origin, track)| PlaybackItem {
item_id: *track.id,
norm_level: match origin {
PlaybackOrigin::Album(_) => NormalizationLevel::Album,
_ => NormalizationLevel::Track,
},
})
.collect();
self.player_sender
.send(PlayerEvent::Command(PlayerCommand::LoadQueue {
Expand Down

0 comments on commit 5b49b34

Please sign in to comment.