rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
mod common;

#[test]
fn test_stft_magnitude_matches_librosa() {
    let ref_data = common::load_audio_reference("stft.json");
    let n_fft = ref_data["n_fft"].as_u64().unwrap() as usize;
    let hop_length = ref_data["hop_length"].as_u64().unwrap() as usize;
    let expected_n_bins = ref_data["n_bins"].as_u64().unwrap() as usize;
    let expected_n_frames = ref_data["n_frames"].as_u64().unwrap() as usize;

    let wav = common::wav_path("20_Pink Noise mono 500Hz_2kHz -20dBFS 48k.wav");
    let (samples, _sr) = common::read_wav_mono(&wav);

    let params = rosa::stft::StftParams::new(n_fft);
    assert_eq!(params.hop_length, hop_length);

    let (mag, phase) = rosa::stft::stft(&samples, &params);

    assert_eq!(
        mag.rows(),
        expected_n_bins,
        "n_bins mismatch: got {}, expected {}",
        mag.rows(),
        expected_n_bins
    );
    assert_eq!(
        mag.cols(),
        expected_n_frames,
        "n_frames mismatch: got {}, expected {}",
        mag.cols(),
        expected_n_frames
    );
    assert_eq!(mag.shape(), phase.shape(), "mag and phase shape mismatch");

    let frames = ref_data["frames"].as_array().unwrap();
    assert!(!frames.is_empty(), "No reference frames found");

    for frame_ref in frames {
        let idx = frame_ref["index"].as_u64().unwrap() as usize;
        let expected_mag: Vec<f64> =
            serde_json::from_value(frame_ref["magnitude"].clone()).unwrap();
        let expected_phase: Vec<f64> = serde_json::from_value(frame_ref["phase"].clone()).unwrap();

        assert_eq!(expected_mag.len(), expected_n_bins);
        assert_eq!(expected_phase.len(), expected_n_bins);

        // Compare magnitude
        let actual_mag: Vec<f64> = (0..expected_n_bins).map(|bin| mag[(bin, idx)]).collect();
        common::assert_approx_eq(
            &actual_mag,
            &expected_mag,
            1e-6,
            &format!("STFT magnitude frame {idx}"),
        );

        // Compare phase
        let actual_phase: Vec<f64> = (0..expected_n_bins).map(|bin| phase[(bin, idx)]).collect();
        common::assert_approx_eq(
            &actual_phase,
            &expected_phase,
            1e-6,
            &format!("STFT phase frame {idx}"),
        );
    }
}

#[test]
fn test_spectrogram_power_matches_librosa() {
    let ref_data = common::load_audio_reference("stft.json");
    let n_fft = ref_data["n_fft"].as_u64().unwrap() as usize;
    let expected_n_bins = ref_data["n_bins"].as_u64().unwrap() as usize;

    let wav = common::wav_path("20_Pink Noise mono 500Hz_2kHz -20dBFS 48k.wav");
    let (samples, _sr) = common::read_wav_mono(&wav);

    let params = rosa::stft::StftParams::new(n_fft);

    // power=2 should give |X|^2
    let power_spec = rosa::stft::spectrogram(&samples, &params, 2.0);

    let frames = ref_data["frames"].as_array().unwrap();
    for frame_ref in frames {
        let idx = frame_ref["index"].as_u64().unwrap() as usize;
        let mag: Vec<f64> = serde_json::from_value(frame_ref["magnitude"].clone()).unwrap();
        let expected_power: Vec<f64> = mag.iter().map(|m| m * m).collect();

        let actual_power: Vec<f64> = (0..expected_n_bins)
            .map(|bin| power_spec[(bin, idx)])
            .collect();
        common::assert_approx_eq(
            &actual_power,
            &expected_power,
            1e-6,
            &format!("spectrogram power frame {idx}"),
        );
    }
}