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 in 0..128u8 {
28 frequencies[note as usize] = 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 in 0..128usize {
44 let pitch_class = note % 12;
45 let cents_offset = offsets[pitch_class];
46 frequencies[note] *= 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}