@@ -61,12 +61,8 @@ struct PlayerInternal {
6161 event_senders : Vec < mpsc:: UnboundedSender < PlayerEvent > > ,
6262 converter : Converter ,
6363
64- limiter_active : bool ,
65- limiter_attack_counter : u32 ,
66- limiter_release_counter : u32 ,
67- limiter_peak_sample : f64 ,
68- limiter_factor : f64 ,
69- limiter_strength : f64 ,
64+ normalisation_integrator : f64 ,
65+ normalisation_peak : f64 ,
7066
7167 auto_normalise_as_album : bool ,
7268}
@@ -208,6 +204,14 @@ pub fn ratio_to_db(ratio: f64) -> f64 {
208204 ratio. log10 ( ) * DB_VOLTAGE_RATIO
209205}
210206
207+ pub fn duration_to_coefficient ( duration : Duration ) -> f64 {
208+ f64:: exp ( -1.0 / ( duration. as_secs_f64 ( ) * SAMPLES_PER_SECOND as f64 ) )
209+ }
210+
211+ pub fn coefficient_to_duration ( coefficient : f64 ) -> Duration {
212+ Duration :: from_secs_f64 ( -1.0 / f64:: ln ( coefficient) / SAMPLES_PER_SECOND as f64 )
213+ }
214+
211215#[ derive( Clone , Copy , Debug ) ]
212216pub struct NormalisationData {
213217 track_gain_db : f32 ,
@@ -241,17 +245,18 @@ impl NormalisationData {
241245 return 1.0 ;
242246 }
243247
244- let [ gain_db, gain_peak] = if config. normalisation_type == NormalisationType :: Album {
245- [ data. album_gain_db , data. album_peak ]
248+ let ( gain_db, gain_peak) = if config. normalisation_type == NormalisationType :: Album {
249+ ( data. album_gain_db as f64 , data. album_peak as f64 )
246250 } else {
247- [ data. track_gain_db , data. track_peak ]
251+ ( data. track_gain_db as f64 , data. track_peak as f64 )
248252 } ;
249253
250- let normalisation_power = gain_db as f64 + config. normalisation_pregain ;
254+ let normalisation_power = gain_db + config. normalisation_pregain_db as f64 ;
251255 let mut normalisation_factor = db_to_ratio ( normalisation_power) ;
252256
253- if normalisation_factor * gain_peak as f64 > config. normalisation_threshold {
254- let limited_normalisation_factor = config. normalisation_threshold / gain_peak as f64 ;
257+ if normalisation_power + ratio_to_db ( gain_peak) > config. normalisation_threshold_dbfs {
258+ let limited_normalisation_factor =
259+ db_to_ratio ( config. normalisation_threshold_dbfs as f64 ) / gain_peak;
255260 let limited_normalisation_power = ratio_to_db ( limited_normalisation_factor) ;
256261
257262 if config. normalisation_method == NormalisationMethod :: Basic {
@@ -295,18 +300,25 @@ impl Player {
295300 debug ! ( "Normalisation Type: {:?}" , config. normalisation_type) ;
296301 debug ! (
297302 "Normalisation Pregain: {:.1} dB" ,
298- config. normalisation_pregain
303+ config. normalisation_pregain_db
299304 ) ;
300305 debug ! (
301306 "Normalisation Threshold: {:.1} dBFS" ,
302- ratio_to_db ( config. normalisation_threshold )
307+ config. normalisation_threshold_dbfs
303308 ) ;
304309 debug ! ( "Normalisation Method: {:?}" , config. normalisation_method) ;
305310
306311 if config. normalisation_method == NormalisationMethod :: Dynamic {
307- debug ! ( "Normalisation Attack: {:?}" , config. normalisation_attack) ;
308- debug ! ( "Normalisation Release: {:?}" , config. normalisation_release) ;
309- debug ! ( "Normalisation Knee: {:?}" , config. normalisation_knee) ;
312+ // as_millis() has rounding errors (truncates)
313+ debug ! (
314+ "Normalisation Attack: {:.0} ms" ,
315+ coefficient_to_duration( config. normalisation_attack_cf) . as_secs_f64( ) * 1000.
316+ ) ;
317+ debug ! (
318+ "Normalisation Release: {:.0} ms" ,
319+ coefficient_to_duration( config. normalisation_release_cf) . as_secs_f64( ) * 1000.
320+ ) ;
321+ debug ! ( "Normalisation Knee: {} dB" , config. normalisation_knee_db) ;
310322 }
311323 }
312324
@@ -329,12 +341,8 @@ impl Player {
329341 event_senders : [ event_sender] . to_vec ( ) ,
330342 converter,
331343
332- limiter_active : false ,
333- limiter_attack_counter : 0 ,
334- limiter_release_counter : 0 ,
335- limiter_peak_sample : 0.0 ,
336- limiter_factor : 1.0 ,
337- limiter_strength : 0.0 ,
344+ normalisation_peak : 0.0 ,
345+ normalisation_integrator : 0.0 ,
338346
339347 auto_normalise_as_album : false ,
340348 } ;
@@ -1275,110 +1283,82 @@ impl PlayerInternal {
12751283 Some ( mut packet) => {
12761284 if !packet. is_empty ( ) {
12771285 if let AudioPacket :: Samples ( ref mut data) = packet {
1286+ // For the basic normalisation method, a normalisation factor of 1.0 indicates that
1287+ // there is nothing to normalise (all samples should pass unaltered). For the
1288+ // dynamic method, there may still be peaks that we want to shave off.
12781289 if self . config . normalisation
12791290 && !( f64:: abs ( normalisation_factor - 1.0 ) <= f64:: EPSILON
12801291 && self . config . normalisation_method == NormalisationMethod :: Basic )
12811292 {
1293+ // zero-cost shorthands
1294+ let threshold_db = self . config . normalisation_threshold_dbfs ;
1295+ let knee_db = self . config . normalisation_knee_db ;
1296+ let attack_cf = self . config . normalisation_attack_cf ;
1297+ let release_cf = self . config . normalisation_release_cf ;
1298+
12821299 for sample in data. iter_mut ( ) {
1283- let mut actual_normalisation_factor = normalisation_factor;
1300+ * sample *= normalisation_factor; // for both the basic and dynamic limiter
1301+
1302+ // Feedforward limiter in the log domain
1303+ // After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic
1304+ // Range Compressor Design—A Tutorial and Analysis. Journal of The Audio
1305+ // Engineering Society, 60, 399-408.
12841306 if self . config . normalisation_method == NormalisationMethod :: Dynamic
12851307 {
1286- if self . limiter_active {
1287- // "S"-shaped curve with a configurable knee during attack and release:
1288- // - > 1.0 yields soft knees at start and end, steeper in between
1289- // - 1.0 yields a linear function from 0-100%
1290- // - between 0.0 and 1.0 yields hard knees at start and end, flatter in between
1291- // - 0.0 yields a step response to 50%, causing distortion
1292- // - Rates < 0.0 invert the limiter and are invalid
1293- let mut shaped_limiter_strength = self . limiter_strength ;
1294- if shaped_limiter_strength > 0.0
1295- && shaped_limiter_strength < 1.0
1296- {
1297- shaped_limiter_strength = 1.0
1298- / ( 1.0
1299- + f64:: powf (
1300- shaped_limiter_strength
1301- / ( 1.0 - shaped_limiter_strength) ,
1302- -self . config . normalisation_knee ,
1303- ) ) ;
1304- }
1305- actual_normalisation_factor =
1306- ( 1.0 - shaped_limiter_strength) * normalisation_factor
1307- + shaped_limiter_strength * self . limiter_factor ;
1308- } ;
1308+ // steps 1 + 2: half-wave rectification and conversion into dB
1309+ let abs_sample_db = ratio_to_db ( sample. abs ( ) ) ;
13091310
1310- // Cast the fields here for better readability
1311- let normalisation_attack =
1312- self . config . normalisation_attack . as_secs_f64 ( ) ;
1313- let normalisation_release =
1314- self . config . normalisation_release . as_secs_f64 ( ) ;
1315- let limiter_release_counter =
1316- self . limiter_release_counter as f64 ;
1317- let limiter_attack_counter = self . limiter_attack_counter as f64 ;
1318- let samples_per_second = SAMPLES_PER_SECOND as f64 ;
1319-
1320- // Always check for peaks, even when the limiter is already active.
1321- // There may be even higher peaks than we initially targeted.
1322- // Check against the normalisation factor that would be applied normally.
1323- let abs_sample = f64:: abs ( * sample * normalisation_factor) ;
1324- if abs_sample > self . config . normalisation_threshold {
1325- self . limiter_active = true ;
1326- if self . limiter_release_counter > 0 {
1327- // A peak was encountered while releasing the limiter;
1328- // synchronize with the current release limiter strength.
1329- self . limiter_attack_counter = ( ( ( samples_per_second
1330- * normalisation_release)
1331- - limiter_release_counter)
1332- / ( normalisation_release / normalisation_attack) )
1333- as u32 ;
1334- self . limiter_release_counter = 0 ;
1335- }
1336-
1337- self . limiter_attack_counter =
1338- self . limiter_attack_counter . saturating_add ( 1 ) ;
1311+ // Some tracks have samples that are precisely 0.0, but ratio_to_db(0.0)
1312+ // returns -inf and gets the peak detector stuck.
1313+ if !abs_sample_db. is_normal ( ) {
1314+ continue ;
1315+ }
13391316
1340- self . limiter_strength = limiter_attack_counter
1341- / ( samples_per_second * normalisation_attack) ;
1317+ // step 3: gain computer with soft knee
1318+ let biased_sample = abs_sample_db - threshold_db;
1319+ let limited_sample = if 2.0 * biased_sample < -knee_db {
1320+ abs_sample_db
1321+ } else if 2.0 * biased_sample. abs ( ) <= knee_db {
1322+ abs_sample_db
1323+ - ( biased_sample + knee_db / 2.0 ) . powi ( 2 )
1324+ / ( 2.0 * knee_db)
1325+ } else {
1326+ threshold_db as f64
1327+ } ;
13421328
1343- if abs_sample > self . limiter_peak_sample {
1344- self . limiter_peak_sample = abs_sample;
1345- self . limiter_factor =
1346- self . config . normalisation_threshold
1347- / self . limiter_peak_sample ;
1348- }
1349- } else if self . limiter_active {
1350- if self . limiter_attack_counter > 0 {
1351- // Release may start within the attack period, before
1352- // the limiter reached full strength. For that reason
1353- // start the release by synchronizing with the current
1354- // attack limiter strength.
1355- self . limiter_release_counter = ( ( ( samples_per_second
1356- * normalisation_attack)
1357- - limiter_attack_counter)
1358- * ( normalisation_release / normalisation_attack) )
1359- as u32 ;
1360- self . limiter_attack_counter = 0 ;
1361- }
1329+ // step 4: subtractor
1330+ let limiter_input = abs_sample_db - limited_sample;
13621331
1363- self . limiter_release_counter =
1364- self . limiter_release_counter . saturating_add ( 1 ) ;
1365-
1366- if self . limiter_release_counter
1367- > ( samples_per_second * normalisation_release) as u32
1368- {
1369- self . reset_limiter ( ) ;
1370- } else {
1371- self . limiter_strength = ( ( samples_per_second
1372- * normalisation_release)
1373- - limiter_release_counter)
1374- / ( samples_per_second * normalisation_release) ;
1375- }
1332+ // Spare the CPU unless the limiter is active or we are riding a peak.
1333+ if !( limiter_input > 0.0
1334+ || self . normalisation_integrator > 0.0
1335+ || self . normalisation_peak > 0.0 )
1336+ {
1337+ continue ;
13761338 }
1339+
1340+ // step 5: smooth, decoupled peak detector
1341+ self . normalisation_integrator = f64:: max (
1342+ limiter_input,
1343+ release_cf * self . normalisation_integrator
1344+ + ( 1.0 - release_cf) * limiter_input,
1345+ ) ;
1346+ self . normalisation_peak = attack_cf * self . normalisation_peak
1347+ + ( 1.0 - attack_cf) * self . normalisation_integrator ;
1348+
1349+ // step 6: make-up gain applied later (volume attenuation)
1350+ // Applying the standard normalisation factor here won't work,
1351+ // because there are tracks with peaks as high as 6 dB above
1352+ // the default threshold, so that would clip.
1353+
1354+ // steps 7-8: conversion into level and multiplication into gain stage
1355+ * sample *= db_to_ratio ( -self . normalisation_peak ) ;
13771356 }
1378- * sample *= actual_normalisation_factor;
13791357 }
13801358 }
13811359
1360+ // Apply volume attenuation last. TODO: make this so we can chain
1361+ // the normaliser and mixer as a processing pipeline.
13821362 if let Some ( ref editor) = self . audio_filter {
13831363 editor. modify_stream ( data)
13841364 }
@@ -1411,15 +1391,6 @@ impl PlayerInternal {
14111391 }
14121392 }
14131393
1414- fn reset_limiter ( & mut self ) {
1415- self . limiter_active = false ;
1416- self . limiter_release_counter = 0 ;
1417- self . limiter_attack_counter = 0 ;
1418- self . limiter_peak_sample = 0.0 ;
1419- self . limiter_factor = 1.0 ;
1420- self . limiter_strength = 0.0 ;
1421- }
1422-
14231394 fn start_playback (
14241395 & mut self ,
14251396 track_id : SpotifyId ,
0 commit comments