@@ -78,8 +78,10 @@ struct PlayerInternal {
7878 event_senders : Vec < mpsc:: UnboundedSender < PlayerEvent > > ,
7979 converter : Converter ,
8080
81- normalisation_integrator : f64 ,
82- normalisation_peak : f64 ,
81+ normalisation_integrators : [ f64 ; 2 ] ,
82+ normalisation_peaks : [ f64 ; 2 ] ,
83+ normalisation_channel : usize ,
84+ normalisation_knee_factor : f64 ,
8385
8486 auto_normalise_as_album : bool ,
8587
@@ -466,6 +468,7 @@ impl Player {
466468 debug ! ( "new Player [{player_id}]" ) ;
467469
468470 let converter = Converter :: new ( config. ditherer ) ;
471+ let normalisation_knee_factor = 1.0 / ( 8.0 * config. normalisation_knee_db ) ;
469472
470473 let internal = PlayerInternal {
471474 session,
@@ -482,8 +485,10 @@ impl Player {
482485 event_senders : vec ! [ ] ,
483486 converter,
484487
485- normalisation_peak : 0.0 ,
486- normalisation_integrator : 0.0 ,
488+ normalisation_peaks : [ 0.0 ; 2 ] ,
489+ normalisation_integrators : [ 0.0 ; 2 ] ,
490+ normalisation_channel : 0 ,
491+ normalisation_knee_factor,
487492
488493 auto_normalise_as_album : false ,
489494
@@ -1574,112 +1579,94 @@ impl PlayerInternal {
15741579 Some ( ( _, mut packet) ) => {
15751580 if !packet. is_empty ( ) {
15761581 if let AudioPacket :: Samples ( ref mut data) = packet {
1577- // Get the volume for the packet.
1578- // In the case of hardware volume control this will
1579- // always be 1.0 (no change).
1582+ // Get the volume for the packet. In the case of hardware volume control
1583+ // this will always be 1.0 (no change).
15801584 let volume = self . volume_getter . attenuation_factor ( ) ;
15811585
1582- // For the basic normalisation method, a normalisation factor of 1.0 indicates that
1583- // there is nothing to normalise (all samples should pass unaltered). For the
1584- // dynamic method, there may still be peaks that we want to shave off.
1585-
1586+ // For the basic normalisation method, a normalisation factor of 1.0
1587+ // indicates that there is nothing to normalise (all samples should pass
1588+ // unaltered). For the dynamic method, there may still be peaks that we
1589+ // want to shave off.
1590+ //
15861591 // No matter the case we apply volume attenuation last if there is any.
1587- if !self . config . normalisation {
1588- if volume < 1.0 {
1589- for sample in data. iter_mut ( ) {
1590- * sample *= volume;
1592+ match ( self . config . normalisation , self . config . normalisation_method ) {
1593+ ( false , _) => {
1594+ if volume < 1.0 {
1595+ for sample in data. iter_mut ( ) {
1596+ * sample *= volume;
1597+ }
15911598 }
15921599 }
1593- } else if self . config . normalisation_method == NormalisationMethod :: Basic
1594- && ( normalisation_factor < 1.0 || volume < 1.0 )
1595- {
1596- for sample in data. iter_mut ( ) {
1597- * sample *= normalisation_factor * volume;
1598- }
1599- } else if self . config . normalisation_method == NormalisationMethod :: Dynamic {
1600- // zero-cost shorthands
1601- let threshold_db = self . config . normalisation_threshold_dbfs ;
1602- let knee_db = self . config . normalisation_knee_db ;
1603- let attack_cf = self . config . normalisation_attack_cf ;
1604- let release_cf = self . config . normalisation_release_cf ;
1605-
1606- for sample in data. iter_mut ( ) {
1607- * sample *= normalisation_factor;
1608-
1609- // Feedforward limiter in the log domain
1610- // After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic
1611- // Range Compressor Design—A Tutorial and Analysis. Journal of The Audio
1612- // Engineering Society, 60, 399-408.
1613-
1614- // Some tracks have samples that are precisely 0.0. That's silence
1615- // and we know we don't need to limit that, in which we can spare
1616- // the CPU cycles.
1617- //
1618- // Also, calling `ratio_to_db(0.0)` returns `inf` and would get the
1619- // peak detector stuck. Also catch the unlikely case where a sample
1620- // is decoded as `NaN` or some other non-normal value.
1621- let limiter_db = if sample. is_normal ( ) {
1622- // step 1-4: half-wave rectification and conversion into dB
1623- // and gain computer with soft knee and subtractor
1624- let bias_db = ratio_to_db ( sample. abs ( ) ) - threshold_db;
1625- let knee_boundary_db = bias_db * 2.0 ;
1626-
1627- if knee_boundary_db < -knee_db {
1628- 0.0
1629- } else if knee_boundary_db. abs ( ) <= knee_db {
1630- // The textbook equation:
1631- // ratio_to_db(sample.abs()) - (ratio_to_db(sample.abs()) - (bias_db + knee_db / 2.0).powi(2) / (2.0 * knee_db))
1632- // Simplifies to:
1633- // ((2.0 * bias_db) + knee_db).powi(2) / (8.0 * knee_db)
1634- // Which in our case further simplifies to:
1635- // (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
1636- // because knee_boundary_db is 2.0 * bias_db.
1637- ( knee_boundary_db + knee_db) . powi ( 2 ) / ( 8.0 * knee_db)
1638- } else {
1639- // Textbook:
1640- // ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db.
1641- bias_db
1642- }
1643- } else {
1644- 0.0
1645- } ;
1646-
1647- // Spare the CPU unless (1) the limiter is engaged, (2) we
1648- // were in attack or (3) we were in release, and that attack/
1649- // release wasn't finished yet.
1650- if limiter_db > 0.0
1651- || self . normalisation_integrator > 0.0
1652- || self . normalisation_peak > 0.0
1653- {
1654- // step 5: smooth, decoupled peak detector
1655- // Textbook:
1656- // release_cf * self.normalisation_integrator + (1.0 - release_cf) * limiter_db
1657- // Simplifies to:
1658- // release_cf * self.normalisation_integrator - release_cf * limiter_db + limiter_db
1659- self . normalisation_integrator = f64:: max (
1600+ ( true , NormalisationMethod :: Dynamic ) => {
1601+ // zero-cost shorthands
1602+ let threshold_db = self . config . normalisation_threshold_dbfs ;
1603+ let knee_db = self . config . normalisation_knee_db ;
1604+ let attack_cf = self . config . normalisation_attack_cf ;
1605+ let release_cf = self . config . normalisation_release_cf ;
1606+
1607+ for sample in data. iter_mut ( ) {
1608+ // Feedforward limiter in the log domain
1609+ // After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012).
1610+ // Digital Dynamic Range Compressor Design—A Tutorial and
1611+ // Analysis. Journal of The Audio Engineering Society, 60,
1612+ // 399-408.
1613+
1614+ // This implementation assumes audio is stereo.
1615+
1616+ // step 0: apply gain stage
1617+ * sample *= normalisation_factor;
1618+
1619+ // step 1-4: half-wave rectification and conversion into dB, and
1620+ // gain computer with soft knee and subtractor
1621+ let limiter_db = {
1622+ // Add slight DC offset. Some samples are silence, which is
1623+ // -inf dB and gets the limiter stuck. Adding a small
1624+ // positive offset prevents this.
1625+ * sample += f64:: MIN_POSITIVE ;
1626+
1627+ let bias_db = ratio_to_db ( sample. abs ( ) ) - threshold_db;
1628+ let knee_boundary_db = bias_db * 2.0 ;
1629+ if knee_boundary_db < -knee_db {
1630+ 0.0
1631+ } else if knee_boundary_db. abs ( ) <= knee_db {
1632+ let term = knee_boundary_db + knee_db;
1633+ term * term * self . normalisation_knee_factor
1634+ } else {
1635+ bias_db
1636+ }
1637+ } ;
1638+
1639+ // track left/right channel
1640+ let channel = self . normalisation_channel ;
1641+ self . normalisation_channel ^= 1 ;
1642+
1643+ // step 5: smooth, decoupled peak detector for each channel
1644+ // Use direct references to reduce repeated array indexing
1645+ let integrator = & mut self . normalisation_integrators [ channel] ;
1646+ let peak = & mut self . normalisation_peaks [ channel] ;
1647+
1648+ * integrator = f64:: max (
16601649 limiter_db,
1661- release_cf * self . normalisation_integrator
1662- - release_cf * limiter_db
1663- + limiter_db,
1650+ release_cf * * integrator + ( 1.0 - release_cf) * limiter_db,
1651+ ) ;
1652+ * peak = attack_cf * * peak + ( 1.0 - attack_cf) * * integrator;
1653+
1654+ // steps 6-8: conversion into level and multiplication into gain
1655+ // stage. Find maximum peak across both channels to couple the
1656+ // gain and maintain stereo imaging.
1657+ let max_peak = f64:: max (
1658+ self . normalisation_peaks [ 0 ] ,
1659+ self . normalisation_peaks [ 1 ] ,
16641660 ) ;
1665- // Textbook:
1666- // attack_cf * self.normalisation_peak + (1.0 - attack_cf) * self.normalisation_integrator
1667- // Simplifies to:
1668- // attack_cf * self.normalisation_peak - attack_cf * self.normalisation_integrator + self.normalisation_integrator
1669- self . normalisation_peak = attack_cf * self . normalisation_peak
1670- - attack_cf * self . normalisation_integrator
1671- + self . normalisation_integrator ;
1672-
1673- // step 6: make-up gain applied later (volume attenuation)
1674- // Applying the standard normalisation factor here won't work,
1675- // because there are tracks with peaks as high as 6 dB above
1676- // the default threshold, so that would clip.
1677-
1678- // steps 7-8: conversion into level and multiplication into gain stage
1679- * sample *= db_to_ratio ( -self . normalisation_peak ) ;
1661+ * sample *= db_to_ratio ( -max_peak) * volume;
1662+ }
1663+ }
1664+ ( true , NormalisationMethod :: Basic ) => {
1665+ if normalisation_factor < 1.0 || volume < 1.0 {
1666+ for sample in data. iter_mut ( ) {
1667+ * sample *= normalisation_factor * volume;
1668+ }
16801669 }
1681-
1682- * sample *= volume;
16831670 }
16841671 }
16851672 }
0 commit comments