aether_midi/tuning.rs
1//! Custom tuning tables for non-Western instruments.
2//!
3//! Standard MIDI assumes 12-tone equal temperament (12-TET).
4//! Many instruments — Ethiopian, Indian, Arabic, Turkish, gamelan —
5//! use different tuning systems. This module lets you define the exact
6//! frequency for each MIDI note number.
7//!
8//! ## Precision
9//!
10//! Frequencies are stored as `f32`, providing approximately 0.0001 Hz precision
11//! at 440 Hz. This is more than sufficient for audio applications, as human pitch
12//! discrimination is typically around 1 Hz at best. For extreme low-frequency
13//! accuracy (<1 Hz), consider using `f64` in custom implementations.
14//!
15//! ## Pitch-Bend Interaction
16//!
17//! When using tuning tables with MIDI pitch-bend:
18//! - Pitch-bend operates **relative to the tuned pitch**, not 12-TET
19//! - Example: A note tuned to 261.63 Hz with +200 cent bend becomes 261.63 * 2^(200/1200)
20//! - This preserves the tuning system's interval relationships
21//! - Vibrato and modulation also operate relative to the tuned frequency
22//!
23//! This ensures that microtonal music remains in the correct tuning system even
24//! when pitch-bend or vibrato is applied.
25
26use serde::{Deserialize, Serialize};
27
28/// Maps MIDI note numbers (0–127) to frequencies in Hz.
29/// Stored as `Vec<f32>` for serde compatibility.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct TuningTable {
32 /// Frequency in Hz for each MIDI note 0–127.
33 pub frequencies: Vec<f32>,
34 /// Human-readable name.
35 pub name: String,
36 /// Description of the tuning system.
37 pub description: String,
38}
39
40impl TuningTable {
41 /// Standard 12-tone equal temperament.
42 /// A4 (MIDI note 69) = concert_a Hz (typically 440.0).
43 pub fn equal_temperament(concert_a: f32) -> Self {
44 let mut frequencies = vec![0.0f32; 128];
45 for (note, freq) in frequencies.iter_mut().enumerate() {
46 *freq = concert_a * 2.0f32.powf((note as f32 - 69.0) / 12.0);
47 }
48 Self {
49 frequencies,
50 name: "12-TET".into(),
51 description: "Standard 12-tone equal temperament, A4=440Hz".into(),
52 }
53 }
54
55 /// Build a tuning table from cents offsets per semitone within an octave.
56 /// `offsets` is a 12-element array of cent offsets from 12-TET for each
57 /// pitch class (C, C#, D, D#, E, F, F#, G, G#, A, A#, B).
58 pub fn from_cents_offsets(concert_a: f32, offsets: &[f32; 12]) -> Self {
59 let base = Self::equal_temperament(concert_a);
60 let mut frequencies = base.frequencies;
61 for (note, freq) in frequencies.iter_mut().enumerate().take(128) {
62 let pitch_class = note % 12;
63 let cents_offset = offsets[pitch_class];
64 *freq *= 2.0f32.powf(cents_offset / 1200.0);
65 }
66 Self {
67 frequencies,
68 name: "Custom".into(),
69 description: "Custom tuning with per-pitch-class cent offsets".into(),
70 }
71 }
72
73 /// Build from explicit frequency list. Length must be 128.
74 pub fn from_frequencies(freqs: Vec<f32>, name: &str, description: &str) -> Option<Self> {
75 if freqs.len() != 128 {
76 return None;
77 }
78 Some(Self {
79 frequencies: freqs,
80 name: name.into(),
81 description: description.into(),
82 })
83 }
84
85 /// Get frequency for a MIDI note number.
86 #[inline]
87 pub fn frequency(&self, note: u8) -> f32 {
88 self.frequencies.get(note as usize).copied().unwrap_or(0.0)
89 }
90
91 /// Convert frequency to the nearest MIDI note + cents deviation.
92 pub fn freq_to_note_cents(&self, freq: f32) -> (u8, f32) {
93 let mut best_note = 0u8;
94 let mut best_dist = f32::MAX;
95 for (i, &f) in self.frequencies.iter().enumerate() {
96 let dist = (freq - f).abs();
97 if dist < best_dist {
98 best_dist = dist;
99 best_note = i as u8;
100 }
101 }
102 let base_freq = self.frequencies[best_note as usize];
103 let cents = if base_freq > 0.0 {
104 1200.0 * (freq / base_freq).log2()
105 } else {
106 0.0
107 };
108 (best_note, cents)
109 }
110
111 /// Ethiopian Tizita major — the most common Ethiopian qenet mode.
112 /// Scale pattern: C - D - E - G - A (major pentatonic).
113 /// Intervals: M2, M2, m3, M2, m3
114 ///
115 /// NOTE: This implementation uses 12-TET. Traditional Ethiopian performance
116 /// includes microtonal inflections and flexible intonation that cannot be
117 /// captured in fixed tuning tables. The scale is defined more by melodic
118 /// contour and emotional intent than exact intervals.
119 ///
120 /// Source: Ethiopian music theory documentation (Scribd, PubPub, Wikipedia).
121 /// Equivalent to Western major pentatonic when played in 12-TET.
122 pub fn ethiopian_tizita(concert_a: f32) -> Self {
123 // Tizita major uses the standard major pentatonic: C D E G A
124 // In 12-TET, this is exactly the major pentatonic scale
125 let offsets = [
126 0.0, // C — root
127 0.0, // C# (not used in pentatonic)
128 0.0, // D — major 2nd
129 0.0, // D# (not used)
130 0.0, // E — major 3rd
131 0.0, // F (not used)
132 0.0, // F# (not used)
133 0.0, // G — perfect 5th
134 0.0, // G# (not used)
135 0.0, // A — major 6th
136 0.0, // A# (not used)
137 0.0, // B (not used)
138 ];
139 let mut t = Self::from_cents_offsets(concert_a, &offsets);
140 t.name = "Ethiopian Tizita (major)".into();
141 t.description = "Ethiopian Tizita major — pentatonic scale, equivalent to Western major pentatonic (C-D-E-G-A)".into();
142 t
143 }
144
145 /// Ethiopian Tizita minor — nostalgic, melancholic variant of Tizita.
146 /// Scale pattern: C - D - Eb - G - Ab
147 /// Intervals: M2, m2, M3, m2, M3
148 ///
149 /// This scale expresses "tizita" (memory, nostalgia, longing) in its minor form,
150 /// commonly used in slower, more introspective Ethiopian music.
151 ///
152 /// NOTE: Traditional performance includes microtonal inflections.
153 ///
154 /// Source: Ethiopian music theory (PubPub 2022, pianoencyclopedia.com).
155 /// Pattern documented as C-D-Eb-G-Ab in academic sources.
156 pub fn ethiopian_tizita_minor(concert_a: f32) -> Self {
157 // Tizita minor: C D Eb G Ab
158 // Only the pentatonic degrees are active, others are chromatic passing tones
159 let offsets = [
160 0.0, // C — root
161 0.0, // C# (not used)
162 0.0, // D — major 2nd
163 0.0, // Eb — minor 3rd (enharmonic with D#)
164 0.0, // E (not used)
165 0.0, // F (not used)
166 0.0, // F# (not used)
167 0.0, // G — perfect 5th
168 0.0, // Ab — minor 6th (enharmonic with G#)
169 0.0, // A (not used)
170 0.0, // A# (not used)
171 0.0, // B (not used)
172 ];
173 let mut t = Self::from_cents_offsets(concert_a, &offsets);
174 t.name = "Ethiopian Tizita (minor)".into();
175 t.description = "Ethiopian Tizita minor — nostalgic pentatonic scale expressing longing and memory (C-D-Eb-G-Ab)".into();
176 t
177 }
178
179 /// Just intonation (5-limit) — pure intervals based on harmonic series.
180 /// Uses ratios with prime factors up to 5 (e.g., 3/2, 5/4).
181 ///
182 /// This produces perfectly consonant major thirds (5/4) and perfect fifths (3/2)
183 /// with no beating, unlike 12-TET which has slight detuning.
184 ///
185 /// Source: Traditional Western just intonation, documented since Ptolemy (2nd century).
186 /// Note: This is 5-limit JI. For septimal intervals (7/4, 7/6), see just_intonation_7_limit.
187 pub fn just_intonation(concert_a: f32) -> Self {
188 let ratios: [f32; 12] = [
189 1.0,
190 16.0 / 15.0,
191 9.0 / 8.0,
192 6.0 / 5.0,
193 5.0 / 4.0,
194 4.0 / 3.0,
195 45.0 / 32.0,
196 3.0 / 2.0,
197 8.0 / 5.0,
198 5.0 / 3.0,
199 9.0 / 5.0,
200 15.0 / 8.0,
201 ];
202 let tet_ratios: [f32; 12] = [
203 1.0,
204 2.0f32.powf(1.0 / 12.0),
205 2.0f32.powf(2.0 / 12.0),
206 2.0f32.powf(3.0 / 12.0),
207 2.0f32.powf(4.0 / 12.0),
208 2.0f32.powf(5.0 / 12.0),
209 2.0f32.powf(6.0 / 12.0),
210 2.0f32.powf(7.0 / 12.0),
211 2.0f32.powf(8.0 / 12.0),
212 2.0f32.powf(9.0 / 12.0),
213 2.0f32.powf(10.0 / 12.0),
214 2.0f32.powf(11.0 / 12.0),
215 ];
216 let offsets: [f32; 12] =
217 std::array::from_fn(|i| 1200.0 * (ratios[i] / tet_ratios[i]).log2());
218 let mut t = Self::from_cents_offsets(concert_a, &offsets);
219 t.name = "Just Intonation (5-limit)".into();
220 t.description = "Pure harmonic ratios — no beating on perfect intervals".into();
221 t
222 }
223
224 /// Just intonation (7-limit) — includes septimal intervals.
225 /// Uses ratios with prime factors up to 7 (e.g., 7/4, 7/6, 7/5).
226 ///
227 /// 7-limit JI adds septimal intervals that appear in blues, barbershop harmony,
228 /// and many non-Western musical traditions. The harmonic seventh (7/4) is
229 /// significantly flatter than the 12-TET minor seventh, creating a characteristic
230 /// "bluesy" sound.
231 ///
232 /// Key septimal intervals:
233 /// - 7/6: Septimal minor third (~267 cents, between minor and major third)
234 /// - 7/5: Septimal tritone (~583 cents, slightly flat of 12-TET tritone)
235 /// - 7/4: Harmonic seventh (~969 cents, much flatter than 12-TET minor 7th)
236 ///
237 /// Source: Extended just intonation theory, used by microtonal composers
238 /// (Harry Partch, Ben Johnston) and in blues/barbershop traditions.
239 pub fn just_intonation_7_limit(concert_a: f32) -> Self {
240 let ratios: [f32; 12] = [
241 1.0, // C — root (1/1)
242 16.0 / 15.0, // C# — minor semitone
243 9.0 / 8.0, // D — major second
244 7.0 / 6.0, // D# — septimal minor third
245 5.0 / 4.0, // E — major third
246 4.0 / 3.0, // F — perfect fourth
247 7.0 / 5.0, // F# — septimal tritone
248 3.0 / 2.0, // G — perfect fifth
249 8.0 / 5.0, // G# — minor sixth
250 5.0 / 3.0, // A — major sixth
251 7.0 / 4.0, // A# — harmonic seventh (characteristic septimal interval)
252 15.0 / 8.0, // B — major seventh
253 ];
254 let tet_ratios: [f32; 12] = [
255 1.0,
256 2.0f32.powf(1.0 / 12.0),
257 2.0f32.powf(2.0 / 12.0),
258 2.0f32.powf(3.0 / 12.0),
259 2.0f32.powf(4.0 / 12.0),
260 2.0f32.powf(5.0 / 12.0),
261 2.0f32.powf(6.0 / 12.0),
262 2.0f32.powf(7.0 / 12.0),
263 2.0f32.powf(8.0 / 12.0),
264 2.0f32.powf(9.0 / 12.0),
265 2.0f32.powf(10.0 / 12.0),
266 2.0f32.powf(11.0 / 12.0),
267 ];
268 let offsets: [f32; 12] =
269 std::array::from_fn(|i| 1200.0 * (ratios[i] / tet_ratios[i]).log2());
270 let mut t = Self::from_cents_offsets(concert_a, &offsets);
271 t.name = "Just Intonation (7-limit)".into();
272 t.description =
273 "Pure harmonic ratios with septimal intervals (7/4, 7/6, 7/5) — blues and barbershop"
274 .into();
275 t
276 }
277}
278
279impl Default for TuningTable {
280 fn default() -> Self {
281 Self::equal_temperament(440.0)
282 }
283}
284
285// ── Additional world music tuning systems ─────────────────────────────────────
286
287impl TuningTable {
288 /// Arabic Maqam Rast — the most common Arabic maqam.
289 /// Uses quarter-tone flats on the 3rd and 7th scale degrees.
290 ///
291 /// NOTE: This implementation uses 24-TET (50-cent quarter-tones), which is
292 /// the modern theoretical standard established by Mikhail Mishaqa (19th century).
293 /// Historical Arabic music theory (al-Farabi, al-Urmawi) used ratio-based
294 /// intervals. Performance practice often deviates from both systems based on
295 /// melodic context and regional tradition.
296 ///
297 /// Source: Modern 24-TET Arabic music theory (24-tone equal temperament).
298 pub fn arabic_maqam_rast(concert_a: f32) -> Self {
299 let offsets = [
300 0.0, // C — root (Rast)
301 0.0, // C#
302 0.0, // D — whole tone
303 -50.0, // D# — E half-flat (quarter tone flat)
304 0.0, // E
305 0.0, // F — perfect fourth
306 0.0, // F#
307 0.0, // G — perfect fifth
308 0.0, // G#
309 0.0, // A
310 -50.0, // A# — B half-flat (quarter tone flat)
311 0.0, // B
312 ];
313 let mut t = Self::from_cents_offsets(concert_a, &offsets);
314 t.name = "Arabic Maqam Rast".into();
315 t.description = "Arabic Maqam Rast — quarter-tone flats on 3rd and 7th degrees".into();
316 t
317 }
318
319 /// Arabic Maqam Bayati — second most common Arabic maqam.
320 /// Characteristic half-flat on the 2nd degree.
321 ///
322 /// NOTE: This implementation uses 24-TET (50-cent quarter-tones), which is
323 /// the modern theoretical standard. Performance practice varies by region
324 /// and melodic context.
325 ///
326 /// Source: Modern 24-TET Arabic music theory (24-tone equal temperament).
327 pub fn arabic_maqam_bayati(concert_a: f32) -> Self {
328 let offsets = [
329 0.0, // C — root
330 -50.0, // C# — D half-flat (characteristic Bayati interval)
331 0.0, // D
332 -30.0, // D# — slightly flat
333 0.0, // E
334 0.0, // F
335 0.0, // F#
336 0.0, // G
337 0.0, // G#
338 0.0, // A
339 -50.0, // A# — B half-flat
340 0.0, // B
341 ];
342 let mut t = Self::from_cents_offsets(concert_a, &offsets);
343 t.name = "Arabic Maqam Bayati".into();
344 t.description =
345 "Arabic Maqam Bayati — half-flat on 2nd degree, characteristic of Arabic music".into();
346 t
347 }
348
349 /// Arabic Maqam Hijaz — characteristic augmented 2nd interval.
350 /// Tetrachord pattern: semitone - augmented 2nd - semitone (1-3-1).
351 ///
352 /// The Hijaz tetrachord is one of the most distinctive sounds in Arabic music,
353 /// featuring a large augmented 2nd (300 cents) between the 2nd and 3rd degrees.
354 /// Also known as "Phrygian dominant" in Western theory and "Freygish" in Jewish music.
355 ///
356 /// Scale structure from root: C - Db - E - F - G - Ab - B - C
357 /// Intervals: semitone (100¢), augmented 2nd (300¢), semitone (100¢), whole tone (200¢),
358 /// semitone (100¢), augmented 2nd (300¢), semitone (100¢)
359 ///
360 /// Source: Traditional Arabic maqam theory, documented in maqamworld.com and
361 /// ethnomusicological literature.
362 pub fn arabic_maqam_hijaz(concert_a: f32) -> Self {
363 let offsets = [
364 0.0, // C — root (Hijaz)
365 0.0, // C# — Db (semitone above root)
366 0.0, // D
367 0.0, // D#
368 0.0, // E — augmented 2nd from Db (characteristic interval)
369 0.0, // F — semitone above E
370 0.0, // F#
371 0.0, // G — whole tone above F
372 -100.0, // G# — Ab (semitone above G)
373 0.0, // A
374 0.0, // A#
375 0.0, // B — augmented 2nd from Ab
376 ];
377 let mut t = Self::from_cents_offsets(concert_a, &offsets);
378 t.name = "Arabic Maqam Hijaz".into();
379 t.description =
380 "Arabic Maqam Hijaz — augmented 2nd between 2nd and 3rd degrees (1-3-1 tetrachord)"
381 .into();
382 t
383 }
384
385 /// Ethiopian Bati minor — the most common Bati variant.
386 /// Scale pattern: C - Eb - F - G - Bb (minor pentatonic)
387 /// Intervals: m3, M2, M2, m3, M2
388 ///
389 /// This is equivalent to the Western minor pentatonic scale and is the
390 /// standard "Bati" used in Ethiopian music. It expresses melancholy and depth.
391 ///
392 /// NOTE: Traditional performance includes microtonal inflections.
393 ///
394 /// Source: Ethiopian music theory. Documented as equivalent to Western
395 /// minor pentatonic in Timothy Johnson's research (Scribd 2018).
396 pub fn ethiopian_bati(concert_a: f32) -> Self {
397 // Bati minor is the standard Western minor pentatonic: C Eb F G Bb
398 let offsets = [
399 0.0, // C — root
400 0.0, // C# (not used)
401 0.0, // D (not used)
402 0.0, // Eb — minor 3rd
403 0.0, // E (not used)
404 0.0, // F — perfect 4th
405 0.0, // F# (not used)
406 0.0, // G — perfect 5th
407 0.0, // G# (not used)
408 0.0, // A (not used)
409 0.0, // Bb — minor 7th
410 0.0, // B (not used)
411 ];
412 let mut t = Self::from_cents_offsets(concert_a, &offsets);
413 t.name = "Ethiopian Bati (minor)".into();
414 t.description = "Ethiopian Bati minor — standard minor pentatonic scale expressing melancholy (C-Eb-F-G-Bb)".into();
415 t
416 }
417
418 /// Ethiopian Bati major — bright, uplifting variant of Bati.
419 /// Scale pattern: C - E - F - G - B
420 /// Intervals: M3, m2, M2, M3, m2
421 ///
422 /// This scale creates a distinctly Ethiopian sound through its unusual
423 /// interval structure, particularly the major third followed by a semitone.
424 /// Less common than Bati minor but used for more joyful or energetic pieces.
425 ///
426 /// NOTE: Traditional performance includes microtonal inflections.
427 ///
428 /// Source: Ethiopian music theory (PubPub 2022).
429 /// Pattern documented as C-E-F-G-B in academic sources.
430 pub fn ethiopian_bati_major(concert_a: f32) -> Self {
431 // Bati major: C E F G B
432 let offsets = [
433 0.0, // C — root
434 0.0, // C# (not used)
435 0.0, // D (not used)
436 0.0, // D# (not used)
437 0.0, // E — major 3rd
438 0.0, // F — perfect 4th
439 0.0, // F# (not used)
440 0.0, // G — perfect 5th
441 0.0, // G# (not used)
442 0.0, // A (not used)
443 0.0, // A# (not used)
444 0.0, // B — major 7th
445 ];
446 let mut t = Self::from_cents_offsets(concert_a, &offsets);
447 t.name = "Ethiopian Bati (major)".into();
448 t.description = "Ethiopian Bati major — bright pentatonic variant with characteristic semitone (C-E-F-G-B)".into();
449 t
450 }
451
452 /// Ethiopian Ambassel — pentatonic with raised 4th.
453 /// Scale pattern: C - Db - F - G - Ab
454 /// Intervals: m2, M3, M2, m2, M3
455 ///
456 /// Ambassel (also spelled Ambasel or Ambessel) is characterized by its
457 /// prominent use of the flat 2nd degree, creating a sound similar to
458 /// Phrygian mode. The raised 4th (F natural) distinguishes it from Bati.
459 ///
460 /// The scale structure creates characteristic "long intervals" (major 3rds)
461 /// that are a hallmark of Ethiopian pentatonic music.
462 ///
463 /// NOTE: Traditional performance includes microtonal inflections.
464 ///
465 /// Source: Wikipedia "Ambassel scale" (2025). Documented as pentatonic
466 /// subset of Phrygian: 1, ♭2, 4, 5, ♭6 (C-Db-F-G-Ab).
467 pub fn ethiopian_ambassel(concert_a: f32) -> Self {
468 // Ambassel: C Db F G Ab (1, b2, 4, 5, b6)
469 let offsets = [
470 0.0, // C — root
471 0.0, // Db — minor 2nd (enharmonic with C#)
472 0.0, // D (not used)
473 0.0, // D# (not used)
474 0.0, // E (not used)
475 0.0, // F — perfect 4th
476 0.0, // F# (not used)
477 0.0, // G — perfect 5th
478 0.0, // Ab — minor 6th (enharmonic with G#)
479 0.0, // A (not used)
480 0.0, // A# (not used)
481 0.0, // B (not used)
482 ];
483 let mut t = Self::from_cents_offsets(concert_a, &offsets);
484 t.name = "Ethiopian Ambassel".into();
485 t.description = "Ethiopian Ambassel — pentatonic with flat 2nd and characteristic long intervals (C-Db-F-G-Ab)".into();
486 t
487 }
488
489 /// Ethiopian Anchihoye — the fourth main qenet mode.
490 /// Scale pattern: C - D - F - G - A
491 /// Intervals: M2, m3, M2, M2, m3
492 ///
493 /// Anchihoye (also spelled Anchi Hoye or አንቺሆዬ in Amharic) is one of the
494 /// four fundamental qenet modes of Ethiopian music. This scale has a unique
495 /// character distinct from the other modes, with its specific pattern of
496 /// whole and minor third intervals.
497 ///
498 /// NOTE: Documentation on Anchihoye is limited compared to other qenet modes.
499 /// This implementation uses the most commonly referenced interval pattern,
500 /// but traditional performance practice may include microtonal variations.
501 ///
502 /// Source: Ethiopian music theory documentation (Scribd, Wikipedia "Qenet").
503 /// Pattern inferred from pentatonic analysis and Ethiopian musical traditions.
504 pub fn ethiopian_anchihoye(concert_a: f32) -> Self {
505 // Anchihoye: C D F G A (1, 2, 4, 5, 6)
506 // Similar to suspended pentatonic with no 3rd
507 let offsets = [
508 0.0, // C — root
509 0.0, // C# (not used)
510 0.0, // D — major 2nd
511 0.0, // D# (not used)
512 0.0, // E (not used)
513 0.0, // F — perfect 4th
514 0.0, // F# (not used)
515 0.0, // G — perfect 5th
516 0.0, // G# (not used)
517 0.0, // A — major 6th
518 0.0, // A# (not used)
519 0.0, // B (not used)
520 ];
521 let mut t = Self::from_cents_offsets(concert_a, &offsets);
522 t.name = "Ethiopian Anchihoye".into();
523 t.description = "Ethiopian Anchihoye — one of four main qenet modes, pentatonic without 3rd degree (C-D-F-G-A)".into();
524 t
525 }
526
527 /// Indian Raga Yaman (Kalyan thaat) — the most common North Indian raga.
528 /// Uses a raised 4th (Ma tivra).
529 ///
530 /// This implementation uses just intonation ratios from Sa (root):
531 /// Sa Re Ga Ma# Pa Dha Ni Sa = 1/1, 9/8, 5/4, 45/32, 3/2, 5/3, 15/8, 2/1
532 ///
533 /// Source: North Indian classical music theory, just intonation ratios.
534 pub fn indian_raga_yaman(concert_a: f32) -> Self {
535 // Yaman uses all natural notes except F# (raised 4th)
536 // In just intonation ratios from Sa (root):
537 // Sa Re Ga Ma# Pa Dha Ni Sa
538 // 1 9/8 5/4 45/32 3/2 5/3 15/8 2
539 let offsets = [
540 0.0, // C — Sa
541 0.0, // C#
542 3.9, // D — Re (9/8 just = +3.9 cents from 12-TET)
543 0.0, // D#
544 -13.7, // E — Ga (5/4 just = -13.7 cents from 12-TET)
545 0.0, // F
546 -9.8, // F# — Ma# (45/32 just = -9.8 cents from 12-TET)
547 2.0, // G — Pa (3/2 just = +2.0 cents from 12-TET)
548 0.0, // G#
549 -15.6, // A — Dha (5/3 just = -15.6 cents from 12-TET)
550 0.0, // A#
551 -11.7, // B — Ni (15/8 just = -11.7 cents from 12-TET)
552 ];
553 let mut t = Self::from_cents_offsets(concert_a, &offsets);
554 t.name = "Indian Raga Yaman".into();
555 t.description = "Indian Raga Yaman (Kalyan thaat) — raised 4th, just intonation".into();
556 t
557 }
558
559 /// Javanese Gamelan Slendro — 5-tone scale.
560 /// Approximate equal division of the octave into 5 parts.
561 ///
562 /// NOTE: This uses exact 2:1 octaves (1200 cents). Real gamelan ensembles
563 /// often have stretched octaves (~1210-1215 cents) due to inharmonic overtones
564 /// of bronze/iron bars. For stretched octave version, see gamelan_slendro_stretched.
565 ///
566 /// Source: Generic approximation. Real gamelan tunings vary by ensemble.
567 /// Reference: "On the Tuning and Stretched Octave of Javanese Gamelans" (2016).
568 pub fn gamelan_slendro(_concert_a: f32) -> Self {
569 // Slendro divides the octave into 5 roughly equal parts (~240 cents each)
570 // but with characteristic deviations. Using a common approximation.
571 let step = 1200.0 / 5.0; // 240 cents per step
572 let mut frequencies = vec![0.0f32; 128];
573 for (note, freq) in frequencies.iter_mut().enumerate() {
574 // Map MIDI notes to Slendro: every 2-3 semitones is one Slendro step
575 let slendro_step = (note as f32 / 2.4).floor();
576 let cents_from_c0 = slendro_step * step;
577 *freq = 16.352 * 2.0f32.powf(cents_from_c0 / 1200.0);
578 }
579 Self {
580 frequencies,
581 name: "Gamelan Slendro".into(),
582 description: "Javanese Gamelan Slendro — 5-tone scale, ~240 cents per step".into(),
583 }
584 }
585
586 /// Javanese Gamelan Slendro with stretched octave — ethnomusicologically accurate.
587 ///
588 /// Real Javanese gamelan instruments have stretched octaves due to the inharmonic
589 /// overtones of bronze and iron bars. Measurements show octaves ranging from
590 /// approximately 1210-1215 cents (not the Western 1200 cents).
591 ///
592 /// This implementation uses 1210-cent octaves, dividing them into 5 roughly equal
593 /// steps of ~242 cents each. This creates the characteristic "pseudo-octave" sound
594 /// of authentic gamelan.
595 ///
596 /// Source: "On the Tuning and Stretched Octave of Javanese Gamelans" (JHU Muse, 2016),
597 /// "Ombak and octave stretching in Balinese gamelan" (ResearchGate, 2020).
598 pub fn gamelan_slendro_stretched(_concert_a: f32) -> Self {
599 let octave_cents = 1210.0; // Stretched octave (measured from real ensembles)
600 let step = octave_cents / 5.0; // ~242 cents per step
601 let mut frequencies = vec![0.0f32; 128];
602 for (note, freq) in frequencies.iter_mut().enumerate() {
603 let slendro_step = (note as f32 / 2.4).floor();
604 let cents_from_c0 = slendro_step * step;
605 *freq = 16.352 * 2.0f32.powf(cents_from_c0 / 1200.0);
606 }
607 Self {
608 frequencies,
609 name: "Gamelan Slendro (Stretched)".into(),
610 description: "Javanese Gamelan Slendro with stretched octave (~1210 cents) — ethnomusicologically accurate".into(),
611 }
612 }
613
614 /// Javanese Gamelan Pelog — 7-tone scale with characteristic large and small intervals.
615 ///
616 /// NOTE: Pelog tuning varies dramatically between gamelan ensembles. This is
617 /// a generic approximation using commonly cited interval patterns. Real gamelan
618 /// instruments are tuned individually and not intended to match Western pitch
619 /// standards or other ensembles.
620 ///
621 /// For authentic reproduction, measure a specific ensemble or use documented
622 /// measurements from ethnomusicological studies.
623 ///
624 /// Source: Generic approximation. Reference: "Javanese Pelog Tunings Reconsidered" (1980).
625 pub fn gamelan_pelog(concert_a: f32) -> Self {
626 // Pelog has 7 tones with unequal steps. Common approximation in cents from root:
627 // 0, 120, 270, 540, 675, 785, 950, 1200
628 let pelog_cents = [0.0f32, 120.0, 270.0, 540.0, 675.0, 785.0, 950.0];
629 let mut frequencies = vec![0.0f32; 128];
630 for (note, freq) in frequencies.iter_mut().enumerate() {
631 let octave = note / 7;
632 let step = note % 7;
633 let cents = pelog_cents[step] + octave as f32 * 1200.0;
634 *freq = 16.352 * 2.0f32.powf(cents / 1200.0);
635 }
636 // Normalize so A4 (MIDI 69) = concert_a
637 let a4_freq = frequencies[69];
638 if a4_freq > 0.0 {
639 let ratio = concert_a / a4_freq;
640 for f in frequencies.iter_mut() {
641 *f *= ratio;
642 }
643 }
644 Self {
645 frequencies,
646 name: "Gamelan Pelog".into(),
647 description:
648 "Javanese Gamelan Pelog — 7-tone scale with characteristic unequal intervals".into(),
649 }
650 }
651}