Skip to main content

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}