1use crate::{FloatType, TuningSystem};
2
3#[derive(Clone, Copy, Debug, PartialEq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6#[must_use]
7pub enum AdaptiveTuningSystem {
8 Recursive {
17 root_tuning_system: TuningSystem,
19
20 local_tuning_system: TuningSystem,
22 },
23}
24
25pub const RECURSIVE_JI: AdaptiveTuningSystem = AdaptiveTuningSystem::Recursive {
34 root_tuning_system: TuningSystem::FiveLimit,
35 local_tuning_system: TuningSystem::FiveLimit,
36};
37
38impl AdaptiveTuningSystem {
39 pub fn frequency_at(self, context: FloatType, index: FloatType) -> FloatType {
51 match self {
52 Self::Recursive {
53 root_tuning_system,
54 local_tuning_system,
55 } => {
56 let root_ratio = root_tuning_system.ratio_at(context);
57 let local_frequency = local_tuning_system.frequency_at(index);
58
59 root_ratio * local_frequency
60 }
61 }
62 }
63
64 pub fn cents_at(self, context: FloatType, index: FloatType) -> FloatType {
69 let reference_frequency = match self {
70 Self::Recursive {
71 root_tuning_system, ..
72 } => TuningSystem::edo(root_tuning_system.degrees_per_period())
73 .frequency_at(context + index),
74 };
75
76 let comparison_frequency = self.frequency_at(context, index);
77
78 1200.0 * (comparison_frequency / reference_frequency).log2()
79 }
80
81 pub fn cents_vs_fixed_at(
85 self,
86 fixed_tuning_system: TuningSystem,
87 context: FloatType,
88 index: FloatType,
89 ) -> FloatType {
90 let fixed_frequency = fixed_tuning_system.frequency_at(context + index);
91 let comparison_frequency = self.frequency_at(context, index);
92
93 1200.0 * (comparison_frequency / fixed_frequency).log2()
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::tuningsystem::{AnyTuningSystem, CN1};
101
102 const CLOSE: FloatType = 1e-9;
104
105 #[test]
109 fn a_third_above_a_third_is_two_just_thirds() {
110 let stacked = RECURSIVE_JI.frequency_at(4.0, 4.0);
112 assert!(
113 (stacked - CN1 * 25.0 / 16.0).abs() < CLOSE,
114 "5/4 of 5/4 is 25/16, got {stacked}"
115 );
116
117 let fixed = RECURSIVE_JI.frequency_at(0.0, 8.0);
121 assert!((fixed - CN1 * 8.0 / 5.0).abs() < CLOSE, "got {fixed}");
122 assert!(
123 (1200.0 * (fixed / stacked).log2() - 41.059).abs() < 1e-3,
124 "the recursive and fixed readings of the same step should differ"
125 );
126 }
127
128 #[test]
135 fn a_triad_is_just_from_any_of_its_notes() {
136 let just =
137 |context: FloatType, index: FloatType| RECURSIVE_JI.frequency_at(context, index) / CN1;
138 assert!((just(0.0, 4.0) - 5.0 / 4.0).abs() < CLOSE);
140 assert!((just(0.0, 7.0) - 3.0 / 2.0).abs() < CLOSE);
141 assert!(
142 (just(4.0, 3.0) - 3.0 / 2.0).abs() < CLOSE,
143 "{}",
144 just(4.0, 3.0)
145 );
146 assert!((just(4.0, 8.0) - 2.0).abs() < CLOSE, "{}", just(4.0, 8.0));
147 assert!((just(7.0, 5.0) - 2.0).abs() < CLOSE, "{}", just(7.0, 5.0));
148 assert!(
149 (just(7.0, 9.0) - 5.0 / 2.0).abs() < CLOSE,
150 "{}",
151 just(7.0, 9.0)
152 );
153 assert!((just(3.0, 4.0) - 3.0 / 2.0).abs() < CLOSE);
155 assert!((just(3.0, 9.0) - 2.0).abs() < CLOSE);
156 assert!((just(7.0, 8.0) - 12.0 / 5.0).abs() < CLOSE);
157 }
158
159 #[test]
162 fn a_context_of_nothing_is_the_fixed_table() {
163 for index in [0.0, 4.0, 7.0, 11.0] {
164 let adaptive = RECURSIVE_JI.frequency_at(0.0, index);
165 let fixed = TuningSystem::FiveLimit.frequency_at(index);
166 assert!(
167 (adaptive - fixed).abs() < CLOSE,
168 "at index {index}: {adaptive} against {fixed}"
169 );
170 assert_eq!(
173 RECURSIVE_JI.cents_vs_fixed_at(TuningSystem::FiveLimit, 0.0, index),
174 0.0
175 );
176 }
177 }
178
179 #[test]
181 fn cents_at_says_how_far_the_stack_lands_from_the_piano() {
182 let stacked = RECURSIVE_JI.cents_at(4.0, 4.0);
184 assert!((stacked + 27.373).abs() < 1e-3, "{stacked}");
185
186 let fifth = RECURSIVE_JI.cents_at(0.0, 7.0);
188 assert!((fifth - 1.955).abs() < 1e-3, "{fifth}");
189
190 assert!(RECURSIVE_JI.cents_at(0.0, 0.0).abs() < CLOSE);
192 }
193
194 #[test]
197 fn cents_vs_fixed_shows_what_recursing_costs() {
198 let against_five_limit = RECURSIVE_JI.cents_vs_fixed_at(TuningSystem::FiveLimit, 4.0, 4.0);
201 assert!(
202 (against_five_limit + 41.059).abs() < 1e-3,
203 "{against_five_limit}"
204 );
205
206 let twelve = TuningSystem::EQUAL_TEMPERAMENT;
209 for (context, index) in [(4.0, 4.0), (0.0, 7.0), (7.0, 3.0)] {
210 assert!(
211 (RECURSIVE_JI.cents_vs_fixed_at(twelve, context, index)
212 - RECURSIVE_JI.cents_at(context, index))
213 .abs()
214 < 1e-9,
215 "at {context}, {index}"
216 );
217 }
218 }
219
220 #[test]
222 fn the_root_and_the_local_table_are_separate() {
223 let mixed = AdaptiveTuningSystem::Recursive {
224 root_tuning_system: TuningSystem::PythagoreanTuning,
225 local_tuning_system: TuningSystem::CarlosHarmonic,
226 };
227 let sounded = mixed.frequency_at(7.0, 10.0);
230 assert!(
231 (sounded - CN1 * (3.0 / 2.0) * (7.0 / 4.0)).abs() < CLOSE,
232 "{sounded}"
233 );
234 assert_ne!(mixed, RECURSIVE_JI);
235 }
236
237 #[test]
239 fn an_adaptive_system_answers_through_the_wrapper_too() {
240 let any: AnyTuningSystem = RECURSIVE_JI.into();
241 assert!(any.is_adaptive());
242 assert!((any.frequency_at(4.0, 4.0) - RECURSIVE_JI.frequency_at(4.0, 4.0)).abs() < CLOSE);
243 assert!((any.cents_at(4.0, 4.0) - RECURSIVE_JI.cents_at(4.0, 4.0)).abs() < 1e-9);
244
245 let fixed: AnyTuningSystem = TuningSystem::FiveLimit.into();
248 assert!(!fixed.is_adaptive());
249 assert!((fixed.frequency_at(4.0, 4.0) - fixed.frequency_at(0.0, 4.0)).abs() < CLOSE);
250 assert!((any.frequency_at(4.0, 4.0) - any.frequency_at(0.0, 4.0)).abs() > 0.4);
251 }
252}