use crate::error::AnalysisError;
use super::OnsetCandidate;
#[derive(Debug, Clone)]
pub struct OnsetConsensus {
pub energy_flux: Vec<usize>,
pub spectral_flux: Vec<usize>,
pub hfc: Vec<usize>,
pub hpss: Vec<usize>,
}
pub fn vote_onsets(
consensus: OnsetConsensus,
weights: [f32; 4],
tolerance_ms: u32,
sample_rate: u32,
) -> Result<Vec<OnsetCandidate>, AnalysisError> {
if sample_rate == 0 {
return Err(AnalysisError::InvalidInput("Sample rate must be > 0".to_string()));
}
if tolerance_ms == 0 {
return Err(AnalysisError::InvalidInput("Tolerance must be > 0".to_string()));
}
for &w in &weights {
if w < 0.0 {
return Err(AnalysisError::InvalidInput("Weights must be non-negative".to_string()));
}
}
log::debug!("Voting on onsets: energy_flux={}, spectral_flux={}, hfc={}, hpss={}, tolerance={}ms",
consensus.energy_flux.len(), consensus.spectral_flux.len(),
consensus.hfc.len(), consensus.hpss.len(), tolerance_ms);
let tolerance_samples = (tolerance_ms as f32 / 1000.0 * sample_rate as f32) as usize;
#[derive(Debug, Clone)]
struct OnsetWithMethod {
sample: usize,
method_idx: usize,
weight: f32,
}
let mut all_onsets = Vec::new();
for &sample in &consensus.energy_flux {
all_onsets.push(OnsetWithMethod {
sample,
method_idx: 0,
weight: weights[0],
});
}
for &sample in &consensus.spectral_flux {
all_onsets.push(OnsetWithMethod {
sample,
method_idx: 1,
weight: weights[1],
});
}
for &sample in &consensus.hfc {
all_onsets.push(OnsetWithMethod {
sample,
method_idx: 2,
weight: weights[2],
});
}
for &sample in &consensus.hpss {
all_onsets.push(OnsetWithMethod {
sample,
method_idx: 3,
weight: weights[3],
});
}
if all_onsets.is_empty() {
return Ok(Vec::new());
}
all_onsets.sort_by_key(|o| o.sample);
let mut clusters: Vec<Vec<&OnsetWithMethod>> = Vec::new();
for onset in &all_onsets {
let mut added = false;
for cluster in &mut clusters {
for existing_onset in cluster.iter() {
if (onset.sample as i32 - existing_onset.sample as i32).abs() as usize <= tolerance_samples {
cluster.push(onset);
added = true;
break;
}
}
if added {
break;
}
}
if !added {
clusters.push(vec![onset]);
}
}
let mut candidates = Vec::with_capacity(clusters.len());
for cluster in &clusters {
let cluster_center = cluster.iter()
.map(|o| o.sample)
.sum::<usize>() / cluster.len();
let mut total_weight = 0.0;
let mut voted_by_methods = [false; 4];
for onset in cluster {
total_weight += onset.weight;
voted_by_methods[onset.method_idx] = true;
}
let voted_by = voted_by_methods.iter().filter(|&&v| v).count() as u32;
let max_weight: f32 = weights.iter().sum();
let confidence = if max_weight > 0.0 {
(total_weight / max_weight).min(1.0).max(0.0)
} else {
0.0
};
let time_seconds = cluster_center as f32 / sample_rate as f32;
candidates.push(OnsetCandidate {
time_samples: cluster_center,
time_seconds,
confidence,
voted_by,
});
}
candidates.sort_by(|a, b| {
b.confidence.partial_cmp(&a.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
});
log::debug!("Consensus voting produced {} onset candidates", candidates.len());
Ok(candidates)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_consensus_voting_basic() {
let consensus = OnsetConsensus {
energy_flux: vec![1000],
spectral_flux: vec![1000],
hfc: vec![1000],
hpss: vec![1000],
};
let weights = [0.25, 0.25, 0.25, 0.25];
let candidates = vote_onsets(consensus, weights, 50, 44100).unwrap();
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0].time_samples, 1000);
assert_eq!(candidates[0].voted_by, 4);
assert!((candidates[0].confidence - 1.0).abs() < 0.01);
}
#[test]
fn test_consensus_voting_clustering() {
let consensus = OnsetConsensus {
energy_flux: vec![1000],
spectral_flux: vec![1050], hfc: vec![980],
hpss: vec![1020],
};
let weights = [0.25, 0.25, 0.25, 0.25];
let candidates = vote_onsets(consensus, weights, 50, 44100).unwrap();
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0].voted_by, 4);
assert!((candidates[0].confidence - 1.0).abs() < 0.01);
}
#[test]
fn test_consensus_voting_separate_onsets() {
let consensus = OnsetConsensus {
energy_flux: vec![1000, 50000],
spectral_flux: vec![1050, 50500],
hfc: vec![980, 50200],
hpss: vec![1020, 49900],
};
let weights = [0.25, 0.25, 0.25, 0.25];
let candidates = vote_onsets(consensus, weights, 50, 44100).unwrap();
assert_eq!(candidates.len(), 2);
assert_eq!(candidates[0].voted_by, 4);
assert_eq!(candidates[1].voted_by, 4);
}
#[test]
fn test_consensus_voting_partial_agreement() {
let consensus = OnsetConsensus {
energy_flux: vec![1000],
spectral_flux: vec![1050],
hfc: vec![], hpss: vec![], };
let weights = [0.3, 0.3, 0.2, 0.2];
let candidates = vote_onsets(consensus, weights, 50, 44100).unwrap();
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0].voted_by, 2);
assert!((candidates[0].confidence - 0.6).abs() < 0.01);
}
#[test]
fn test_consensus_voting_weighted() {
let consensus = OnsetConsensus {
energy_flux: vec![1000],
spectral_flux: vec![],
hfc: vec![],
hpss: vec![],
};
let weights = [0.5, 0.2, 0.2, 0.1];
let candidates = vote_onsets(consensus, weights, 50, 44100).unwrap();
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0].voted_by, 1);
assert!((candidates[0].confidence - 0.5).abs() < 0.01);
}
#[test]
fn test_consensus_voting_empty() {
let consensus = OnsetConsensus {
energy_flux: vec![],
spectral_flux: vec![],
hfc: vec![],
hpss: vec![],
};
let weights = [0.25, 0.25, 0.25, 0.25];
let candidates = vote_onsets(consensus, weights, 50, 44100).unwrap();
assert!(candidates.is_empty());
}
#[test]
fn test_consensus_voting_sorted_by_confidence() {
let consensus = OnsetConsensus {
energy_flux: vec![1000, 20000], spectral_flux: vec![1050, 20050],
hfc: vec![980, 20100],
hpss: vec![1020, 19950],
};
let mut consensus2 = consensus.clone();
consensus2.energy_flux.push(50000);
consensus2.spectral_flux.push(50500);
let weights = [0.25, 0.25, 0.25, 0.25];
let candidates = vote_onsets(consensus2, weights, 50, 44100).unwrap();
assert!(candidates.len() >= 2);
assert_eq!(candidates[0].voted_by, 4);
for i in 1..candidates.len() {
assert!(candidates[i].confidence <= candidates[i-1].confidence);
}
}
#[test]
fn test_consensus_voting_invalid_parameters() {
let consensus = OnsetConsensus {
energy_flux: vec![1000],
spectral_flux: vec![],
hfc: vec![],
hpss: vec![],
};
let weights = [0.25, 0.25, 0.25, 0.25];
let result = vote_onsets(consensus.clone(), weights, 50, 0);
assert!(result.is_err());
let result = vote_onsets(consensus.clone(), weights, 0, 44100);
assert!(result.is_err());
let negative_weights = [-0.1, 0.25, 0.25, 0.25];
let result = vote_onsets(consensus, negative_weights, 50, 44100);
assert!(result.is_err());
}
#[test]
fn test_consensus_voting_time_conversion() {
let consensus = OnsetConsensus {
energy_flux: vec![44100], spectral_flux: vec![],
hfc: vec![],
hpss: vec![],
};
let weights = [1.0, 0.0, 0.0, 0.0];
let candidates = vote_onsets(consensus, weights, 50, 44100).unwrap();
assert_eq!(candidates.len(), 1);
assert!((candidates[0].time_seconds - 1.0).abs() < 0.001);
}
}