1use crate::{
2 FloatType, TuningSystem, UnsignedIntegerType,
3 tuningsystem::{get_frequency_at, get_ratio_at},
4};
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[must_use]
10pub enum AdaptiveTuningSystem {
11 Recursive {
20 root_tuning_system: TuningSystem,
22
23 local_tuning_system: TuningSystem,
25 },
26}
27
28pub const RECURSIVE_JI: AdaptiveTuningSystem = AdaptiveTuningSystem::Recursive {
37 root_tuning_system: TuningSystem::FiveLimit,
38 local_tuning_system: TuningSystem::FiveLimit,
39};
40
41impl AdaptiveTuningSystem {
42 pub fn frequency_at(
54 self,
55 context: FloatType,
56 index: FloatType,
57 size: Option<UnsignedIntegerType>,
58 ) -> FloatType {
59 match self {
60 Self::Recursive {
61 root_tuning_system,
62 local_tuning_system,
63 } => {
64 let root_ratio = get_ratio_at(root_tuning_system, context, size);
65 let local_frequency = get_frequency_at(local_tuning_system, index, size);
66
67 root_ratio * local_frequency
68 }
69 }
70 }
71
72 pub fn cents_at(
77 self,
78 context: FloatType,
79 index: FloatType,
80 size: Option<UnsignedIntegerType>,
81 ) -> FloatType {
82 let octave_size = size.unwrap_or_else(|| match self {
83 Self::Recursive {
84 root_tuning_system, ..
85 } => root_tuning_system.octave_size(),
86 });
87
88 let reference_frequency = get_frequency_at(
89 TuningSystem::EqualTemperament { octave_size },
90 context + index,
91 Some(octave_size),
92 );
93
94 let comparison_frequency = self.frequency_at(context, index, size);
95
96 1200.0 * (comparison_frequency / reference_frequency).log2()
97 }
98
99 pub fn cents_vs_fixed_at(
103 self,
104 fixed_tuning_system: TuningSystem,
105 context: FloatType,
106 index: FloatType,
107 size: Option<UnsignedIntegerType>,
108 ) -> FloatType {
109 let fixed_frequency = get_frequency_at(fixed_tuning_system, context + index, size);
110 let comparison_frequency = self.frequency_at(context, index, size);
111
112 1200.0 * (comparison_frequency / fixed_frequency).log2()
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::tuningsystem::{AnyTuningSystem, CN1};
120
121 const CLOSE: FloatType = 1e-9;
123
124 #[test]
128 fn a_third_above_a_third_is_two_just_thirds() {
129 let stacked = RECURSIVE_JI.frequency_at(4.0, 4.0, None);
131 assert!(
132 (stacked - CN1 * 25.0 / 16.0).abs() < CLOSE,
133 "5/4 of 5/4 is 25/16, got {stacked}"
134 );
135
136 let fixed = RECURSIVE_JI.frequency_at(0.0, 8.0, None);
140 assert!((fixed - CN1 * 8.0 / 5.0).abs() < CLOSE, "got {fixed}");
141 assert!(
142 (1200.0 * (fixed / stacked).log2() - 41.059).abs() < 1e-3,
143 "the recursive and fixed readings of the same step should differ"
144 );
145 }
146
147 #[test]
154 fn a_triad_is_just_from_any_of_its_notes() {
155 let just = |context: FloatType, index: FloatType| {
156 RECURSIVE_JI.frequency_at(context, index, None) / CN1
157 };
158 assert!((just(0.0, 4.0) - 5.0 / 4.0).abs() < CLOSE);
160 assert!((just(0.0, 7.0) - 3.0 / 2.0).abs() < CLOSE);
161 assert!(
162 (just(4.0, 3.0) - 3.0 / 2.0).abs() < CLOSE,
163 "{}",
164 just(4.0, 3.0)
165 );
166 assert!((just(4.0, 8.0) - 2.0).abs() < CLOSE, "{}", just(4.0, 8.0));
167 assert!((just(7.0, 5.0) - 2.0).abs() < CLOSE, "{}", just(7.0, 5.0));
168 assert!(
169 (just(7.0, 9.0) - 5.0 / 2.0).abs() < CLOSE,
170 "{}",
171 just(7.0, 9.0)
172 );
173 assert!((just(3.0, 4.0) - 3.0 / 2.0).abs() < CLOSE);
175 assert!((just(3.0, 9.0) - 2.0).abs() < CLOSE);
176 assert!((just(7.0, 8.0) - 12.0 / 5.0).abs() < CLOSE);
177 }
178
179 #[test]
182 fn a_context_of_nothing_is_the_fixed_table() {
183 for index in [0.0, 4.0, 7.0, 11.0] {
184 let adaptive = RECURSIVE_JI.frequency_at(0.0, index, None);
185 let fixed = TuningSystem::FiveLimit.frequency_at(index);
186 assert!(
187 (adaptive - fixed).abs() < CLOSE,
188 "at index {index}: {adaptive} against {fixed}"
189 );
190 assert_eq!(
193 RECURSIVE_JI.cents_vs_fixed_at(TuningSystem::FiveLimit, 0.0, index, None),
194 0.0
195 );
196 }
197 }
198
199 #[test]
201 fn cents_at_says_how_far_the_stack_lands_from_the_piano() {
202 let stacked = RECURSIVE_JI.cents_at(4.0, 4.0, None);
204 assert!((stacked + 27.373).abs() < 1e-3, "{stacked}");
205
206 let fifth = RECURSIVE_JI.cents_at(0.0, 7.0, None);
208 assert!((fifth - 1.955).abs() < 1e-3, "{fifth}");
209
210 assert!(RECURSIVE_JI.cents_at(0.0, 0.0, None).abs() < CLOSE);
212 }
213
214 #[test]
217 fn cents_vs_fixed_shows_what_recursing_costs() {
218 let against_five_limit =
221 RECURSIVE_JI.cents_vs_fixed_at(TuningSystem::FiveLimit, 4.0, 4.0, None);
222 assert!(
223 (against_five_limit + 41.059).abs() < 1e-3,
224 "{against_five_limit}"
225 );
226
227 let twelve = TuningSystem::EqualTemperament { octave_size: 12 };
230 for (context, index) in [(4.0, 4.0), (0.0, 7.0), (7.0, 3.0)] {
231 assert!(
232 (RECURSIVE_JI.cents_vs_fixed_at(twelve, context, index, None)
233 - RECURSIVE_JI.cents_at(context, index, None))
234 .abs()
235 < 1e-9,
236 "at {context}, {index}"
237 );
238 }
239 }
240
241 #[test]
243 fn an_octave_size_given_matches_the_one_it_would_have_chosen() {
244 for (context, index) in [(4.0, 4.0), (0.0, 7.0), (2.0, 9.0)] {
245 assert!(
246 (RECURSIVE_JI.cents_at(context, index, Some(12))
247 - RECURSIVE_JI.cents_at(context, index, None))
248 .abs()
249 < 1e-9
250 );
251 assert!(
252 (RECURSIVE_JI.frequency_at(context, index, Some(12))
253 - RECURSIVE_JI.frequency_at(context, index, None))
254 .abs()
255 < CLOSE
256 );
257 }
258 }
259
260 #[test]
262 fn the_root_and_the_local_table_are_separate() {
263 let mixed = AdaptiveTuningSystem::Recursive {
264 root_tuning_system: TuningSystem::PythagoreanTuning,
265 local_tuning_system: TuningSystem::CarlosHarmonic,
266 };
267 let sounded = mixed.frequency_at(7.0, 10.0, None);
270 assert!(
271 (sounded - CN1 * (3.0 / 2.0) * (7.0 / 4.0)).abs() < CLOSE,
272 "{sounded}"
273 );
274 assert_ne!(mixed, RECURSIVE_JI);
275 }
276
277 #[test]
279 fn an_adaptive_system_answers_through_the_wrapper_too() {
280 let any: AnyTuningSystem = RECURSIVE_JI.into();
281 assert!(any.is_adaptive());
282 assert!(
283 (any.frequency_at(4.0, 4.0, None) - RECURSIVE_JI.frequency_at(4.0, 4.0, None)).abs()
284 < CLOSE
285 );
286 assert!(
287 (any.cents_at(4.0, 4.0, None) - RECURSIVE_JI.cents_at(4.0, 4.0, None)).abs() < 1e-9
288 );
289
290 let fixed: AnyTuningSystem = TuningSystem::FiveLimit.into();
293 assert!(!fixed.is_adaptive());
294 assert!(
295 (fixed.frequency_at(4.0, 4.0, None) - fixed.frequency_at(0.0, 4.0, None)).abs() < CLOSE
296 );
297 assert!((any.frequency_at(4.0, 4.0, None) - any.frequency_at(0.0, 4.0, None)).abs() > 0.4);
298 }
299}