1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct TuningTable {
14 pub frequencies: Vec<f32>,
16 pub name: String,
18 pub description: String,
20}
21
22impl TuningTable {
23 pub fn equal_temperament(concert_a: f32) -> Self {
26 let mut frequencies = vec![0.0f32; 128];
27 for (note, freq) in frequencies.iter_mut().enumerate() {
28 *freq = concert_a * 2.0f32.powf((note as f32 - 69.0) / 12.0);
29 }
30 Self {
31 frequencies,
32 name: "12-TET".into(),
33 description: "Standard 12-tone equal temperament, A4=440Hz".into(),
34 }
35 }
36
37 pub fn from_cents_offsets(concert_a: f32, offsets: &[f32; 12]) -> Self {
41 let base = Self::equal_temperament(concert_a);
42 let mut frequencies = base.frequencies;
43 for (note, freq) in frequencies.iter_mut().enumerate().take(128) {
44 let pitch_class = note % 12;
45 let cents_offset = offsets[pitch_class];
46 *freq *= 2.0f32.powf(cents_offset / 1200.0);
47 }
48 Self {
49 frequencies,
50 name: "Custom".into(),
51 description: "Custom tuning with per-pitch-class cent offsets".into(),
52 }
53 }
54
55 pub fn from_frequencies(freqs: Vec<f32>, name: &str, description: &str) -> Option<Self> {
57 if freqs.len() != 128 {
58 return None;
59 }
60 Some(Self {
61 frequencies: freqs,
62 name: name.into(),
63 description: description.into(),
64 })
65 }
66
67 #[inline]
69 pub fn frequency(&self, note: u8) -> f32 {
70 self.frequencies.get(note as usize).copied().unwrap_or(0.0)
71 }
72
73 pub fn freq_to_note_cents(&self, freq: f32) -> (u8, f32) {
75 let mut best_note = 0u8;
76 let mut best_dist = f32::MAX;
77 for (i, &f) in self.frequencies.iter().enumerate() {
78 let dist = (freq - f).abs();
79 if dist < best_dist {
80 best_dist = dist;
81 best_note = i as u8;
82 }
83 }
84 let base_freq = self.frequencies[best_note as usize];
85 let cents = if base_freq > 0.0 {
86 1200.0 * (freq / base_freq).log2()
87 } else {
88 0.0
89 };
90 (best_note, cents)
91 }
92
93 pub fn ethiopian_tizita(concert_a: f32) -> Self {
96 let offsets = [
97 0.0, -50.0, 0.0, -30.0, 0.0, 0.0, -20.0, 0.0, -40.0, 0.0, -30.0, 0.0, ];
110 let mut t = Self::from_cents_offsets(concert_a, &offsets);
111 t.name = "Ethiopian Tizita".into();
112 t.description = "Approximation of Ethiopian Tizita major pentatonic scale".into();
113 t
114 }
115
116 pub fn just_intonation(concert_a: f32) -> Self {
118 let ratios: [f32; 12] = [
119 1.0, 16.0/15.0, 9.0/8.0, 6.0/5.0, 5.0/4.0, 4.0/3.0,
120 45.0/32.0, 3.0/2.0, 8.0/5.0, 5.0/3.0, 9.0/5.0, 15.0/8.0,
121 ];
122 let tet_ratios: [f32; 12] = [
123 1.0, 2.0f32.powf(1.0/12.0), 2.0f32.powf(2.0/12.0), 2.0f32.powf(3.0/12.0),
124 2.0f32.powf(4.0/12.0), 2.0f32.powf(5.0/12.0), 2.0f32.powf(6.0/12.0),
125 2.0f32.powf(7.0/12.0), 2.0f32.powf(8.0/12.0), 2.0f32.powf(9.0/12.0),
126 2.0f32.powf(10.0/12.0), 2.0f32.powf(11.0/12.0),
127 ];
128 let offsets: [f32; 12] = std::array::from_fn(|i| {
129 1200.0 * (ratios[i] / tet_ratios[i]).log2()
130 });
131 let mut t = Self::from_cents_offsets(concert_a, &offsets);
132 t.name = "Just Intonation".into();
133 t.description = "Pure harmonic ratios — no beating on perfect intervals".into();
134 t
135 }
136}
137
138impl Default for TuningTable {
139 fn default() -> Self {
140 Self::equal_temperament(440.0)
141 }
142}
143
144impl TuningTable {
147 pub fn arabic_maqam_rast(concert_a: f32) -> Self {
150 let offsets = [
151 0.0, 0.0, 0.0, -50.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -50.0, 0.0, ];
164 let mut t = Self::from_cents_offsets(concert_a, &offsets);
165 t.name = "Arabic Maqam Rast".into();
166 t.description = "Arabic Maqam Rast — quarter-tone flats on 3rd and 7th degrees".into();
167 t
168 }
169
170 pub fn arabic_maqam_bayati(concert_a: f32) -> Self {
173 let offsets = [
174 0.0, -50.0, 0.0, -30.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -50.0, 0.0, ];
187 let mut t = Self::from_cents_offsets(concert_a, &offsets);
188 t.name = "Arabic Maqam Bayati".into();
189 t.description = "Arabic Maqam Bayati — half-flat on 2nd degree, characteristic of Arabic music".into();
190 t
191 }
192
193 pub fn ethiopian_bati(concert_a: f32) -> Self {
195 let offsets = [
196 0.0, 0.0, -20.0, 0.0, 0.0, 0.0, -30.0, 0.0, 0.0, -20.0, 0.0, 0.0, ];
209 let mut t = Self::from_cents_offsets(concert_a, &offsets);
210 t.name = "Ethiopian Bati".into();
211 t.description = "Ethiopian Bati scale — minor pentatonic variant used in traditional music".into();
212 t
213 }
214
215 pub fn indian_raga_yaman(concert_a: f32) -> Self {
218 let offsets = [
223 0.0, 0.0, 3.9, 0.0, -13.7, 0.0, -9.8, 2.0, 0.0, -15.6, 0.0, -11.7, ];
236 let mut t = Self::from_cents_offsets(concert_a, &offsets);
237 t.name = "Indian Raga Yaman".into();
238 t.description = "Indian Raga Yaman (Kalyan thaat) — raised 4th, just intonation".into();
239 t
240 }
241
242 pub fn gamelan_slendro(_concert_a: f32) -> Self {
245 let step = 1200.0 / 5.0; let mut frequencies = vec![0.0f32; 128];
249 for (note, freq) in frequencies.iter_mut().enumerate() {
250 let slendro_step = (note as f32 / 2.4).floor();
252 let cents_from_c0 = slendro_step * step;
253 *freq = 16.352 * 2.0f32.powf(cents_from_c0 / 1200.0);
254 }
255 Self {
256 frequencies,
257 name: "Gamelan Slendro".into(),
258 description: "Javanese Gamelan Slendro — 5-tone scale, ~240 cents per step".into(),
259 }
260 }
261
262 pub fn gamelan_pelog(concert_a: f32) -> Self {
264 let pelog_cents = [0.0f32, 120.0, 270.0, 540.0, 675.0, 785.0, 950.0];
267 let mut frequencies = vec![0.0f32; 128];
268 for (note, freq) in frequencies.iter_mut().enumerate() {
269 let octave = note / 7;
270 let step = note % 7;
271 let cents = pelog_cents[step] + octave as f32 * 1200.0;
272 *freq = 16.352 * 2.0f32.powf(cents / 1200.0);
273 }
274 let a4_freq = frequencies[69];
276 if a4_freq > 0.0 {
277 let ratio = concert_a / a4_freq;
278 for f in frequencies.iter_mut() { *f *= ratio; }
279 }
280 Self {
281 frequencies,
282 name: "Gamelan Pelog".into(),
283 description: "Javanese Gamelan Pelog — 7-tone scale with characteristic unequal intervals".into(),
284 }
285 }
286}