Skip to main content

music21_rs/features/
jsymbolic.rs

1//! The features music21 ports from Cory McKay's jSymbolic: music21's
2//! `features.jSymbolic`. Each is an [`Extractor`] in [`JSYMBOLIC`], found
3//! by its id with [`extractor`].
4//!
5//! The melodic features (`M`) read the intervals between neighbouring notes
6//! of each part; the rhythmic features (`R`) read when each note sounds, in
7//! seconds at the piece's tempi, and its meters; the texture features (`T`)
8//! read how many parts sound in each chord of the piece chordified; the
9//! instrument features (`I`) read the piece partitioned by instrument and
10//! each part's General MIDI program; the pitch features (`P`) read the
11//! piece's pitches. Several
12//! of music21's habits are kept, since the values are the comparison: the
13//! pitch variety (`P8`) counts the MIDI numbers sounding but MIDI nought, a
14//! tie between two pitches equally common goes to the one sounded first, and
15//! a melody with no arpeggiation at all (`M8`) is refused as one with no
16//! notes.
17
18use super::{DataInstance, Extractor, Value};
19use crate::stream::StreamElement;
20use crate::{
21    defaults::{FloatType, IntegerType},
22    error::{Error, Result},
23};
24
25/// Every jSymbolic extractor music21 implements, in music21's order.
26pub const JSYMBOLIC: &[Extractor] = &[
27    Extractor {
28        id: "M1",
29        name: "Melodic Interval Histogram",
30        description: "A features array with bins corresponding to the values of the melodic interval histogram.",
31        dimensions: 128,
32        discrete: true,
33        normalize: true,
34        process: melodic_interval_histogram,
35    },
36    Extractor {
37        id: "M2",
38        name: "Average Melodic Interval",
39        description: "Average melodic interval (in semitones).",
40        dimensions: 1,
41        discrete: true,
42        normalize: false,
43        process: average_melodic_interval,
44    },
45    Extractor {
46        id: "M3",
47        name: "Most Common Melodic Interval",
48        description: "Melodic interval with the highest frequency.",
49        dimensions: 1,
50        discrete: true,
51        normalize: false,
52        process: most_common_melodic_interval,
53    },
54    Extractor {
55        id: "M4",
56        name: "Distance Between Most Common Melodic Intervals",
57        description: "Absolute value of the difference between the most common melodic interval and the second most common melodic interval.",
58        dimensions: 1,
59        discrete: true,
60        normalize: false,
61        process: distance_between_most_common_melodic_intervals,
62    },
63    Extractor {
64        id: "M5",
65        name: "Most Common Melodic Interval Prevalence",
66        description: "Fraction of melodic intervals that belong to the most common interval.",
67        dimensions: 1,
68        discrete: true,
69        normalize: false,
70        process: most_common_melodic_interval_prevalence,
71    },
72    Extractor {
73        id: "M6",
74        name: "Relative Strength of Most Common Intervals",
75        description: "Fraction of melodic intervals that belong to the second most common interval divided by the fraction of melodic intervals belonging to the most common interval.",
76        dimensions: 1,
77        discrete: true,
78        normalize: false,
79        process: relative_strength_of_most_common_intervals,
80    },
81    Extractor {
82        id: "M7",
83        name: "Number of Common Melodic Intervals",
84        description: "Number of melodic intervals that represent at least 9% of all melodic intervals.",
85        dimensions: 1,
86        discrete: true,
87        normalize: false,
88        process: number_of_common_melodic_intervals,
89    },
90    Extractor {
91        id: "M8",
92        name: "Amount of Arpeggiation",
93        description: "Fraction of horizontal intervals that are repeated notes, minor thirds, major thirds, perfect fifths, minor sevenths, major sevenths, octaves, minor tenths or major tenths.",
94        dimensions: 1,
95        discrete: true,
96        normalize: false,
97        process: amount_of_arpeggiation,
98    },
99    Extractor {
100        id: "M9",
101        name: "Repeated Notes",
102        description: "Fraction of notes that are repeated melodically.",
103        dimensions: 1,
104        discrete: true,
105        normalize: false,
106        process: repeated_notes,
107    },
108    Extractor {
109        id: "m10",
110        name: "Chromatic Motion",
111        description: "Fraction of melodic intervals corresponding to a semi-tone.",
112        dimensions: 1,
113        discrete: true,
114        normalize: false,
115        process: chromatic_motion,
116    },
117    Extractor {
118        id: "M11",
119        name: "Stepwise Motion",
120        description: "Fraction of melodic intervals that corresponded to a minor or major second.",
121        dimensions: 1,
122        discrete: true,
123        normalize: false,
124        process: stepwise_motion,
125    },
126    Extractor {
127        id: "M12",
128        name: "Melodic Thirds",
129        description: "Fraction of melodic intervals that are major or minor thirds.",
130        dimensions: 1,
131        discrete: true,
132        normalize: false,
133        process: melodic_thirds,
134    },
135    Extractor {
136        id: "M13",
137        name: "Melodic Fifths",
138        description: "Fraction of melodic intervals that are perfect fifths.",
139        dimensions: 1,
140        discrete: true,
141        normalize: false,
142        process: melodic_fifths,
143    },
144    Extractor {
145        id: "M14",
146        name: "Melodic Tritones",
147        description: "Fraction of melodic intervals that are tritones.",
148        dimensions: 1,
149        discrete: true,
150        normalize: false,
151        process: melodic_tritones,
152    },
153    Extractor {
154        id: "M15",
155        name: "Melodic Octaves",
156        description: "Fraction of melodic intervals that are octaves.",
157        dimensions: 1,
158        discrete: true,
159        normalize: false,
160        process: melodic_octaves,
161    },
162    Extractor {
163        id: "m17",
164        name: "Direction of Motion",
165        description: "Fraction of melodic intervals that are rising rather than falling.",
166        dimensions: 1,
167        discrete: true,
168        normalize: false,
169        process: direction_of_motion,
170    },
171    Extractor {
172        id: "M18",
173        name: "Duration of Melodic Arcs",
174        description: "Average number of notes that separate melodic peaks and troughs in any part. This is calculated as the total number of intervals (not counting unisons) divided by the number of times the melody changes direction.",
175        dimensions: 1,
176        discrete: true,
177        normalize: false,
178        process: duration_of_melodic_arcs,
179    },
180    Extractor {
181        id: "M19",
182        name: "Size of Melodic Arcs",
183        description: "Average span (in semitones) between melodic peaks and troughs in any part. Each time the melody changes direction begins a new arc. The average size ofmelodic arcs is defined as the total size of melodicintervals between changes of directions - or betweenthe start of the melody and the first change ofdirection - divided by the number of direction changes.",
184        dimensions: 1,
185        discrete: true,
186        normalize: false,
187        process: size_of_melodic_arcs,
188    },
189    Extractor {
190        id: "I1",
191        name: "Pitched Instruments Present",
192        description: "Which pitched General MIDI Instruments are present. There is one entry for each instrument, which is set to 1.0 if there is at least one Note On in the recording corresponding to the instrument and to 0.0 if there is not.",
193        dimensions: 128,
194        discrete: true,
195        normalize: false,
196        process: pitched_instruments_present,
197    },
198    Extractor {
199        id: "I3",
200        name: "Note Prevalence of Pitched Instruments",
201        description: "The fraction of (pitched) notes played by each General MIDI Instrument. There is one entry for each instrument, which is set to the number of Note Ons played using the corresponding MIDI patch divided by the total number of Note Ons in the recording.",
202        dimensions: 128,
203        discrete: true,
204        normalize: false,
205        process: note_prevalence_of_pitched_instruments,
206    },
207    Extractor {
208        id: "I6",
209        name: "Variability of Note Prevalence of Pitched Instruments",
210        description: "Standard deviation of the fraction of Note Ons played by each (pitched) General MIDI instrument that is used to play at least one note.",
211        dimensions: 1,
212        discrete: true,
213        normalize: false,
214        process: variability_of_note_prevalence_of_pitched_instruments,
215    },
216    Extractor {
217        id: "I8",
218        name: "Number of Pitched Instruments",
219        description: "Total number of General MIDI patches that are used to play at least one note.",
220        dimensions: 1,
221        discrete: true,
222        normalize: false,
223        process: number_of_pitched_instruments,
224    },
225    Extractor {
226        id: "I11",
227        name: "String Keyboard Fraction",
228        description: "Fraction of all Note Ons belonging to string keyboard patches (General MIDI patches 1 to 8).",
229        dimensions: 1,
230        discrete: true,
231        normalize: false,
232        process: string_keyboard_fraction,
233    },
234    Extractor {
235        id: "I12",
236        name: "Acoustic Guitar Fraction",
237        description: "Fraction of all Note Ons belonging to acoustic guitar patches (General MIDI patches 25 and 26).",
238        dimensions: 1,
239        discrete: true,
240        normalize: false,
241        process: acoustic_guitar_fraction,
242    },
243    Extractor {
244        id: "I13",
245        name: "Electric Guitar Fraction",
246        description: "Fraction of all Note Ons belonging to electric guitar patches (General MIDI patches 27 to 32).",
247        dimensions: 1,
248        discrete: true,
249        normalize: false,
250        process: electric_guitar_fraction,
251    },
252    Extractor {
253        id: "I14",
254        name: "Violin Fraction",
255        description: "Fraction of all Note Ons belonging to violin patches (General MIDI patches 41 or 111).",
256        dimensions: 1,
257        discrete: true,
258        normalize: false,
259        process: violin_fraction,
260    },
261    Extractor {
262        id: "I15",
263        name: "Saxophone Fraction",
264        description: "Fraction of all Note Ons belonging to saxophone patches (General MIDI patches 65 through 68).",
265        dimensions: 1,
266        discrete: true,
267        normalize: false,
268        process: saxophone_fraction,
269    },
270    Extractor {
271        id: "I16",
272        name: "Brass Fraction",
273        description: "Fraction of all Note Ons belonging to brass patches (General MIDI patches 57 through 68).",
274        dimensions: 1,
275        discrete: true,
276        normalize: false,
277        process: brass_fraction,
278    },
279    Extractor {
280        id: "I17",
281        name: "Woodwinds Fraction",
282        description: "Fraction of all Note Ons belonging to woodwind patches (General MIDI patches 69 through 76).",
283        dimensions: 1,
284        discrete: true,
285        normalize: false,
286        process: woodwinds_fraction,
287    },
288    Extractor {
289        id: "I18",
290        name: "Orchestral Strings Fraction",
291        description: "Fraction of all Note Ons belonging to orchestral strings patches (General MIDI patches 41 or 47).",
292        dimensions: 1,
293        discrete: true,
294        normalize: false,
295        process: orchestral_strings_fraction,
296    },
297    Extractor {
298        id: "I19",
299        name: "String Ensemble Fraction",
300        description: "Fraction of all Note Ons belonging to string ensemble patches (General MIDI patches 49 to 52).",
301        dimensions: 1,
302        discrete: true,
303        normalize: false,
304        process: string_ensemble_fraction,
305    },
306    Extractor {
307        id: "I20",
308        name: "Electric Instrument Fraction",
309        description: "Fraction of all Note Ons belonging to electric instrument patches (General MIDI patches 5, 6, 17, 19, 27 to 32 or 34 to 40).",
310        dimensions: 1,
311        discrete: true,
312        normalize: false,
313        process: electric_instrument_fraction,
314    },
315    Extractor {
316        id: "R15",
317        name: "Note Density",
318        description: "Average number of notes per second.",
319        dimensions: 1,
320        discrete: true,
321        normalize: false,
322        process: note_density,
323    },
324    Extractor {
325        id: "R17",
326        name: "Average Note Duration",
327        description: "Average duration of notes in seconds.",
328        dimensions: 1,
329        discrete: true,
330        normalize: false,
331        process: average_note_duration,
332    },
333    Extractor {
334        id: "R18",
335        name: "Variability of Note Duration",
336        description: "Standard deviation of note durations in seconds.",
337        dimensions: 1,
338        discrete: true,
339        normalize: false,
340        process: variability_of_note_duration,
341    },
342    Extractor {
343        id: "R19",
344        name: "Maximum Note Duration",
345        description: "Duration of the longest note (in seconds).",
346        dimensions: 1,
347        discrete: true,
348        normalize: false,
349        process: maximum_note_duration,
350    },
351    Extractor {
352        id: "R20",
353        name: "Minimum Note Duration",
354        description: "Duration of the shortest note (in seconds).",
355        dimensions: 1,
356        discrete: true,
357        normalize: false,
358        process: minimum_note_duration,
359    },
360    Extractor {
361        id: "R21",
362        name: "Staccato Incidence",
363        description: "Number of notes with durations of less than a 10th of a second divided by the total number of notes in the recording.",
364        dimensions: 1,
365        discrete: true,
366        normalize: false,
367        process: staccato_incidence,
368    },
369    Extractor {
370        id: "R22",
371        name: "Average Time Between Attacks",
372        description: "Average time in seconds between Note On events (regardless of channel).",
373        dimensions: 1,
374        discrete: true,
375        normalize: false,
376        process: average_time_between_attacks,
377    },
378    Extractor {
379        id: "R23",
380        name: "Variability of Time Between Attacks",
381        description: "Standard deviation of the times, in seconds, between Note On events (regardless of channel).",
382        dimensions: 1,
383        discrete: true,
384        normalize: false,
385        process: variability_of_time_between_attacks,
386    },
387    Extractor {
388        id: "R24",
389        name: "Average Time Between Attacks For Each Voice",
390        description: "Average of average times in seconds between Note On events on individual channels that contain at least one note.",
391        dimensions: 1,
392        discrete: true,
393        normalize: false,
394        process: average_time_between_attacks_for_each_voice,
395    },
396    Extractor {
397        id: "R25",
398        name: "Average Variability of Time Between Attacks For Each Voice",
399        description: "Average standard deviation, in seconds, of time between Note On events on individual channels that contain at least one note.",
400        dimensions: 1,
401        discrete: true,
402        normalize: false,
403        process: average_variability_of_time_between_attacks_for_each_voice,
404    },
405    Extractor {
406        id: "R30",
407        name: "Initial Tempo",
408        description: "Tempo in beats per minute at the start of the recording.",
409        dimensions: 1,
410        discrete: true,
411        normalize: false,
412        process: initial_tempo,
413    },
414    Extractor {
415        id: "R31",
416        name: "Initial Time Signature",
417        description: "A feature array with two elements. The first is the numerator of the first occurring time signature and the second is the denominator of the first occurring time signature. Both are set to 0 if no time signature is present.",
418        dimensions: 2,
419        discrete: true,
420        normalize: false,
421        process: initial_time_signature,
422    },
423    Extractor {
424        id: "R32",
425        name: "Compound Or Simple Meter",
426        description: "Set to 1 if the initial meter is compound (numerator of time signature is greater than or equal to 6 and is evenly divisible by 3) and to 0 if it is simple (if the above condition is not fulfilled).",
427        dimensions: 1,
428        discrete: true,
429        normalize: false,
430        process: compound_or_simple_meter,
431    },
432    Extractor {
433        id: "R33",
434        name: "Triple Meter",
435        description: "Set to 1 if numerator of initial time signature is 3, set to 0 otherwise.",
436        dimensions: 1,
437        discrete: true,
438        normalize: false,
439        process: triple_meter,
440    },
441    Extractor {
442        id: "R34",
443        name: "Quintuple Meter",
444        description: "Set to 1 if numerator of initial time signature is 5, set to 0 otherwise.",
445        dimensions: 1,
446        discrete: true,
447        normalize: false,
448        process: quintuple_meter,
449    },
450    Extractor {
451        id: "R35",
452        name: "Changes of Meter",
453        description: "Set to 1 if the time signature is changed one or more times during the recording.",
454        dimensions: 1,
455        discrete: true,
456        normalize: false,
457        process: changes_of_meter,
458    },
459    Extractor {
460        id: "R36",
461        name: "Duration",
462        description: "The total duration in seconds of the music.",
463        dimensions: 1,
464        discrete: false,
465        normalize: false,
466        process: duration,
467    },
468    Extractor {
469        id: "T1",
470        name: "Maximum Number of Independent Voices",
471        description: "Maximum number of different channels in which notes have sounded simultaneously. Here, Parts are treated as channels.",
472        dimensions: 1,
473        discrete: true,
474        normalize: false,
475        process: maximum_number_of_independent_voices,
476    },
477    Extractor {
478        id: "T2",
479        name: "Average Number of Independent Voices",
480        description: "Average number of different channels in which notes have sounded simultaneously. Rests are not included in this calculation. Here, Parts are treated as voices.",
481        dimensions: 1,
482        discrete: true,
483        normalize: false,
484        process: average_number_of_independent_voices,
485    },
486    Extractor {
487        id: "T3",
488        name: "Variability of Number of Independent Voices",
489        description: "Standard deviation of number of different channels in which notes have sounded simultaneously. Rests are not included in this calculation.",
490        dimensions: 1,
491        discrete: true,
492        normalize: false,
493        process: variability_of_number_of_independent_voices,
494    },
495    Extractor {
496        id: "P1",
497        name: "Most Common Pitch Prevalence",
498        description: "Fraction of Note Ons corresponding to the most common pitch.",
499        dimensions: 1,
500        discrete: false,
501        normalize: false,
502        process: most_common_pitch_prevalence,
503    },
504    Extractor {
505        id: "P2",
506        name: "Most Common Pitch Class Prevalence",
507        description: "Fraction of Note Ons corresponding to the most common pitch class.",
508        dimensions: 1,
509        discrete: false,
510        normalize: false,
511        process: most_common_pitch_class_prevalence,
512    },
513    Extractor {
514        id: "P3",
515        name: "Relative Strength of Top Pitches",
516        description: "The frequency of the 2nd most common pitch divided by the frequency of the most common pitch.",
517        dimensions: 1,
518        discrete: false,
519        normalize: false,
520        process: relative_strength_of_top_pitches,
521    },
522    Extractor {
523        id: "P4",
524        name: "Relative Strength of Top Pitch Classes",
525        description: "The frequency of the 2nd most common pitch class divided by the frequency of the most common pitch class.",
526        dimensions: 1,
527        discrete: false,
528        normalize: false,
529        process: relative_strength_of_top_pitch_classes,
530    },
531    Extractor {
532        id: "P5",
533        name: "Interval Between Strongest Pitches",
534        description: "Absolute value of the difference between the pitches of the two most common MIDI pitches.",
535        dimensions: 1,
536        discrete: true,
537        normalize: false,
538        process: interval_between_strongest_pitches,
539    },
540    Extractor {
541        id: "P6",
542        name: "Interval Between Strongest Pitch Classes",
543        description: "Absolute value of the difference between the pitch classes of the two most common MIDI pitch classes.",
544        dimensions: 1,
545        discrete: true,
546        normalize: false,
547        process: interval_between_strongest_pitch_classes,
548    },
549    Extractor {
550        id: "P7",
551        name: "Number of Common Pitches",
552        description: "Number of pitches that account individually for at least 9% of all notes.",
553        dimensions: 1,
554        discrete: true,
555        normalize: false,
556        process: number_of_common_pitches,
557    },
558    Extractor {
559        id: "P8",
560        name: "Pitch Variety",
561        description: "Number of pitches used at least once.",
562        dimensions: 1,
563        discrete: true,
564        normalize: false,
565        process: pitch_variety,
566    },
567    Extractor {
568        id: "P9",
569        name: "Pitch Class Variety",
570        description: "Number of pitch classes used at least once.",
571        dimensions: 1,
572        discrete: true,
573        normalize: false,
574        process: pitch_class_variety,
575    },
576    Extractor {
577        id: "P10",
578        name: "Range",
579        description: "Difference between highest and lowest pitches.",
580        dimensions: 1,
581        discrete: true,
582        normalize: false,
583        process: range,
584    },
585    Extractor {
586        id: "P11",
587        name: "Most Common Pitch",
588        description: "Bin label of the most common pitch.",
589        dimensions: 1,
590        discrete: false,
591        normalize: false,
592        process: most_common_pitch,
593    },
594    Extractor {
595        id: "P12",
596        name: "Primary Register",
597        description: "Average MIDI pitch.",
598        dimensions: 1,
599        discrete: false,
600        normalize: false,
601        process: primary_register,
602    },
603    Extractor {
604        id: "P13",
605        name: "Importance of Bass Register",
606        description: "Fraction of Note Ons between MIDI pitches 0 and 54.",
607        dimensions: 1,
608        discrete: false,
609        normalize: false,
610        process: importance_of_bass_register,
611    },
612    Extractor {
613        id: "P14",
614        name: "Importance of Middle Register",
615        description: "Fraction of Note Ons between MIDI pitches 55 and 72.",
616        dimensions: 1,
617        discrete: false,
618        normalize: false,
619        process: importance_of_middle_register,
620    },
621    Extractor {
622        id: "P15",
623        name: "Importance of High Register",
624        description: "Fraction of Note Ons between MIDI pitches 73 and 127.",
625        dimensions: 1,
626        discrete: false,
627        normalize: false,
628        process: importance_of_high_register,
629    },
630    Extractor {
631        id: "P16",
632        name: "Most Common Pitch Class",
633        description: "Bin label of the most common pitch class.",
634        dimensions: 1,
635        discrete: true,
636        normalize: false,
637        process: most_common_pitch_class,
638    },
639    Extractor {
640        id: "P19",
641        name: "Basic Pitch Histogram",
642        description: "A features array with bins corresponding to the values of the basic pitch histogram.",
643        dimensions: 128,
644        discrete: true,
645        normalize: true,
646        process: basic_pitch_histogram,
647    },
648    Extractor {
649        id: "P20",
650        name: "Pitch Class Distribution",
651        description: "A feature array with 12 entries where the first holds the frequency of the bin of the pitch class histogram with the highest frequency, and the following entries holding the successive bins of the histogram, wrapping around if necessary.",
652        dimensions: 12,
653        discrete: false,
654        normalize: true,
655        process: pitch_class_distribution,
656    },
657    Extractor {
658        id: "P21",
659        name: "Fifths Pitch Histogram",
660        description: "A feature array with bins corresponding to the values of the 5ths pitch class histogram.",
661        dimensions: 12,
662        discrete: true,
663        normalize: true,
664        process: fifths_pitch_histogram,
665    },
666    Extractor {
667        id: "P22",
668        name: "Quality",
669        description: "Set to 0 if the key signature indicates that a recording is major, set to 1 if it indicates that it is minor and set to 0 if key signature is unknown.",
670        dimensions: 1,
671        discrete: true,
672        normalize: false,
673        process: quality,
674    },
675];
676
677/// The jSymbolic extractor with this id, such as `P10` or music21's `m10`.
678pub fn extractor(id: &str) -> Option<&'static Extractor> {
679    JSYMBOLIC.iter().find(|extractor| extractor.id == id)
680}
681
682fn lacks_notes() -> Error {
683    Error::Feature("input lacks notes".to_string())
684}
685
686/// The index of the first of the greatest values: Python's
687/// `list.index(max(list))`.
688fn first_max(values: &[usize]) -> usize {
689    let most = values.iter().copied().max().unwrap_or(0);
690    values.iter().position(|value| *value == most).unwrap_or(0)
691}
692
693/// A counter's entries most common first, as `Counter.most_common` gives
694/// them: a tie goes to the one counted first.
695fn most_common(counts: &[(IntegerType, usize)]) -> Vec<(IntegerType, usize)> {
696    let mut sorted = counts.to_vec();
697    sorted.sort_by_key(|entry| std::cmp::Reverse(entry.1));
698    sorted
699}
700
701/// The share of the intervals counted that are of the sizes given.
702fn share_of(histogram: &[usize], targets: &[usize]) -> Result<FloatType> {
703    let total: usize = histogram.iter().sum();
704    if total == 0 {
705        return Err(lacks_notes());
706    }
707    let count: usize = targets.iter().map(|target| histogram[*target]).sum();
708    Ok(count as FloatType / total as FloatType)
709}
710
711fn melodic_interval_histogram(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
712    for (value, count) in vector.iter_mut().zip(data.midi_interval_histogram()) {
713        *value = (*count).into();
714    }
715    Ok(())
716}
717
718fn average_melodic_interval(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
719    let histogram = data.midi_interval_histogram();
720    let count: usize = histogram.iter().sum();
721    if count == 0 {
722        return Err(lacks_notes());
723    }
724    let total: usize = histogram
725        .iter()
726        .enumerate()
727        .map(|(size, count)| size * count)
728        .sum();
729    vector[0] = (total as FloatType / count as FloatType).into();
730    Ok(())
731}
732
733fn most_common_melodic_interval(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
734    vector[0] = first_max(data.midi_interval_histogram()).into();
735    Ok(())
736}
737
738fn distance_between_most_common_melodic_intervals(
739    data: &DataInstance,
740    vector: &mut [Value],
741) -> Result<()> {
742    let mut histogram = data.midi_interval_histogram().to_vec();
743    let first = first_max(&histogram);
744    histogram[first] = 0;
745    let second = first_max(&histogram);
746    vector[0] = first.abs_diff(second).into();
747    Ok(())
748}
749
750fn most_common_melodic_interval_prevalence(
751    data: &DataInstance,
752    vector: &mut [Value],
753) -> Result<()> {
754    let histogram = data.midi_interval_histogram();
755    let most = histogram.iter().copied().max().unwrap_or(0);
756    let count: usize = histogram.iter().sum();
757    if count == 0 {
758        return Err(lacks_notes());
759    }
760    vector[0] = (most as FloatType / count as FloatType).into();
761    Ok(())
762}
763
764fn relative_strength_of_most_common_intervals(
765    data: &DataInstance,
766    vector: &mut [Value],
767) -> Result<()> {
768    let mut histogram = data.midi_interval_histogram().to_vec();
769    let count: usize = histogram.iter().sum();
770    if count == 0 {
771        return Err(lacks_notes());
772    }
773    let first = first_max(&histogram);
774    let most = histogram[first];
775    histogram[first] = 0;
776    let second = histogram.iter().copied().max().unwrap_or(0);
777    let count = count as FloatType;
778    vector[0] = ((second as FloatType / count) / (most as FloatType / count)).into();
779    Ok(())
780}
781
782fn number_of_common_melodic_intervals(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
783    let histogram = data.midi_interval_histogram();
784    let total: usize = histogram.iter().sum();
785    if total == 0 {
786        return Err(lacks_notes());
787    }
788    vector[0] = histogram
789        .iter()
790        .filter(|count| **count as FloatType / total as FloatType >= 0.09)
791        .count()
792        .into();
793    Ok(())
794}
795
796fn amount_of_arpeggiation(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
797    let histogram = data.midi_interval_histogram();
798    let total: usize = histogram.iter().sum();
799    if total == 0 {
800        return Ok(());
801    }
802    let count: usize = [0, 3, 4, 7, 10, 11, 12, 15, 16]
803        .iter()
804        .map(|target| histogram[*target])
805        .sum();
806    // music21 takes a melody with no arpeggiation at all for one with no
807    // notes.
808    if count == 0 {
809        return Err(lacks_notes());
810    }
811    vector[0] = (count as FloatType / total as FloatType).into();
812    Ok(())
813}
814
815fn repeated_notes(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
816    let histogram = data.midi_interval_histogram();
817    let total: usize = histogram.iter().sum();
818    if total == 0 {
819        return Ok(());
820    }
821    vector[0] = (histogram[0] as FloatType / total as FloatType).into();
822    Ok(())
823}
824
825fn chromatic_motion(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
826    vector[0] = share_of(data.midi_interval_histogram(), &[1])?.into();
827    Ok(())
828}
829
830fn stepwise_motion(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
831    vector[0] = share_of(data.midi_interval_histogram(), &[1, 2])?.into();
832    Ok(())
833}
834
835fn melodic_thirds(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
836    vector[0] = share_of(data.midi_interval_histogram(), &[3, 4])?.into();
837    Ok(())
838}
839
840fn melodic_fifths(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
841    vector[0] = share_of(data.midi_interval_histogram(), &[7])?.into();
842    Ok(())
843}
844
845fn melodic_tritones(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
846    vector[0] = share_of(data.midi_interval_histogram(), &[6])?.into();
847    Ok(())
848}
849
850fn melodic_octaves(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
851    vector[0] = share_of(
852        data.midi_interval_histogram(),
853        &[12, 24, 48, 60, 72, 84, 96, 108, 120],
854    )?
855    .into();
856    Ok(())
857}
858
859/// The contour of each part of a score, or of the piece where it is not
860/// one.
861fn contours(data: &DataInstance) -> Vec<&[IntegerType]> {
862    if data.parts_count() > 0 {
863        data.parts()[..data.parts_count()]
864            .iter()
865            .map(DataInstance::contour_list)
866            .collect()
867    } else {
868        vec![data.contour_list()]
869    }
870}
871
872fn direction_of_motion(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
873    let (mut rising, mut falling) = (0usize, 0usize);
874    for contour in contours(data) {
875        for step in contour {
876            if *step > 0 {
877                rising += 1;
878            } else if *step < 0 {
879                falling += 1;
880            }
881        }
882    }
883    if rising + falling == 0 {
884        return Err(lacks_notes());
885    }
886    vector[0] = (rising as FloatType / (falling + rising) as FloatType).into();
887    Ok(())
888}
889
890fn duration_of_melodic_arcs(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
891    let (mut changes, mut moving) = (0usize, 0usize);
892    for contour in contours(data) {
893        let mut direction = 0;
894        for step in contour {
895            if *step != 0 {
896                moving += 1;
897            }
898            match direction {
899                1 if *step < 0 => {
900                    changes += 1;
901                    direction = -1;
902                }
903                -1 if *step > 0 => {
904                    changes += 1;
905                    direction = 1;
906                }
907                0 => direction = step.signum(),
908                _ => {}
909            }
910        }
911    }
912    if changes > 0 {
913        vector[0] = (moving as FloatType / changes as FloatType).into();
914    }
915    Ok(())
916}
917
918fn size_of_melodic_arcs(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
919    let (mut changes, mut total) = (0usize, 0i64);
920    for contour in contours(data) {
921        let mut direction = 0;
922        let mut arc = 0i64;
923        for step in contour {
924            let size = i64::from(step.abs());
925            match direction {
926                1 if *step > 0 => arc += size,
927                -1 if *step < 0 => arc += size,
928                1 | -1 if *step != 0 => {
929                    total += arc;
930                    changes += 1;
931                    direction = step.signum();
932                    arc = size;
933                }
934                0 if *step != 0 => {
935                    direction = step.signum();
936                    arc += size;
937                }
938                _ => {}
939            }
940        }
941    }
942    if changes > 0 {
943        vector[0] = (total as FloatType / changes as FloatType).into();
944    }
945    Ok(())
946}
947
948fn total_of(counts: &[(IntegerType, usize)]) -> usize {
949    counts.iter().map(|(_, count)| count).sum()
950}
951
952fn most_common_pitch_prevalence(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
953    let histogram = data.midi_pitch_histogram();
954    if histogram.is_empty() {
955        return Err(lacks_notes());
956    }
957    let most = histogram.iter().map(|(_, count)| *count).max().unwrap_or(0);
958    vector[0] = (most as FloatType / total_of(histogram) as FloatType).into();
959    Ok(())
960}
961
962fn most_common_pitch_class_prevalence(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
963    let histogram = data.pitch_class_histogram();
964    let class = first_max(histogram);
965    let total: usize = histogram.iter().sum();
966    if total == 0 {
967        return Err(lacks_notes());
968    }
969    vector[0] = (histogram[class] as FloatType / total as FloatType).into();
970    Ok(())
971}
972
973fn relative_strength_of_top_pitches(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
974    vector[0] = match most_common(data.midi_pitch_histogram()).as_slice() {
975        [first, second, ..] => (second.1 as FloatType / first.1 as FloatType).into(),
976        _ => Value::Float(0.0),
977    };
978    Ok(())
979}
980
981fn relative_strength_of_top_pitch_classes(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
982    let mut histogram = *data.pitch_class_histogram();
983    let first = first_max(&histogram);
984    let most = histogram[first];
985    if most == 0 {
986        return Err(lacks_notes());
987    }
988    histogram[first] = 0;
989    let second = histogram[first_max(&histogram)];
990    vector[0] = (second as FloatType / most as FloatType).into();
991    Ok(())
992}
993
994fn interval_between_strongest_pitches(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
995    vector[0] = match most_common(data.midi_pitch_histogram()).as_slice() {
996        [first, second, ..] => (second.0 - first.0).abs().into(),
997        _ => Value::Float(0.0),
998    };
999    Ok(())
1000}
1001
1002fn interval_between_strongest_pitch_classes(
1003    data: &DataInstance,
1004    vector: &mut [Value],
1005) -> Result<()> {
1006    let mut histogram = *data.pitch_class_histogram();
1007    let first = first_max(&histogram);
1008    histogram[first] = 0;
1009    let second = first_max(&histogram);
1010    vector[0] = first.abs_diff(second).into();
1011    Ok(())
1012}
1013
1014fn number_of_common_pitches(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1015    let histogram = data.midi_pitch_histogram();
1016    let total = total_of(histogram) as FloatType;
1017    vector[0] = histogram
1018        .iter()
1019        .filter(|(_, count)| *count as FloatType / total >= 0.09)
1020        .count()
1021        .into();
1022    Ok(())
1023}
1024
1025fn pitch_variety(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1026    // music21 walks the counter's keys, the MIDI numbers, and counts those
1027    // at least one, which leaves out MIDI nought.
1028    vector[0] = data
1029        .midi_pitch_histogram()
1030        .iter()
1031        .filter(|(midi, _)| *midi >= 1)
1032        .count()
1033        .into();
1034    Ok(())
1035}
1036
1037fn pitch_class_variety(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1038    vector[0] = data
1039        .pitch_class_histogram()
1040        .iter()
1041        .filter(|count| **count >= 1)
1042        .count()
1043        .into();
1044    Ok(())
1045}
1046
1047fn range(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1048    let histogram = data.midi_pitch_histogram();
1049    let lowest = histogram.iter().map(|(midi, _)| *midi).min();
1050    let highest = histogram.iter().map(|(midi, _)| *midi).max();
1051    let (Some(lowest), Some(highest)) = (lowest, highest) else {
1052        return Err(lacks_notes());
1053    };
1054    vector[0] = (highest - lowest).into();
1055    Ok(())
1056}
1057
1058fn most_common_pitch(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1059    vector[0] = most_common(data.midi_pitch_histogram())
1060        .first()
1061        .map_or(Value::Float(0.0), |(midi, _)| (*midi).into());
1062    Ok(())
1063}
1064
1065fn primary_register(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1066    let pitches = data.pitches();
1067    if pitches.is_empty() {
1068        return Err(lacks_notes());
1069    }
1070    let heights: Vec<FloatType> = pitches.iter().map(|pitch| pitch.ps()).collect();
1071    vector[0] = crate::statistics::mean(&heights).into();
1072    Ok(())
1073}
1074
1075/// The share of the notes sounding a MIDI number `within` accepts.
1076fn register_share(data: &DataInstance, within: impl Fn(IntegerType) -> bool) -> Result<FloatType> {
1077    let histogram = data.midi_pitch_histogram();
1078    if histogram.is_empty() {
1079        return Err(lacks_notes());
1080    }
1081    let matched: usize = histogram
1082        .iter()
1083        .filter(|(midi, _)| within(*midi))
1084        .map(|(_, count)| count)
1085        .sum();
1086    Ok(matched as FloatType / total_of(histogram) as FloatType)
1087}
1088
1089fn importance_of_bass_register(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1090    vector[0] = register_share(data, |midi| midi <= 54)?.into();
1091    Ok(())
1092}
1093
1094fn importance_of_middle_register(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1095    vector[0] = register_share(data, |midi| (55..=72).contains(&midi))?.into();
1096    Ok(())
1097}
1098
1099fn importance_of_high_register(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1100    vector[0] = register_share(data, |midi| midi >= 73)?.into();
1101    Ok(())
1102}
1103
1104fn most_common_pitch_class(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1105    vector[0] = first_max(data.pitch_class_histogram()).into();
1106    Ok(())
1107}
1108
1109fn basic_pitch_histogram(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1110    for (midi, count) in data.midi_pitch_histogram() {
1111        if let Some(value) = usize::try_from(*midi)
1112            .ok()
1113            .and_then(|midi| vector.get_mut(midi))
1114        {
1115            *value = (*count).into();
1116        }
1117    }
1118    Ok(())
1119}
1120
1121fn pitch_class_distribution(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1122    let histogram = data.pitch_class_histogram();
1123    let most = first_max(histogram);
1124    for (class, count) in histogram.iter().enumerate() {
1125        vector[(class + 12 - most) % 12] = (*count).into();
1126    }
1127    Ok(())
1128}
1129
1130/// Where each pitch class goes in a histogram ordered by fifths.
1131const FIFTHS: [usize; 12] = [0, 7, 2, 9, 4, 11, 6, 1, 8, 3, 10, 5];
1132
1133fn fifths_pitch_histogram(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1134    for (class, count) in data.pitch_class_histogram().iter().enumerate() {
1135        vector[FIFTHS[class]] = (*count).into();
1136    }
1137    Ok(())
1138}
1139
1140fn quality(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1141    vector[0] = data
1142        .key_modes()
1143        .iter()
1144        .find_map(|mode| match mode.as_str() {
1145            "major" => Some(Value::Integer(0)),
1146            "minor" => Some(Value::Integer(1)),
1147            _ => None,
1148        })
1149        .unwrap_or(Value::Integer(0));
1150    Ok(())
1151}
1152
1153/// When each note of a piece starts, in seconds, earliest first.
1154fn onsets(data: &DataInstance) -> Result<Vec<FloatType>> {
1155    let mut onsets: Vec<FloatType> = data.seconds_map()?.iter().map(|note| note.offset).collect();
1156    onsets.sort_by(FloatType::total_cmp);
1157    Ok(onsets)
1158}
1159
1160/// The time between each onset and the next, leaving out notes struck
1161/// together: music21's `isclose(dif, 0.0, abs_tol=1e-7)`.
1162fn time_between(onsets: &[FloatType]) -> Vec<FloatType> {
1163    onsets
1164        .windows(2)
1165        .map(|pair| pair[1] - pair[0])
1166        .filter(|gap| gap.abs() > 1e-7)
1167        .collect()
1168}
1169
1170/// The onsets of each part of a score, or of the piece where it is not one.
1171fn onsets_by_part(data: &DataInstance) -> Result<Vec<Vec<FloatType>>> {
1172    if data.parts_count() > 0 {
1173        data.parts()[..data.parts_count()]
1174            .iter()
1175            .map(onsets)
1176            .collect()
1177    } else {
1178        Ok(vec![onsets(data)?])
1179    }
1180}
1181
1182fn note_density(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1183    let seconds = data.seconds_map()?;
1184    let last = seconds
1185        .iter()
1186        .map(|note| note.end)
1187        .fold(None, |most: Option<FloatType>, end| {
1188            Some(most.map_or(end, |most| most.max(end)))
1189        });
1190    vector[0] = match last {
1191        Some(0.0) => return Err(Error::Feature("float division by zero".to_string())),
1192        Some(last) => (seconds.len() as FloatType / last).into(),
1193        None => Value::Float(0.0),
1194    };
1195    Ok(())
1196}
1197
1198fn average_note_duration(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1199    let seconds = data.seconds_map()?;
1200    if seconds.is_empty() {
1201        return Err(lacks_notes());
1202    }
1203    let mut total = 0.0;
1204    for note in seconds {
1205        total += note.duration;
1206    }
1207    vector[0] = (total / seconds.len() as FloatType).into();
1208    Ok(())
1209}
1210
1211fn variability_of_note_duration(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1212    let seconds = data.seconds_map()?;
1213    if seconds.is_empty() {
1214        return Err(lacks_notes());
1215    }
1216    let durations: Vec<FloatType> = seconds.iter().map(|note| note.duration).collect();
1217    vector[0] = crate::statistics::pstdev(&durations).into();
1218    Ok(())
1219}
1220
1221fn maximum_note_duration(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1222    let seconds = data.seconds_map()?;
1223    if seconds.is_empty() {
1224        return Err(lacks_notes());
1225    }
1226    let mut longest = 0.0;
1227    for note in seconds {
1228        if note.duration > longest {
1229            longest = note.duration;
1230        }
1231    }
1232    vector[0] = longest.into();
1233    Ok(())
1234}
1235
1236fn minimum_note_duration(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1237    let seconds = data.seconds_map()?;
1238    let Some(first) = seconds.first() else {
1239        return Err(lacks_notes());
1240    };
1241    let mut shortest = first.duration;
1242    for note in seconds {
1243        if note.duration < shortest {
1244            shortest = note.duration;
1245        }
1246    }
1247    vector[0] = shortest.into();
1248    Ok(())
1249}
1250
1251fn staccato_incidence(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1252    let seconds = data.seconds_map()?;
1253    if seconds.is_empty() {
1254        return Err(lacks_notes());
1255    }
1256    let short = seconds.iter().filter(|note| note.duration < 0.10).count();
1257    vector[0] = (short as FloatType / seconds.len() as FloatType).into();
1258    Ok(())
1259}
1260
1261fn average_time_between_attacks(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1262    let onsets = onsets(data)?;
1263    if onsets.is_empty() {
1264        return Err(lacks_notes());
1265    }
1266    let gaps = time_between(&onsets);
1267    if gaps.is_empty() {
1268        return Err(Error::Feature("division by zero".to_string()));
1269    }
1270    vector[0] = (crate::statistics::python_sum(&gaps) / gaps.len() as FloatType).into();
1271    Ok(())
1272}
1273
1274fn variability_of_time_between_attacks(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1275    let onsets = onsets(data)?;
1276    if onsets.is_empty() {
1277        return Err(lacks_notes());
1278    }
1279    let gaps = time_between(&onsets);
1280    if gaps.is_empty() {
1281        return Err(Error::Feature(
1282            "pstdev requires at least one data point".to_string(),
1283        ));
1284    }
1285    vector[0] = crate::statistics::pstdev(&gaps).into();
1286    Ok(())
1287}
1288
1289fn average_time_between_attacks_for_each_voice(
1290    data: &DataInstance,
1291    vector: &mut [Value],
1292) -> Result<()> {
1293    let mut averages = Vec::new();
1294    for onsets in onsets_by_part(data)? {
1295        let gaps = time_between(&onsets);
1296        if gaps.is_empty() {
1297            return Err(Error::Feature("at least one part lacks notes".to_string()));
1298        }
1299        averages.push(crate::statistics::python_sum(&gaps) / gaps.len() as FloatType);
1300    }
1301    vector[0] = (crate::statistics::python_sum(&averages) / averages.len() as FloatType).into();
1302    Ok(())
1303}
1304
1305fn average_variability_of_time_between_attacks_for_each_voice(
1306    data: &DataInstance,
1307    vector: &mut [Value],
1308) -> Result<()> {
1309    let mut deviations = Vec::new();
1310    for onsets in onsets_by_part(data)? {
1311        let gaps = time_between(&onsets);
1312        if gaps.is_empty() {
1313            return Err(Error::Feature("at least one part lacks notes".to_string()));
1314        }
1315        deviations.push(crate::statistics::pstdev(&gaps));
1316    }
1317    vector[0] = (crate::statistics::python_sum(&deviations) / deviations.len() as FloatType).into();
1318    Ok(())
1319}
1320
1321fn initial_tempo(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1322    let boundaries = data.metronome_mark_boundaries()?;
1323    vector[0] = boundaries
1324        .first()
1325        .and_then(|(_, _, mark)| mark.sounding_quarter_bpm())
1326        .ok_or_else(|| Error::Feature("the first tempo says no number".to_string()))?
1327        .into();
1328    Ok(())
1329}
1330
1331fn initial_time_signature(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1332    if let Some(meter) = data.time_signatures().first() {
1333        vector[0] = meter.numerator().into();
1334        vector[1] = meter.denominator().into();
1335    }
1336    Ok(())
1337}
1338
1339fn compound_or_simple_meter(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1340    if data
1341        .time_signatures()
1342        .first()
1343        .is_some_and(|meter| meter.beat_division_count_name() == "Compound")
1344    {
1345        vector[0] = Value::Integer(1);
1346    }
1347    Ok(())
1348}
1349
1350fn triple_meter(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1351    if data
1352        .time_signatures()
1353        .first()
1354        .is_some_and(|meter| meter.numerator() == 3)
1355    {
1356        vector[0] = Value::Integer(1);
1357    }
1358    Ok(())
1359}
1360
1361fn quintuple_meter(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1362    if data
1363        .time_signatures()
1364        .first()
1365        .is_some_and(|meter| meter.numerator() == 5)
1366    {
1367        vector[0] = Value::Integer(1);
1368    }
1369    Ok(())
1370}
1371
1372fn changes_of_meter(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1373    let meters = data.time_signatures();
1374    if let [first, rest @ ..] = meters.as_slice()
1375        && rest.iter().any(|meter| !first.ratio_equal(meter))
1376    {
1377        vector[0] = Value::Integer(1);
1378    }
1379    Ok(())
1380}
1381
1382fn duration(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1383    let seconds = data.seconds_map()?;
1384    if seconds.is_empty() {
1385        return Err(Error::Feature("input lacks duration".to_string()));
1386    }
1387    vector[0] = seconds
1388        .iter()
1389        .map(|note| note.end)
1390        .fold(FloatType::NEG_INFINITY, FloatType::max)
1391        .into();
1392    Ok(())
1393}
1394
1395fn maximum_number_of_independent_voices(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1396    vector[0] = data
1397        .parts_sounding()?
1398        .iter()
1399        .copied()
1400        .max()
1401        .unwrap_or(0)
1402        .into();
1403    Ok(())
1404}
1405
1406fn average_number_of_independent_voices(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1407    let sounding = data.parts_sounding()?;
1408    if sounding.is_empty() {
1409        return Err(lacks_notes());
1410    }
1411    vector[0] = (sounding.iter().sum::<usize>() as FloatType / sounding.len() as FloatType).into();
1412    Ok(())
1413}
1414
1415fn variability_of_number_of_independent_voices(
1416    data: &DataInstance,
1417    vector: &mut [Value],
1418) -> Result<()> {
1419    let sounding = data.parts_sounding()?;
1420    if sounding.is_empty() {
1421        return Err(lacks_notes());
1422    }
1423    let counts: Vec<FloatType> = sounding.iter().map(|count| *count as FloatType).collect();
1424    vector[0] = crate::statistics::pstdev(&counts).into();
1425    Ok(())
1426}
1427
1428/// Each part of the piece partitioned by instrument, as the program of its
1429/// first instrument (`None` where it states none) and how many notes it
1430/// holds: what music21's instrument features read off
1431/// `partitionByInstrument`. Nothing where the partition is empty.
1432fn instrument_parts(data: &DataInstance) -> Result<Vec<(Option<Option<u8>>, usize)>> {
1433    let partitioned = crate::instrument::partition_by_instrument(data.prepared());
1434    if partitioned.is_empty() {
1435        return Err(Error::Feature("input lacks instruments".to_string()));
1436    }
1437    Ok(partitioned
1438        .parts()
1439        .into_iter()
1440        .map(|part| {
1441            let program = part
1442                .events()
1443                .iter()
1444                .find_map(|event| match event.element() {
1445                    StreamElement::Instrument(instrument) => Some(instrument.midi_program()),
1446                    _ => None,
1447                });
1448            let notes = part
1449                .recurse()
1450                .into_iter()
1451                .filter(|(_, element)| {
1452                    matches!(
1453                        element,
1454                        StreamElement::Note(_)
1455                            | StreamElement::Chord(_)
1456                            | StreamElement::Unpitched(_)
1457                            | StreamElement::PercussionChord(_)
1458                            | StreamElement::ChordSymbol(_)
1459                    )
1460                })
1461                .count();
1462            (program, notes)
1463        })
1464        .collect())
1465}
1466
1467/// The program of a part's first instrument, refused where it has none or
1468/// the instrument names no program, as music21 refuses it.
1469fn program_of(program: Option<Option<u8>>) -> Result<usize> {
1470    match program {
1471        Some(Some(program)) => Ok(usize::from(program)),
1472        Some(None) => Err(Error::Feature(
1473            "an instrument lacks a midiProgram".to_string(),
1474        )),
1475        None => Err(Error::Feature(
1476            "'NoneType' object has no attribute 'midiProgram'".to_string(),
1477        )),
1478    }
1479}
1480
1481/// How many pitches sound in the piece, which the instrument features
1482/// divide by.
1483fn pitch_total(data: &DataInstance) -> usize {
1484    data.pitch_class_histogram().iter().sum()
1485}
1486
1487fn pitched_instruments_present(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1488    for (program, notes) in instrument_parts(data)? {
1489        if notes > 0 {
1490            vector[program_of(program)?] = Value::Integer(1);
1491        }
1492    }
1493    Ok(())
1494}
1495
1496fn note_prevalence_of_pitched_instruments(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1497    let total = pitch_total(data);
1498    for (program, notes) in instrument_parts(data)? {
1499        if notes > 0 {
1500            let program = program_of(program)?;
1501            if total == 0 {
1502                return Err(Error::Feature("division by zero".to_string()));
1503            }
1504            vector[program] = (notes as FloatType / total as FloatType).into();
1505        }
1506    }
1507    Ok(())
1508}
1509
1510fn variability_of_note_prevalence_of_pitched_instruments(
1511    data: &DataInstance,
1512    vector: &mut [Value],
1513) -> Result<()> {
1514    let parts = instrument_parts(data)?;
1515    let total = pitch_total(data);
1516    if total == 0 {
1517        return Err(lacks_notes());
1518    }
1519    let shares: Vec<FloatType> = parts
1520        .iter()
1521        .filter(|(_, notes)| *notes > 0)
1522        .map(|(_, notes)| *notes as FloatType / total as FloatType)
1523        .collect();
1524    if shares.is_empty() {
1525        return Err(Error::Feature("division by zero".to_string()));
1526    }
1527    let mean = crate::statistics::python_sum(&shares) / shares.len() as FloatType;
1528    let squares: Vec<FloatType> = shares.iter().map(|share| (share - mean).powi(2)).collect();
1529    vector[0] = (crate::statistics::python_sum(&squares) / squares.len() as FloatType)
1530        .sqrt()
1531        .into();
1532    Ok(())
1533}
1534
1535fn number_of_pitched_instruments(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1536    vector[0] = instrument_parts(data)?
1537        .iter()
1538        .filter(|(_, notes)| *notes > 0)
1539        .count()
1540        .into();
1541    Ok(())
1542}
1543
1544/// The share of the piece's pitches played by instruments of these
1545/// programs: music21's `InstrumentFractionFeature`.
1546fn instrument_fraction(data: &DataInstance, programs: &[usize]) -> Result<FloatType> {
1547    let parts = instrument_parts(data)?;
1548    let total = pitch_total(data);
1549    if total == 0 {
1550        return Err(lacks_notes());
1551    }
1552    let mut count = 0;
1553    for (program, notes) in parts {
1554        // An instrument naming no program is in no family; a part with no
1555        // instrument at all is refused.
1556        let Some(program) = program else {
1557            return Err(program_of(None).unwrap_err());
1558        };
1559        if program.is_some_and(|program| programs.contains(&usize::from(program))) {
1560            count += notes;
1561        }
1562    }
1563    Ok(count as FloatType / total as FloatType)
1564}
1565
1566fn string_keyboard_fraction(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1567    vector[0] = instrument_fraction(data, &[0, 1, 2, 3, 4, 5, 6, 7])?.into();
1568    Ok(())
1569}
1570
1571fn acoustic_guitar_fraction(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1572    vector[0] = instrument_fraction(data, &[24, 25])?.into();
1573    Ok(())
1574}
1575
1576fn electric_guitar_fraction(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1577    vector[0] = instrument_fraction(data, &[26, 27, 28, 29, 30, 31])?.into();
1578    Ok(())
1579}
1580
1581fn violin_fraction(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1582    vector[0] = instrument_fraction(data, &[40, 110])?.into();
1583    Ok(())
1584}
1585
1586fn saxophone_fraction(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1587    vector[0] = instrument_fraction(data, &[64, 65, 66, 67])?.into();
1588    Ok(())
1589}
1590
1591fn brass_fraction(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1592    vector[0] = instrument_fraction(data, &[56, 57, 58, 59, 60, 61])?.into();
1593    Ok(())
1594}
1595
1596fn woodwinds_fraction(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1597    vector[0] =
1598        instrument_fraction(data, &[68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79])?.into();
1599    Ok(())
1600}
1601
1602fn orchestral_strings_fraction(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1603    vector[0] = instrument_fraction(data, &[41, 42, 43, 44, 45])?.into();
1604    Ok(())
1605}
1606
1607fn string_ensemble_fraction(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1608    vector[0] = instrument_fraction(data, &[48, 49, 50, 51])?.into();
1609    Ok(())
1610}
1611
1612fn electric_instrument_fraction(data: &DataInstance, vector: &mut [Value]) -> Result<()> {
1613    vector[0] = instrument_fraction(
1614        data,
1615        &[
1616            4, 5, 16, 18, 26, 27, 28, 29, 30, 31, 33, 34, 35, 36, 37, 38, 39,
1617        ],
1618    )?
1619    .into();
1620    Ok(())
1621}