#![warn(missing_docs)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::similar_names)]
#![allow(clippy::many_single_char_names)]
#![allow(clippy::unreadable_literal)]
#![allow(clippy::let_and_return)]
#![allow(clippy::if_same_then_else)]
#![allow(clippy::trivially_copy_pass_by_ref)]
#![allow(clippy::unused_self)]
#![allow(clippy::module_name_repetitions)]
#![allow(dead_code)]
pub mod audio_events;
pub mod audio_features;
pub mod beat;
pub mod beat_tracker;
pub mod beat_tracking;
pub mod chord;
pub mod chord_recognition;
pub mod chorus_detect;
pub mod cover_detect;
pub mod dynamic_range;
pub mod energy_contour;
pub mod fade_detect;
pub mod fingerprint;
pub mod genre;
pub mod genre_classifier;
pub mod genre_classify;
pub mod harmonic;
pub mod harmonic_analysis;
pub mod instrument;
pub mod instrument_detection;
pub mod key;
pub mod key_detection;
pub mod loudness;
pub mod melody;
pub mod midi;
pub mod mir_feature;
pub mod mood;
pub mod mood_detection;
pub mod music_summary;
pub mod onset_strength;
pub mod pitch_key;
pub mod pitch_track;
pub mod playlist;
pub mod playlist_gen;
pub mod rhythm;
pub mod rhythm_pattern;
pub mod segmentation;
pub mod similarity;
pub mod source_separation;
pub mod spectral;
pub mod spectral_contrast;
pub mod spectral_features;
pub mod streaming;
pub mod structure;
pub mod structure_analysis;
pub mod tempo;
pub mod tempo_map;
pub mod tuning_detect;
pub mod vocal_detect;
pub mod chromagram;
pub mod chroma_cache;
pub mod watermark;
pub mod watermark_detect;
pub mod audio_similarity;
pub mod cover_art_features;
pub mod dj_features;
pub mod genre_classify_new;
pub mod harmonic_percussive;
pub mod harmonic_spectral;
pub mod instrument_classifier;
pub mod lsh_similarity;
pub mod lyrics_align;
pub mod melody_extract;
pub mod melody_extractor;
pub mod multitrack;
pub mod onset_peak;
pub mod section_segmenter;
pub mod similarity_search;
pub mod subgenre;
pub mod tempo_stability;
pub mod thumbnail;
#[cfg(feature = "onnx")]
pub mod ml;
mod error;
mod types;
mod utils;
pub use error::{MirError, MirResult};
pub use midi::{AudioToMidi, AudioToMidiConfig, MidiNote, MidiTempo, MidiTranscription};
#[cfg(feature = "onnx")]
pub use ml::{
activate_and_rank, apply_activation, MusicTagger, MusicTags, TagActivation, TagActivationScore,
DEFAULT_TOP_K,
};
pub use streaming::{
StreamingAnalysisSummary, StreamingAnalyzer, StreamingConfig, StreamingFrameFeatures,
};
pub use types::{
AnalysisResult, BeatResult, ChordResult, FeatureSet, GenreResult, HarmonicResult, KeyResult,
LoudnessResult, MelodyResult, MoodResult, RhythmResult, SpectralResult, StructureResult,
TempoResult,
};
use rayon::prelude::*;
use std::collections::HashMap;
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct MirConfig {
pub window_size: usize,
pub hop_size: usize,
pub min_tempo: f32,
pub max_tempo: f32,
pub enable_beat_tracking: bool,
pub enable_key_detection: bool,
pub enable_chord_recognition: bool,
pub enable_melody_extraction: bool,
pub enable_structure_analysis: bool,
pub enable_genre_classification: bool,
pub enable_mood_detection: bool,
pub enable_spectral_features: bool,
pub enable_rhythm_features: bool,
pub enable_harmonic_analysis: bool,
pub confidence_threshold_tempo: f32,
pub confidence_threshold_key: f32,
pub confidence_threshold_chord: f32,
pub confidence_threshold_genre: f32,
pub confidence_threshold_mood: f32,
pub num_channels: u8,
}
impl Default for MirConfig {
fn default() -> Self {
Self {
window_size: 2048,
hop_size: 512,
min_tempo: 60.0,
max_tempo: 200.0,
enable_beat_tracking: true,
enable_key_detection: true,
enable_chord_recognition: true,
enable_melody_extraction: true,
enable_structure_analysis: true,
enable_genre_classification: true,
enable_mood_detection: true,
enable_spectral_features: true,
enable_rhythm_features: true,
enable_harmonic_analysis: true,
confidence_threshold_tempo: 0.0,
confidence_threshold_key: 0.0,
confidence_threshold_chord: 0.0,
confidence_threshold_genre: 0.0,
confidence_threshold_mood: 0.0,
num_channels: 1,
}
}
}
pub struct MirAnalyzer {
config: MirConfig,
}
impl MirAnalyzer {
#[must_use]
pub fn new(config: MirConfig) -> Self {
Self { config }
}
#[allow(clippy::too_many_lines)]
#[allow(clippy::cast_precision_loss)]
pub fn analyze(&self, samples: &[f32], sample_rate: f32) -> MirResult<AnalysisResult> {
let mono = if self.config.num_channels == 2 {
let half = samples.len() / 2;
let mut out = Vec::with_capacity(half);
for i in 0..half {
out.push((samples[i * 2] + samples[i * 2 + 1]) * 0.5);
}
out
} else {
self.to_mono(samples)
};
let tempo = if self.config.enable_beat_tracking {
let detector = tempo::TempoDetector::new(
sample_rate,
self.config.min_tempo,
self.config.max_tempo,
);
Some(detector.detect(&mono)?)
} else {
None
};
let beat = if self.config.enable_beat_tracking {
let tracker = beat::BeatTracker::new(sample_rate, self.config.hop_size);
Some(tracker.track(&mono, tempo.as_ref())?)
} else {
None
};
#[allow(clippy::large_enum_variant)]
enum BranchOutput {
Key(MirResult<Option<KeyResult>>),
Chord(MirResult<Option<ChordResult>>),
Melody(MirResult<Option<MelodyResult>>),
Structure(MirResult<Option<StructureResult>>),
Genre(MirResult<Option<GenreResult>>),
Mood(MirResult<Option<MoodResult>>),
Spectral(MirResult<Option<SpectralResult>>),
Rhythm(MirResult<Option<RhythmResult>>),
Harmonic(MirResult<Option<HarmonicResult>>),
}
let cfg = &self.config;
let mono_ref: &[f32] = &mono;
let results: Vec<BranchOutput> = (0_u8..9)
.into_par_iter()
.map(|branch| match branch {
0 => BranchOutput::Key(if cfg.enable_key_detection {
let det = key::KeyDetector::new(sample_rate, cfg.window_size);
det.detect(mono_ref).map(Some)
} else {
Ok(None)
}),
1 => BranchOutput::Chord(if cfg.enable_chord_recognition {
let rec =
chord::ChordRecognizer::new(sample_rate, cfg.window_size, cfg.hop_size);
rec.recognize(mono_ref).map(Some)
} else {
Ok(None)
}),
2 => BranchOutput::Melody(if cfg.enable_melody_extraction {
let ext =
melody::MelodyExtractor::new(sample_rate, cfg.window_size, cfg.hop_size);
ext.extract(mono_ref).map(Some)
} else {
Ok(None)
}),
3 => BranchOutput::Structure(if cfg.enable_structure_analysis {
let ana = structure::StructureAnalyzer::new(
sample_rate,
cfg.window_size,
cfg.hop_size,
);
ana.analyze(mono_ref).map(Some)
} else {
Ok(None)
}),
4 => BranchOutput::Genre(if cfg.enable_genre_classification {
let cls = genre::GenreClassifier::new(sample_rate);
cls.classify(mono_ref).map(Some)
} else {
Ok(None)
}),
5 => BranchOutput::Mood(if cfg.enable_mood_detection {
let det = mood::MoodDetector::new(sample_rate);
det.detect(mono_ref).map(Some)
} else {
Ok(None)
}),
6 => BranchOutput::Spectral(if cfg.enable_spectral_features {
let ana =
spectral::SpectralAnalyzer::new(sample_rate, cfg.window_size, cfg.hop_size);
ana.analyze(mono_ref).map(Some)
} else {
Ok(None)
}),
7 => BranchOutput::Rhythm(if cfg.enable_rhythm_features {
let ana = rhythm::RhythmAnalyzer::new(sample_rate, cfg.hop_size);
ana.analyze(mono_ref).map(Some)
} else {
Ok(None)
}),
_ => BranchOutput::Harmonic(if cfg.enable_harmonic_analysis {
let ana =
harmonic::HarmonicAnalyzer::new(sample_rate, cfg.window_size, cfg.hop_size);
ana.analyze(mono_ref).map(Some)
} else {
Ok(None)
}),
})
.collect();
let mut key_res: Option<KeyResult> = None;
let mut chord_res: Option<ChordResult> = None;
let mut melody_res: Option<MelodyResult> = None;
let mut structure_res: Option<StructureResult> = None;
let mut genre_res: Option<GenreResult> = None;
let mut mood_res: Option<MoodResult> = None;
let mut spectral_res: Option<SpectralResult> = None;
let mut rhythm_res: Option<RhythmResult> = None;
let mut harmonic_res: Option<HarmonicResult> = None;
for output in results {
match output {
BranchOutput::Key(r) => key_res = r?,
BranchOutput::Chord(r) => chord_res = r?,
BranchOutput::Melody(r) => melody_res = r?,
BranchOutput::Structure(r) => structure_res = r?,
BranchOutput::Genre(r) => genre_res = r?,
BranchOutput::Mood(r) => mood_res = r?,
BranchOutput::Spectral(r) => spectral_res = r?,
BranchOutput::Rhythm(r) => rhythm_res = r?,
BranchOutput::Harmonic(r) => harmonic_res = r?,
}
}
let tempo = tempo.and_then(|t| {
if t.confidence >= self.config.confidence_threshold_tempo {
Some(t)
} else {
None
}
});
let key = key_res.and_then(|k| {
if k.confidence >= self.config.confidence_threshold_key {
Some(k)
} else {
None
}
});
let chord = chord_res.map(|mut c| {
if self.config.confidence_threshold_chord > 0.0 {
c.chords
.retain(|ch| ch.confidence >= self.config.confidence_threshold_chord);
}
c
});
let genre = genre_res.and_then(|g| {
if g.top_genre_confidence >= self.config.confidence_threshold_genre {
Some(g)
} else {
None
}
});
let mood = mood_res.and_then(|m| {
if m.intensity >= self.config.confidence_threshold_mood {
Some(m)
} else {
None
}
});
Ok(AnalysisResult {
tempo,
beat,
key,
chord,
melody: melody_res,
structure: structure_res,
genre,
mood,
spectral: spectral_res,
rhythm: rhythm_res,
harmonic: harmonic_res,
sample_rate,
duration: mono.len() as f32 / sample_rate,
})
}
fn to_mono(&self, samples: &[f32]) -> Vec<f32> {
if samples.len() < 4 || samples.len() % 2 != 0 {
return samples.to_vec();
}
let half = samples.len() / 2;
let mut sum_l = 0.0_f64;
let mut sum_r = 0.0_f64;
let mut sum_ll = 0.0_f64;
let mut sum_rr = 0.0_f64;
let mut sum_lr = 0.0_f64;
let check_count = half.min(4096);
for i in 0..check_count {
let l = f64::from(samples[i * 2]);
let r = f64::from(samples[i * 2 + 1]);
sum_l += l;
sum_r += r;
sum_ll += l * l;
sum_rr += r * r;
sum_lr += l * r;
}
let n = check_count as f64;
let var_l = (sum_ll / n) - (sum_l / n).powi(2);
let var_r = (sum_rr / n) - (sum_r / n).powi(2);
if var_l < 1e-10 && var_r < 1e-10 {
return samples.to_vec();
}
let denom = (var_l * var_r).sqrt();
let correlation = if denom > 1e-12 {
((sum_lr / n) - (sum_l / n) * (sum_r / n)) / denom
} else {
1.0 };
if correlation > 0.98 {
return samples.to_vec();
}
let mut mono = Vec::with_capacity(half);
for i in 0..half {
mono.push((samples[i * 2] + samples[i * 2 + 1]) * 0.5);
}
mono
}
pub fn extract_features(
&self,
samples: &[f32],
sample_rate: f32,
features: FeatureSet,
) -> MirResult<HashMap<String, Vec<f32>>> {
let mono = self.to_mono(samples);
let mut result = HashMap::new();
if features.contains(FeatureSet::SPECTRAL) {
let analyzer = spectral::SpectralAnalyzer::new(
sample_rate,
self.config.window_size,
self.config.hop_size,
);
let spectral = analyzer.analyze(&mono)?;
result.insert("spectral_centroid".to_string(), spectral.centroid);
result.insert("spectral_rolloff".to_string(), spectral.rolloff);
result.insert("spectral_flux".to_string(), spectral.flux);
}
if features.contains(FeatureSet::RHYTHM) {
let analyzer = rhythm::RhythmAnalyzer::new(sample_rate, self.config.hop_size);
let rhythm = analyzer.analyze(&mono)?;
result.insert("onset_strength".to_string(), rhythm.onset_strength);
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f32::consts::TAU;
#[test]
fn test_mir_config_default() {
let config = MirConfig::default();
assert_eq!(config.window_size, 2048);
assert_eq!(config.hop_size, 512);
assert!(config.enable_beat_tracking);
assert!((config.confidence_threshold_tempo - 0.0).abs() < f32::EPSILON);
assert!((config.confidence_threshold_key - 0.0).abs() < f32::EPSILON);
assert_eq!(config.num_channels, 1);
}
#[test]
fn test_mir_analyzer_creation() {
let config = MirConfig::default();
let _analyzer = MirAnalyzer::new(config);
}
#[test]
fn test_analyze_silence() {
let config = MirConfig {
enable_beat_tracking: false, enable_genre_classification: false,
enable_structure_analysis: false,
..MirConfig::default()
};
let analyzer = MirAnalyzer::new(config);
let samples = vec![0.0_f32; 44100]; let result = analyzer.analyze(&samples, 44100.0);
assert!(result.is_ok());
}
#[test]
fn test_to_mono_mono_input() {
let config = MirConfig::default();
let analyzer = MirAnalyzer::new(config);
let mono = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = analyzer.to_mono(&mono);
assert_eq!(result.len(), 5);
}
#[test]
fn test_to_mono_stereo_detection() {
let config = MirConfig::default();
let analyzer = MirAnalyzer::new(config);
let sr = 44100.0;
let n = 8820;
let mut stereo = Vec::with_capacity(n * 2);
for i in 0..n {
let t = i as f32 / sr;
let left = (TAU * 440.0 * t).sin(); let right = (TAU * 554.37 * t).sin(); stereo.push(left);
stereo.push(right);
}
let result = analyzer.to_mono(&stereo);
assert_eq!(result.len(), n);
let expected = (stereo[0] + stereo[1]) * 0.5;
assert!((result[0] - expected).abs() < 1e-6);
}
#[test]
fn test_to_mono_identical_channels_treated_as_mono() {
let config = MirConfig::default();
let analyzer = MirAnalyzer::new(config);
let mut data = Vec::with_capacity(8000);
for i in 0..4000 {
let v = (i as f32 / 100.0).sin();
data.push(v);
data.push(v); }
let result = analyzer.to_mono(&data);
assert_eq!(result.len(), 8000);
}
#[test]
fn test_to_mono_short_signal() {
let config = MirConfig::default();
let analyzer = MirAnalyzer::new(config);
let short = vec![1.0, 2.0];
let result = analyzer.to_mono(&short);
assert_eq!(result.len(), 2);
}
#[test]
fn test_confidence_threshold_filters_tempo() {
let config = MirConfig {
enable_beat_tracking: true,
enable_key_detection: false,
enable_chord_recognition: false,
enable_melody_extraction: false,
enable_structure_analysis: false,
enable_genre_classification: false,
enable_mood_detection: false,
enable_spectral_features: false,
enable_rhythm_features: false,
enable_harmonic_analysis: false,
confidence_threshold_tempo: 0.999, ..MirConfig::default()
};
let analyzer = MirAnalyzer::new(config);
let sr = 44100.0;
let mut signal = Vec::new();
for i in 0..(sr as usize * 3) {
let t = i as f32 / sr;
signal.push((TAU * 440.0 * t).sin());
}
let result = analyzer.analyze(&signal, sr);
assert!(result.is_ok());
}
#[test]
fn test_confidence_threshold_zero_keeps_all() {
let config = MirConfig {
enable_beat_tracking: false,
enable_key_detection: true,
enable_chord_recognition: false,
enable_melody_extraction: false,
enable_structure_analysis: false,
enable_genre_classification: false,
enable_mood_detection: false,
enable_spectral_features: false,
enable_rhythm_features: false,
enable_harmonic_analysis: false,
confidence_threshold_key: 0.0, ..MirConfig::default()
};
let analyzer = MirAnalyzer::new(config);
let sr = 22050.0;
let mut signal = Vec::new();
for i in 0..(sr as usize * 2) {
let t = i as f32 / sr;
signal.push((TAU * 261.63 * t).sin()); }
let result = analyzer.analyze(&signal, sr);
assert!(result.is_ok());
let r = result.expect("should succeed");
assert!(r.key.is_some());
}
#[test]
fn test_num_channels_forced_stereo() {
let config = MirConfig {
num_channels: 2,
enable_beat_tracking: false,
enable_key_detection: false,
enable_chord_recognition: false,
enable_melody_extraction: false,
enable_structure_analysis: false,
enable_genre_classification: false,
enable_mood_detection: false,
enable_spectral_features: true,
enable_rhythm_features: false,
enable_harmonic_analysis: false,
..MirConfig::default()
};
let analyzer = MirAnalyzer::new(config);
let _stereo = vec![0.5, -0.5, 0.3, -0.3];
let sr = 44100.0;
let mut stereo_long = Vec::new();
for i in 0..44100 {
let t = i as f32 / sr;
stereo_long.push((TAU * 440.0 * t).sin());
stereo_long.push((TAU * 550.0 * t).sin());
}
let result = analyzer.analyze(&stereo_long, sr);
assert!(result.is_ok());
let r = result.expect("should succeed");
assert!((r.duration - 1.0).abs() < 0.1);
}
}