1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
//! Custom tuning tables for non-Western instruments.
//!
//! Standard MIDI assumes 12-tone equal temperament (12-TET).
//! Many instruments — Ethiopian, Indian, Arabic, Turkish, gamelan —
//! use different tuning systems. This module lets you define the exact
//! frequency for each MIDI note number.
use serde::{Deserialize, Serialize};
/// Maps MIDI note numbers (0–127) to frequencies in Hz.
/// Stored as Vec<f32> for serde compatibility.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TuningTable {
/// Frequency in Hz for each MIDI note 0–127.
pub frequencies: Vec<f32>,
/// Human-readable name.
pub name: String,
/// Description of the tuning system.
pub description: String,
}
impl TuningTable {
/// Standard 12-tone equal temperament.
/// A4 (MIDI note 69) = concert_a Hz (typically 440.0).
pub fn equal_temperament(concert_a: f32) -> Self {
let mut frequencies = vec![0.0f32; 128];
for (note, freq) in frequencies.iter_mut().enumerate() {
*freq = concert_a * 2.0f32.powf((note as f32 - 69.0) / 12.0);
}
Self {
frequencies,
name: "12-TET".into(),
description: "Standard 12-tone equal temperament, A4=440Hz".into(),
}
}
/// Build a tuning table from cents offsets per semitone within an octave.
/// `offsets` is a 12-element array of cent offsets from 12-TET for each
/// pitch class (C, C#, D, D#, E, F, F#, G, G#, A, A#, B).
pub fn from_cents_offsets(concert_a: f32, offsets: &[f32; 12]) -> Self {
let base = Self::equal_temperament(concert_a);
let mut frequencies = base.frequencies;
for (note, freq) in frequencies.iter_mut().enumerate().take(128) {
let pitch_class = note % 12;
let cents_offset = offsets[pitch_class];
*freq *= 2.0f32.powf(cents_offset / 1200.0);
}
Self {
frequencies,
name: "Custom".into(),
description: "Custom tuning with per-pitch-class cent offsets".into(),
}
}
/// Build from explicit frequency list. Length must be 128.
pub fn from_frequencies(freqs: Vec<f32>, name: &str, description: &str) -> Option<Self> {
if freqs.len() != 128 {
return None;
}
Some(Self {
frequencies: freqs,
name: name.into(),
description: description.into(),
})
}
/// Get frequency for a MIDI note number.
#[inline]
pub fn frequency(&self, note: u8) -> f32 {
self.frequencies.get(note as usize).copied().unwrap_or(0.0)
}
/// Convert frequency to the nearest MIDI note + cents deviation.
pub fn freq_to_note_cents(&self, freq: f32) -> (u8, f32) {
let mut best_note = 0u8;
let mut best_dist = f32::MAX;
for (i, &f) in self.frequencies.iter().enumerate() {
let dist = (freq - f).abs();
if dist < best_dist {
best_dist = dist;
best_note = i as u8;
}
}
let base_freq = self.frequencies[best_note as usize];
let cents = if base_freq > 0.0 {
1200.0 * (freq / base_freq).log2()
} else {
0.0
};
(best_note, cents)
}
/// Ethiopian Kiñit (pentatonic) approximation.
/// Uses the Tizita major scale pattern.
pub fn ethiopian_tizita(concert_a: f32) -> Self {
let offsets = [
0.0, // C — root
-50.0, // C# — slightly flat
0.0, // D
-30.0, // D# — slightly flat
0.0, // E
0.0, // F
-20.0, // F# — slightly flat
0.0, // G
-40.0, // G# — slightly flat
0.0, // A
-30.0, // A# — slightly flat
0.0, // B
];
let mut t = Self::from_cents_offsets(concert_a, &offsets);
t.name = "Ethiopian Tizita".into();
t.description = "Approximation of Ethiopian Tizita major pentatonic scale".into();
t
}
/// Just intonation (pure intervals based on harmonic series).
pub fn just_intonation(concert_a: f32) -> Self {
let ratios: [f32; 12] = [
1.0, 16.0/15.0, 9.0/8.0, 6.0/5.0, 5.0/4.0, 4.0/3.0,
45.0/32.0, 3.0/2.0, 8.0/5.0, 5.0/3.0, 9.0/5.0, 15.0/8.0,
];
let tet_ratios: [f32; 12] = [
1.0, 2.0f32.powf(1.0/12.0), 2.0f32.powf(2.0/12.0), 2.0f32.powf(3.0/12.0),
2.0f32.powf(4.0/12.0), 2.0f32.powf(5.0/12.0), 2.0f32.powf(6.0/12.0),
2.0f32.powf(7.0/12.0), 2.0f32.powf(8.0/12.0), 2.0f32.powf(9.0/12.0),
2.0f32.powf(10.0/12.0), 2.0f32.powf(11.0/12.0),
];
let offsets: [f32; 12] = std::array::from_fn(|i| {
1200.0 * (ratios[i] / tet_ratios[i]).log2()
});
let mut t = Self::from_cents_offsets(concert_a, &offsets);
t.name = "Just Intonation".into();
t.description = "Pure harmonic ratios — no beating on perfect intervals".into();
t
}
}
impl Default for TuningTable {
fn default() -> Self {
Self::equal_temperament(440.0)
}
}
// ── Additional world music tuning systems ─────────────────────────────────────
impl TuningTable {
/// Arabic Maqam Rast — the most common Arabic maqam.
/// Uses quarter-tone flats on the 3rd and 7th scale degrees.
pub fn arabic_maqam_rast(concert_a: f32) -> Self {
let offsets = [
0.0, // C — root (Rast)
0.0, // C#
0.0, // D — whole tone
-50.0, // D# — E half-flat (quarter tone flat)
0.0, // E
0.0, // F — perfect fourth
0.0, // F#
0.0, // G — perfect fifth
0.0, // G#
0.0, // A
-50.0, // A# — B half-flat (quarter tone flat)
0.0, // B
];
let mut t = Self::from_cents_offsets(concert_a, &offsets);
t.name = "Arabic Maqam Rast".into();
t.description = "Arabic Maqam Rast — quarter-tone flats on 3rd and 7th degrees".into();
t
}
/// Arabic Maqam Bayati — second most common Arabic maqam.
/// Characteristic half-flat on the 2nd degree.
pub fn arabic_maqam_bayati(concert_a: f32) -> Self {
let offsets = [
0.0, // C — root
-50.0, // C# — D half-flat (characteristic Bayati interval)
0.0, // D
-30.0, // D# — slightly flat
0.0, // E
0.0, // F
0.0, // F#
0.0, // G
0.0, // G#
0.0, // A
-50.0, // A# — B half-flat
0.0, // B
];
let mut t = Self::from_cents_offsets(concert_a, &offsets);
t.name = "Arabic Maqam Bayati".into();
t.description = "Arabic Maqam Bayati — half-flat on 2nd degree, characteristic of Arabic music".into();
t
}
/// Ethiopian Bati scale — minor pentatonic variant.
pub fn ethiopian_bati(concert_a: f32) -> Self {
let offsets = [
0.0, // C
0.0, // C#
-20.0, // D — slightly flat
0.0, // D#
0.0, // E
0.0, // F
-30.0, // F# — slightly flat
0.0, // G
0.0, // G#
-20.0, // A — slightly flat
0.0, // A#
0.0, // B
];
let mut t = Self::from_cents_offsets(concert_a, &offsets);
t.name = "Ethiopian Bati".into();
t.description = "Ethiopian Bati scale — minor pentatonic variant used in traditional music".into();
t
}
/// Indian Raga Yaman (Kalyan thaat) — the most common North Indian raga.
/// Uses a raised 4th (Ma tivra).
pub fn indian_raga_yaman(concert_a: f32) -> Self {
// Yaman uses all natural notes except F# (raised 4th)
// In just intonation ratios from Sa (root):
// Sa Re Ga Ma# Pa Dha Ni Sa
// 1 9/8 5/4 45/32 3/2 5/3 15/8 2
let offsets = [
0.0, // C — Sa
0.0, // C#
3.9, // D — Re (9/8 just = +3.9 cents from 12-TET)
0.0, // D#
-13.7, // E — Ga (5/4 just = -13.7 cents from 12-TET)
0.0, // F
-9.8, // F# — Ma# (45/32 just = -9.8 cents from 12-TET)
2.0, // G — Pa (3/2 just = +2.0 cents from 12-TET)
0.0, // G#
-15.6, // A — Dha (5/3 just = -15.6 cents from 12-TET)
0.0, // A#
-11.7, // B — Ni (15/8 just = -11.7 cents from 12-TET)
];
let mut t = Self::from_cents_offsets(concert_a, &offsets);
t.name = "Indian Raga Yaman".into();
t.description = "Indian Raga Yaman (Kalyan thaat) — raised 4th, just intonation".into();
t
}
/// Javanese Gamelan Slendro — 5-tone scale.
/// Approximate equal division of the octave into 5 parts.
pub fn gamelan_slendro(_concert_a: f32) -> Self {
// Slendro divides the octave into 5 roughly equal parts (~240 cents each)
// but with characteristic deviations. Using a common approximation.
let step = 1200.0 / 5.0; // 240 cents per step
let mut frequencies = vec![0.0f32; 128];
for (note, freq) in frequencies.iter_mut().enumerate() {
// Map MIDI notes to Slendro: every 2-3 semitones is one Slendro step
let slendro_step = (note as f32 / 2.4).floor();
let cents_from_c0 = slendro_step * step;
*freq = 16.352 * 2.0f32.powf(cents_from_c0 / 1200.0);
}
Self {
frequencies,
name: "Gamelan Slendro".into(),
description: "Javanese Gamelan Slendro — 5-tone scale, ~240 cents per step".into(),
}
}
/// Javanese Gamelan Pelog — 7-tone scale with characteristic large and small intervals.
pub fn gamelan_pelog(concert_a: f32) -> Self {
// Pelog has 7 tones with unequal steps. Common approximation in cents from root:
// 0, 120, 270, 540, 675, 785, 950, 1200
let pelog_cents = [0.0f32, 120.0, 270.0, 540.0, 675.0, 785.0, 950.0];
let mut frequencies = vec![0.0f32; 128];
for (note, freq) in frequencies.iter_mut().enumerate() {
let octave = note / 7;
let step = note % 7;
let cents = pelog_cents[step] + octave as f32 * 1200.0;
*freq = 16.352 * 2.0f32.powf(cents / 1200.0);
}
// Normalize so A4 (MIDI 69) = concert_a
let a4_freq = frequencies[69];
if a4_freq > 0.0 {
let ratio = concert_a / a4_freq;
for f in frequencies.iter_mut() { *f *= ratio; }
}
Self {
frequencies,
name: "Gamelan Pelog".into(),
description: "Javanese Gamelan Pelog — 7-tone scale with characteristic unequal intervals".into(),
}
}
}