use std::collections::BTreeSet;
use crate::{PitchRatio, PitchRatioError, RatioPolicy};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum MeanDialect {
#[default]
Standard,
LegacyTunedNoDivision,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RatioCoverage {
pub policy: RatioPolicy,
pub admitted_tones: usize,
pub rejected_tones: usize,
pub matrix_entries: usize,
pub distinct_intervals: usize,
pub octave_classes: usize,
pub rejected_intervals: usize,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RatioChordReport {
pub matrix: Vec<Vec<PitchRatio>>,
pub cost: f64,
pub covered: RatioCoverage,
}
pub fn analyze_ratio_chord(
tones: &[PitchRatio],
policy: RatioPolicy,
) -> Result<RatioChordReport, PitchRatioError> {
analyze_ratio_chord_with_root(tones, 0, policy, 2.0, MeanDialect::Standard)
}
pub fn analyze_ratio_chord_with_root(
tones: &[PitchRatio],
root_index: usize,
policy: RatioPolicy,
mean_exponent: f64,
dialect: MeanDialect,
) -> Result<RatioChordReport, PitchRatioError> {
let normalized = root_normalized_tones(tones, root_index, policy)?;
let matrix = ratio_interval_matrix(&normalized, policy)?;
let cost = generalized_mean_chord_cost(&matrix, policy, mean_exponent, dialect)?;
let covered = ratio_coverage(tones, &matrix, policy);
Ok(RatioChordReport {
matrix,
cost,
covered,
})
}
pub fn root_normalized_tones(
tones: &[PitchRatio],
root_index: usize,
policy: RatioPolicy,
) -> Result<Vec<PitchRatio>, PitchRatioError> {
if tones.is_empty() {
return Err(PitchRatioError::EmptyChord);
}
let root = tones
.get(root_index)
.copied()
.ok_or(PitchRatioError::InvalidRootIndex {
root_index,
len: tones.len(),
})?;
tones
.iter()
.map(|tone| tone.divide(root)?.canonical(policy))
.collect()
}
pub fn ratio_interval_matrix(
tones: &[PitchRatio],
policy: RatioPolicy,
) -> Result<Vec<Vec<PitchRatio>>, PitchRatioError> {
if tones.is_empty() {
return Err(PitchRatioError::EmptyChord);
}
tones
.iter()
.map(|from| {
tones
.iter()
.map(|to| to.divide(*from)?.canonical(policy))
.collect()
})
.collect()
}
pub fn generalized_mean_chord_cost(
matrix: &[Vec<PitchRatio>],
policy: RatioPolicy,
mean_exponent: f64,
dialect: MeanDialect,
) -> Result<f64, PitchRatioError> {
if !mean_exponent.is_finite() || mean_exponent == 0.0 {
return Err(PitchRatioError::InvalidMeanExponent);
}
let mut sum = 0.0;
let mut count = 0usize;
for (row_index, row) in matrix.iter().enumerate() {
for (column_index, ratio) in row.iter().enumerate() {
if row_index == column_index {
continue;
}
let complexity = ratio_complexity(*ratio, policy)? as f64;
sum += complexity.powf(mean_exponent);
count += 1;
}
}
if count == 0 {
return Ok(0.0);
}
let mean_power = match dialect {
MeanDialect::Standard => sum / count as f64,
MeanDialect::LegacyTunedNoDivision => sum,
};
Ok(mean_power.powf(1.0 / mean_exponent))
}
pub fn ratio_coverage(
input_tones: &[PitchRatio],
matrix: &[Vec<PitchRatio>],
policy: RatioPolicy,
) -> RatioCoverage {
let mut distinct_intervals = BTreeSet::new();
let mut octave_classes = BTreeSet::new();
let mut rejected_intervals = 0usize;
for ratio in matrix.iter().flatten().copied() {
distinct_intervals.insert(ratio);
if let Ok(canonical) = ratio.canonical(policy) {
octave_classes.insert(canonical);
} else {
rejected_intervals += 1;
}
}
let admitted_tones = input_tones
.iter()
.filter(|tone| tone.canonical(policy).is_ok())
.count();
RatioCoverage {
policy,
admitted_tones,
rejected_tones: input_tones.len().saturating_sub(admitted_tones),
matrix_entries: matrix.iter().map(Vec::len).sum(),
distinct_intervals: distinct_intervals.len(),
octave_classes: octave_classes.len(),
rejected_intervals,
}
}
fn ratio_complexity(ratio: PitchRatio, policy: RatioPolicy) -> Result<u32, PitchRatioError> {
Ok(ratio
.factor_vector(policy)?
.exponents
.iter()
.map(|exponent| u32::from(exponent.unsigned_abs()))
.sum())
}