rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
//! Decibel conversions matching `librosa.core.spectrum`.
//!
//! Conversions between amplitude, power, and decibel representations.

/// Convert a power spectrogram to decibel (dB) units.
///
/// Matches `librosa.power_to_db`.
///
/// - `ref_val`: reference power (default: 1.0)
/// - `amin`: minimum amplitude threshold to avoid log(0) (default: 1e-10)
/// - `top_db`: threshold the output at `top_db` below the peak (default: Some(80.0))
///
/// # Panics
///
/// Panics if `top_db` is `Some(value)` and `value < 0.0`.
///
/// # Examples
///
/// ```
/// use rosa::dsp::db::power_to_db;
///
/// let power = vec![1.0, 0.1, 0.01];
/// let db = power_to_db(&power, 1.0, 1e-10, None);
/// assert!((db[0] - 0.0).abs() < 1e-10);
/// assert!((db[1] - -10.0).abs() < 1e-10);
/// ```
pub fn power_to_db(s: &[f64], ref_val: f64, amin: f64, top_db: Option<f64>) -> Vec<f64> {
    let ref_val = ref_val.abs().max(amin);
    let log_ref = 10.0 * ref_val.log10();

    let mut result: Vec<f64> = s
        .iter()
        .map(|&val| 10.0 * val.max(amin).log10() - log_ref)
        .collect();

    if let Some(top_db) = top_db {
        assert!(top_db >= 0.0, "top_db must be non-negative");
        let max_val = result.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
        let threshold = max_val - top_db;
        for val in result.iter_mut() {
            *val = val.max(threshold);
        }
    }

    result
}

/// Convert decibel (dB) units back to power.
///
/// Matches `librosa.db_to_power`.
///
/// - `ref_val`: reference power (default: 1.0)
pub fn db_to_power(s_db: &[f64], ref_val: f64) -> Vec<f64> {
    s_db.iter()
        .map(|&val| ref_val * 10.0_f64.powf(val * 0.1))
        .collect()
}

/// Convert an amplitude spectrogram to decibel (dB) units.
///
/// Matches `librosa.amplitude_to_db`.
///
/// Delegates to `power_to_db` with squared values:
/// `amplitude_to_db(S) = power_to_db(S², ref², amin², top_db)`
///
/// - `ref_val`: reference amplitude (default: 1.0)
/// - `amin`: minimum amplitude threshold (default: 1e-5)
/// - `top_db`: threshold below peak (default: Some(80.0))
pub fn amplitude_to_db(s: &[f64], ref_val: f64, amin: f64, top_db: Option<f64>) -> Vec<f64> {
    let power: Vec<f64> = s.iter().map(|&val| val.abs().max(amin).powi(2)).collect();
    let ref_power = ref_val.abs().max(amin).powi(2);
    power_to_db(&power, ref_power, amin * amin, top_db)
}

/// Convert decibel (dB) units back to amplitude.
///
/// Matches `librosa.db_to_amplitude`.
///
/// - `ref_val`: reference amplitude (default: 1.0)
pub fn db_to_amplitude(s_db: &[f64], ref_val: f64) -> Vec<f64> {
    let ref_power = ref_val * ref_val;
    db_to_power(s_db, ref_power)
        .iter()
        .map(|&val| val.sqrt())
        .collect()
}

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

    #[derive(Deserialize)]
    struct DbTestCase {
        name: String,
        function: String,
        input: Vec<f64>,
        #[serde(default = "default_ref")]
        ref_val: f64,
        #[serde(default)]
        amin: Option<f64>,
        #[serde(default)]
        top_db: Option<f64>,
        expected: Vec<f64>,
    }

    fn default_ref() -> f64 {
        1.0
    }

    fn load_db_references() -> Vec<DbTestCase> {
        let data = include_str!("tests/references/db_conversions.json");
        serde_json::from_str(data).expect("Failed to parse db_conversions.json")
    }

    fn assert_approx_eq(actual: &[f64], expected: &[f64], tol: f64, name: &str) {
        assert_eq!(
            actual.len(),
            expected.len(),
            "{name}: length mismatch: got {} expected {}",
            actual.len(),
            expected.len()
        );
        for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
            let diff = (a - e).abs();
            assert!(
                diff < tol,
                "{name}[{i}]: got {a}, expected {e}, diff {diff} > tol {tol}"
            );
        }
    }

    #[test]
    fn test_power_to_db_matches_librosa() {
        let refs = load_db_references();
        for case in refs.iter().filter(|c| c.function == "power_to_db") {
            let amin = case.amin.unwrap_or(1e-10);
            let result = power_to_db(&case.input, case.ref_val, amin, case.top_db);
            assert_approx_eq(&result, &case.expected, 1e-10, &case.name);
        }
    }

    #[test]
    fn test_db_to_power_matches_librosa() {
        let refs = load_db_references();
        for case in refs.iter().filter(|c| c.function == "db_to_power") {
            let result = db_to_power(&case.input, case.ref_val);
            assert_approx_eq(&result, &case.expected, 1e-10, &case.name);
        }
    }

    #[test]
    fn test_amplitude_to_db_matches_librosa() {
        let refs = load_db_references();
        for case in refs.iter().filter(|c| c.function == "amplitude_to_db") {
            let amin = case.amin.unwrap_or(1e-5);
            let result = amplitude_to_db(&case.input, case.ref_val, amin, case.top_db);
            assert_approx_eq(&result, &case.expected, 1e-10, &case.name);
        }
    }

    #[test]
    fn test_db_to_amplitude_matches_librosa() {
        let refs = load_db_references();
        for case in refs.iter().filter(|c| c.function == "db_to_amplitude") {
            let result = db_to_amplitude(&case.input, case.ref_val);
            assert_approx_eq(&result, &case.expected, 1e-10, &case.name);
        }
    }

    #[test]
    fn test_power_roundtrip() {
        let original = vec![0.001, 0.01, 0.1, 1.0, 10.0];
        let db = power_to_db(&original, 1.0, 1e-10, None);
        let recovered = db_to_power(&db, 1.0);
        assert_approx_eq(&recovered, &original, 1e-10, "power_roundtrip");
    }

    #[test]
    fn test_amplitude_roundtrip() {
        let original = vec![0.01, 0.1, 1.0, 10.0];
        let db = amplitude_to_db(&original, 1.0, 1e-10, None);
        let recovered = db_to_amplitude(&db, 1.0);
        assert_approx_eq(&recovered, &original, 1e-10, "amplitude_roundtrip");
    }
}