rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
//! Frequency weighting functions matching librosa.

/// Compute A-weighting of frequencies (in dB).
///
/// Matches `librosa.A_weighting`.
///
/// - `min_db`: Clip weights below this threshold. `None` for no clipping.
pub fn a_weighting(frequencies: &[f64], min_db: Option<f64>) -> Vec<f64> {
    let c = [
        12194.217_f64.powi(2),
        20.598997_f64.powi(2),
        107.65265_f64.powi(2),
        737.86223_f64.powi(2),
    ];
    frequencies
        .iter()
        .map(|&f| {
            let f_sq = f * f;
            let mut w = 2.0
                + 20.0
                    * (c[0].log10() + 2.0 * f_sq.log10()
                        - (f_sq + c[0]).log10()
                        - (f_sq + c[1]).log10()
                        - 0.5 * (f_sq + c[2]).log10()
                        - 0.5 * (f_sq + c[3]).log10());
            if let Some(min) = min_db {
                w = w.max(min);
            }
            w
        })
        .collect()
}

/// Compute B-weighting of frequencies (in dB).
///
/// Matches `librosa.B_weighting`.
pub fn b_weighting(frequencies: &[f64], min_db: Option<f64>) -> Vec<f64> {
    let c = [
        12194.217_f64.powi(2),
        20.598997_f64.powi(2),
        158.48932_f64.powi(2),
    ];
    frequencies
        .iter()
        .map(|&f| {
            let f_sq = f * f;
            let mut w = 0.17
                + 20.0
                    * (c[0].log10() + 1.5 * f_sq.log10()
                        - (f_sq + c[0]).log10()
                        - (f_sq + c[1]).log10()
                        - 0.5 * (f_sq + c[2]).log10());
            if let Some(min) = min_db {
                w = w.max(min);
            }
            w
        })
        .collect()
}

/// Compute C-weighting of frequencies (in dB).
///
/// Matches `librosa.C_weighting`.
pub fn c_weighting(frequencies: &[f64], min_db: Option<f64>) -> Vec<f64> {
    let c = [12194.217_f64.powi(2), 20.598997_f64.powi(2)];
    frequencies
        .iter()
        .map(|&f| {
            let f_sq = f * f;
            let mut w = 0.062
                + 20.0
                    * (c[0].log10() + f_sq.log10() - (f_sq + c[0]).log10() - (f_sq + c[1]).log10());
            if let Some(min) = min_db {
                w = w.max(min);
            }
            w
        })
        .collect()
}

/// Compute D-weighting of frequencies (in dB).
///
/// Matches `librosa.D_weighting`.
pub fn d_weighting(frequencies: &[f64], min_db: Option<f64>) -> Vec<f64> {
    let c = [
        8.3046305e-3_f64.powi(2),
        1018.7_f64.powi(2),
        1039.6_f64.powi(2),
        3136.5_f64.powi(2),
        3424.0_f64.powi(2),
        282.7_f64.powi(2),
        1160.0_f64.powi(2),
    ];
    frequencies
        .iter()
        .map(|&f| {
            let f_sq = f * f;
            let mut w = 20.0
                * (0.5 * f_sq.log10() - c[0].log10()
                    + 0.5
                        * (((c[1] - f_sq).powi(2) + c[2] * f_sq).log10()
                            - ((c[3] - f_sq).powi(2) + c[4] * f_sq).log10()
                            - (c[5] + f_sq).log10()
                            - (c[6] + f_sq).log10()));
            if let Some(min) = min_db {
                w = w.max(min);
            }
            w
        })
        .collect()
}

/// Z-weighting (no weighting). Returns all zeros.
///
/// Matches `librosa.Z_weighting`.
pub fn z_weighting(frequencies: &[f64], min_db: Option<f64>) -> Vec<f64> {
    let w = min_db.unwrap_or(0.0).min(0.0);
    vec![w; frequencies.len()]
}

/// Compute frequency weighting by kind name.
///
/// Matches `librosa.frequency_weighting`.
///
/// `kind`: `"A"`, `"B"`, `"C"`, `"D"`, or `"Z"` (case-insensitive).
pub fn frequency_weighting(
    frequencies: &[f64],
    kind: &str,
    min_db: Option<f64>,
) -> Result<Vec<f64>, String> {
    match kind.to_uppercase().as_str() {
        "A" => Ok(a_weighting(frequencies, min_db)),
        "B" => Ok(b_weighting(frequencies, min_db)),
        "C" => Ok(c_weighting(frequencies, min_db)),
        "D" => Ok(d_weighting(frequencies, min_db)),
        "Z" => Ok(z_weighting(frequencies, min_db)),
        _ => Err(format!("Unknown weighting kind: {}", kind)),
    }
}

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

    #[derive(Deserialize)]
    struct WeightingTestCase {
        name: String,
        function: String,
        frequencies: Vec<f64>,
        min_db: Option<f64>,
        expected: Vec<f64>,
    }

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

    fn assert_f64_eq(actual: &[f64], expected: &[f64], tol: f64, name: &str) {
        assert_eq!(actual.len(), expected.len(), "{name}: length mismatch");
        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}"
            );
        }
    }

    #[test]
    fn test_a_weighting_matches_librosa() {
        for case in load_weighting_references()
            .into_iter()
            .filter(|c| c.function == "A_weighting")
        {
            let result = a_weighting(&case.frequencies, case.min_db);
            assert_f64_eq(&result, &case.expected, 1e-10, &case.name);
        }
    }

    #[test]
    fn test_b_weighting_matches_librosa() {
        for case in load_weighting_references()
            .into_iter()
            .filter(|c| c.function == "B_weighting")
        {
            let result = b_weighting(&case.frequencies, case.min_db);
            assert_f64_eq(&result, &case.expected, 1e-10, &case.name);
        }
    }

    #[test]
    fn test_c_weighting_matches_librosa() {
        for case in load_weighting_references()
            .into_iter()
            .filter(|c| c.function == "C_weighting")
        {
            let result = c_weighting(&case.frequencies, case.min_db);
            assert_f64_eq(&result, &case.expected, 1e-10, &case.name);
        }
    }

    #[test]
    fn test_d_weighting_matches_librosa() {
        for case in load_weighting_references()
            .into_iter()
            .filter(|c| c.function == "D_weighting")
        {
            let result = d_weighting(&case.frequencies, case.min_db);
            assert_f64_eq(&result, &case.expected, 1e-10, &case.name);
        }
    }

    #[test]
    fn test_a_weighting_1khz_approx_zero() {
        let w = a_weighting(&[1000.0], None);
        assert!(
            w[0].abs() < 1.0,
            "A-weighting at 1kHz should be ~0 dB, got {}",
            w[0]
        );
    }

    #[test]
    fn test_z_weighting_all_zeros() {
        let w = z_weighting(&[100.0, 1000.0, 10000.0], None);
        assert_eq!(w, vec![0.0; 3]);
    }

    #[test]
    fn test_frequency_weighting_dispatcher() {
        let freqs = [1000.0];
        let a = frequency_weighting(&freqs, "A", None).unwrap();
        assert_eq!(a, a_weighting(&freqs, None));
        assert!(frequency_weighting(&freqs, "X", None).is_err());
    }
}