dasp_rs/utils/
notation.rs

1/// Returns a list of all chromatic note names, ignoring the provided key.
2///
3/// # Arguments
4/// * `key` - Key signature (currently unused, e.g., "C:maj")
5/// * `_unicode` - Optional flag for Unicode accidentals (unused, defaults to None)
6/// * `_natural` - Optional flag for natural notes only (unused, defaults to None)
7///
8/// # Returns
9/// Returns a `Vec<String>` containing all 12 chromatic note names (C through B).
10///
11/// # Examples
12/// ```
13/// let notes = key_to_notes("C:maj", None, None);
14/// assert_eq!(notes, vec!["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]);
15/// ```
16pub fn key_to_notes(_key: &str, _unicode: Option<bool>, _natural: Option<bool>) -> Vec<String> {
17    let notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
18    notes.iter().map(|&n| n.to_string()).collect()
19}
20
21/// Converts a key signature to scale degrees.
22///
23/// # Arguments
24/// * `key` - Key signature in the format "tonic:mode" (e.g., "C:maj", "F#:min")
25///
26/// # Returns
27/// Returns a `Vec<usize>` containing the scale degrees (0-11) relative to the chromatic scale.
28///
29/// # Notes
30/// - Supports major ("maj", "major") and minor ("min", "minor") modes.
31/// - Defaults to major scale if mode is unspecified or unrecognized.
32/// - Tonic is case-insensitive and supports sharp/flat synonyms (e.g., "C#", "Db").
33///
34/// # Examples
35/// ```
36/// let degrees = key_to_degrees("C:maj");
37/// assert_eq!(degrees, vec![0, 2, 4, 5, 7, 9, 11]); // C major scale
38/// let degrees = key_to_degrees("F#:min");
39/// assert_eq!(degrees, vec![6, 8, 9, 11, 1, 2, 4]); // F# minor scale
40/// ```
41pub fn key_to_degrees(key: &str) -> Vec<usize> {
42    let key = key.to_lowercase();
43    let (tonic, mode) = key.split_once(':').unwrap_or((&key, "maj"));
44    let tonic_shift = match tonic {
45        "c" => 0, "c#" | "db" => 1, "d" => 2, "d#" | "eb" => 3, "e" => 4,
46        "f" => 5, "f#" | "gb" => 6, "g" => 7, "g#" | "ab" => 8, "a" => 9,
47        "a#" | "bb" => 10, "b" => 11, _ => 0,
48    };
49    let major = vec![0, 2, 4, 5, 7, 9, 11];
50    let minor = vec![0, 2, 3, 5, 7, 8, 10];
51    let degrees = match mode {
52        "maj" | "major" => major,
53        "min" | "minor" => minor,
54        _ => major,
55    };
56    degrees.into_iter().map(|d| (d + tonic_shift) % 12).collect()
57}
58
59/// Converts a melakarta raga index to Carnatic svara names.
60///
61/// # Arguments
62/// * `mela` - Melakarta raga index (1-72)
63/// * `abbr` - Optional flag for abbreviated notation (defaults to false)
64/// * `unicode` - Optional flag for Unicode transliteration (defaults to false)
65///
66/// # Returns
67/// Returns a `Vec<String>` containing svara names for the melakarta raga.
68///
69/// # Notes
70/// - If `mela` is out of range (1-72), defaults to a major scale-like pattern.
71/// - Abbreviated notation uses "S", "R1", etc.; full notation uses "Shadjam", "Shuddha Rishabham", etc.
72///
73/// # Examples
74/// ```
75/// let svaras = mela_to_svara(29, Some(true), None); // Dheerashankarabharanam
76/// assert_eq!(svaras, vec!["S", "R2", "G3", "M1", "P", "D2", "N3"]);
77/// let svaras = mela_to_svara(1, None, None); // Kanakangi
78/// assert_eq!(svaras, vec!["shadjam", "rishabham1", "gandharam1", "madhyamam1", "panchamam", "dhaivatam1", "nishadam1"]);
79/// ```
80pub fn mela_to_svara(mela: usize, abbr: Option<bool>, unicode: Option<bool>) -> Vec<String> {
81    let abbr = abbr.unwrap_or(false);
82    let unicode = unicode.unwrap_or(false);
83    let degrees = mela_to_degrees(mela);
84    let svara_full = if unicode {
85        vec!["ṣaḍjam", "ṛṣabham", "gāndhāram", "madhyamam", "pañcamam", "dhaivatam", "niṣādam"]
86    } else {
87        vec!["shadjam", "rishabham", "gandharam", "madhyamam", "panchamam", "dhaivatam", "nishadam"]
88    };
89    let mut result = Vec::new();
90    for (i, deg) in degrees.iter().enumerate() {
91        let base = match i {
92            0 => "S", 1..=3 => "R", 4..=6 => "G", 7 => "M", 8 => "P", 9..=11 => "D", 12..=14 => "N",
93            _ => "S",
94        };
95        let variant = match deg % 12 {
96            1 => "1", 2 => "2", 3 => "3", 5 => "1", 6 => "2", 7 => "3", 8 => "1", 9 => "2", 10 => "3", _ => "",
97        };
98        let name = if abbr {
99            format!("{}{}", base, variant)
100        } else {
101            let idx = match base {
102                "S" => 0, "R" => 1, "G" => 2, "M" => 3, "P" => 4, "D" => 5, "N" => 6, _ => 0,
103            };
104            format!("{}{}", svara_full[idx], if variant.is_empty() { "" } else { variant })
105        };
106        result.push(name);
107    }
108    result
109}
110
111/// Converts a melakarta raga index to scale degrees.
112///
113/// # Arguments
114/// * `mela` - Melakarta raga index (1-72)
115///
116/// # Returns
117/// Returns a `Vec<usize>` containing the scale degrees (0-11) for the melakarta raga.
118///
119/// # Notes
120/// - If `mela` is out of range (1-72), returns a default major scale (0, 2, 4, 5, 7, 9, 11).
121/// - Uses traditional melakarta rules to determine R, G, M, D, N positions.
122///
123/// # Examples
124/// ```
125/// let degrees = mela_to_degrees(29); // Dheerashankarabharanam
126/// assert_eq!(degrees, vec![0, 2, 4, 5, 7, 9, 11]);
127/// let degrees = mela_to_degrees(1); // Kanakangi
128/// assert_eq!(degrees, vec![0, 1, 2, 5, 7, 8, 10]);
129/// ```
130pub fn mela_to_degrees(mela: usize) -> Vec<usize> {
131    if !(1..=72).contains(&mela) { return vec![0, 2, 4, 5, 7, 9, 11]; }
132    let mela = mela - 1;
133    let r = (mela / 36) % 2;
134    let g = (mela / 18) % 2;
135    let m = (mela / 9) % 2;
136    let d = (mela / 3) % 3;
137    let n = mela % 3;
138    vec![
139        0,
140        if r == 0 { 1 } else { 2 + g },
141        if r == 0 { 2 + g } else { 4 },
142        5 + m,
143        7,
144        8 + d,
145        10 + n,
146    ]
147}
148
149/// Converts a Hindustani thaat to scale degrees.
150///
151/// # Arguments
152/// * `thaat` - Name of the thaat (e.g., "Bilaval", "Kafi")
153///
154/// # Returns
155/// Returns a `Vec<usize>` containing the scale degrees (0-11) for the thaat.
156///
157/// # Notes
158/// - Case-insensitive; defaults to Bilaval scale if thaat is unrecognized.
159/// - Recognizes 10 traditional thaats.
160///
161/// # Examples
162/// ```
163/// let degrees = thaat_to_degrees("Bilaval");
164/// assert_eq!(degrees, vec![0, 2, 4, 5, 7, 9, 11]);
165/// let degrees = thaat_to_degrees("Kafi");
166/// assert_eq!(degrees, vec![0, 2, 3, 5, 7, 9, 10]);
167/// ```
168pub fn thaat_to_degrees(thaat: &str) -> Vec<usize> {
169    match thaat.to_lowercase().as_str() {
170        "bilaval" => vec![0, 2, 4, 5, 7, 9, 11],
171        "kalyani" => vec![0, 2, 4, 6, 7, 9, 11],
172        "khamaj" => vec![0, 2, 4, 5, 7, 9, 10],
173        "bhairav" => vec![0, 1, 4, 5, 6, 9, 11],
174        "purvi" => vec![0, 1, 4, 6, 7, 9, 11],
175        "marwa" => vec![0, 1, 3, 6, 7, 9, 11],
176        "kafi" => vec![0, 2, 3, 5, 7, 9, 10],
177        "asavari" => vec![0, 2, 3, 5, 7, 8, 10],
178        "todi" => vec![0, 1, 3, 6, 7, 8, 11],
179        "bhoopali" => vec![0, 2, 4, 7, 9],
180        _ => vec![0, 2, 4, 5, 7, 9, 11],
181    }
182}
183
184/// Lists all 72 melakarta ragas with their indices and names.
185///
186/// # Returns
187/// Returns a `Vec<(usize, String)>` containing tuples of (index, name) for all melakarta ragas.
188///
189/// # Examples
190/// ```
191/// let melas = list_mela();
192/// assert_eq!(melas[0], (1, "Kanakangi".to_string()));
193/// assert_eq!(melas.len(), 72);
194/// ```
195pub fn list_mela() -> Vec<(usize, String)> {
196    let names = vec![
197        "Kanakangi", "Ratnangi", "Ganamurti", "Vanaspati", "Manavati", "Tanarupi",
198        "Senavati", "Hanumatodi", "Dhenuka", "Natakapriya", "Kokilapriya", "Rupavati",
199        "Gayakapriya", "Vakulabharanam", "Mayamalavagowla", "Chakravakam", "Suryakantam",
200        "Hatakambari", "Jhankaradhwani", "Natabhairavi", "Keeravani", "Kharaharapriya",
201        "Gourimanohari", "Varunapriya", "Mararanjani", "Charukesi", "Sarasangi",
202        "Harikambhoji", "Dheerasankarabharanam", "Naganandini", "Yagapriya", "Ragavardhini",
203        "Gangeyabhushani", "Vagadheeswari", "Shulini", "Chalanata", "Salagam", "Jalarnavam",
204        "Jhalavarali", "Navaneetam", "Pavani", "Raghupriya", "Gavambodhi", "Bhavapriya",
205        "Shubhapantuvarali", "Shadvidamargini", "Suvarnangi", "Divyamani", "Dhavalambari",
206        "Namanarayani", "Kamavardhini", "Ramapriya", "Gamanashrama", "Vishwambari",
207        "Shamalangi", "Shanmukhapriya", "Simhendramadhyamam", "Hemavati", "Dharmavati",
208        "Neetimati", "Kantamani", "Rishabhapriya", "Latangi", "Vachaspati", "Mechakalyani",
209        "Chitrambari", "Sucharitra", "Jyotiswarupini", "Dhatuvardhani", "Nasikabhushani",
210        "Kosalam", "Rasikapriya",
211    ];
212    names.into_iter().enumerate().map(|(i, name)| (i + 1, name.to_string())).collect()
213}
214
215/// Lists the 10 traditional Hindustani thaats.
216///
217/// # Returns
218/// Returns a `Vec<String>` containing the names of all 10 thaats.
219///
220/// # Examples
221/// ```
222/// let thaats = list_thaat();
223/// assert_eq!(thaats, vec!["Bilaval", "Kalyani", "Khamaj", "Bhairav", "Purvi", "Marwa", "Kafi", "Asavari", "Todi", "Bhoopali"]);
224/// ```
225pub fn list_thaat() -> Vec<String> {
226    vec![
227        "Bilaval".to_string(),
228        "Kalyani".to_string(),
229        "Khamaj".to_string(),
230        "Bhairav".to_string(),
231        "Purvi".to_string(),
232        "Marwa".to_string(),
233        "Kafi".to_string(),
234        "Asavari".to_string(),
235        "Todi".to_string(),
236        "Bhoopali".to_string(),
237    ]
238}
239
240/// Generates a note name based on a number of perfect fifths from a unison note.
241///
242/// # Arguments
243/// * `unison` - Starting note (e.g., "C", "F#")
244/// * `fifths` - Number of fifths (positive or negative)
245/// * `unicode` - Optional flag for Unicode accidentals (defaults to false)
246///
247/// # Returns
248/// Returns a `String` representing the resulting note name with octave (e.g., "G4", "F♯-1").
249///
250/// # Examples
251/// ```
252/// let note = fifths_to_note("C", 1, None);
253/// assert_eq!(note, "G");
254/// let note = fifths_to_note("C", 7, Some(true));
255/// assert_eq!(note, "F♯1");
256/// ```
257pub fn fifths_to_note(unison: &str, fifths: i32, unicode: Option<bool>) -> String {
258    let unicode = unicode.unwrap_or(false);
259    let semitones = (fifths * 7) % 12;
260    let octave_shift = (fifths * 7) / 12;
261    let base = match unison.to_lowercase().as_str() {
262        "c" => 0, "c#" | "db" => 1, "d" => 2, "d#" | "eb" => 3, "e" => 4,
263        "f" => 5, "f#" | "gb" => 6, "g" => 7, "g#" | "ab" => 8, "a" => 9,
264        "a#" | "bb" => 10, "b" => 11, _ => 0,
265    };
266    let note_idx = (base + semitones + 12) % 12;
267    let note = match note_idx {
268        0 => "C", 1 => if unicode { "C♯" } else { "C#" }, 2 => "D",
269        3 => if unicode { "D♯" } else { "D#" }, 4 => "E", 5 => "F",
270        6 => if unicode { "F♯" } else { "F#" }, 7 => "G",
271        8 => if unicode { "G♯" } else { "G#" }, 9 => "A",
272        10 => if unicode { "A♯" } else { "A#" }, 11 => "B",
273        _ => "C",
274    };
275    format!("{}{}", note, if octave_shift != 0 { octave_shift.to_string() } else { "".to_string() })
276}
277
278/// Converts an interval ratio to Functional Just System (FJS) notation.
279///
280/// # Arguments
281/// * `interval` - Interval ratio (e.g., 1.5 for a perfect fifth)
282/// * `unison` - Optional unison ratio (defaults to 1.0)
283///
284/// # Returns
285/// Returns a `String` representing the interval in FJS notation (e.g., "3/2").
286///
287/// # Notes
288/// - Recognizes common just intervals (1/1, 3/2, 4/3, 5/4, 6/5); otherwise approximates as a fraction.
289///
290/// # Examples
291/// ```
292/// let fjs = interval_to_fjs(1.5, None);
293/// assert_eq!(fjs, "3/2");
294/// let fjs = interval_to_fjs(1.333, None);
295/// assert_eq!(fjs, "1.33/1");
296/// ```
297pub fn interval_to_fjs(interval: f32, unison: Option<f32>) -> String {
298    let unison = unison.unwrap_or(1.0);
299    let ratio = interval / unison;
300    match ratio {
301        r if (r - 1.0).abs() < 1e-6 => "1/1".to_string(),
302        r if (r - 3.0/2.0).abs() < 1e-6 => "3/2".to_string(),
303        r if (r - 4.0/3.0).abs() < 1e-6 => "4/3".to_string(),
304        r if (r - 5.0/4.0).abs() < 1e-6 => "5/4".to_string(),
305        r if (r - 6.0/5.0).abs() < 1e-6 => "6/5".to_string(),
306        _ => format!("{:.2}/1", ratio),
307    }
308}
309
310/// Generates frequencies based on a sequence of intervals.
311///
312/// # Arguments
313/// * `n_bins` - Number of frequency bins to generate
314/// * `fmin` - Starting frequency in Hz
315/// * `intervals` - Array of interval ratios
316///
317/// # Returns
318/// Returns a `Vec<f32>` containing frequencies generated by applying intervals cyclically.
319///
320/// # Examples
321/// ```
322/// let freqs = interval_frequencies(3, 261.63, &[3.0/2.0, 4.0/3.0]);
323/// assert!(freqs[0] == 261.63);
324/// assert!(freqs[1] > 391.0 && freqs[1] < 392.0); // ~391.945
325/// ```
326pub fn interval_frequencies(n_bins: usize, fmin: f32, intervals: &[f32]) -> Vec<f32> {
327    let mut freqs = Vec::with_capacity(n_bins);
328    let mut f = fmin;
329    let mut interval_idx = 0;
330    for _ in 0..n_bins {
331        freqs.push(f);
332        f *= intervals[interval_idx % intervals.len()];
333        interval_idx += 1;
334    }
335    freqs
336}
337
338/// Generates Pythagorean tuning intervals.
339///
340/// # Arguments
341/// * `bins_per_octave` - Optional number of bins per octave (defaults to 12)
342///
343/// # Returns
344/// Returns a `Vec<f32>` containing sorted Pythagorean interval ratios within an octave (1 to 2).
345///
346/// # Examples
347/// ```
348/// let intervals = pythagorean_intervals(Some(3));
349/// assert_eq!(intervals, vec![1.0, 1.5, 1.125]); // 1/1, 3/2, 9/8 adjusted
350/// ```
351pub fn pythagorean_intervals(bins_per_octave: Option<usize>) -> Vec<f32> {
352    let bins = bins_per_octave.unwrap_or(12);
353    let mut intervals = Vec::with_capacity(bins);
354    let fifth = 3.0 / 2.0;
355    let mut ratio = 1.0;
356    for i in 0..bins {
357        intervals.push(ratio);
358        ratio *= if i % 2 == 0 { fifth } else { 1.0 / fifth };
359        while ratio > 2.0 { ratio /= 2.0; }
360        while ratio < 1.0 { ratio *= 2.0; }
361    }
362    intervals.sort_by(|a, b| a.partial_cmp(b).unwrap());
363    intervals
364}
365
366/// Generates intervals based on prime number limits.
367///
368/// # Arguments
369/// * `primes` - Array of prime numbers to generate intervals from
370///
371/// # Returns
372/// Returns a `Vec<f32>` containing sorted unique interval ratios within an octave (1 to 2).
373///
374/// # Examples
375/// ```
376/// let intervals = plimit_intervals(&[2, 3]);
377/// assert!(intervals.contains(&1.0));
378/// assert!(intervals.contains(&1.5));
379/// assert!(intervals.contains(&1.3333333)); // ~4/3
380/// ```
381pub fn plimit_intervals(primes: &[usize]) -> Vec<f32> {
382    let mut intervals = vec![1.0];
383    for &p in primes {
384        let mut new_intervals = Vec::new();
385        for &i in &intervals {
386            let mut n = i;
387            while n < 2.0 {
388                new_intervals.push(n);
389                n *= p as f32;
390            }
391            let mut d = i;
392            while d > 0.5 {
393                new_intervals.push(d);
394                d /= p as f32;
395            }
396        }
397        intervals.extend(new_intervals);
398    }
399    intervals.sort_by(|a, b| a.partial_cmp(b).unwrap());
400    intervals.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
401    intervals.retain(|&x| (1.0..=2.0).contains(&x));
402    intervals
403}