1use 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
27pub 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
218pub 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
227fn 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 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
296fn 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
334fn 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
355fn 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
397fn 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
458fn 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
480fn bass_class(symbol: &ChordSymbol) -> usize {
483 pitch_class(symbol.bass().unwrap_or(symbol.root()))
484}
485
486fn 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 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#[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 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 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 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}