Skip to main content

music21_rs/features/
native.rs

1//! The features music21 adds of its own: music21's `features.native`. Each
2//! is an [`Extractor`] in [`NATIVE`], found by its id with [`extractor`].
3//!
4//! The key features read the keys the piece states or, failing those, the
5//! key it is analysed to be in; the quarter-length features (`QL`) read how
6//! long its notes are; the simultaneity features (`CS`) read the chords of
7//! the piece chordified, and its chord symbols; the Landini cadence (`MC1`)
8//! reads how each part's melody ends. music21's habits are kept where they
9//! decide a value: the most common length is the last of those equally
10//! common, and a chord symbol that sounds nothing refuses the bass-motion
11//! feature, as music21 finds no bass in it.
12//!
13//! music21's language feature (`TX1`) reads the lyrics against texts in
14//! seven languages, about 1.9 MB of them, so it is here only with the
15//! `language-detection` feature.
16
17use super::{DataInstance, Extractor, Value};
18use crate::{
19    chord::Chord,
20    chordsymbol::ChordSymbol,
21    defaults::{FloatType, IntegerType},
22    error::{Error, Result},
23    pitch::Pitch,
24    stream::StreamElement,
25};
26
27/// Every native extractor music21 lists, in music21's order: its language
28/// feature last, and only with the `language-detection` feature.
29pub const NATIVE: &[Extractor] = if cfg!(feature = "language-detection") {
30    &ALL
31} else {
32    ALL.split_at(ALL.len() - 1).0
33};
34
35const ALL: [Extractor; 20] = [
36    Extractor {
37        id: "P22",
38        name: "Quality",
39        description: "Set to 0 if the Key or KeySignature indicates that a recording is major, set to 1 if it indicates that it is minor. Music21 addition: if no key mode is found in the piece, or conflicting modes in the keys, analyze the piece to discover what mode it is most likely in.",
40        dimensions: 1,
41        discrete: true,
42        normalize: false,
43        process: quality,
44    },
45    Extractor {
46        id: "K1",
47        name: "Tonal Certainty",
48        description: "A floating point magnitude value that suggest tonal certainty based on automatic key analysis.",
49        dimensions: 1,
50        discrete: false,
51        normalize: false,
52        process: tonal_certainty,
53    },
54    Extractor {
55        id: "QL1",
56        name: "Unique Note Quarter Lengths",
57        description: "The number of unique note quarter lengths.",
58        dimensions: 1,
59        discrete: true,
60        normalize: false,
61        process: unique_note_quarter_lengths,
62    },
63    Extractor {
64        id: "QL2",
65        name: "Most Common Note Quarter Length",
66        description: "The value of the most common quarter length.",
67        dimensions: 1,
68        discrete: false,
69        normalize: false,
70        process: most_common_note_quarter_length,
71    },
72    Extractor {
73        id: "QL3",
74        name: "Most Common Note Quarter Length Prevalence",
75        description: "Fraction of notes that have the most common quarter length.",
76        dimensions: 1,
77        discrete: false,
78        normalize: false,
79        process: most_common_note_quarter_length_prevalence,
80    },
81    Extractor {
82        id: "QL4",
83        name: "Range of Note Quarter Lengths",
84        description: "Difference between the longest and shortest quarter lengths.",
85        dimensions: 1,
86        discrete: false,
87        normalize: false,
88        process: range_of_note_quarter_lengths,
89    },
90    Extractor {
91        id: "CS1",
92        name: "Unique Pitch Class Set Simultaneities",
93        description: "Number of unique pitch class simultaneities.",
94        dimensions: 1,
95        discrete: false,
96        normalize: false,
97        process: unique_pitch_class_set_simultaneities,
98    },
99    Extractor {
100        id: "CS2",
101        name: "Unique Set Class Simultaneities",
102        description: "Number of unique set class simultaneities.",
103        dimensions: 1,
104        discrete: false,
105        normalize: false,
106        process: unique_set_class_simultaneities,
107    },
108    Extractor {
109        id: "CS3",
110        name: "Most Common Pitch Class Set Simultaneity Prevalence",
111        description: "Fraction of all pitch class simultaneities that are the most common simultaneity.",
112        dimensions: 1,
113        discrete: false,
114        normalize: false,
115        process: most_common_pitch_class_set_simultaneity_prevalence,
116    },
117    Extractor {
118        id: "CS4",
119        name: "Most Common Set Class Simultaneity Prevalence",
120        description: "Fraction of all set class simultaneities that are the most common simultaneity.",
121        dimensions: 1,
122        discrete: false,
123        normalize: false,
124        process: most_common_set_class_simultaneity_prevalence,
125    },
126    Extractor {
127        id: "CS5",
128        name: "Major Triad Simultaneity Prevalence",
129        description: "Percentage of all simultaneities that are major triads.",
130        dimensions: 1,
131        discrete: false,
132        normalize: false,
133        process: major_triad_simultaneity_prevalence,
134    },
135    Extractor {
136        id: "CS6",
137        name: "Minor Triad Simultaneity Prevalence",
138        description: "Percentage of all simultaneities that are minor triads.",
139        dimensions: 1,
140        discrete: false,
141        normalize: false,
142        process: minor_triad_simultaneity_prevalence,
143    },
144    Extractor {
145        id: "CS7",
146        name: "Dominant Seventh Simultaneity Prevalence",
147        description: "Percentage of all simultaneities that are dominant seventh.",
148        dimensions: 1,
149        discrete: false,
150        normalize: false,
151        process: dominant_seventh_simultaneity_prevalence,
152    },
153    Extractor {
154        id: "CS8",
155        name: "Diminished Triad Simultaneity Prevalence",
156        description: "Percentage of all simultaneities that are diminished triads.",
157        dimensions: 1,
158        discrete: false,
159        normalize: false,
160        process: diminished_triad_simultaneity_prevalence,
161    },
162    Extractor {
163        id: "CS9",
164        name: "Triad Simultaneity Prevalence",
165        description: "Proportion of all simultaneities that form triads.",
166        dimensions: 1,
167        discrete: false,
168        normalize: false,
169        process: triad_simultaneity_prevalence,
170    },
171    Extractor {
172        id: "CS10",
173        name: "Diminished Seventh Simultaneity Prevalence",
174        description: "Percentage of all simultaneities that are diminished seventh chords.",
175        dimensions: 1,
176        discrete: false,
177        normalize: false,
178        process: diminished_seventh_simultaneity_prevalence,
179    },
180    Extractor {
181        id: "CS11",
182        name: "Incorrectly Spelled Triad Prevalence",
183        description: "Percentage of all triads that are spelled incorrectly.",
184        dimensions: 1,
185        discrete: false,
186        normalize: false,
187        process: incorrectly_spelled_triad_prevalence,
188    },
189    Extractor {
190        id: "CS12",
191        name: "Chord Bass Motion",
192        description: "12-element vector showing the fraction of chords that move by x semitones (where x=0 is always 0 unless there are 0 or 1 harmonies, in which case it is 1).",
193        dimensions: 12,
194        discrete: false,
195        normalize: false,
196        process: chord_bass_motion,
197    },
198    Extractor {
199        id: "MC1",
200        name: "Ends With Landini Melodic Contour",
201        description: "Boolean that indicates the presence of a Landini-like cadential figure in one or more parts.",
202        dimensions: 1,
203        discrete: false,
204        normalize: false,
205        process: landini_cadence,
206    },
207    Extractor {
208        id: "TX1",
209        name: "Language Feature",
210        description: "Language of the lyrics of the piece given as a numeric value from text.LanguageDetector.mostLikelyLanguageNumeric().",
211        dimensions: 1,
212        discrete: true,
213        normalize: false,
214        process: language,
215    },
216];
217
218/// The native extractor with this id, such as `QL1` or `CS12`.
219pub fn extractor(id: &str) -> Option<&'static Extractor> {
220    NATIVE.iter().find(|extractor| extractor.id == id)
221}
222
223fn lacks_notes() -> Error {
224    Error::Feature("input lacks notes".to_string())
225}
226
227/// music21's `pitchClass`: the pitch space rounded half to even, folded
228/// into the octave.
229fn pitch_class(pitch: &Pitch) -> usize {
230    (pitch.ps().round_ties_even() as i64).rem_euclid(12) as usize
231}
232
233fn mode_value(mode: &str) -> Option<Value> {
234    match mode {
235        "major" => Some(Value::Integer(0)),
236        "minor" => Some(Value::Integer(1)),
237        _ => None,
238    }
239}
240
241fn quality(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
242    // Several stated keys are taken where they agree in mode, as the keys
243    // of transposing parts do.
244    let modes = data.key_modes();
245    if let Some((first, rest)) = modes.split_first()
246        && rest.iter().all(|mode| mode == first)
247        && let Some(value) = mode_value(first)
248    {
249        vector[0] = value;
250        return Ok(());
251    }
252    let mode = data
253        .analyzed_key()
254        .and_then(<[_]>::first)
255        .ok_or_else(|| {
256            Error::Feature("failed to get likely keys for Stream component".to_string())
257        })?
258        .key()
259        .mode()
260        .to_string();
261    vector[0] = mode_value(&mode).ok_or_else(|| {
262        Error::Feature(
263            "should be able to get a mode from something here -- perhaps there are no notes?"
264                .to_string(),
265        )
266    })?;
267    Ok(())
268}
269
270fn tonal_certainty(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
271    let estimates = data.analyzed_key().ok_or_else(|| {
272        Error::Feature("failed to get likely keys for Stream component".to_string())
273    })?;
274    vector[0] = crate::analysis::tonal_certainty(estimates).into();
275    Ok(())
276}
277
278fn unique_note_quarter_lengths(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
279    vector[0] = data.quarter_length_histogram().len().into();
280    Ok(())
281}
282
283fn most_common_note_quarter_length(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
284    let mut maximum = 0;
285    let mut most_common = Value::Integer(0);
286    for &(length, count) in data.quarter_length_histogram() {
287        if count >= maximum {
288            maximum = count;
289            most_common = length.into();
290        }
291    }
292    vector[0] = most_common;
293    Ok(())
294}
295
296/// The share of a histogram's counts its largest count has.
297fn prevalence<'a>(counts: impl Iterator<Item = &'a usize>) -> FloatType {
298    let (total, largest) = counts.fold((0, 0), |(total, largest), &count| {
299        (total + count, largest.max(count))
300    });
301    if total == 0 {
302        0.0
303    } else {
304        largest as FloatType / total as FloatType
305    }
306}
307
308fn most_common_note_quarter_length_prevalence(
309    data: &DataInstance,
310    vector: &mut [Value],
311) -> Result<()> {
312    let histogram = data.quarter_length_histogram();
313    if histogram.is_empty() {
314        return Err(lacks_notes());
315    }
316    vector[0] = prevalence(histogram.iter().map(|(_, count)| count)).into();
317    Ok(())
318}
319
320fn range_of_note_quarter_lengths(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
321    let lengths = data
322        .quarter_length_histogram()
323        .iter()
324        .map(|(length, _)| *length);
325    let shortest = lengths
326        .clone()
327        .reduce(FloatType::min)
328        .ok_or_else(lacks_notes)?;
329    let longest = lengths.reduce(FloatType::max).ok_or_else(lacks_notes)?;
330    vector[0] = (longest - shortest).into();
331    Ok(())
332}
333
334/// How often each label comes among the chords of the piece chordified, in
335/// the order each is first met: a `Counter` music21 keeps of them.
336fn chord_histogram(
337    data: &DataInstance,
338    label: impl Fn(&Chord) -> String,
339) -> Result<Vec<(String, usize)>> {
340    let mut counts: Vec<(String, usize)> = Vec::new();
341    for chord in data.chordified_chords()? {
342        let label = label(chord);
343        match counts.iter_mut().find(|(known, _)| *known == label) {
344            Some((_, count)) => *count += 1,
345            None => counts.push((label, 1)),
346        }
347    }
348    Ok(counts)
349}
350
351fn pitch_class_set_histogram(data: &DataInstance) -> Result<Vec<(String, usize)>> {
352    chord_histogram(data, Chord::ordered_pitch_classes_string)
353}
354
355/// The set classes of the chords chordified, music21's `forteClassTnI`,
356/// which is `N/A` for a chord that sounds nothing.
357fn set_class_histogram(data: &DataInstance) -> Result<Vec<(String, usize)>> {
358    chord_histogram(data, |chord| {
359        chord.forte_class_tni().unwrap_or_else(|| "N/A".to_string())
360    })
361}
362
363fn unique_pitch_class_set_simultaneities(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
364    vector[0] = pitch_class_set_histogram(data)?.len().into();
365    Ok(())
366}
367
368fn unique_set_class_simultaneities(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
369    vector[0] = set_class_histogram(data)?.len().into();
370    Ok(())
371}
372
373fn most_common_pitch_class_set_simultaneity_prevalence(
374    data: &DataInstance,
375    vector: &mut [Value],
376) -> Result<()> {
377    let histogram = pitch_class_set_histogram(data)?;
378    if histogram.is_empty() {
379        return Err(lacks_notes());
380    }
381    vector[0] = prevalence(histogram.iter().map(|(_, count)| count)).into();
382    Ok(())
383}
384
385fn most_common_set_class_simultaneity_prevalence(
386    data: &DataInstance,
387    vector: &mut [Value],
388) -> Result<()> {
389    let histogram = set_class_histogram(data)?;
390    if histogram.is_empty() {
391        return Err(lacks_notes());
392    }
393    vector[0] = prevalence(histogram.iter().map(|(_, count)| count)).into();
394    Ok(())
395}
396
397/// Some of music21's `typesHistogram` counts over the chords chordified,
398/// as a share of how many chords there are, nought where there are none.
399fn share_of_chords(
400    data: &DataInstance,
401    vector: &mut [Value],
402    kinds: &[fn(&Chord) -> bool],
403) -> Result<()> {
404    let chords = data.chordified_chords()?;
405    let counted: usize = kinds
406        .iter()
407        .map(|kind| chords.iter().filter(|chord| kind(chord)).count())
408        .sum();
409    vector[0] = if chords.is_empty() {
410        Value::Integer(0)
411    } else {
412        (counted as FloatType / chords.len() as FloatType).into()
413    };
414    Ok(())
415}
416
417fn major_triad_simultaneity_prevalence(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
418    share_of_chords(
419        data,
420        vector,
421        &[Chord::is_major_triad, Chord::is_incomplete_major_triad],
422    )
423}
424
425fn minor_triad_simultaneity_prevalence(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
426    share_of_chords(
427        data,
428        vector,
429        &[Chord::is_minor_triad, Chord::is_incomplete_minor_triad],
430    )
431}
432
433fn dominant_seventh_simultaneity_prevalence(
434    data: &DataInstance,
435    vector: &mut [Value],
436) -> Result<()> {
437    share_of_chords(data, vector, &[Chord::is_dominant_seventh])
438}
439
440fn diminished_triad_simultaneity_prevalence(
441    data: &DataInstance,
442    vector: &mut [Value],
443) -> Result<()> {
444    share_of_chords(data, vector, &[Chord::is_diminished_triad])
445}
446
447fn triad_simultaneity_prevalence(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
448    share_of_chords(data, vector, &[Chord::is_triad])
449}
450
451fn diminished_seventh_simultaneity_prevalence(
452    data: &DataInstance,
453    vector: &mut [Value],
454) -> Result<()> {
455    share_of_chords(data, vector, &[Chord::is_diminished_seventh])
456}
457
458/// The share of the chords whose set class is a triad's -- major, minor,
459/// diminished or augmented -- that are not spelled as a triad.
460fn incorrectly_spelled_triad_prevalence(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
461    let chords = data.chordified_chords()?;
462    if chords.is_empty() {
463        return Err(lacks_notes());
464    }
465    let spelled = chords.iter().filter(|chord| chord.is_triad()).count() as FloatType;
466    let set_classes = set_class_histogram(data)?;
467    let triads: usize = set_classes
468        .iter()
469        .filter(|(class, _)| matches!(class.as_str(), "3-10" | "3-11" | "3-12"))
470        .map(|(_, count)| count)
471        .sum();
472    if triads == 0 {
473        return Err(Error::Feature("input lacks Forte triads".to_string()));
474    }
475    let triads = triads as FloatType;
476    vector[0] = ((triads - spelled) / triads).into();
477    Ok(())
478}
479
480/// The pitch class a chord symbol's bass is on, its root where it names
481/// no other bass.
482fn bass_class(symbol: &ChordSymbol) -> usize {
483    pitch_class(symbol.bass().unwrap_or(symbol.root()))
484}
485
486/// How far the bass moves down, in semitones folded into the octave, from
487/// each chord symbol to the next that has another bass, each as a share of
488/// all such moves; the first value, a bass staying put, is one where the
489/// bass never moves. A symbol that sounds nothing is passed over.
490fn chord_bass_motion(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
491    let flat = data.prepared().flatten();
492    let mut motion = [0_usize; 12];
493    let mut total = 0;
494    let mut last: Option<&ChordSymbol> = None;
495    for event in flat.events() {
496        let StreamElement::ChordSymbol(this) = event.element() else {
497            continue;
498        };
499        if this.is_no_chord() {
500            continue;
501        }
502        let Some(previous) = last else {
503            last = Some(this);
504            continue;
505        };
506        let last_bass = bass_class(previous);
507        let this_bass = bass_class(this);
508        if last_bass != this_bass {
509            motion[(last_bass + 12 - this_bass) % 12] += 1;
510            total += 1;
511            last = Some(this);
512        }
513    }
514    // music21 writes every value as a float here, the ones it never
515    // reaches too.
516    for value in vector.iter_mut() {
517        *value = Value::Float(0.0);
518    }
519    if total == 0 {
520        vector[0] = Value::Float(1.0);
521    } else {
522        for (value, count) in vector.iter_mut().zip(motion).skip(1) {
523            *value = (count as FloatType / total as FloatType).into();
524        }
525    }
526    Ok(())
527}
528
529/// Whether any part's melody, its repeated notes left out, ends falling a
530/// tone and rising a minor third, or falling a semitone and a tone and
531/// rising a minor third.
532/// The language of the piece's lyrics, assembled from its first verse, as
533/// one more than its place among the languages music21 knows, or nought
534/// for none: `TX1`.
535#[cfg(feature = "language-detection")]
536fn language(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
537    let lyrics = crate::text::assemble_lyrics(data.prepared(), 1, " ");
538    let number = super::language::most_likely_language_numeric(&lyrics)?;
539    vector[0] = Value::Integer(number as i64);
540    Ok(())
541}
542
543#[cfg(not(feature = "language-detection"))]
544fn language(_: &DataInstance, _: &mut [Value]) -> Result<()> {
545    Err(Error::Feature(
546        "the language feature needs the language-detection feature".to_string(),
547    ))
548}
549
550fn landini_cadence(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
551    const ENDINGS: [&[IntegerType]; 2] = [&[-2, 3], &[-1, -2, 3]];
552    let contours: Vec<&[IntegerType]> = if data.parts_count() > 0 {
553        data.parts()[..data.parts_count()]
554            .iter()
555            .map(DataInstance::contour_list)
556            .collect()
557    } else {
558        vec![data.contour_list()]
559    };
560    let found = contours.iter().any(|contour| {
561        let moving: Vec<IntegerType> = contour.iter().copied().filter(|step| *step != 0).collect();
562        ENDINGS.iter().any(|ending| moving.ends_with(ending))
563    });
564    if found {
565        vector[0] = Value::Integer(1);
566    }
567    Ok(())
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573    use crate::stream::Stream;
574    use crate::tinynotation::from_tiny_notation;
575
576    fn vector(id: &str, data: &DataInstance) -> Vec<FloatType> {
577        extractor(id)
578            .expect("a native extractor")
579            .extract(data)
580            .expect("a feature")
581            .vector()
582            .to_vec()
583    }
584
585    fn symbols(figures: &[&str]) -> Stream {
586        let mut stream = Stream::new();
587        for (offset, figure) in figures.iter().enumerate() {
588            let symbol = ChordSymbol::parse_music21(*figure).expect("a chord symbol");
589            stream.insert(offset as FloatType, StreamElement::ChordSymbol(symbol));
590        }
591        stream
592    }
593
594    #[test]
595    fn the_bass_motion_is_read_from_chord_symbols() -> Result<()> {
596        // Each read off music21's ChordBassMotionFeature.
597        let data = DataInstance::new(&symbols(&["C", "F", "G", "C"]))?;
598        let mut expected = vec![0.0; 12];
599        expected[7] = 2.0 / 3.0;
600        expected[10] = 1.0 / 3.0;
601        assert_eq!(vector("CS12", &data), expected);
602
603        // A symbol that sounds nothing is passed over, wherever it stands.
604        let mut silent = symbols(&["C", "G"]);
605        silent.insert(0.5, StreamElement::ChordSymbol(ChordSymbol::no_chord(None)));
606        silent.insert(2.0, StreamElement::ChordSymbol(ChordSymbol::no_chord(None)));
607        let data = DataInstance::new(&silent)?;
608        let mut expected = vec![0.0; 12];
609        expected[5] = 1.0;
610        assert_eq!(vector("CS12", &data), expected);
611        let mut alone = symbols(&["C"]);
612        alone.insert(1.0, StreamElement::ChordSymbol(ChordSymbol::no_chord(None)));
613        let data = DataInstance::new(&alone)?;
614        let mut still = vec![0.0; 12];
615        still[0] = 1.0;
616        assert_eq!(vector("CS12", &data), still);
617        Ok(())
618    }
619
620    #[test]
621    fn a_landini_cadence_is_found_at_the_end_of_a_melody() -> Result<()> {
622        let cadence = DataInstance::new(&from_tiny_notation("4/4 c'4 b a c'")?)?;
623        assert_eq!(vector("MC1", &cadence), [1.0]);
624        let plain = DataInstance::new(&from_tiny_notation("4/4 c'4 b c' c'")?)?;
625        assert_eq!(vector("MC1", &plain), [0.0]);
626        Ok(())
627    }
628
629    #[test]
630    fn note_lengths_and_the_key_are_read_as_music21_reads_them() -> Result<()> {
631        // Each read off music21's native extractors.
632        let data = DataInstance::new(&from_tiny_notation("4/4 c4 d8 e8 f2")?)?;
633        assert_eq!(vector("QL1", &data), [3.0]);
634        assert_eq!(vector("QL2", &data), [0.5]);
635        assert_eq!(vector("QL3", &data), [0.5]);
636        assert_eq!(vector("QL4", &data), [1.5]);
637        assert_eq!(vector("CS1", &data), [0.0]);
638        assert_eq!(vector("P22", &data), [0.0]);
639        let certainty = vector("K1", &data)[0];
640        assert!((certainty - 0.788_866_872_590_800_5).abs() < 1e-12);
641        Ok(())
642    }
643}