audiobook-creation-exchange 0.1.0

ACX-compliant audio post-processing: normalisation, limiting, gating, LUFS measurement, and spectral analysis for AI-generated speech audio.
Documentation
//! Loudness preset factory — maps a delivery character to a tuned [`AcxConfig`].
//!
//! Each preset shifts the normalisation target within or adjacent to the
//! ACX window (−23…−18 dBFS) so that different loudness styles land at a
//! consistent perceived level without manual gain tweaking.

use crate::AcxConfig;

/// Delivery loudness character.
///
/// Select the preset that best matches the intended loudness of the source
/// material, then pass the resulting [`AcxConfig`] to [`crate::process_with_config`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoudnessPreset {
    /// Very soft, close-mic delivery — intimate narration.
    Whispered,
    /// Calm, low-energy delivery.
    Soft,
    /// Balanced delivery — equivalent to [`AcxConfig::default`].
    Standard,
    /// Clear, forward-placed delivery — authoritative narration.
    Projected,
    /// High-energy, emphatic delivery.
    Loud,
}

impl LoudnessPreset {
    /// Return an [`AcxConfig`] tuned for this loudness character.
    pub fn config(&self) -> AcxConfig {
        let base = AcxConfig::default();
        match self {
            LoudnessPreset::Whispered => AcxConfig {
                rms_target_db: -22.0,
                rms_min_db: -23.0,
                rms_max_db: -21.0,
                ..base
            },
            LoudnessPreset::Soft => AcxConfig {
                rms_target_db: -21.0,
                rms_min_db: -23.0,
                rms_max_db: -19.0,
                ..base
            },
            LoudnessPreset::Standard => base,
            LoudnessPreset::Projected => AcxConfig {
                rms_target_db: -20.0,
                rms_min_db: -22.0,
                rms_max_db: -18.0,
                ..base
            },
            LoudnessPreset::Loud => AcxConfig {
                rms_target_db: -19.0,
                rms_min_db: -21.0,
                rms_max_db: -17.0,
                ..base
            },
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn standard_matches_default_config() {
        let std_cfg = LoudnessPreset::Standard.config();
        let default = AcxConfig::default();
        assert_eq!(std_cfg.rms_target_db, default.rms_target_db);
        assert_eq!(std_cfg.peak_ceiling_db, default.peak_ceiling_db);
    }

    #[test]
    fn whispered_is_quieter_than_loud() {
        let w = LoudnessPreset::Whispered.config();
        let l = LoudnessPreset::Loud.config();
        assert!(
            w.rms_target_db < l.rms_target_db,
            "Whispered target ({}) should be below Loud target ({})",
            w.rms_target_db,
            l.rms_target_db
        );
    }

    #[test]
    fn all_presets_share_same_peak_ceiling() {
        let ceiling = AcxConfig::default().peak_ceiling_db;
        for p in [
            LoudnessPreset::Whispered,
            LoudnessPreset::Soft,
            LoudnessPreset::Standard,
            LoudnessPreset::Projected,
            LoudnessPreset::Loud,
        ] {
            assert_eq!(
                p.config().peak_ceiling_db,
                ceiling,
                "{:?} has wrong peak ceiling",
                p
            );
        }
    }
}