Skip to content

Commit 7efc62b

Browse files
authored
Remove the volume sample iteration (#986)
Move volume calculations out of their own separate samples iteration and into the normalisation iteration
1 parent 70de575 commit 7efc62b

5 files changed

Lines changed: 122 additions & 121 deletions

File tree

examples/play.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use librespot::core::session::Session;
66
use librespot::core::spotify_id::SpotifyId;
77
use librespot::playback::audio_backend;
88
use librespot::playback::config::{AudioFormat, PlayerConfig};
9+
use librespot::playback::mixer::NoOpVolume;
910
use librespot::playback::player::Player;
1011

1112
#[tokio::main]
@@ -30,7 +31,7 @@ async fn main() {
3031
.await
3132
.unwrap();
3233

33-
let (mut player, _) = Player::new(player_config, session, None, move || {
34+
let (mut player, _) = Player::new(player_config, session, Box::new(NoOpVolume), move || {
3435
backend(None, audio_format)
3536
});
3637

playback/src/mixer/mod.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use crate::config::VolumeCtrl;
33
pub mod mappings;
44
use self::mappings::MappedCtrl;
55

6+
pub struct NoOpVolume;
7+
68
pub trait Mixer: Send {
79
fn open(config: MixerConfig) -> Self
810
where
@@ -11,13 +13,19 @@ pub trait Mixer: Send {
1113
fn set_volume(&self, volume: u16);
1214
fn volume(&self) -> u16;
1315

14-
fn get_audio_filter(&self) -> Option<Box<dyn AudioFilter + Send>> {
15-
None
16+
fn get_soft_volume(&self) -> Box<dyn VolumeGetter + Send> {
17+
Box::new(NoOpVolume)
1618
}
1719
}
1820

19-
pub trait AudioFilter {
20-
fn modify_stream(&self, data: &mut [f64]);
21+
pub trait VolumeGetter {
22+
fn attenuation_factor(&self) -> f64;
23+
}
24+
25+
impl VolumeGetter for NoOpVolume {
26+
fn attenuation_factor(&self) -> f64 {
27+
1.0
28+
}
2129
}
2230

2331
pub mod softmixer;

playback/src/mixer/softmixer.rs

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::sync::atomic::{AtomicU64, Ordering};
22
use std::sync::Arc;
33

4-
use super::AudioFilter;
4+
use super::VolumeGetter;
55
use super::{MappedCtrl, VolumeCtrl};
66
use super::{Mixer, MixerConfig};
77

@@ -35,28 +35,19 @@ impl Mixer for SoftMixer {
3535
.store(mapped_volume.to_bits(), Ordering::Relaxed)
3636
}
3737

38-
fn get_audio_filter(&self) -> Option<Box<dyn AudioFilter + Send>> {
39-
Some(Box::new(SoftVolumeApplier {
40-
volume: self.volume.clone(),
41-
}))
38+
fn get_soft_volume(&self) -> Box<dyn VolumeGetter + Send> {
39+
Box::new(SoftVolume(self.volume.clone()))
4240
}
4341
}
4442

4543
impl SoftMixer {
4644
pub const NAME: &'static str = "softvol";
4745
}
4846

49-
struct SoftVolumeApplier {
50-
volume: Arc<AtomicU64>,
51-
}
47+
struct SoftVolume(Arc<AtomicU64>);
5248

53-
impl AudioFilter for SoftVolumeApplier {
54-
fn modify_stream(&self, data: &mut [f64]) {
55-
let volume = f64::from_bits(self.volume.load(Ordering::Relaxed));
56-
if volume < 1.0 {
57-
for x in data.iter_mut() {
58-
*x *= volume;
59-
}
60-
}
49+
impl VolumeGetter for SoftVolume {
50+
fn attenuation_factor(&self) -> f64 {
51+
f64::from_bits(self.0.load(Ordering::Relaxed))
6152
}
6253
}

playback/src/player.rs

Lines changed: 99 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::core::spotify_id::SpotifyId;
2525
use crate::core::util::SeqGenerator;
2626
use crate::decoder::{AudioDecoder, AudioPacket, DecoderError, PassthroughDecoder, VorbisDecoder};
2727
use crate::metadata::{AudioItem, FileFormat};
28-
use crate::mixer::AudioFilter;
28+
use crate::mixer::VolumeGetter;
2929

3030
use crate::{MS_PER_PAGE, NUM_CHANNELS, PAGES_PER_MS, SAMPLES_PER_SECOND};
3131

@@ -58,7 +58,7 @@ struct PlayerInternal {
5858
sink: Box<dyn Sink>,
5959
sink_status: SinkStatus,
6060
sink_event_callback: Option<SinkEventCallback>,
61-
audio_filter: Option<Box<dyn AudioFilter + Send>>,
61+
volume_getter: Box<dyn VolumeGetter + Send>,
6262
event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
6363
converter: Converter,
6464

@@ -319,7 +319,7 @@ impl Player {
319319
pub fn new<F>(
320320
config: PlayerConfig,
321321
session: Session,
322-
audio_filter: Option<Box<dyn AudioFilter + Send>>,
322+
volume_getter: Box<dyn VolumeGetter + Send>,
323323
sink_builder: F,
324324
) -> (Player, PlayerEventChannel)
325325
where
@@ -369,7 +369,7 @@ impl Player {
369369
sink: sink_builder(),
370370
sink_status: SinkStatus::Closed,
371371
sink_event_callback: None,
372-
audio_filter,
372+
volume_getter,
373373
event_senders: [event_sender].to_vec(),
374374
converter,
375375

@@ -1314,109 +1314,110 @@ impl PlayerInternal {
13141314
Some(mut packet) => {
13151315
if !packet.is_empty() {
13161316
if let AudioPacket::Samples(ref mut data) = packet {
1317+
// Get the volume for the packet.
1318+
// In the case of hardware volume control this will
1319+
// always be 1.0 (no change).
1320+
let volume = self.volume_getter.attenuation_factor();
1321+
13171322
// For the basic normalisation method, a normalisation factor of 1.0 indicates that
13181323
// there is nothing to normalise (all samples should pass unaltered). For the
13191324
// dynamic method, there may still be peaks that we want to shave off.
1320-
if self.config.normalisation {
1321-
if self.config.normalisation_method == NormalisationMethod::Basic
1322-
&& normalisation_factor < 1.0
1323-
{
1324-
for sample in data.iter_mut() {
1325-
*sample *= normalisation_factor;
1326-
}
1327-
} else if self.config.normalisation_method
1328-
== NormalisationMethod::Dynamic
1329-
{
1330-
// zero-cost shorthands
1331-
let threshold_db = self.config.normalisation_threshold_dbfs;
1332-
let knee_db = self.config.normalisation_knee_db;
1333-
let attack_cf = self.config.normalisation_attack_cf;
1334-
let release_cf = self.config.normalisation_release_cf;
1335-
1336-
for sample in data.iter_mut() {
1337-
*sample *= normalisation_factor;
1338-
1339-
// Feedforward limiter in the log domain
1340-
// After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic
1341-
// Range Compressor Design—A Tutorial and Analysis. Journal of The Audio
1342-
// Engineering Society, 60, 399-408.
1343-
1344-
// Some tracks have samples that are precisely 0.0. That's silence
1345-
// and we know we don't need to limit that, in which we can spare
1346-
// the CPU cycles.
1347-
//
1348-
// Also, calling `ratio_to_db(0.0)` returns `inf` and would get the
1349-
// peak detector stuck. Also catch the unlikely case where a sample
1350-
// is decoded as `NaN` or some other non-normal value.
1351-
let limiter_db = if sample.is_normal() {
1352-
// step 1-4: half-wave rectification and conversion into dB
1353-
// and gain computer with soft knee and subtractor
1354-
let bias_db = ratio_to_db(sample.abs()) - threshold_db;
1355-
let knee_boundary_db = bias_db * 2.0;
1356-
1357-
if knee_boundary_db < -knee_db {
1358-
0.0
1359-
} else if knee_boundary_db.abs() <= knee_db {
1360-
// The textbook equation:
1361-
// ratio_to_db(sample.abs()) - (ratio_to_db(sample.abs()) - (bias_db + knee_db / 2.0).powi(2) / (2.0 * knee_db))
1362-
// Simplifies to:
1363-
// ((2.0 * bias_db) + knee_db).powi(2) / (8.0 * knee_db)
1364-
// Which in our case further simplifies to:
1365-
// (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
1366-
// because knee_boundary_db is 2.0 * bias_db.
1367-
(knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
1368-
} else {
1369-
// Textbook:
1370-
// ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db.
1371-
bias_db
1372-
}
1373-
} else {
1325+
// No matter the case we apply volume attenuation last if there is any.
1326+
if !self.config.normalisation && volume < 1.0 {
1327+
for sample in data.iter_mut() {
1328+
*sample *= volume;
1329+
}
1330+
} else if self.config.normalisation_method == NormalisationMethod::Basic
1331+
&& (normalisation_factor < 1.0 || volume < 1.0)
1332+
{
1333+
for sample in data.iter_mut() {
1334+
*sample *= normalisation_factor * volume;
1335+
}
1336+
} else if self.config.normalisation_method == NormalisationMethod::Dynamic {
1337+
// zero-cost shorthands
1338+
let threshold_db = self.config.normalisation_threshold_dbfs;
1339+
let knee_db = self.config.normalisation_knee_db;
1340+
let attack_cf = self.config.normalisation_attack_cf;
1341+
let release_cf = self.config.normalisation_release_cf;
1342+
1343+
for sample in data.iter_mut() {
1344+
*sample *= normalisation_factor;
1345+
1346+
// Feedforward limiter in the log domain
1347+
// After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic
1348+
// Range Compressor Design—A Tutorial and Analysis. Journal of The Audio
1349+
// Engineering Society, 60, 399-408.
1350+
1351+
// Some tracks have samples that are precisely 0.0. That's silence
1352+
// and we know we don't need to limit that, in which we can spare
1353+
// the CPU cycles.
1354+
//
1355+
// Also, calling `ratio_to_db(0.0)` returns `inf` and would get the
1356+
// peak detector stuck. Also catch the unlikely case where a sample
1357+
// is decoded as `NaN` or some other non-normal value.
1358+
let limiter_db = if sample.is_normal() {
1359+
// step 1-4: half-wave rectification and conversion into dB
1360+
// and gain computer with soft knee and subtractor
1361+
let bias_db = ratio_to_db(sample.abs()) - threshold_db;
1362+
let knee_boundary_db = bias_db * 2.0;
1363+
1364+
if knee_boundary_db < -knee_db {
13741365
0.0
1375-
};
1376-
1377-
// Spare the CPU unless (1) the limiter is engaged, (2) we
1378-
// were in attack or (3) we were in release, and that attack/
1379-
// release wasn't finished yet.
1380-
if limiter_db > 0.0
1381-
|| self.normalisation_integrator > 0.0
1382-
|| self.normalisation_peak > 0.0
1383-
{
1384-
// step 5: smooth, decoupled peak detector
1385-
// Textbook:
1386-
// release_cf * self.normalisation_integrator + (1.0 - release_cf) * limiter_db
1366+
} else if knee_boundary_db.abs() <= knee_db {
1367+
// The textbook equation:
1368+
// ratio_to_db(sample.abs()) - (ratio_to_db(sample.abs()) - (bias_db + knee_db / 2.0).powi(2) / (2.0 * knee_db))
13871369
// Simplifies to:
1388-
// release_cf * self.normalisation_integrator - release_cf * limiter_db + limiter_db
1389-
self.normalisation_integrator = f64::max(
1390-
limiter_db,
1391-
release_cf * self.normalisation_integrator
1392-
- release_cf * limiter_db
1393-
+ limiter_db,
1394-
);
1370+
// ((2.0 * bias_db) + knee_db).powi(2) / (8.0 * knee_db)
1371+
// Which in our case further simplifies to:
1372+
// (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
1373+
// because knee_boundary_db is 2.0 * bias_db.
1374+
(knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
1375+
} else {
13951376
// Textbook:
1396-
// attack_cf * self.normalisation_peak + (1.0 - attack_cf) * self.normalisation_integrator
1397-
// Simplifies to:
1398-
// attack_cf * self.normalisation_peak - attack_cf * self.normalisation_integrator + self.normalisation_integrator
1399-
self.normalisation_peak = attack_cf
1400-
* self.normalisation_peak
1401-
- attack_cf * self.normalisation_integrator
1402-
+ self.normalisation_integrator;
1403-
1404-
// step 6: make-up gain applied later (volume attenuation)
1405-
// Applying the standard normalisation factor here won't work,
1406-
// because there are tracks with peaks as high as 6 dB above
1407-
// the default threshold, so that would clip.
1408-
1409-
// steps 7-8: conversion into level and multiplication into gain stage
1410-
*sample *= db_to_ratio(-self.normalisation_peak);
1377+
// ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db.
1378+
bias_db
14111379
}
1380+
} else {
1381+
0.0
1382+
};
1383+
1384+
// Spare the CPU unless (1) the limiter is engaged, (2) we
1385+
// were in attack or (3) we were in release, and that attack/
1386+
// release wasn't finished yet.
1387+
if limiter_db > 0.0
1388+
|| self.normalisation_integrator > 0.0
1389+
|| self.normalisation_peak > 0.0
1390+
{
1391+
// step 5: smooth, decoupled peak detector
1392+
// Textbook:
1393+
// release_cf * self.normalisation_integrator + (1.0 - release_cf) * limiter_db
1394+
// Simplifies to:
1395+
// release_cf * self.normalisation_integrator - release_cf * limiter_db + limiter_db
1396+
self.normalisation_integrator = f64::max(
1397+
limiter_db,
1398+
release_cf * self.normalisation_integrator
1399+
- release_cf * limiter_db
1400+
+ limiter_db,
1401+
);
1402+
// Textbook:
1403+
// attack_cf * self.normalisation_peak + (1.0 - attack_cf) * self.normalisation_integrator
1404+
// Simplifies to:
1405+
// attack_cf * self.normalisation_peak - attack_cf * self.normalisation_integrator + self.normalisation_integrator
1406+
self.normalisation_peak = attack_cf * self.normalisation_peak
1407+
- attack_cf * self.normalisation_integrator
1408+
+ self.normalisation_integrator;
1409+
1410+
// step 6: make-up gain applied later (volume attenuation)
1411+
// Applying the standard normalisation factor here won't work,
1412+
// because there are tracks with peaks as high as 6 dB above
1413+
// the default threshold, so that would clip.
1414+
1415+
// steps 7-8: conversion into level and multiplication into gain stage
1416+
*sample *= db_to_ratio(-self.normalisation_peak);
14121417
}
1413-
}
1414-
}
14151418

1416-
// Apply volume attenuation last. TODO: make this so we can chain
1417-
// the normaliser and mixer as a processing pipeline.
1418-
if let Some(ref editor) = self.audio_filter {
1419-
editor.modify_stream(data)
1419+
*sample *= volume;
1420+
}
14201421
}
14211422
}
14221423

src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1648,12 +1648,12 @@ async fn main() {
16481648
let player_config = setup.player_config.clone();
16491649
let connect_config = setup.connect_config.clone();
16501650

1651-
let audio_filter = mixer.get_audio_filter();
1651+
let soft_volume = mixer.get_soft_volume();
16521652
let format = setup.format;
16531653
let backend = setup.backend;
16541654
let device = setup.device.clone();
16551655
let (player, event_channel) =
1656-
Player::new(player_config, session.clone(), audio_filter, move || {
1656+
Player::new(player_config, session.clone(), soft_volume, move || {
16571657
(backend)(device, format)
16581658
});
16591659

0 commit comments

Comments
 (0)