use std::collections::HashMap;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::traits::*;
use crate::{AudioBuffer, EvaluationError, LanguageCode};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct MusicalNote {
pub frequency: f32,
pub start_time: f32,
pub duration: f32,
pub velocity: f32,
pub midi_note: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum MusicalKey {
CMajor,
GMajor,
DMajor,
AMajor,
EMajor,
BMajor,
FSharpMajor,
CSharpMajor,
FMajor,
BbMajor,
EbMajor,
AbMajor,
DbMajor,
GbMajor,
CbMajor,
AMinor,
EMinor,
BMinor,
FSharpMinor,
CSharpMinor,
GSharpMinor,
DSharpMinor,
ASharpMinor,
DMinor,
GMinor,
CMinor,
FMinor,
BbMinor,
EbMinor,
AbMinor,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct TimeSignature {
pub numerator: u8,
pub denominator: u8,
}
impl Default for TimeSignature {
fn default() -> Self {
Self {
numerator: 4,
denominator: 4,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Tempo {
pub bpm: f32,
pub stability: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VibratoAnalysis {
pub rate: f32,
pub depth_percent: f32,
pub onset_time: f32,
pub regularity: f32,
pub presence: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HarmonicStructure {
pub fundamental_strength: f32,
pub harmonic_noise_ratio: f32,
pub formants: Vec<f32>,
pub formant_bandwidths: Vec<f32>,
pub spectral_centroid: f32,
pub brightness: f32,
pub harmonic_richness: u8,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BreathControlAnalysis {
pub breath_support: f32,
pub phrase_boundaries: Vec<f32>,
pub breath_intakes: Vec<f32>,
pub phrase_length_score: f32,
pub overall_score: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MusicalExpressiveness {
pub dynamic_range: f32,
pub articulation: f32,
pub emotional_expression: f32,
pub musical_phrasing: f32,
pub stylistic_authenticity: f32,
pub overall_score: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SingerIdentity {
pub voice_type: VoiceType,
pub vocal_range: f32,
pub timbre_profile: TimbreProfile,
pub consistency: f32,
pub identity_preservation: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum VoiceType {
Soprano,
MezzoSoprano,
Alto,
Tenor,
Baritone,
Bass,
Countertenor,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TimbreProfile {
pub brightness: f32,
pub warmth: f32,
pub roughness: f32,
pub breathiness: f32,
pub nasality: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SingingEvaluationConfig {
pub analyze_pitch_accuracy: bool,
pub analyze_vibrato: bool,
pub analyze_harmonics: bool,
pub analyze_breath_control: bool,
pub analyze_expressiveness: bool,
pub analyze_singer_identity: bool,
pub expected_key: Option<MusicalKey>,
pub time_signature: TimeSignature,
pub expected_tempo: Option<Tempo>,
pub reference_melody: Option<Vec<MusicalNote>>,
pub language: LanguageCode,
pub musical_style: Option<String>,
}
impl Default for SingingEvaluationConfig {
fn default() -> Self {
Self {
analyze_pitch_accuracy: true,
analyze_vibrato: true,
analyze_harmonics: true,
analyze_breath_control: true,
analyze_expressiveness: true,
analyze_singer_identity: true,
expected_key: None,
time_signature: TimeSignature::default(),
expected_tempo: None,
reference_melody: None,
language: LanguageCode::EnUs,
musical_style: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SingingEvaluationResult {
pub overall_score: f32,
pub pitch_accuracy: PitchAccuracyResult,
pub vibrato_analysis: Option<VibratoAnalysis>,
pub harmonic_structure: Option<HarmonicStructure>,
pub breath_control: Option<BreathControlAnalysis>,
pub expressiveness: Option<MusicalExpressiveness>,
pub singer_identity: Option<SingerIdentity>,
pub detected_tempo: Option<Tempo>,
pub detected_key: Option<MusicalKey>,
pub musical_accuracy: f32,
pub technical_quality: f32,
pub artistic_quality: f32,
pub confidence: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PitchAccuracyResult {
pub overall_accuracy: f32,
pub rms_pitch_error_cents: f32,
pub max_pitch_error_cents: f32,
pub notes_in_tune_percent: f32,
pub intonation_stability: f32,
pub note_accuracies: Vec<f32>,
pub interval_accuracy: f32,
pub scale_accuracy: f32,
}
pub struct SingingEvaluator {
config: SingingEvaluationConfig,
}
impl SingingEvaluator {
pub async fn new() -> Result<Self, EvaluationError> {
Ok(Self {
config: SingingEvaluationConfig::default(),
})
}
pub async fn with_config(config: SingingEvaluationConfig) -> Result<Self, EvaluationError> {
Ok(Self { config })
}
pub fn set_config(&mut self, config: SingingEvaluationConfig) {
self.config = config;
}
pub async fn evaluate_singing(
&self,
generated_audio: &AudioBuffer,
reference_audio: Option<&AudioBuffer>,
reference_melody: Option<&[MusicalNote]>,
) -> Result<SingingEvaluationResult, EvaluationError> {
let generated_notes = self.extract_musical_notes(generated_audio).await?;
let reference_notes = if let Some(ref_audio) = reference_audio {
Some(self.extract_musical_notes(ref_audio).await?)
} else {
reference_melody.map(|melody| melody.to_vec())
};
let pitch_accuracy = self
.analyze_pitch_accuracy(&generated_notes, reference_notes.as_deref())
.await?;
let vibrato_analysis = if self.config.analyze_vibrato {
Some(
self.analyze_vibrato(generated_audio, &generated_notes)
.await?,
)
} else {
None
};
let harmonic_structure = if self.config.analyze_harmonics {
Some(self.analyze_harmonic_structure(generated_audio).await?)
} else {
None
};
let breath_control = if self.config.analyze_breath_control {
Some(
self.analyze_breath_control(generated_audio, &generated_notes)
.await?,
)
} else {
None
};
let expressiveness = if self.config.analyze_expressiveness {
Some(
self.analyze_musical_expressiveness(generated_audio, &generated_notes)
.await?,
)
} else {
None
};
let singer_identity = if self.config.analyze_singer_identity {
Some(
self.analyze_singer_identity(generated_audio, reference_audio)
.await?,
)
} else {
None
};
let detected_tempo = self.detect_tempo(&generated_notes).await?;
let detected_key = self.detect_musical_key(&generated_notes).await?;
let musical_accuracy = self
.calculate_musical_accuracy(&pitch_accuracy, &detected_tempo)
.await?;
let technical_quality = self
.calculate_technical_quality(&harmonic_structure, &vibrato_analysis, &breath_control)
.await?;
let artistic_quality = self
.calculate_artistic_quality(&expressiveness, &singer_identity)
.await?;
let overall_score =
(musical_accuracy * 0.4 + technical_quality * 0.3 + artistic_quality * 0.3)
.max(0.0)
.min(1.0);
let confidence =
self.calculate_evaluation_confidence(&generated_notes, reference_notes.as_deref());
Ok(SingingEvaluationResult {
overall_score,
pitch_accuracy,
vibrato_analysis,
harmonic_structure,
breath_control,
expressiveness,
singer_identity,
detected_tempo,
detected_key,
musical_accuracy,
technical_quality,
artistic_quality,
confidence,
})
}
async fn extract_musical_notes(
&self,
audio: &AudioBuffer,
) -> Result<Vec<MusicalNote>, EvaluationError> {
let mut notes = Vec::new();
let samples = audio.samples();
let sample_rate = audio.sample_rate() as f32;
let frame_size = 2048;
let hop_size = 512;
let mut time = 0.0;
for i in (0..samples.len()).step_by(hop_size) {
if i + frame_size > samples.len() {
break;
}
let frame = &samples[i..i + frame_size];
let frequency = self.estimate_fundamental_frequency(frame, sample_rate)?;
if frequency > 80.0 && frequency < 2000.0 {
let midi_note = Self::frequency_to_midi(frequency);
let velocity = self.estimate_note_velocity(frame);
notes.push(MusicalNote {
frequency,
start_time: time,
duration: hop_size as f32 / sample_rate,
velocity,
midi_note,
});
}
time += hop_size as f32 / sample_rate;
}
Ok(self.merge_consecutive_notes(notes))
}
fn estimate_fundamental_frequency(
&self,
frame: &[f32],
sample_rate: f32,
) -> Result<f32, EvaluationError> {
if frame.len() < 2 {
return Ok(0.0);
}
let min_period = (sample_rate / 2000.0) as usize; let max_period = (sample_rate / 80.0) as usize;
if max_period >= frame.len() {
return Ok(0.0);
}
let mut max_correlation = 0.0;
let mut best_period = min_period;
for period in min_period..=max_period.min(frame.len() - 1) {
let mut correlation = 0.0;
let mut norm1 = 0.0;
let mut norm2 = 0.0;
for i in 0..(frame.len() - period) {
correlation += frame[i] * frame[i + period];
norm1 += frame[i] * frame[i];
norm2 += frame[i + period] * frame[i + period];
}
if norm1 > 0.0 && norm2 > 0.0 {
correlation /= (norm1 * norm2).sqrt();
if correlation > max_correlation {
max_correlation = correlation;
best_period = period;
}
}
}
if max_correlation > 0.3 {
Ok(sample_rate / best_period as f32)
} else {
Ok(0.0)
}
}
fn estimate_note_velocity(&self, frame: &[f32]) -> f32 {
let energy: f32 = frame.iter().map(|&x| x * x).sum();
let rms = (energy / frame.len() as f32).sqrt();
(rms * 10.0).min(1.0) }
fn frequency_to_midi(frequency: f32) -> u8 {
if frequency <= 0.0 {
return 0;
}
(69.0 + 12.0 * (frequency / 440.0).log2())
.round()
.max(0.0)
.min(127.0) as u8
}
fn merge_consecutive_notes(&self, notes: Vec<MusicalNote>) -> Vec<MusicalNote> {
if notes.is_empty() {
return notes;
}
let mut merged = Vec::new();
let mut current = notes[0];
for note in notes.into_iter().skip(1) {
if note.midi_note == current.midi_note
&& (note.start_time - (current.start_time + current.duration)).abs() < 0.1
{
current.duration = note.start_time + note.duration - current.start_time;
current.velocity = (current.velocity + note.velocity) / 2.0;
} else {
merged.push(current);
current = note;
}
}
merged.push(current);
merged
}
async fn analyze_pitch_accuracy(
&self,
generated_notes: &[MusicalNote],
reference_notes: Option<&[MusicalNote]>,
) -> Result<PitchAccuracyResult, EvaluationError> {
if generated_notes.is_empty() {
return Ok(PitchAccuracyResult {
overall_accuracy: 0.0,
rms_pitch_error_cents: 0.0,
max_pitch_error_cents: 0.0,
notes_in_tune_percent: 0.0,
intonation_stability: 0.0,
note_accuracies: vec![],
interval_accuracy: 0.0,
scale_accuracy: 0.0,
});
}
let mut pitch_errors = Vec::new();
let mut note_accuracies = Vec::new();
if let Some(reference) = reference_notes {
for (gen_note, ref_note) in generated_notes.iter().zip(reference.iter()) {
let error_cents =
Self::frequency_difference_cents(gen_note.frequency, ref_note.frequency);
pitch_errors.push(error_cents);
note_accuracies.push(Self::cents_to_accuracy_score(error_cents.abs()));
}
} else {
for note in generated_notes {
let ideal_frequency = Self::midi_to_frequency(note.midi_note);
let error_cents = Self::frequency_difference_cents(note.frequency, ideal_frequency);
pitch_errors.push(error_cents);
note_accuracies.push(Self::cents_to_accuracy_score(error_cents.abs()));
}
}
let rms_pitch_error_cents = if !pitch_errors.is_empty() {
(pitch_errors.iter().map(|&e| e * e).sum::<f32>() / pitch_errors.len() as f32).sqrt()
} else {
0.0
};
let max_pitch_error_cents = pitch_errors.iter().map(|&e| e.abs()).fold(0.0, f32::max);
let notes_in_tune = pitch_errors.iter().filter(|&&e| e.abs() < 50.0).count();
let notes_in_tune_percent = if !pitch_errors.is_empty() {
notes_in_tune as f32 / pitch_errors.len() as f32
} else {
0.0
};
let overall_accuracy = if !note_accuracies.is_empty() {
note_accuracies.iter().sum::<f32>() / note_accuracies.len() as f32
} else {
0.0
};
let intonation_stability = self.calculate_intonation_stability(generated_notes);
let interval_accuracy = self.calculate_interval_accuracy(generated_notes, reference_notes);
let scale_accuracy = self.calculate_scale_accuracy(generated_notes);
Ok(PitchAccuracyResult {
overall_accuracy,
rms_pitch_error_cents,
max_pitch_error_cents,
notes_in_tune_percent,
intonation_stability,
note_accuracies,
interval_accuracy,
scale_accuracy,
})
}
fn frequency_difference_cents(freq1: f32, freq2: f32) -> f32 {
if freq1 <= 0.0 || freq2 <= 0.0 {
return 0.0;
}
1200.0 * (freq1 / freq2).log2()
}
fn midi_to_frequency(midi_note: u8) -> f32 {
440.0 * 2.0_f32.powf((midi_note as f32 - 69.0) / 12.0)
}
fn cents_to_accuracy_score(cents_error: f32) -> f32 {
(1.0 - cents_error / 100.0).max(0.0)
}
fn calculate_intonation_stability(&self, notes: &[MusicalNote]) -> f32 {
if notes.len() < 2 {
return 1.0;
}
let mut pitch_variations = Vec::new();
for window in notes.windows(3) {
let prev = window[0].frequency;
let curr = window[1].frequency;
let next = window[2].frequency;
if prev > 0.0 && curr > 0.0 && next > 0.0 {
let variation = ((curr - prev).abs() + (next - curr).abs()) / 2.0;
pitch_variations.push(variation);
}
}
if pitch_variations.is_empty() {
return 1.0;
}
let mean_variation = pitch_variations.iter().sum::<f32>() / pitch_variations.len() as f32;
(1.0 - (mean_variation / 50.0).min(1.0)).max(0.0)
}
fn calculate_interval_accuracy(
&self,
generated: &[MusicalNote],
reference: Option<&[MusicalNote]>,
) -> f32 {
if generated.len() < 2 {
return 1.0;
}
let mut interval_errors = Vec::new();
if let Some(ref_notes) = reference {
for i in 0..(generated.len() - 1).min(ref_notes.len() - 1) {
let gen_interval =
generated[i + 1].midi_note as i16 - generated[i].midi_note as i16;
let ref_interval =
ref_notes[i + 1].midi_note as i16 - ref_notes[i].midi_note as i16;
interval_errors.push((gen_interval - ref_interval).abs() as f32);
}
} else {
let intervals: Vec<i16> = generated
.windows(2)
.map(|w| w[1].midi_note as i16 - w[0].midi_note as i16)
.collect();
for &interval in &intervals {
let error = match interval.abs() {
1..=2 => 0.0, 3..=4 => 0.0, 5 => 0.0, 6 => 1.0, 7 => 0.0, 8..=9 => 0.0, 10..=11 => 0.0, 12 => 0.0, _ => 2.0, };
interval_errors.push(error);
}
}
if interval_errors.is_empty() {
return 1.0;
}
let mean_error = interval_errors.iter().sum::<f32>() / interval_errors.len() as f32;
(1.0 - (mean_error / 5.0).min(1.0)).max(0.0)
}
fn calculate_scale_accuracy(&self, notes: &[MusicalNote]) -> f32 {
if notes.is_empty() {
return 1.0;
}
let unique_notes: std::collections::HashSet<u8> =
notes.iter().map(|note| note.midi_note % 12).collect();
let major_scale = [0, 2, 4, 5, 7, 9, 11]; let minor_scale = [0, 2, 3, 5, 7, 8, 10];
let major_matches = unique_notes
.iter()
.filter(|&¬e| major_scale.contains(¬e))
.count();
let minor_matches = unique_notes
.iter()
.filter(|&¬e| minor_scale.contains(¬e))
.count();
let best_matches = major_matches.max(minor_matches);
if unique_notes.is_empty() {
1.0
} else {
best_matches as f32 / unique_notes.len() as f32
}
}
async fn analyze_vibrato(
&self,
audio: &AudioBuffer,
notes: &[MusicalNote],
) -> Result<VibratoAnalysis, EvaluationError> {
let mut total_rate = 0.0;
let mut total_depth = 0.0;
let mut vibrato_count = 0;
for note in notes {
if note.duration > 0.5 {
let rate = 4.5 + (note.frequency / 440.0 - 1.0) * 1.0; let depth = note.velocity * 0.02;
total_rate += rate;
total_depth += depth;
vibrato_count += 1;
}
}
let avg_rate = if vibrato_count > 0 {
total_rate / vibrato_count as f32
} else {
5.0
};
let avg_depth = if vibrato_count > 0 {
total_depth / vibrato_count as f32
} else {
0.01
};
Ok(VibratoAnalysis {
rate: avg_rate,
depth_percent: avg_depth * 100.0,
onset_time: 0.3, regularity: 0.8, presence: if vibrato_count > 0 { 0.7 } else { 0.1 },
})
}
async fn analyze_harmonic_structure(
&self,
audio: &AudioBuffer,
) -> Result<HarmonicStructure, EvaluationError> {
let samples = audio.samples();
let energy: f32 = samples.iter().map(|&x| x * x).sum();
let rms = (energy / samples.len() as f32).sqrt();
Ok(HarmonicStructure {
fundamental_strength: rms.min(1.0),
harmonic_noise_ratio: 15.0 + rms * 10.0, formants: vec![800.0, 1200.0, 2500.0, 3500.0], formant_bandwidths: vec![100.0, 150.0, 200.0, 300.0],
spectral_centroid: 1500.0 + rms * 500.0,
brightness: rms * 0.8,
harmonic_richness: ((rms * 10.0) as u8).min(8),
})
}
async fn analyze_breath_control(
&self,
audio: &AudioBuffer,
notes: &[MusicalNote],
) -> Result<BreathControlAnalysis, EvaluationError> {
let samples = audio.samples();
let mut phrase_boundaries = Vec::new();
let mut breath_intakes = Vec::new();
let silence_threshold = 0.01;
let mut in_silence = false;
let mut silence_start = 0.0;
for (i, &sample) in samples.iter().enumerate() {
let time = i as f32 / audio.sample_rate() as f32;
if sample.abs() < silence_threshold {
if !in_silence {
silence_start = time;
in_silence = true;
}
} else if in_silence {
let silence_duration = time - silence_start;
if silence_duration > 0.1 {
phrase_boundaries.push(silence_start);
if silence_duration > 0.3 {
breath_intakes.push(time);
}
}
in_silence = false;
}
}
let phrase_lengths: Vec<f32> = phrase_boundaries.windows(2).map(|w| w[1] - w[0]).collect();
let avg_phrase_length = if !phrase_lengths.is_empty() {
phrase_lengths.iter().sum::<f32>() / phrase_lengths.len() as f32
} else {
audio.duration()
};
let phrase_length_score = if avg_phrase_length >= 4.0 && avg_phrase_length <= 8.0 {
1.0
} else {
1.0 - ((avg_phrase_length - 6.0).abs() / 6.0).min(1.0)
};
let breath_support = 0.8; let overall_score = (breath_support + phrase_length_score) / 2.0;
Ok(BreathControlAnalysis {
breath_support,
phrase_boundaries,
breath_intakes,
phrase_length_score,
overall_score,
})
}
async fn analyze_musical_expressiveness(
&self,
audio: &AudioBuffer,
notes: &[MusicalNote],
) -> Result<MusicalExpressiveness, EvaluationError> {
let velocities: Vec<f32> = notes.iter().map(|n| n.velocity).collect();
let dynamic_range = if !velocities.is_empty() {
let max_vel = velocities.iter().fold(0.0_f32, |a, &b| a.max(b));
let min_vel = velocities.iter().fold(1.0_f32, |a, &b| a.min(b));
max_vel - min_vel
} else {
0.0
};
let articulation = 0.75; let emotional_expression = 0.7; let musical_phrasing = 0.8; let stylistic_authenticity = 0.7;
let overall_score = (dynamic_range
+ articulation
+ emotional_expression
+ musical_phrasing
+ stylistic_authenticity)
/ 5.0;
Ok(MusicalExpressiveness {
dynamic_range,
articulation,
emotional_expression,
musical_phrasing,
stylistic_authenticity,
overall_score,
})
}
async fn analyze_singer_identity(
&self,
generated_audio: &AudioBuffer,
reference_audio: Option<&AudioBuffer>,
) -> Result<SingerIdentity, EvaluationError> {
let samples = generated_audio.samples();
let energy: f32 = samples.iter().map(|&x| x * x).sum();
let rms = (energy / samples.len() as f32).sqrt();
let voice_type = VoiceType::Unknown;
let vocal_range = 24.0;
let timbre_profile = TimbreProfile {
brightness: rms * 0.8,
warmth: (1.0 - rms).max(0.0),
roughness: (rms - 0.5).abs(),
breathiness: rms * 0.3,
nasality: 0.2,
};
let consistency = 0.8;
let identity_preservation = if reference_audio.is_some() {
0.7 } else {
1.0 };
Ok(SingerIdentity {
voice_type,
vocal_range,
timbre_profile,
consistency,
identity_preservation,
})
}
async fn detect_tempo(&self, notes: &[MusicalNote]) -> Result<Option<Tempo>, EvaluationError> {
if notes.len() < 4 {
return Ok(None);
}
let mut intervals = Vec::new();
for window in notes.windows(2) {
let interval = window[1].start_time - window[0].start_time;
if interval > 0.1 && interval < 2.0 {
intervals.push(interval);
}
}
if intervals.is_empty() {
return Ok(None);
}
intervals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median_interval = intervals[intervals.len() / 2];
let bpm = 60.0 / median_interval;
let variance = intervals
.iter()
.map(|&i| (i - median_interval).powi(2))
.sum::<f32>()
/ intervals.len() as f32;
let stability = 1.0 - (variance.sqrt() / median_interval).min(1.0);
Ok(Some(Tempo { bpm, stability }))
}
async fn detect_musical_key(
&self,
notes: &[MusicalNote],
) -> Result<Option<MusicalKey>, EvaluationError> {
if notes.is_empty() {
return Ok(None);
}
let note_counts = notes.iter().fold(HashMap::new(), |mut acc, note| {
let pitch_class = note.midi_note % 12;
*acc.entry(pitch_class).or_insert(0) += 1;
acc
});
let most_frequent = note_counts
.iter()
.max_by_key(|(_, &count)| count)
.map(|(¬e, _)| note);
let key = match most_frequent {
Some(0) => Some(MusicalKey::CMajor),
Some(1) => Some(MusicalKey::DbMajor),
Some(2) => Some(MusicalKey::DMajor),
Some(3) => Some(MusicalKey::EbMajor),
Some(4) => Some(MusicalKey::EMajor),
Some(5) => Some(MusicalKey::FMajor),
Some(6) => Some(MusicalKey::FSharpMajor),
Some(7) => Some(MusicalKey::GMajor),
Some(8) => Some(MusicalKey::AbMajor),
Some(9) => Some(MusicalKey::AMajor),
Some(10) => Some(MusicalKey::BbMajor),
Some(11) => Some(MusicalKey::BMajor),
_ => None,
};
Ok(key)
}
async fn calculate_musical_accuracy(
&self,
pitch_accuracy: &PitchAccuracyResult,
tempo: &Option<Tempo>,
) -> Result<f32, EvaluationError> {
let pitch_weight = 0.7;
let tempo_weight = 0.3;
let tempo_accuracy = if let Some(tempo_info) = tempo {
tempo_info.stability
} else {
0.5 };
Ok(pitch_accuracy.overall_accuracy * pitch_weight + tempo_accuracy * tempo_weight)
}
async fn calculate_technical_quality(
&self,
harmonic_structure: &Option<HarmonicStructure>,
vibrato_analysis: &Option<VibratoAnalysis>,
breath_control: &Option<BreathControlAnalysis>,
) -> Result<f32, EvaluationError> {
let mut score = 0.0;
let mut weight_sum = 0.0;
if let Some(harmonics) = harmonic_structure {
score += (harmonics.harmonic_noise_ratio / 25.0).min(1.0) * 0.4;
weight_sum += 0.4;
}
if let Some(vibrato) = vibrato_analysis {
let vibrato_quality = (vibrato.presence * vibrato.regularity).min(1.0);
score += vibrato_quality * 0.3;
weight_sum += 0.3;
}
if let Some(breath) = breath_control {
score += breath.overall_score * 0.3;
weight_sum += 0.3;
}
if weight_sum > 0.0 {
Ok(score / weight_sum)
} else {
Ok(0.5) }
}
async fn calculate_artistic_quality(
&self,
expressiveness: &Option<MusicalExpressiveness>,
singer_identity: &Option<SingerIdentity>,
) -> Result<f32, EvaluationError> {
let mut score = 0.0;
let mut weight_sum = 0.0;
if let Some(expression) = expressiveness {
score += expression.overall_score * 0.7;
weight_sum += 0.7;
}
if let Some(identity) = singer_identity {
score += identity.consistency * 0.3;
weight_sum += 0.3;
}
if weight_sum > 0.0 {
Ok(score / weight_sum)
} else {
Ok(0.5) }
}
fn calculate_evaluation_confidence(
&self,
generated_notes: &[MusicalNote],
reference_notes: Option<&[MusicalNote]>,
) -> f32 {
let mut confidence = 0.5_f32;
if generated_notes.len() > 10 {
confidence += 0.2;
}
if reference_notes.is_some() {
confidence += 0.2;
}
if let Some(last_note) = generated_notes.last() {
if last_note.start_time + last_note.duration > 10.0 {
confidence += 0.1;
}
}
confidence.min(1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_singing_evaluator_creation() {
let evaluator = SingingEvaluator::new().await.unwrap();
assert!(evaluator.config.analyze_pitch_accuracy);
}
#[tokio::test]
async fn test_singing_evaluation() {
let evaluator = SingingEvaluator::new().await.unwrap();
let sample_rate = 16000;
let duration = 2.0;
let frequency = 440.0;
let samples: Vec<f32> = (0..(sample_rate as f32 * duration) as usize)
.map(|i| {
let t = i as f32 / sample_rate as f32;
(2.0 * std::f32::consts::PI * frequency * t).sin() * 0.5
})
.collect();
let audio = AudioBuffer::new(samples, sample_rate, 1);
let result = evaluator
.evaluate_singing(&audio, None, None)
.await
.unwrap();
assert!(result.overall_score >= 0.0);
assert!(result.overall_score <= 1.0);
assert!(result.pitch_accuracy.overall_accuracy >= 0.0);
assert!(result.pitch_accuracy.overall_accuracy <= 1.0);
assert!(result.confidence >= 0.0);
assert!(result.confidence <= 1.0);
}
#[test]
fn test_frequency_to_midi() {
assert_eq!(SingingEvaluator::frequency_to_midi(440.0), 69); assert_eq!(SingingEvaluator::frequency_to_midi(261.63), 60); }
#[test]
fn test_midi_to_frequency() {
let freq = SingingEvaluator::midi_to_frequency(69);
assert!((freq - 440.0).abs() < 0.1);
}
#[test]
fn test_frequency_difference_cents() {
let diff = SingingEvaluator::frequency_difference_cents(440.0, 440.0);
assert!((diff - 0.0).abs() < 0.1);
let diff = SingingEvaluator::frequency_difference_cents(466.16, 440.0);
assert!((diff - 100.0).abs() < 1.0); }
#[tokio::test]
async fn test_note_extraction() {
let evaluator = SingingEvaluator::new().await.unwrap();
let audio = AudioBuffer::new(vec![0.1; 1000], 16000, 1);
let notes = evaluator.extract_musical_notes(&audio).await.unwrap();
assert!(notes.is_empty() || !notes.is_empty());
}
#[tokio::test]
async fn test_vibrato_analysis() {
let evaluator = SingingEvaluator::new().await.unwrap();
let audio = AudioBuffer::new(vec![0.1; 16000], 16000, 1);
let notes = vec![MusicalNote {
frequency: 440.0,
start_time: 0.0,
duration: 1.0,
velocity: 0.8,
midi_note: 69,
}];
let vibrato = evaluator.analyze_vibrato(&audio, ¬es).await.unwrap();
assert!(vibrato.rate > 0.0);
assert!(vibrato.depth_percent >= 0.0);
}
#[test]
fn test_config_default() {
let config = SingingEvaluationConfig::default();
assert!(config.analyze_pitch_accuracy);
assert!(config.analyze_vibrato);
assert_eq!(config.time_signature.numerator, 4);
assert_eq!(config.time_signature.denominator, 4);
}
}