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}