selene-core 0.9.0-alpha.2

selene-core is the backend for Selene, a local-first music player
Documentation
use std::f64;

use ebur128::{EbuR128, Mode};
use serde::{Deserialize, Serialize};
use symphonia::core::audio::{Audio, GenericAudioBufferRef};
use thiserror::Error;

use crate::{
    config::common_config,
    media_container::MediaContainer,
    symphonia_helpers::raw_decoder::{Decoder, DecodingError},
};

#[derive(Debug, Error)]
pub enum LoudnormAnalysisError {
    #[error("{0}")]
    Ebur128(#[from] ebur128::Error),

    #[error("Decoding Error: {0}")]
    Decoding(#[from] DecodingError),
}

/// [`LoudnormAnalysis`] represents the measured values from the first-pass of EBU R 128 loudnorm
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Copy)]
pub struct LoudnormAnalysis {
    pub(crate) accurate_true_peak: bool,

    pub(crate) i: f64,
    pub(crate) tp: f64,
}

impl LoudnormAnalysis {
    #[must_use]
    pub fn accurate_true_peak(&self) -> bool {
        self.accurate_true_peak
    }

    #[must_use]
    pub fn measured_i(&self) -> f64 {
        self.i
    }

    #[must_use]
    pub fn measured_tp(&self) -> f64 {
        self.tp
    }

    #[must_use]
    pub fn calculated_gain_db(&self) -> f64 {
        let config = common_config().loudnorm;
        (config.target_i - self.i - config.target_offset).min(config.target_tp - self.tp)
    }

    #[must_use]
    pub fn calculated_gain(&self) -> f64 {
        10f64.powf(self.calculated_gain_db() / 20.0)
    }

    #[must_use]
    pub fn calculated_replay_gain_peak(&self) -> f64 {
        10.0_f64.powf(self.tp / 20.0)
    }

    /// Uses ffmpeg to measure the input file and return a [`LoudnormAnalysis`]
    pub fn from_container(
        container: &MediaContainer,
        accurate_true_peak: bool,
    ) -> Result<Self, LoudnormAnalysisError> {
        let peak_mode = if accurate_true_peak {
            Mode::TRUE_PEAK
        } else {
            Mode::SAMPLE_PEAK
        };

        let mut analyzer = EbuR128::new(
            container.stream().codec_params.channels as u32,
            container.stream().codec_params.sample_rate,
            Mode::I | peak_mode,
        )?;

        let mut raw_decoder = Decoder::from_container(container, 512 * 1024)?;

        while let Some(packet) = raw_decoder.decode_next_packet()? {
            match packet {
                GenericAudioBufferRef::S16(buf) => {
                    let mut frames = vec![0; buf.samples_interleaved()];
                    buf.copy_to_slice_interleaved(&mut frames);
                    analyzer.add_frames_i16(&frames)?;
                }
                GenericAudioBufferRef::S32(buf) => {
                    let mut frames = vec![0; buf.samples_interleaved()];
                    buf.copy_to_slice_interleaved(&mut frames);
                    analyzer.add_frames_i32(&frames)?;
                }
                GenericAudioBufferRef::F64(buf) => {
                    let mut frames = vec![0.0; buf.samples_interleaved()];
                    buf.copy_to_slice_interleaved(&mut frames);
                    analyzer.add_frames_f64(&frames)?;
                }
                buf => {
                    let mut frames = vec![0.0; buf.samples_interleaved()];
                    buf.copy_to_slice_interleaved(&mut frames);
                    analyzer.add_frames_f32(&frames)?;
                }
            }
        }

        let i = analyzer.loudness_global()?;
        let tp = (0..container.stream().codec_params.channels as u32).try_fold(
            f64::NEG_INFINITY,
            |max, c| {
                let peak = if accurate_true_peak {
                    analyzer.true_peak(c)
                } else {
                    analyzer.sample_peak(c)
                };
                peak.map(|tp| f64::max(max, tp))
            },
        )?;

        Ok(LoudnormAnalysis {
            accurate_true_peak,
            i,
            tp,
        })
    }
}