mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Remove the volume sample iteration (#986)
Move volume calculations out of their own separate samples iteration and into the normalisation iteration
This commit is contained in:
parent
70de5752dc
commit
7efc62b9ca
5 changed files with 126 additions and 125 deletions
|
@ -6,6 +6,7 @@ use librespot::core::session::Session;
|
||||||
use librespot::core::spotify_id::SpotifyId;
|
use librespot::core::spotify_id::SpotifyId;
|
||||||
use librespot::playback::audio_backend;
|
use librespot::playback::audio_backend;
|
||||||
use librespot::playback::config::{AudioFormat, PlayerConfig};
|
use librespot::playback::config::{AudioFormat, PlayerConfig};
|
||||||
|
use librespot::playback::mixer::NoOpVolume;
|
||||||
use librespot::playback::player::Player;
|
use librespot::playback::player::Player;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -30,7 +31,7 @@ async fn main() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (mut player, _) = Player::new(player_config, session, None, move || {
|
let (mut player, _) = Player::new(player_config, session, Box::new(NoOpVolume), move || {
|
||||||
backend(None, audio_format)
|
backend(None, audio_format)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ use crate::config::VolumeCtrl;
|
||||||
pub mod mappings;
|
pub mod mappings;
|
||||||
use self::mappings::MappedCtrl;
|
use self::mappings::MappedCtrl;
|
||||||
|
|
||||||
|
pub struct NoOpVolume;
|
||||||
|
|
||||||
pub trait Mixer: Send {
|
pub trait Mixer: Send {
|
||||||
fn open(config: MixerConfig) -> Self
|
fn open(config: MixerConfig) -> Self
|
||||||
where
|
where
|
||||||
|
@ -11,13 +13,19 @@ pub trait Mixer: Send {
|
||||||
fn set_volume(&self, volume: u16);
|
fn set_volume(&self, volume: u16);
|
||||||
fn volume(&self) -> u16;
|
fn volume(&self) -> u16;
|
||||||
|
|
||||||
fn get_audio_filter(&self) -> Option<Box<dyn AudioFilter + Send>> {
|
fn get_soft_volume(&self) -> Box<dyn VolumeGetter + Send> {
|
||||||
None
|
Box::new(NoOpVolume)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait AudioFilter {
|
pub trait VolumeGetter {
|
||||||
fn modify_stream(&self, data: &mut [f64]);
|
fn attenuation_factor(&self) -> f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VolumeGetter for NoOpVolume {
|
||||||
|
fn attenuation_factor(&self) -> f64 {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod softmixer;
|
pub mod softmixer;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::AudioFilter;
|
use super::VolumeGetter;
|
||||||
use super::{MappedCtrl, VolumeCtrl};
|
use super::{MappedCtrl, VolumeCtrl};
|
||||||
use super::{Mixer, MixerConfig};
|
use super::{Mixer, MixerConfig};
|
||||||
|
|
||||||
|
@ -35,10 +35,8 @@ impl Mixer for SoftMixer {
|
||||||
.store(mapped_volume.to_bits(), Ordering::Relaxed)
|
.store(mapped_volume.to_bits(), Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_audio_filter(&self) -> Option<Box<dyn AudioFilter + Send>> {
|
fn get_soft_volume(&self) -> Box<dyn VolumeGetter + Send> {
|
||||||
Some(Box::new(SoftVolumeApplier {
|
Box::new(SoftVolume(self.volume.clone()))
|
||||||
volume: self.volume.clone(),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,17 +44,10 @@ impl SoftMixer {
|
||||||
pub const NAME: &'static str = "softvol";
|
pub const NAME: &'static str = "softvol";
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SoftVolumeApplier {
|
struct SoftVolume(Arc<AtomicU64>);
|
||||||
volume: Arc<AtomicU64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AudioFilter for SoftVolumeApplier {
|
impl VolumeGetter for SoftVolume {
|
||||||
fn modify_stream(&self, data: &mut [f64]) {
|
fn attenuation_factor(&self) -> f64 {
|
||||||
let volume = f64::from_bits(self.volume.load(Ordering::Relaxed));
|
f64::from_bits(self.0.load(Ordering::Relaxed))
|
||||||
if volume < 1.0 {
|
|
||||||
for x in data.iter_mut() {
|
|
||||||
*x *= volume;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ use crate::core::spotify_id::SpotifyId;
|
||||||
use crate::core::util::SeqGenerator;
|
use crate::core::util::SeqGenerator;
|
||||||
use crate::decoder::{AudioDecoder, AudioPacket, DecoderError, PassthroughDecoder, VorbisDecoder};
|
use crate::decoder::{AudioDecoder, AudioPacket, DecoderError, PassthroughDecoder, VorbisDecoder};
|
||||||
use crate::metadata::{AudioItem, FileFormat};
|
use crate::metadata::{AudioItem, FileFormat};
|
||||||
use crate::mixer::AudioFilter;
|
use crate::mixer::VolumeGetter;
|
||||||
|
|
||||||
use crate::{MS_PER_PAGE, NUM_CHANNELS, PAGES_PER_MS, SAMPLES_PER_SECOND};
|
use crate::{MS_PER_PAGE, NUM_CHANNELS, PAGES_PER_MS, SAMPLES_PER_SECOND};
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ struct PlayerInternal {
|
||||||
sink: Box<dyn Sink>,
|
sink: Box<dyn Sink>,
|
||||||
sink_status: SinkStatus,
|
sink_status: SinkStatus,
|
||||||
sink_event_callback: Option<SinkEventCallback>,
|
sink_event_callback: Option<SinkEventCallback>,
|
||||||
audio_filter: Option<Box<dyn AudioFilter + Send>>,
|
volume_getter: Box<dyn VolumeGetter + Send>,
|
||||||
event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
|
event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
|
||||||
converter: Converter,
|
converter: Converter,
|
||||||
|
|
||||||
|
@ -319,7 +319,7 @@ impl Player {
|
||||||
pub fn new<F>(
|
pub fn new<F>(
|
||||||
config: PlayerConfig,
|
config: PlayerConfig,
|
||||||
session: Session,
|
session: Session,
|
||||||
audio_filter: Option<Box<dyn AudioFilter + Send>>,
|
volume_getter: Box<dyn VolumeGetter + Send>,
|
||||||
sink_builder: F,
|
sink_builder: F,
|
||||||
) -> (Player, PlayerEventChannel)
|
) -> (Player, PlayerEventChannel)
|
||||||
where
|
where
|
||||||
|
@ -369,7 +369,7 @@ impl Player {
|
||||||
sink: sink_builder(),
|
sink: sink_builder(),
|
||||||
sink_status: SinkStatus::Closed,
|
sink_status: SinkStatus::Closed,
|
||||||
sink_event_callback: None,
|
sink_event_callback: None,
|
||||||
audio_filter,
|
volume_getter,
|
||||||
event_senders: [event_sender].to_vec(),
|
event_senders: [event_sender].to_vec(),
|
||||||
converter,
|
converter,
|
||||||
|
|
||||||
|
@ -1314,109 +1314,110 @@ impl PlayerInternal {
|
||||||
Some(mut packet) => {
|
Some(mut packet) => {
|
||||||
if !packet.is_empty() {
|
if !packet.is_empty() {
|
||||||
if let AudioPacket::Samples(ref mut data) = packet {
|
if let AudioPacket::Samples(ref mut data) = packet {
|
||||||
|
// Get the volume for the packet.
|
||||||
|
// In the case of hardware volume control this will
|
||||||
|
// always be 1.0 (no change).
|
||||||
|
let volume = self.volume_getter.attenuation_factor();
|
||||||
|
|
||||||
// For the basic normalisation method, a normalisation factor of 1.0 indicates that
|
// For the basic normalisation method, a normalisation factor of 1.0 indicates that
|
||||||
// there is nothing to normalise (all samples should pass unaltered). For the
|
// there is nothing to normalise (all samples should pass unaltered). For the
|
||||||
// dynamic method, there may still be peaks that we want to shave off.
|
// dynamic method, there may still be peaks that we want to shave off.
|
||||||
if self.config.normalisation {
|
// No matter the case we apply volume attenuation last if there is any.
|
||||||
if self.config.normalisation_method == NormalisationMethod::Basic
|
if !self.config.normalisation && volume < 1.0 {
|
||||||
&& normalisation_factor < 1.0
|
for sample in data.iter_mut() {
|
||||||
{
|
*sample *= volume;
|
||||||
for sample in data.iter_mut() {
|
|
||||||
*sample *= normalisation_factor;
|
|
||||||
}
|
|
||||||
} else if self.config.normalisation_method
|
|
||||||
== NormalisationMethod::Dynamic
|
|
||||||
{
|
|
||||||
// zero-cost shorthands
|
|
||||||
let threshold_db = self.config.normalisation_threshold_dbfs;
|
|
||||||
let knee_db = self.config.normalisation_knee_db;
|
|
||||||
let attack_cf = self.config.normalisation_attack_cf;
|
|
||||||
let release_cf = self.config.normalisation_release_cf;
|
|
||||||
|
|
||||||
for sample in data.iter_mut() {
|
|
||||||
*sample *= normalisation_factor;
|
|
||||||
|
|
||||||
// Feedforward limiter in the log domain
|
|
||||||
// After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic
|
|
||||||
// Range Compressor Design—A Tutorial and Analysis. Journal of The Audio
|
|
||||||
// Engineering Society, 60, 399-408.
|
|
||||||
|
|
||||||
// Some tracks have samples that are precisely 0.0. That's silence
|
|
||||||
// and we know we don't need to limit that, in which we can spare
|
|
||||||
// the CPU cycles.
|
|
||||||
//
|
|
||||||
// Also, calling `ratio_to_db(0.0)` returns `inf` and would get the
|
|
||||||
// peak detector stuck. Also catch the unlikely case where a sample
|
|
||||||
// is decoded as `NaN` or some other non-normal value.
|
|
||||||
let limiter_db = if sample.is_normal() {
|
|
||||||
// step 1-4: half-wave rectification and conversion into dB
|
|
||||||
// and gain computer with soft knee and subtractor
|
|
||||||
let bias_db = ratio_to_db(sample.abs()) - threshold_db;
|
|
||||||
let knee_boundary_db = bias_db * 2.0;
|
|
||||||
|
|
||||||
if knee_boundary_db < -knee_db {
|
|
||||||
0.0
|
|
||||||
} else if knee_boundary_db.abs() <= knee_db {
|
|
||||||
// The textbook equation:
|
|
||||||
// ratio_to_db(sample.abs()) - (ratio_to_db(sample.abs()) - (bias_db + knee_db / 2.0).powi(2) / (2.0 * knee_db))
|
|
||||||
// Simplifies to:
|
|
||||||
// ((2.0 * bias_db) + knee_db).powi(2) / (8.0 * knee_db)
|
|
||||||
// Which in our case further simplifies to:
|
|
||||||
// (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
|
|
||||||
// because knee_boundary_db is 2.0 * bias_db.
|
|
||||||
(knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
|
|
||||||
} else {
|
|
||||||
// Textbook:
|
|
||||||
// ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db.
|
|
||||||
bias_db
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Spare the CPU unless (1) the limiter is engaged, (2) we
|
|
||||||
// were in attack or (3) we were in release, and that attack/
|
|
||||||
// release wasn't finished yet.
|
|
||||||
if limiter_db > 0.0
|
|
||||||
|| self.normalisation_integrator > 0.0
|
|
||||||
|| self.normalisation_peak > 0.0
|
|
||||||
{
|
|
||||||
// step 5: smooth, decoupled peak detector
|
|
||||||
// Textbook:
|
|
||||||
// release_cf * self.normalisation_integrator + (1.0 - release_cf) * limiter_db
|
|
||||||
// Simplifies to:
|
|
||||||
// release_cf * self.normalisation_integrator - release_cf * limiter_db + limiter_db
|
|
||||||
self.normalisation_integrator = f64::max(
|
|
||||||
limiter_db,
|
|
||||||
release_cf * self.normalisation_integrator
|
|
||||||
- release_cf * limiter_db
|
|
||||||
+ limiter_db,
|
|
||||||
);
|
|
||||||
// Textbook:
|
|
||||||
// attack_cf * self.normalisation_peak + (1.0 - attack_cf) * self.normalisation_integrator
|
|
||||||
// Simplifies to:
|
|
||||||
// attack_cf * self.normalisation_peak - attack_cf * self.normalisation_integrator + self.normalisation_integrator
|
|
||||||
self.normalisation_peak = attack_cf
|
|
||||||
* self.normalisation_peak
|
|
||||||
- attack_cf * self.normalisation_integrator
|
|
||||||
+ self.normalisation_integrator;
|
|
||||||
|
|
||||||
// step 6: make-up gain applied later (volume attenuation)
|
|
||||||
// Applying the standard normalisation factor here won't work,
|
|
||||||
// because there are tracks with peaks as high as 6 dB above
|
|
||||||
// the default threshold, so that would clip.
|
|
||||||
|
|
||||||
// steps 7-8: conversion into level and multiplication into gain stage
|
|
||||||
*sample *= db_to_ratio(-self.normalisation_peak);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
} else if self.config.normalisation_method == NormalisationMethod::Basic
|
||||||
|
&& (normalisation_factor < 1.0 || volume < 1.0)
|
||||||
|
{
|
||||||
|
for sample in data.iter_mut() {
|
||||||
|
*sample *= normalisation_factor * volume;
|
||||||
|
}
|
||||||
|
} else if self.config.normalisation_method == NormalisationMethod::Dynamic {
|
||||||
|
// zero-cost shorthands
|
||||||
|
let threshold_db = self.config.normalisation_threshold_dbfs;
|
||||||
|
let knee_db = self.config.normalisation_knee_db;
|
||||||
|
let attack_cf = self.config.normalisation_attack_cf;
|
||||||
|
let release_cf = self.config.normalisation_release_cf;
|
||||||
|
|
||||||
// Apply volume attenuation last. TODO: make this so we can chain
|
for sample in data.iter_mut() {
|
||||||
// the normaliser and mixer as a processing pipeline.
|
*sample *= normalisation_factor;
|
||||||
if let Some(ref editor) = self.audio_filter {
|
|
||||||
editor.modify_stream(data)
|
// Feedforward limiter in the log domain
|
||||||
|
// After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic
|
||||||
|
// Range Compressor Design—A Tutorial and Analysis. Journal of The Audio
|
||||||
|
// Engineering Society, 60, 399-408.
|
||||||
|
|
||||||
|
// Some tracks have samples that are precisely 0.0. That's silence
|
||||||
|
// and we know we don't need to limit that, in which we can spare
|
||||||
|
// the CPU cycles.
|
||||||
|
//
|
||||||
|
// Also, calling `ratio_to_db(0.0)` returns `inf` and would get the
|
||||||
|
// peak detector stuck. Also catch the unlikely case where a sample
|
||||||
|
// is decoded as `NaN` or some other non-normal value.
|
||||||
|
let limiter_db = if sample.is_normal() {
|
||||||
|
// step 1-4: half-wave rectification and conversion into dB
|
||||||
|
// and gain computer with soft knee and subtractor
|
||||||
|
let bias_db = ratio_to_db(sample.abs()) - threshold_db;
|
||||||
|
let knee_boundary_db = bias_db * 2.0;
|
||||||
|
|
||||||
|
if knee_boundary_db < -knee_db {
|
||||||
|
0.0
|
||||||
|
} else if knee_boundary_db.abs() <= knee_db {
|
||||||
|
// The textbook equation:
|
||||||
|
// ratio_to_db(sample.abs()) - (ratio_to_db(sample.abs()) - (bias_db + knee_db / 2.0).powi(2) / (2.0 * knee_db))
|
||||||
|
// Simplifies to:
|
||||||
|
// ((2.0 * bias_db) + knee_db).powi(2) / (8.0 * knee_db)
|
||||||
|
// Which in our case further simplifies to:
|
||||||
|
// (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
|
||||||
|
// because knee_boundary_db is 2.0 * bias_db.
|
||||||
|
(knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
|
||||||
|
} else {
|
||||||
|
// Textbook:
|
||||||
|
// ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db.
|
||||||
|
bias_db
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spare the CPU unless (1) the limiter is engaged, (2) we
|
||||||
|
// were in attack or (3) we were in release, and that attack/
|
||||||
|
// release wasn't finished yet.
|
||||||
|
if limiter_db > 0.0
|
||||||
|
|| self.normalisation_integrator > 0.0
|
||||||
|
|| self.normalisation_peak > 0.0
|
||||||
|
{
|
||||||
|
// step 5: smooth, decoupled peak detector
|
||||||
|
// Textbook:
|
||||||
|
// release_cf * self.normalisation_integrator + (1.0 - release_cf) * limiter_db
|
||||||
|
// Simplifies to:
|
||||||
|
// release_cf * self.normalisation_integrator - release_cf * limiter_db + limiter_db
|
||||||
|
self.normalisation_integrator = f64::max(
|
||||||
|
limiter_db,
|
||||||
|
release_cf * self.normalisation_integrator
|
||||||
|
- release_cf * limiter_db
|
||||||
|
+ limiter_db,
|
||||||
|
);
|
||||||
|
// Textbook:
|
||||||
|
// attack_cf * self.normalisation_peak + (1.0 - attack_cf) * self.normalisation_integrator
|
||||||
|
// Simplifies to:
|
||||||
|
// attack_cf * self.normalisation_peak - attack_cf * self.normalisation_integrator + self.normalisation_integrator
|
||||||
|
self.normalisation_peak = attack_cf * self.normalisation_peak
|
||||||
|
- attack_cf * self.normalisation_integrator
|
||||||
|
+ self.normalisation_integrator;
|
||||||
|
|
||||||
|
// step 6: make-up gain applied later (volume attenuation)
|
||||||
|
// Applying the standard normalisation factor here won't work,
|
||||||
|
// because there are tracks with peaks as high as 6 dB above
|
||||||
|
// the default threshold, so that would clip.
|
||||||
|
|
||||||
|
// steps 7-8: conversion into level and multiplication into gain stage
|
||||||
|
*sample *= db_to_ratio(-self.normalisation_peak);
|
||||||
|
}
|
||||||
|
|
||||||
|
*sample *= volume;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1648,12 +1648,12 @@ async fn main() {
|
||||||
let player_config = setup.player_config.clone();
|
let player_config = setup.player_config.clone();
|
||||||
let connect_config = setup.connect_config.clone();
|
let connect_config = setup.connect_config.clone();
|
||||||
|
|
||||||
let audio_filter = mixer.get_audio_filter();
|
let soft_volume = mixer.get_soft_volume();
|
||||||
let format = setup.format;
|
let format = setup.format;
|
||||||
let backend = setup.backend;
|
let backend = setup.backend;
|
||||||
let device = setup.device.clone();
|
let device = setup.device.clone();
|
||||||
let (player, event_channel) =
|
let (player, event_channel) =
|
||||||
Player::new(player_config, session.clone(), audio_filter, move || {
|
Player::new(player_config, session.clone(), soft_volume, move || {
|
||||||
(backend)(device, format)
|
(backend)(device, format)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue