sim_lib_pitch_ratio/
chord.rs1use std::collections::BTreeSet;
4
5use crate::{PitchRatio, PitchRatioError, RatioPolicy};
6
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
9pub enum MeanDialect {
10 #[default]
12 Standard,
13 LegacyTunedNoDivision,
15}
16
17#[derive(Clone, Debug, PartialEq)]
19pub struct RatioCoverage {
20 pub policy: RatioPolicy,
22 pub admitted_tones: usize,
24 pub rejected_tones: usize,
26 pub matrix_entries: usize,
28 pub distinct_intervals: usize,
30 pub octave_classes: usize,
32 pub rejected_intervals: usize,
34}
35
36#[derive(Clone, Debug, PartialEq)]
38pub struct RatioChordReport {
39 pub matrix: Vec<Vec<PitchRatio>>,
41 pub cost: f64,
43 pub covered: RatioCoverage,
45}
46
47pub fn analyze_ratio_chord(
49 tones: &[PitchRatio],
50 policy: RatioPolicy,
51) -> Result<RatioChordReport, PitchRatioError> {
52 analyze_ratio_chord_with_root(tones, 0, policy, 2.0, MeanDialect::Standard)
53}
54
55pub fn analyze_ratio_chord_with_root(
57 tones: &[PitchRatio],
58 root_index: usize,
59 policy: RatioPolicy,
60 mean_exponent: f64,
61 dialect: MeanDialect,
62) -> Result<RatioChordReport, PitchRatioError> {
63 let normalized = root_normalized_tones(tones, root_index, policy)?;
64 let matrix = ratio_interval_matrix(&normalized, policy)?;
65 let cost = generalized_mean_chord_cost(&matrix, policy, mean_exponent, dialect)?;
66 let covered = ratio_coverage(tones, &matrix, policy);
67 Ok(RatioChordReport {
68 matrix,
69 cost,
70 covered,
71 })
72}
73
74pub fn root_normalized_tones(
76 tones: &[PitchRatio],
77 root_index: usize,
78 policy: RatioPolicy,
79) -> Result<Vec<PitchRatio>, PitchRatioError> {
80 if tones.is_empty() {
81 return Err(PitchRatioError::EmptyChord);
82 }
83 let root = tones
84 .get(root_index)
85 .copied()
86 .ok_or(PitchRatioError::InvalidRootIndex {
87 root_index,
88 len: tones.len(),
89 })?;
90 tones
91 .iter()
92 .map(|tone| tone.divide(root)?.canonical(policy))
93 .collect()
94}
95
96pub fn ratio_interval_matrix(
98 tones: &[PitchRatio],
99 policy: RatioPolicy,
100) -> Result<Vec<Vec<PitchRatio>>, PitchRatioError> {
101 if tones.is_empty() {
102 return Err(PitchRatioError::EmptyChord);
103 }
104 tones
105 .iter()
106 .map(|from| {
107 tones
108 .iter()
109 .map(|to| to.divide(*from)?.canonical(policy))
110 .collect()
111 })
112 .collect()
113}
114
115pub fn generalized_mean_chord_cost(
117 matrix: &[Vec<PitchRatio>],
118 policy: RatioPolicy,
119 mean_exponent: f64,
120 dialect: MeanDialect,
121) -> Result<f64, PitchRatioError> {
122 if !mean_exponent.is_finite() || mean_exponent == 0.0 {
123 return Err(PitchRatioError::InvalidMeanExponent);
124 }
125 let mut sum = 0.0;
126 let mut count = 0usize;
127 for (row_index, row) in matrix.iter().enumerate() {
128 for (column_index, ratio) in row.iter().enumerate() {
129 if row_index == column_index {
130 continue;
131 }
132 let complexity = ratio_complexity(*ratio, policy)? as f64;
133 sum += complexity.powf(mean_exponent);
134 count += 1;
135 }
136 }
137 if count == 0 {
138 return Ok(0.0);
139 }
140 let mean_power = match dialect {
141 MeanDialect::Standard => sum / count as f64,
142 MeanDialect::LegacyTunedNoDivision => sum,
143 };
144 Ok(mean_power.powf(1.0 / mean_exponent))
145}
146
147pub fn ratio_coverage(
149 input_tones: &[PitchRatio],
150 matrix: &[Vec<PitchRatio>],
151 policy: RatioPolicy,
152) -> RatioCoverage {
153 let mut distinct_intervals = BTreeSet::new();
154 let mut octave_classes = BTreeSet::new();
155 let mut rejected_intervals = 0usize;
156
157 for ratio in matrix.iter().flatten().copied() {
158 distinct_intervals.insert(ratio);
159 if let Ok(canonical) = ratio.canonical(policy) {
160 octave_classes.insert(canonical);
161 } else {
162 rejected_intervals += 1;
163 }
164 }
165
166 let admitted_tones = input_tones
167 .iter()
168 .filter(|tone| tone.canonical(policy).is_ok())
169 .count();
170
171 RatioCoverage {
172 policy,
173 admitted_tones,
174 rejected_tones: input_tones.len().saturating_sub(admitted_tones),
175 matrix_entries: matrix.iter().map(Vec::len).sum(),
176 distinct_intervals: distinct_intervals.len(),
177 octave_classes: octave_classes.len(),
178 rejected_intervals,
179 }
180}
181
182fn ratio_complexity(ratio: PitchRatio, policy: RatioPolicy) -> Result<u32, PitchRatioError> {
183 Ok(ratio
184 .factor_vector(policy)?
185 .exponents
186 .iter()
187 .map(|exponent| u32::from(exponent.unsigned_abs()))
188 .sum())
189}