dasp-rs 0.3.1

Pure-Rust digital audio signal processing: I/O, STFT/CQT, spectral & MIR features, pitch, and music/phonetics notation.
Documentation
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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
/// Returns a list of all chromatic note names, ignoring the provided key.
///
/// # Arguments
/// * `key` - Key signature (currently unused, e.g., "C:maj")
/// * `_unicode` - Optional flag for Unicode accidentals (unused, defaults to None)
/// * `_natural` - Optional flag for natural notes only (unused, defaults to None)
///
/// # Returns
/// Returns a `Vec<String>` containing all 12 chromatic note names (C through B).
///
/// # Examples
/// ```
/// use dasp_rs::util::*;
/// use dasp_rs::types::*;
/// let notes = key_to_notes("C:maj", None, None);
/// assert_eq!(notes, vec!["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]);
/// ```
pub fn key_to_notes(_key: &str, _unicode: Option<bool>, _natural: Option<bool>) -> Vec<String> {
    let notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
    notes.iter().map(|&n| n.to_string()).collect()
}

/// Converts a key signature to scale degrees.
///
/// # Arguments
/// * `key` - Key signature in the format "tonic:mode" (e.g., "C:maj", "F#:min")
///
/// # Returns
/// Returns a `Vec<usize>` containing the scale degrees (0-11) relative to the chromatic scale.
///
/// # Notes
/// - Supports major ("maj", "major") and minor ("min", "minor") modes.
/// - Defaults to major scale if mode is unspecified or unrecognized.
/// - Tonic is case-insensitive and supports sharp/flat synonyms (e.g., "C#", "Db").
///
/// # Examples
/// ```
/// use dasp_rs::util::*;
/// use dasp_rs::types::*;
/// let degrees = key_to_degrees("C:maj");
/// assert_eq!(degrees, vec![0, 2, 4, 5, 7, 9, 11]); // C major scale
/// let degrees = key_to_degrees("F#:min");
/// assert_eq!(degrees, vec![6, 8, 9, 11, 1, 2, 4]); // F# minor scale
/// ```
pub fn key_to_degrees(key: &str) -> Vec<usize> {
    let key = key.to_lowercase();
    let (tonic, mode) = key.split_once(':').unwrap_or((&key, "maj"));
    let tonic_shift = match tonic {
        "c" => 0, "c#" | "db" => 1, "d" => 2, "d#" | "eb" => 3, "e" => 4,
        "f" => 5, "f#" | "gb" => 6, "g" => 7, "g#" | "ab" => 8, "a" => 9,
        "a#" | "bb" => 10, "b" => 11, _ => 0,
    };
    let major = vec![0, 2, 4, 5, 7, 9, 11];
    let minor = vec![0, 2, 3, 5, 7, 8, 10];
    let degrees = match mode {
        "maj" | "major" => major,
        "min" | "minor" => minor,
        _ => major,
    };
    degrees.into_iter().map(|d| (d + tonic_shift) % 12).collect()
}

/// Converts a melakarta raga index to Carnatic svara names.
///
/// # Arguments
/// * `mela` - Melakarta raga index (1-72)
/// * `abbr` - Optional flag for abbreviated notation (defaults to false)
/// * `unicode` - Optional flag for Unicode transliteration (defaults to false)
///
/// # Returns
/// Returns a `Vec<String>` containing svara names for the melakarta raga.
///
/// # Notes
/// - If `mela` is out of range (1-72), defaults to a major scale-like pattern.
/// - Abbreviated notation uses "S", "R1", etc.; full notation uses "Shadjam", "Shuddha Rishabham", etc.
///
/// # Examples
/// ```
/// use dasp_rs::util::*;
/// use dasp_rs::types::*;
/// let svaras = mela_to_svara(29, Some(true), None); // Dheerashankarabharanam
/// assert_eq!(svaras, vec!["S", "R2", "G3", "M1", "P", "D2", "N3"]);
/// let svaras = mela_to_svara(1, None, None); // Kanakangi
/// assert_eq!(svaras, vec!["shadjam", "rishabham1", "gandharam1", "madhyamam1", "panchamam", "dhaivatam1", "nishadam1"]);
/// ```
pub fn mela_to_svara(mela: usize, abbr: Option<bool>, unicode: Option<bool>) -> Vec<String> {
    let abbr = abbr.unwrap_or(false);
    let unicode = unicode.unwrap_or(false);
    let degrees = mela_to_degrees(mela);
    let svara_full = if unicode {
        vec!["ṣaḍjam", "ṛṣabham", "gāndhāram", "madhyamam", "pañcamam", "dhaivatam", "niṣādam"]
    } else {
        vec!["shadjam", "rishabham", "gandharam", "madhyamam", "panchamam", "dhaivatam", "nishadam"]
    };
    let mut result = Vec::new();
    for (i, &deg) in degrees.iter().enumerate() {
        let (base, variant) = match i {
            0 => ("S", ""),
            1 => ("R", match deg { 1 => "1", 2 => "2", 3 => "3", _ => "" }),
            2 => ("G", match deg { 2 => "1", 3 => "2", 4 => "3", _ => "" }),
            3 => ("M", match deg { 5 => "1", 6 => "2", _ => "" }),
            4 => ("P", ""),
            5 => ("D", match deg { 8 => "1", 9 => "2", 10 => "3", _ => "" }),
            6 => ("N", match deg { 9 => "1", 10 => "2", 11 => "3", _ => "" }),
            _ => ("S", ""),
        };
        let name = if abbr {
            format!("{}{}", base, variant)
        } else {
            let idx = match base {
                "S" => 0, "R" => 1, "G" => 2, "M" => 3, "P" => 4, "D" => 5, "N" => 6, _ => 0,
            };
            format!("{}{}", svara_full[idx], variant)
        };
        result.push(name);
    }
    result
}

/// Converts a melakarta raga index to scale degrees.
///
/// # Arguments
/// * `mela` - Melakarta raga index (1-72)
///
/// # Returns
/// Returns a `Vec<usize>` containing the scale degrees (0-11) for the melakarta raga.
///
/// # Notes
/// - If `mela` is out of range (1-72), returns a default major scale (0, 2, 4, 5, 7, 9, 11).
/// - Uses traditional melakarta rules to determine R, G, M, D, N positions.
///
/// # Examples
/// ```
/// use dasp_rs::util::*;
/// use dasp_rs::types::*;
/// let degrees = mela_to_degrees(29); // Dheerasankarabharanam
/// assert_eq!(degrees, vec![0, 2, 4, 5, 7, 9, 11]);
/// let degrees = mela_to_degrees(1); // Kanakangi
/// assert_eq!(degrees, vec![0, 1, 2, 5, 7, 8, 9]);
/// ```
pub fn mela_to_degrees(mela: usize) -> Vec<usize> {
    if !(1..=72).contains(&mela) { return vec![0, 2, 4, 5, 7, 9, 11]; }
    let index = mela - 1;
    let (ri, ga) = match (index % 36) / 6 {
        0 => (1, 2),
        1 => (1, 3),
        2 => (1, 4),
        3 => (2, 3),
        4 => (2, 4),
        _ => (3, 4),
    };
    let ma = if index < 36 { 5 } else { 6 };
    let (dha, ni) = match index % 6 {
        0 => (8, 9),
        1 => (8, 10),
        2 => (8, 11),
        3 => (9, 10),
        4 => (9, 11),
        _ => (10, 11),
    };
    vec![0, ri, ga, ma, 7, dha, ni]
}

/// Converts a Hindustani thaat to scale degrees.
///
/// # Arguments
/// * `thaat` - Name of the thaat (e.g., "Bilaval", "Kafi")
///
/// # Returns
/// Returns a `Vec<usize>` containing the scale degrees (0-11) for the thaat.
///
/// # Notes
/// - Case-insensitive; defaults to Bilaval scale if thaat is unrecognized.
/// - Recognizes 10 traditional thaats.
///
/// # Examples
/// ```
/// use dasp_rs::util::*;
/// use dasp_rs::types::*;
/// let degrees = thaat_to_degrees("Bilaval");
/// assert_eq!(degrees, vec![0, 2, 4, 5, 7, 9, 11]);
/// let degrees = thaat_to_degrees("Kafi");
/// assert_eq!(degrees, vec![0, 2, 3, 5, 7, 9, 10]);
/// ```
pub fn thaat_to_degrees(thaat: &str) -> Vec<usize> {
    match thaat.to_lowercase().as_str() {
        "bilaval" => vec![0, 2, 4, 5, 7, 9, 11],
        "kalyani" => vec![0, 2, 4, 6, 7, 9, 11],
        "khamaj" => vec![0, 2, 4, 5, 7, 9, 10],
        "bhairav" => vec![0, 1, 4, 5, 6, 9, 11],
        "purvi" => vec![0, 1, 4, 6, 7, 9, 11],
        "marwa" => vec![0, 1, 3, 6, 7, 9, 11],
        "kafi" => vec![0, 2, 3, 5, 7, 9, 10],
        "asavari" => vec![0, 2, 3, 5, 7, 8, 10],
        "todi" => vec![0, 1, 3, 6, 7, 8, 11],
        "bhoopali" => vec![0, 2, 4, 7, 9],
        _ => vec![0, 2, 4, 5, 7, 9, 11],
    }
}

/// Lists all 72 melakarta ragas with their indices and names.
///
/// # Returns
/// Returns a `Vec<(usize, String)>` containing tuples of (index, name) for all melakarta ragas.
///
/// # Examples
/// ```
/// use dasp_rs::util::*;
/// use dasp_rs::types::*;
/// let melas = list_mela();
/// assert_eq!(melas[0], (1, "Kanakangi".to_string()));
/// assert_eq!(melas.len(), 72);
/// ```
pub fn list_mela() -> Vec<(usize, String)> {
    let names = vec![
        "Kanakangi", "Ratnangi", "Ganamurti", "Vanaspati", "Manavati", "Tanarupi",
        "Senavati", "Hanumatodi", "Dhenuka", "Natakapriya", "Kokilapriya", "Rupavati",
        "Gayakapriya", "Vakulabharanam", "Mayamalavagowla", "Chakravakam", "Suryakantam",
        "Hatakambari", "Jhankaradhwani", "Natabhairavi", "Keeravani", "Kharaharapriya",
        "Gourimanohari", "Varunapriya", "Mararanjani", "Charukesi", "Sarasangi",
        "Harikambhoji", "Dheerasankarabharanam", "Naganandini", "Yagapriya", "Ragavardhini",
        "Gangeyabhushani", "Vagadheeswari", "Shulini", "Chalanata", "Salagam", "Jalarnavam",
        "Jhalavarali", "Navaneetam", "Pavani", "Raghupriya", "Gavambodhi", "Bhavapriya",
        "Shubhapantuvarali", "Shadvidamargini", "Suvarnangi", "Divyamani", "Dhavalambari",
        "Namanarayani", "Kamavardhini", "Ramapriya", "Gamanashrama", "Vishwambari",
        "Shamalangi", "Shanmukhapriya", "Simhendramadhyamam", "Hemavati", "Dharmavati",
        "Neetimati", "Kantamani", "Rishabhapriya", "Latangi", "Vachaspati", "Mechakalyani",
        "Chitrambari", "Sucharitra", "Jyotiswarupini", "Dhatuvardhani", "Nasikabhushani",
        "Kosalam", "Rasikapriya",
    ];
    names.into_iter().enumerate().map(|(i, name)| (i + 1, name.to_string())).collect()
}

/// Lists the 10 traditional Hindustani thaats.
///
/// # Returns
/// Returns a `Vec<String>` containing the names of all 10 thaats.
///
/// # Examples
/// ```
/// use dasp_rs::util::*;
/// use dasp_rs::types::*;
/// let thaats = list_thaat();
/// assert_eq!(thaats, vec!["Bilaval", "Kalyani", "Khamaj", "Bhairav", "Purvi", "Marwa", "Kafi", "Asavari", "Todi", "Bhoopali"]);
/// ```
pub fn list_thaat() -> Vec<String> {
    vec![
        "Bilaval".to_string(),
        "Kalyani".to_string(),
        "Khamaj".to_string(),
        "Bhairav".to_string(),
        "Purvi".to_string(),
        "Marwa".to_string(),
        "Kafi".to_string(),
        "Asavari".to_string(),
        "Todi".to_string(),
        "Bhoopali".to_string(),
    ]
}

/// Generates a note name based on a number of perfect fifths from a unison note.
///
/// # Arguments
/// * `unison` - Starting note (e.g., "C", "F#")
/// * `fifths` - Number of fifths (positive or negative)
/// * `unicode` - Optional flag for Unicode accidentals (defaults to false)
///
/// # Returns
/// Returns a `String` representing the resulting note name with octave (e.g., "G4", "F♯-1").
///
/// # Examples
/// ```
/// use dasp_rs::util::*;
/// use dasp_rs::types::*;
/// let note = fifths_to_note("C", 1, None);
/// assert_eq!(note, "G");
/// let note = fifths_to_note("C", 6, Some(true));
/// assert_eq!(note, "F♯3");
/// ```
pub fn fifths_to_note(unison: &str, fifths: i32, unicode: Option<bool>) -> String {
    let unicode = unicode.unwrap_or(false);
    let semitones = (fifths * 7) % 12;
    let octave_shift = (fifths * 7) / 12;
    let base = match unison.to_lowercase().as_str() {
        "c" => 0, "c#" | "db" => 1, "d" => 2, "d#" | "eb" => 3, "e" => 4,
        "f" => 5, "f#" | "gb" => 6, "g" => 7, "g#" | "ab" => 8, "a" => 9,
        "a#" | "bb" => 10, "b" => 11, _ => 0,
    };
    let note_idx = (base + semitones + 12) % 12;
    let note = match note_idx {
        0 => "C", 1 => if unicode { "C♯" } else { "C#" }, 2 => "D",
        3 => if unicode { "D♯" } else { "D#" }, 4 => "E", 5 => "F",
        6 => if unicode { "F♯" } else { "F#" }, 7 => "G",
        8 => if unicode { "G♯" } else { "G#" }, 9 => "A",
        10 => if unicode { "A♯" } else { "A#" }, 11 => "B",
        _ => "C",
    };
    format!("{}{}", note, if octave_shift != 0 { octave_shift.to_string() } else { "".to_string() })
}

/// Converts an interval ratio to Functional Just System (FJS) notation.
///
/// # Arguments
/// * `interval` - Interval ratio (e.g., 1.5 for a perfect fifth)
/// * `unison` - Optional unison ratio (defaults to 1.0)
///
/// # Returns
/// Returns a `String` representing the interval in FJS notation (e.g., "3/2").
///
/// # Notes
/// - Recognizes common just intervals (1/1, 3/2, 4/3, 5/4, 6/5); otherwise approximates as a fraction.
///
/// # Examples
/// ```
/// use dasp_rs::util::*;
/// use dasp_rs::types::*;
/// let fjs = interval_to_fjs(1.5, None);
/// assert_eq!(fjs, "3/2");
/// let fjs = interval_to_fjs(1.333, None);
/// assert_eq!(fjs, "1.33/1");
/// ```
pub fn interval_to_fjs(interval: f32, unison: Option<f32>) -> String {
    let unison = unison.unwrap_or(1.0);
    let ratio = interval / unison;
    match ratio {
        r if (r - 1.0).abs() < 1e-6 => "1/1".to_string(),
        r if (r - 3.0/2.0).abs() < 1e-6 => "3/2".to_string(),
        r if (r - 4.0/3.0).abs() < 1e-6 => "4/3".to_string(),
        r if (r - 5.0/4.0).abs() < 1e-6 => "5/4".to_string(),
        r if (r - 6.0/5.0).abs() < 1e-6 => "6/5".to_string(),
        _ => format!("{:.2}/1", ratio),
    }
}

/// Generates frequencies based on a sequence of intervals.
///
/// # Arguments
/// * `n_bins` - Number of frequency bins to generate
/// * `fmin` - Starting frequency in Hz
/// * `intervals` - Array of interval ratios
///
/// # Returns
/// Returns a `Vec<f32>` containing frequencies generated by applying intervals cyclically.
///
/// # Examples
/// ```no_run
/// use dasp_rs::util::*;
/// use dasp_rs::types::*;
/// let freqs = interval_frequencies(3, 261.63, &[3.0/2.0, 4.0/3.0]);
/// assert!(freqs[0] == 261.63);
/// assert!(freqs[1] > 391.0 && freqs[1] < 392.0); // ~391.945
/// ```
pub fn interval_frequencies(n_bins: usize, fmin: f32, intervals: &[f32]) -> Vec<f32> {
    let mut freqs = Vec::with_capacity(n_bins);
    let mut f = fmin;
    let mut interval_idx = 0;
    for _ in 0..n_bins {
        freqs.push(f);
        f *= intervals[interval_idx % intervals.len()];
        interval_idx += 1;
    }
    freqs
}

/// Generates Pythagorean tuning intervals.
///
/// # Arguments
/// * `bins_per_octave` - Optional number of bins per octave (defaults to 12)
///
/// # Returns
/// Returns a `Vec<f32>` containing sorted Pythagorean interval ratios within an octave (1 to 2).
///
/// # Examples
/// ```no_run
/// use dasp_rs::util::*;
/// use dasp_rs::types::*;
/// let intervals = pythagorean_intervals(Some(3));
/// assert_eq!(intervals, vec![1.0, 1.5, 1.125]); // 1/1, 3/2, 9/8 adjusted
/// ```
pub fn pythagorean_intervals(bins_per_octave: Option<usize>) -> Vec<f32> {
    let bins = bins_per_octave.unwrap_or(12);
    let mut intervals = Vec::with_capacity(bins);
    let fifth = 3.0 / 2.0;
    let mut ratio = 1.0;
    for i in 0..bins {
        intervals.push(ratio);
        ratio *= if i % 2 == 0 { fifth } else { 1.0 / fifth };
        while ratio > 2.0 { ratio /= 2.0; }
        while ratio < 1.0 { ratio *= 2.0; }
    }
    intervals.sort_by(|a, b| a.partial_cmp(b).unwrap());
    intervals
}

/// Generates intervals based on prime number limits.
///
/// # Arguments
/// * `primes` - Array of prime numbers to generate intervals from
///
/// # Returns
/// Returns a `Vec<f32>` containing sorted unique interval ratios within an octave (1 to 2).
///
/// # Examples
/// ```no_run
/// use dasp_rs::util::*;
/// use dasp_rs::types::*;
/// let intervals = plimit_intervals(&[2, 3]);
/// assert!(intervals.contains(&1.0));
/// assert!(intervals.contains(&1.5));
/// assert!(intervals.contains(&1.3333333)); // ~4/3
/// ```
pub fn plimit_intervals(primes: &[usize]) -> Vec<f32> {
    let mut intervals = vec![1.0];
    for &p in primes {
        let mut new_intervals = Vec::new();
        for &i in &intervals {
            let mut n = i;
            while n < 2.0 {
                new_intervals.push(n);
                n *= p as f32;
            }
            let mut d = i;
            while d > 0.5 {
                new_intervals.push(d);
                d /= p as f32;
            }
        }
        intervals.extend(new_intervals);
    }
    intervals.sort_by(|a, b| a.partial_cmp(b).unwrap());
    intervals.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
    intervals.retain(|&x| (1.0..=2.0).contains(&x));
    intervals
}

#[cfg(test)]
mod tests {
    use super::*;

    fn approx_eq(a: f32, b: f32) -> bool {
        (a - b).abs() < 1e-4
    }

    #[test]
    fn key_and_degrees_cover_major_minor() {
        let notes = key_to_notes("C:maj", None, None);
        assert_eq!(notes[0], "C");

        let c_major = key_to_degrees("C:maj");
        assert_eq!(c_major, vec![0, 2, 4, 5, 7, 9, 11]);

        let f_sharp_minor = key_to_degrees("F#:min");
        assert!(f_sharp_minor.contains(&6));
        assert!(f_sharp_minor.contains(&1));
    }

    #[test]
    fn mela_and_thaat_mappings_return_expected_sizes() {
        let svaras = mela_to_svara(29, Some(true), Some(false));
        assert_eq!(svaras, vec!["S", "R2", "G3", "M1", "P", "D2", "N3"]);

        let degrees = mela_to_degrees(1);
        assert_eq!(degrees, vec![0, 1, 2, 5, 7, 8, 9]);

        let thaats = list_thaat();
        assert_eq!(thaats.len(), 10);
        assert!(thaats.contains(&"Bilaval".to_string()));
    }

    #[test]
    fn fifths_and_intervals_generate_consistent_values() {
        let fifth = fifths_to_note("C", 1, None);
        assert_eq!(fifth, "G");
        let unicode = fifths_to_note("C", 6, Some(true));
        assert!(unicode.starts_with("F"));

        let fjs = interval_to_fjs(1.5, None);
        assert_eq!(fjs, "3/2");
        let approx = interval_to_fjs(1.333, None);
        assert!(approx.starts_with("1.33"));
    }

    #[test]
    fn interval_generators_produce_sorted_ranges() {
        let freqs = interval_frequencies(3, 100.0, &[1.5, 4.0 / 3.0]);
        assert!(approx_eq(freqs[0], 100.0));
        assert!(freqs[1] > freqs[0]);

        let pyth = pythagorean_intervals(Some(5));
        assert_eq!(pyth.first().copied().unwrap(), 1.0);
        assert!(pyth.windows(2).all(|w| w[0] <= w[1]));

        let plimit = plimit_intervals(&[2, 3]);
        assert!(plimit.contains(&1.0));
        assert!(plimit.iter().all(|v| *v >= 1.0 && *v <= 2.0));
    }

    #[test]
    fn list_mela_covers_all_entries() {
        let melas = list_mela();
        assert_eq!(melas.len(), 72);
        assert_eq!(melas[0].0, 1);
        assert!(melas.iter().any(|(_, name)| name == "Mechakalyani"));
    }
}