rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
//! Frequency conversion utilities matching librosa.core.convert.

/// Convert Hz to octave number.
///
/// Matches `librosa.hz_to_octs(freq)`.
/// A440 maps to octave 4.0.
pub fn hz_to_octs(freq: f64) -> f64 {
    (freq / (440.0 / 16.0)).log2()
}

/// Convert Hz to octave number with tuning correction.
///
/// Matches `librosa.hz_to_octs(freq, tuning=tuning, bins_per_octave=bpo)`.
pub fn hz_to_octs_tuned(freq: f64, tuning: f64, bins_per_octave: usize) -> f64 {
    let a440 = 440.0 * 2.0_f64.powf(tuning / bins_per_octave as f64);
    (freq / (a440 / 16.0)).log2()
}

/// Compute center frequencies for CQT bins.
///
/// `f_k = fmin * 2^(tuning / bpo) * 2^(k / bpo)` for k in 0..n_bins.
/// Matches `librosa.cqt_frequencies(n_bins, fmin=fmin, bins_per_octave=bpo, tuning=tuning)`.
pub fn cqt_frequencies(n_bins: usize, fmin: f64, bins_per_octave: usize, tuning: f64) -> Vec<f64> {
    let correction = 2.0_f64.powf(tuning / bins_per_octave as f64);
    (0..n_bins)
        .map(|k| correction * fmin * 2.0_f64.powf(k as f64 / bins_per_octave as f64))
        .collect()
}

/// Convert octave number to Hz.
///
/// Matches `librosa.octs_to_hz(octs)`.
pub fn octs_to_hz(octs: f64) -> f64 {
    (440.0 / 16.0) * 2.0_f64.powf(octs)
}

/// Convert Hz to MIDI note number.
///
/// Matches `librosa.hz_to_midi(freq)`.
/// A440 = MIDI note 69.
pub fn hz_to_midi(freq: f64) -> f64 {
    12.0 * (freq / 440.0).log2() + 69.0
}

/// Convert MIDI note number to Hz.
///
/// Matches `librosa.midi_to_hz(note)`.
pub fn midi_to_hz(note: f64) -> f64 {
    440.0 * 2.0_f64.powf((note - 69.0) / 12.0)
}

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

    #[test]
    fn test_hz_to_octs() {
        assert!((hz_to_octs(440.0) - 4.0).abs() < 1e-12);
        assert!((hz_to_octs(880.0) - 5.0).abs() < 1e-12);
        assert!((hz_to_octs(220.0) - 3.0).abs() < 1e-12);
        assert!((hz_to_octs(27.5) - 0.0).abs() < 1e-12);
    }

    #[test]
    fn test_octs_to_hz() {
        assert!((octs_to_hz(4.0) - 440.0).abs() < 1e-10);
        assert!((octs_to_hz(5.0) - 880.0).abs() < 1e-10);
        assert!((octs_to_hz(0.0) - 27.5).abs() < 1e-10);
    }

    #[test]
    fn test_hz_to_octs_roundtrip() {
        for &freq in &[27.5, 55.0, 110.0, 220.0, 440.0, 880.0, 1760.0, 4186.0] {
            let roundtrip = octs_to_hz(hz_to_octs(freq));
            assert!(
                (roundtrip - freq).abs() < 1e-10,
                "roundtrip failed for {freq}: got {roundtrip}"
            );
        }
    }

    #[test]
    fn test_hz_to_midi() {
        assert!((hz_to_midi(440.0) - 69.0).abs() < 1e-12);
        assert!((hz_to_midi(880.0) - 81.0).abs() < 1e-12);
        assert!((hz_to_midi(261.6255653005986) - 60.0).abs() < 1e-10);
    }

    #[test]
    fn test_midi_to_hz() {
        assert!((midi_to_hz(69.0) - 440.0).abs() < 1e-10);
        assert!((midi_to_hz(81.0) - 880.0).abs() < 1e-10);
        assert!((midi_to_hz(60.0) - 261.6255653005986).abs() < 1e-10);
    }

    #[test]
    fn test_hz_midi_roundtrip() {
        for &note in &[21.0, 33.0, 45.0, 57.0, 69.0, 81.0, 93.0, 108.0] {
            let roundtrip = hz_to_midi(midi_to_hz(note));
            assert!(
                (roundtrip - note).abs() < 1e-10,
                "roundtrip failed for MIDI {note}: got {roundtrip}"
            );
        }
    }
}